From cea0660104676c3a92a8150ba8ca946d2a78284c Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Mon, 18 Dec 2017 14:39:06 +0200 Subject: [PATCH] first divorce from Tigger GUI, time to get independent --- README.rst | 42 +- Tigger/AboutDialog.py | 111 -- Tigger/Images/ColormapTables/Karma.py | 2278 ---------------------- Tigger/Images/ColormapTables/__init__.py | 0 Tigger/Images/Colormaps.py | 407 ---- Tigger/Images/ControlDialog.py | 951 --------- Tigger/Images/Controller.py | 468 ----- Tigger/Images/Manager.py | 522 ----- Tigger/Images/RenderControl.py | 389 ---- Tigger/Images/SkyImage.py | 649 ------ Tigger/Images/__init__.py | 32 - Tigger/MainWindow.py | 684 ------- Tigger/Plot/MouseModes.py | 209 -- Tigger/Plot/SkyModelPlot.py | 1874 ------------------ Tigger/Plot/__init__.py | 0 Tigger/SkyModelTreeWidget.py | 693 ------- Tigger/{Images => Tools}/FITSHeaders.py | 0 Tigger/Tools/Imaging.py | 4 +- Tigger/Tools/__init__.py | 9 - Tigger/Tools/add_brick.py | 180 -- Tigger/Tools/export_karma.py | 150 -- Tigger/Tools/make_brick.py | 313 --- Tigger/Tools/restore_image.py | 212 -- Tigger/Tools/source_selector.py | 297 --- Tigger/Widgets.py | 310 --- Tigger/__init__.py | 24 +- Tigger/bin/tigger | 1 - Tigger/bin/tigger-make-brick | 17 +- Tigger/icons/astron_logo.png | Bin 7373 -> 0 bytes Tigger/icons/big_minus.png | Bin 332 -> 0 bytes Tigger/icons/big_plus.png | Bin 564 -> 0 bytes Tigger/icons/center_image.png | Bin 735 -> 0 bytes Tigger/icons/colours.png | Bin 495 -> 0 bytes Tigger/icons/full_range.png | Bin 557 -> 0 bytes Tigger/icons/intensity_graph.png | Bin 665 -> 0 bytes Tigger/icons/locked.png | Bin 750 -> 0 bytes Tigger/icons/raise_up.png | Bin 503 -> 0 bytes Tigger/icons/ruler.png | Bin 761 -> 0 bytes Tigger/icons/subset_range.png | Bin 320 -> 0 bytes Tigger/icons/tigger_logo.png | Bin 19456 -> 0 bytes Tigger/icons/tigger_splash.png | Bin 74528 -> 0 bytes Tigger/icons/tigger_starface.png | Bin 19456 -> 0 bytes Tigger/icons/unlocked.png | Bin 923 -> 0 bytes Tigger/icons/window_larger.png | Bin 785 -> 0 bytes Tigger/icons/window_sigma.png | Bin 693 -> 0 bytes Tigger/icons/window_smaller.png | Bin 772 -> 0 bytes Tigger/icons/wizard.png | Bin 1325 -> 0 bytes Tigger/icons/zoom-out.png | Bin 1866 -> 0 bytes Tigger/icons/zoom_colours.png | Bin 933 -> 0 bytes Tigger/icons/zoom_in.png | Bin 791 -> 0 bytes Tigger/icons/zoom_out.png | Bin 396 -> 0 bytes Tigger/icons/zoom_range.png | Bin 367 -> 0 bytes Tigger/tigger | 152 -- Tigger/tigger.conf | 107 - setup.py | 27 +- 55 files changed, 31 insertions(+), 11081 deletions(-) delete mode 100644 Tigger/AboutDialog.py delete mode 100644 Tigger/Images/ColormapTables/Karma.py delete mode 100644 Tigger/Images/ColormapTables/__init__.py delete mode 100644 Tigger/Images/Colormaps.py delete mode 100644 Tigger/Images/ControlDialog.py delete mode 100644 Tigger/Images/Controller.py delete mode 100644 Tigger/Images/Manager.py delete mode 100644 Tigger/Images/RenderControl.py delete mode 100644 Tigger/Images/SkyImage.py delete mode 100644 Tigger/Images/__init__.py delete mode 100644 Tigger/MainWindow.py delete mode 100644 Tigger/Plot/MouseModes.py delete mode 100644 Tigger/Plot/SkyModelPlot.py delete mode 100644 Tigger/Plot/__init__.py delete mode 100644 Tigger/SkyModelTreeWidget.py rename Tigger/{Images => Tools}/FITSHeaders.py (100%) delete mode 100644 Tigger/Tools/add_brick.py delete mode 100644 Tigger/Tools/export_karma.py delete mode 100644 Tigger/Tools/make_brick.py delete mode 100644 Tigger/Tools/restore_image.py delete mode 100644 Tigger/Tools/source_selector.py delete mode 100644 Tigger/Widgets.py delete mode 120000 Tigger/bin/tigger delete mode 100644 Tigger/icons/astron_logo.png delete mode 100644 Tigger/icons/big_minus.png delete mode 100644 Tigger/icons/big_plus.png delete mode 100644 Tigger/icons/center_image.png delete mode 100644 Tigger/icons/colours.png delete mode 100644 Tigger/icons/full_range.png delete mode 100644 Tigger/icons/intensity_graph.png delete mode 100644 Tigger/icons/locked.png delete mode 100644 Tigger/icons/raise_up.png delete mode 100644 Tigger/icons/ruler.png delete mode 100644 Tigger/icons/subset_range.png delete mode 100644 Tigger/icons/tigger_logo.png delete mode 100644 Tigger/icons/tigger_splash.png delete mode 100644 Tigger/icons/tigger_starface.png delete mode 100644 Tigger/icons/unlocked.png delete mode 100644 Tigger/icons/window_larger.png delete mode 100644 Tigger/icons/window_sigma.png delete mode 100644 Tigger/icons/window_smaller.png delete mode 100644 Tigger/icons/wizard.png delete mode 100644 Tigger/icons/zoom-out.png delete mode 100644 Tigger/icons/zoom_colours.png delete mode 100644 Tigger/icons/zoom_in.png delete mode 100644 Tigger/icons/zoom_out.png delete mode 100644 Tigger/icons/zoom_range.png delete mode 100755 Tigger/tigger delete mode 100644 Tigger/tigger.conf diff --git a/README.rst b/README.rst index 1d6d2b5..4aefbf9 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,14 @@ -====== -Tigger -====== +========================== +Tigger-LSM: LSM Libs/utils +========================== -Installing Tigger -================= +Installing Tigger-LSM +===================== Ubuntu package -------------- -Enable the -`radio astro launchpad PPA `_ -and install the python-tigger package. +Enable the KERN suite and install the tigger-lsm package. from pypi or from source @@ -18,18 +16,17 @@ from pypi or from source requirements: - * Assorted python packages: PyQt4, PyQwt5, pyfits, numpy, scipy, astLib. + * Assorted python packages: pyfits, numpy, scipy, astLib. With the exception of astLib, these are already present in most Linux distros. astLib may be downloaded here: http://astlib.sourceforge.net/ - * Purr/Kittens. Easiest to install the purr package from a MeqTrees binary - distribution (see http://www.astron.nl/meqwiki/Downloading). Alternatively, - check it out from svn (see below), and make sure the parent - of the Kittens directory is in your PYTHONPATH. + * Purr/Kittens. Available from pip as astro-kittens. Else, install the purr package from a MeqTrees binary + distribution (see http://www.astron.nl/meqwiki/Downloading). Alternatively, check it out from svn (see below), + and make sure the parent of the Kittens directory is in your PYTHONPATH. To obtain on ubuntu you can run:: - $ sudo apt-get install python-kittens python-pyfits python-astlib python-scipy python-numpy python-qt4 python-qwt5-qt4 libicu48 + $ sudo apt-get install python-kittens python-pyfits python-astlib python-scipy python-numpy now from pip:: @@ -37,15 +34,22 @@ now from pip:: or from source:: - $ git clone https://github.com/ska-sa/tigger - $ cd tigger + $ git clone https://github.com/ska-sa/tigger-lsm + $ cd tigger-lsm $ python setup.py install Running Tigger ============== -Run the installed tigger binary. +In python: + + $ import Tigger + $ model = Tigger.load("foo.lsm.html") + +In the shell + + $ tigger-convert foo.txt foo.lsm.html Questions or problems @@ -53,11 +57,11 @@ Questions or problems Open an issue on github -https://github.com/ska-sa/tigger +https://github.com/ska-sa/tigger-lsm Travis ====== -.. image:: https://travis-ci.org/ska-sa/tigger.svg?branch=master +.. image:: https://travis-ci.org/ska-sa/tigger-lsm.svg?branch=master :target: https://travis-ci.org/ska-sa/tigger diff --git a/Tigger/AboutDialog.py b/Tigger/AboutDialog.py deleted file mode 100644 index a9da2c5..0000000 --- a/Tigger/AboutDialog.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -from Tigger import release_string,svn_revision_html,pixmaps - -import os.path -import time -import sys -import fnmatch -import traceback - -from PyQt4.Qt import * - - - -class AboutDialog (QDialog): - def __init__(self,parent=None,name=None,modal=0,fl=None): - if fl is None: - fl = Qt.Dialog|Qt.WindowTitleHint; - QDialog.__init__(self,parent,Qt.Dialog|Qt.WindowTitleHint); - self.setModal(modal); - - image0 = pixmaps.tigger_logo.pm(); - - # self.setSizeGripEnabled(0) - LayoutWidget = QWidget(self) - LayoutWidget.setSizePolicy(QSizePolicy.MinimumExpanding,QSizePolicy.MinimumExpanding); - - lo_top = QVBoxLayout(LayoutWidget) - - lo_title = QHBoxLayout(None) - - self.title_icon = QLabel(LayoutWidget) - self.title_icon.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed); - self.title_icon.setPixmap(image0) - self.title_icon.setAlignment(Qt.AlignCenter) - lo_title.addWidget(self.title_icon) - - self.title_label = QLabel(LayoutWidget) - self.title_label.setWordWrap(True); - lo_title.addWidget(self.title_label) - lo_top.addLayout(lo_title) - - lo_logos = QHBoxLayout(None) - lo_top.addLayout(lo_logos); - for logo in ("astron",): - icon = QLabel(LayoutWidget) - icon.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed); - icon.setPixmap(getattr(pixmaps,logo+"_logo").pm()); - icon.setAlignment(Qt.AlignCenter) - lo_logos.addWidget(icon) - - lo_mainbtn = QHBoxLayout(None) - lo_mainbtn.addItem(QSpacerItem(20,20,QSizePolicy.Expanding,QSizePolicy.Minimum)) - lo_top.addLayout(lo_mainbtn); - - self.btn_ok = QPushButton(LayoutWidget) - self.btn_ok.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed); - self.btn_ok.setMinimumSize(QSize(60,0)) - self.btn_ok.setAutoDefault(1) - self.btn_ok.setDefault(1) - lo_mainbtn.addWidget(self.btn_ok) - lo_mainbtn.addItem(QSpacerItem(20,20,QSizePolicy.Expanding,QSizePolicy.Minimum)) - - self.languageChange() - - LayoutWidget.adjustSize(); - - #LayoutWidget.resize(QSize(489,330).expandedTo(LayoutWidget.minimumSizeHint())) - #self.resize(QSize(489,330).expandedTo(self.minimumSizeHint())) - # self.clearWState(Qt.WState_Polished) - - self.connect(self.btn_ok,SIGNAL("clicked()"),self.accept) - - def languageChange(self): - self.setWindowTitle(self.__tr("About Tigger")) - self.title_label.setText(self.__tr( \ - """

Tigger %s

-

(C) 2010-2012 Oleg Smirnov & ASTRON
(Netherlands Institude for Radioastronomy)
- Oude Hoogeveensedijk 4
- 7991 PD Dwingeloo, The Netherlands
- http://www.astron.nl
-
Please direct feedback and bug reports to osmirnov@gmail.com

- """%(release_string) \ - )); - - self.btn_ok.setText(self.__tr("&OK")) - - def __tr(self,s,c = None): - return qApp.translate("About",s,c) - diff --git a/Tigger/Images/ColormapTables/Karma.py b/Tigger/Images/ColormapTables/Karma.py deleted file mode 100644 index dfae0f3..0000000 --- a/Tigger/Images/ColormapTables/Karma.py +++ /dev/null @@ -1,2278 +0,0 @@ -# -*- coding: utf-8 -*- -Background = [ - [0.00000, 0.00000, 0.00000], [0.01587, 0.01587, 0.01587], [0.03174, - 0.03174, 0.03174], [0.04761, 0.04761, 0.04761], [0.06348, 0.06348, - 0.06348], [0.07935, 0.07935, 0.07935], [0.09522, 0.09522, 0.09522], - [0.11109, 0.11109, 0.11109], [0.12696, 0.12696, 0.12696], [0.14283, - 0.14283, 0.14283], [0.15870, 0.15870, 0.15870], [0.17457, 0.17457, - 0.17457], [0.19044, 0.19044, 0.19044], [0.20631, 0.20631, 0.20631], - [0.22218, 0.22218, 0.22218], [0.23805, 0.23805, 0.23805], [0.25392, - 0.25392, 0.25392], [0.26979, 0.26979, 0.26979], [0.28566, 0.28566, - 0.28566], [0.30153, 0.30153, 0.30153], [0.31740, 0.31740, 0.31740], - [0.33327, 0.33327, 0.33327], [0.34914, 0.34914, 0.34914], [0.36501, - 0.36501, 0.36501], [0.38088, 0.38088, 0.38088], [0.39675, 0.39675, - 0.39675], [0.41262, 0.41262, 0.41262], [0.42849, 0.42849, 0.42849], - [0.44436, 0.44436, 0.44436], [0.46023, 0.46023, 0.46023], [0.47610, - 0.47610, 0.47610], [0.49197, 0.49197, 0.49197], [0.50784, 0.50784, - 0.50784], [0.52371, 0.52371, 0.52371], [0.53958, 0.53958, 0.53958], - [0.55545, 0.55545, 0.55545], [0.57132, 0.57132, 0.57132], [0.58719, - 0.58719, 0.58719], [0.60306, 0.60306, 0.60306], [0.61893, 0.61893, - 0.61893], [0.63480, 0.63480, 0.63480], [0.65067, 0.65067, 0.65067], - [0.66654, 0.66654, 0.66654], [0.68241, 0.68241, 0.68241], [0.69828, - 0.69828, 0.69828], [0.71415, 0.71415, 0.71415], [0.73002, 0.73002, - 0.73002], [0.74589, 0.74589, 0.74589], [0.76176, 0.76176, 0.76176], - [0.77763, 0.77763, 0.77763], [0.79350, 0.79350, 0.79350], [0.80937, - 0.80937, 0.80937], [0.82524, 0.82524, 0.82524], [0.84111, 0.84111, - 0.84111], [0.85698, 0.85698, 0.85698], [0.87285, 0.87285, 0.87285], - [0.88872, 0.88872, 0.88872], [0.90459, 0.90459, 0.90459], [0.92046, - 0.92046, 0.92046], [0.93633, 0.93633, 0.93633], [0.95220, 0.95220, - 0.95220], [0.96807, 0.96807, 0.96807], [0.98394, 0.98394, 0.98394], - [0.99981, 0.99981, 0.99981], [0.00000, 0.00000, 0.99981], [0.00000, - 0.01587, 0.98394], [0.00000, 0.03174, 0.96807], [0.00000, 0.04761, - 0.95220], [0.00000, 0.06348, 0.93633], [0.00000, 0.07935, 0.92046], - [0.00000, 0.09522, 0.90459], [0.00000, 0.11109, 0.88872], [0.00000, - 0.12696, 0.87285], [0.00000, 0.14283, 0.85698], [0.00000, 0.15870, - 0.84111], [0.00000, 0.17457, 0.82524], [0.00000, 0.19044, 0.80937], - [0.00000, 0.20631, 0.79350], [0.00000, 0.22218, 0.77763], [0.00000, - 0.23805, 0.76176], [0.00000, 0.25392, 0.74589], [0.00000, 0.26979, - 0.73002], [0.00000, 0.28566, 0.71415], [0.00000, 0.30153, 0.69828], - [0.00000, 0.31740, 0.68241], [0.00000, 0.33327, 0.66654], [0.00000, - 0.34914, 0.65067], [0.00000, 0.36501, 0.63480], [0.00000, 0.38088, - 0.61893], [0.00000, 0.39675, 0.60306], [0.00000, 0.41262, 0.58719], - [0.00000, 0.42849, 0.57132], [0.00000, 0.44436, 0.55545], [0.00000, - 0.46023, 0.53958], [0.00000, 0.47610, 0.52371], [0.00000, 0.49197, - 0.50784], [0.00000, 0.50784, 0.49197], [0.00000, 0.52371, 0.47610], - [0.00000, 0.53958, 0.46023], [0.00000, 0.55545, 0.44436], [0.00000, - 0.57132, 0.42849], [0.00000, 0.58719, 0.41262], [0.00000, 0.60306, - 0.39675], [0.00000, 0.61893, 0.38088], [0.00000, 0.63480, 0.36501], - [0.00000, 0.65067, 0.34914], [0.00000, 0.66654, 0.33327], [0.00000, - 0.68241, 0.31740], [0.00000, 0.69828, 0.30153], [0.00000, 0.71415, - 0.28566], [0.00000, 0.73002, 0.26979], [0.00000, 0.74589, 0.25392], - [0.00000, 0.76176, 0.23805], [0.00000, 0.77763, 0.22218], [0.00000, - 0.79350, 0.20631], [0.00000, 0.80937, 0.19044], [0.00000, 0.82524, - 0.17457], [0.00000, 0.84111, 0.15870], [0.00000, 0.85698, 0.14283], - [0.00000, 0.87285, 0.12696], [0.00000, 0.88872, 0.11109], [0.00000, - 0.90459, 0.09522], [0.00000, 0.92046, 0.07935], [0.00000, 0.93633, - 0.06348], [0.00000, 0.95220, 0.04761], [0.00000, 0.96807, 0.03174], - [0.00000, 0.98394, 0.01587], [0.00000, 0.99981, 0.00000], [0.00000, - 1.00000, 0.00000], [0.01587, 1.00000, 0.00000], [0.03174, 1.00000, - 0.00000], [0.04761, 1.00000, 0.00000], [0.06348, 1.00000, 0.00000], - [0.07935, 1.00000, 0.00000], [0.09522, 1.00000, 0.00000], [0.11109, - 1.00000, 0.00000], [0.12696, 1.00000, 0.00000], [0.14283, 1.00000, - 0.00000], [0.15870, 1.00000, 0.00000], [0.17457, 1.00000, 0.00000], - [0.19044, 1.00000, 0.00000], [0.20631, 1.00000, 0.00000], [0.22218, - 1.00000, 0.00000], [0.23805, 1.00000, 0.00000], [0.25392, 1.00000, - 0.00000], [0.26979, 1.00000, 0.00000], [0.28566, 1.00000, 0.00000], - [0.30153, 1.00000, 0.00000], [0.31740, 1.00000, 0.00000], [0.33327, - 1.00000, 0.00000], [0.34914, 1.00000, 0.00000], [0.36501, 1.00000, - 0.00000], [0.38088, 1.00000, 0.00000], [0.39675, 1.00000, 0.00000], - [0.41262, 1.00000, 0.00000], [0.42849, 1.00000, 0.00000], [0.44436, - 1.00000, 0.00000], [0.46023, 1.00000, 0.00000], [0.47610, 1.00000, - 0.00000], [0.49197, 1.00000, 0.00000], [0.50784, 1.00000, 0.00000], - [0.52371, 1.00000, 0.00000], [0.53958, 1.00000, 0.00000], [0.55545, - 1.00000, 0.00000], [0.57132, 1.00000, 0.00000], [0.58719, 1.00000, - 0.00000], [0.60306, 1.00000, 0.00000], [0.61893, 1.00000, 0.00000], - [0.63480, 1.00000, 0.00000], [0.65067, 1.00000, 0.00000], [0.66654, - 1.00000, 0.00000], [0.68241, 1.00000, 0.00000], [0.69828, 1.00000, - 0.00000], [0.71415, 1.00000, 0.00000], [0.73002, 1.00000, 0.00000], - [0.74589, 1.00000, 0.00000], [0.76176, 1.00000, 0.00000], [0.77763, - 1.00000, 0.00000], [0.79350, 1.00000, 0.00000], [0.80937, 1.00000, - 0.00000], [0.82524, 1.00000, 0.00000], [0.84111, 1.00000, 0.00000], - [0.85698, 1.00000, 0.00000], [0.87285, 1.00000, 0.00000], [0.88872, - 1.00000, 0.00000], [0.90459, 1.00000, 0.00000], [0.92046, 1.00000, - 0.00000], [0.93633, 1.00000, 0.00000], [0.95220, 1.00000, 0.00000], - [0.96807, 1.00000, 0.00000], [0.98394, 1.00000, 0.00000], [0.99981, - 1.00000, 0.00000], [1.00000, 0.99981, 0.00000], [1.00000, 0.98394, - 0.00000], [1.00000, 0.96807, 0.00000], [1.00000, 0.95220, 0.00000], - [1.00000, 0.93633, 0.00000], [1.00000, 0.92046, 0.00000], [1.00000, - 0.90459, 0.00000], [1.00000, 0.88872, 0.00000], [1.00000, 0.87285, - 0.00000], [1.00000, 0.85698, 0.00000], [1.00000, 0.84111, 0.00000], - [1.00000, 0.82524, 0.00000], [1.00000, 0.80937, 0.00000], [1.00000, - 0.79350, 0.00000], [1.00000, 0.77763, 0.00000], [1.00000, 0.76176, - 0.00000], [1.00000, 0.74589, 0.00000], [1.00000, 0.73002, 0.00000], - [1.00000, 0.71415, 0.00000], [1.00000, 0.69828, 0.00000], [1.00000, - 0.68241, 0.00000], [1.00000, 0.66654, 0.00000], [1.00000, 0.65067, - 0.00000], [1.00000, 0.63480, 0.00000], [1.00000, 0.61893, 0.00000], - [1.00000, 0.60306, 0.00000], [1.00000, 0.58719, 0.00000], [1.00000, - 0.57132, 0.00000], [1.00000, 0.55545, 0.00000], [1.00000, 0.53958, - 0.00000], [1.00000, 0.52371, 0.00000], [1.00000, 0.50784, 0.00000], - [1.00000, 0.49197, 0.00000], [1.00000, 0.47610, 0.00000], [1.00000, - 0.46023, 0.00000], [1.00000, 0.44436, 0.00000], [1.00000, 0.42849, - 0.00000], [1.00000, 0.41262, 0.00000], [1.00000, 0.39675, 0.00000], - [1.00000, 0.38088, 0.00000], [1.00000, 0.36501, 0.00000], [1.00000, - 0.34914, 0.00000], [1.00000, 0.33327, 0.00000], [1.00000, 0.31740, - 0.00000], [1.00000, 0.30153, 0.00000], [1.00000, 0.28566, 0.00000], - [1.00000, 0.26979, 0.00000], [1.00000, 0.25392, 0.00000], [1.00000, - 0.23805, 0.00000], [1.00000, 0.22218, 0.00000], [1.00000, 0.20631, - 0.00000], [1.00000, 0.19044, 0.00000], [1.00000, 0.17457, 0.00000], - [1.00000, 0.15870, 0.00000], [1.00000, 0.14283, 0.00000], [1.00000, - 0.12696, 0.00000], [1.00000, 0.11109, 0.00000], [1.00000, 0.09522, - 0.00000], [1.00000, 0.07935, 0.00000], [1.00000, 0.06348, 0.00000], - [1.00000, 0.04761, 0.00000], [1.00000, 0.03174, 0.00000], [1.00000, - 0.01587, 0.00000], [1.00000, 0.00000, 0.00000], ]; - -Heat = [ - [0.00000, 0.00000, 0.00000], [0.01176, 0.00392, 0.00000], [0.02353, - 0.00784, 0.00000], [0.03529, 0.01176, 0.00000], [0.04706, 0.01569, - 0.00000], [0.05882, 0.01961, 0.00000], [0.07059, 0.02353, 0.00000], - [0.08235, 0.02745, 0.00000], [0.09412, 0.03137, 0.00000], [0.10588, - 0.03529, 0.00000], [0.11765, 0.03922, 0.00000], [0.12941, 0.04314, - 0.00000], [0.14118, 0.04706, 0.00000], [0.15294, 0.05098, 0.00000], - [0.16471, 0.05490, 0.00000], [0.17647, 0.05882, 0.00000], [0.18824, - 0.06275, 0.00000], [0.20000, 0.06667, 0.00000], [0.21176, 0.07059, - 0.00000], [0.22353, 0.07451, 0.00000], [0.23529, 0.07843, 0.00000], - [0.24706, 0.08235, 0.00000], [0.25882, 0.08627, 0.00000], [0.27059, - 0.09020, 0.00000], [0.28235, 0.09412, 0.00000], [0.29412, 0.09804, - 0.00000], [0.30588, 0.10196, 0.00000], [0.31765, 0.10588, 0.00000], - [0.32941, 0.10980, 0.00000], [0.34118, 0.11373, 0.00000], [0.35294, - 0.11765, 0.00000], [0.36471, 0.12157, 0.00000], [0.37647, 0.12549, - 0.00000], [0.38824, 0.12941, 0.00000], [0.40000, 0.13333, 0.00000], - [0.41176, 0.13725, 0.00000], [0.42353, 0.14118, 0.00000], [0.43529, - 0.14510, 0.00000], [0.44706, 0.14902, 0.00000], [0.45882, 0.15294, - 0.00000], [0.47059, 0.15686, 0.00000], [0.48235, 0.16078, 0.00000], - [0.49412, 0.16471, 0.00000], [0.50588, 0.16863, 0.00000], [0.51765, - 0.17255, 0.00000], [0.52941, 0.17647, 0.00000], [0.54118, 0.18039, - 0.00000], [0.55294, 0.18431, 0.00000], [0.56471, 0.18824, 0.00000], - [0.57647, 0.19216, 0.00000], [0.58824, 0.19608, 0.00000], [0.60000, - 0.20000, 0.00000], [0.61176, 0.20392, 0.00000], [0.62353, 0.20784, - 0.00000], [0.63529, 0.21176, 0.00000], [0.64706, 0.21569, 0.00000], - [0.65882, 0.21961, 0.00000], [0.67059, 0.22353, 0.00000], [0.68235, - 0.22745, 0.00000], [0.69412, 0.23137, 0.00000], [0.70588, 0.23529, - 0.00000], [0.71765, 0.23922, 0.00000], [0.72941, 0.24314, 0.00000], - [0.74118, 0.24706, 0.00000], [0.75294, 0.25098, 0.00000], [0.76471, - 0.25490, 0.00000], [0.77647, 0.25882, 0.00000], [0.78824, 0.26275, - 0.00000], [0.80000, 0.26667, 0.00000], [0.81176, 0.27059, 0.00000], - [0.82353, 0.27451, 0.00000], [0.83529, 0.27843, 0.00000], [0.84706, - 0.28235, 0.00000], [0.85882, 0.28627, 0.00000], [0.87059, 0.29020, - 0.00000], [0.88235, 0.29412, 0.00000], [0.89412, 0.29804, 0.00000], - [0.90588, 0.30196, 0.00000], [0.91765, 0.30588, 0.00000], [0.92941, - 0.30980, 0.00000], [0.94118, 0.31373, 0.00000], [0.95294, 0.31765, - 0.00000], [0.96471, 0.32157, 0.00000], [0.97647, 0.32549, 0.00000], - [0.98824, 0.32941, 0.00000], [1.00000, 0.33333, 0.00000], [1.00000, - 0.33725, 0.00000], [1.00000, 0.34118, 0.00000], [1.00000, 0.34510, - 0.00000], [1.00000, 0.34902, 0.00000], [1.00000, 0.35294, 0.00000], - [1.00000, 0.35686, 0.00000], [1.00000, 0.36078, 0.00000], [1.00000, - 0.36471, 0.00000], [1.00000, 0.36863, 0.00000], [1.00000, 0.37255, - 0.00000], [1.00000, 0.37647, 0.00000], [1.00000, 0.38039, 0.00000], - [1.00000, 0.38431, 0.00000], [1.00000, 0.38824, 0.00000], [1.00000, - 0.39216, 0.00000], [1.00000, 0.39608, 0.00000], [1.00000, 0.40000, - 0.00000], [1.00000, 0.40392, 0.00000], [1.00000, 0.40784, 0.00000], - [1.00000, 0.41176, 0.00000], [1.00000, 0.41569, 0.00000], [1.00000, - 0.41961, 0.00000], [1.00000, 0.42353, 0.00000], [1.00000, 0.42745, - 0.00000], [1.00000, 0.43137, 0.00000], [1.00000, 0.43529, 0.00000], - [1.00000, 0.43922, 0.00000], [1.00000, 0.44314, 0.00000], [1.00000, - 0.44706, 0.00000], [1.00000, 0.45098, 0.00000], [1.00000, 0.45490, - 0.00000], [1.00000, 0.45882, 0.00000], [1.00000, 0.46275, 0.00000], - [1.00000, 0.46667, 0.00000], [1.00000, 0.47059, 0.00000], [1.00000, - 0.47451, 0.00000], [1.00000, 0.47843, 0.00000], [1.00000, 0.48235, - 0.00000], [1.00000, 0.48627, 0.00000], [1.00000, 0.49020, 0.00000], - [1.00000, 0.49412, 0.00000], [1.00000, 0.49804, 0.00000], [1.00000, - 0.50196, 0.00000], [1.00000, 0.50588, 0.00000], [1.00000, 0.50980, - 0.00000], [1.00000, 0.51373, 0.00000], [1.00000, 0.51765, 0.00000], - [1.00000, 0.52157, 0.00000], [1.00000, 0.52549, 0.00000], [1.00000, - 0.52941, 0.00000], [1.00000, 0.53333, 0.00000], [1.00000, 0.53725, - 0.00000], [1.00000, 0.54118, 0.00000], [1.00000, 0.54510, 0.00000], - [1.00000, 0.54902, 0.00000], [1.00000, 0.55294, 0.00000], [1.00000, - 0.55686, 0.00000], [1.00000, 0.56078, 0.00000], [1.00000, 0.56471, - 0.00000], [1.00000, 0.56863, 0.00000], [1.00000, 0.57255, 0.00000], - [1.00000, 0.57647, 0.00000], [1.00000, 0.58039, 0.00000], [1.00000, - 0.58431, 0.00000], [1.00000, 0.58824, 0.00000], [1.00000, 0.59216, - 0.00000], [1.00000, 0.59608, 0.00000], [1.00000, 0.60000, 0.00000], - [1.00000, 0.60392, 0.00000], [1.00000, 0.60784, 0.00000], [1.00000, - 0.61176, 0.00000], [1.00000, 0.61569, 0.00000], [1.00000, 0.61961, - 0.00000], [1.00000, 0.62353, 0.00000], [1.00000, 0.62745, 0.00000], - [1.00000, 0.63137, 0.00000], [1.00000, 0.63529, 0.00000], [1.00000, - 0.63922, 0.00000], [1.00000, 0.64314, 0.00000], [1.00000, 0.64706, - 0.00000], [1.00000, 0.65098, 0.01176], [1.00000, 0.65490, 0.02353], - [1.00000, 0.65882, 0.03529], [1.00000, 0.66275, 0.04706], [1.00000, - 0.66667, 0.05882], [1.00000, 0.67059, 0.07059], [1.00000, 0.67451, - 0.08235], [1.00000, 0.67843, 0.09412], [1.00000, 0.68235, 0.10588], - [1.00000, 0.68627, 0.11765], [1.00000, 0.69020, 0.12941], [1.00000, - 0.69412, 0.14118], [1.00000, 0.69804, 0.15294], [1.00000, 0.70196, - 0.16471], [1.00000, 0.70588, 0.17647], [1.00000, 0.70980, 0.18824], - [1.00000, 0.71373, 0.20000], [1.00000, 0.71765, 0.21176], [1.00000, - 0.72157, 0.22353], [1.00000, 0.72549, 0.23529], [1.00000, 0.72941, - 0.24706], [1.00000, 0.73333, 0.25882], [1.00000, 0.73725, 0.27059], - [1.00000, 0.74118, 0.28235], [1.00000, 0.74510, 0.29412], [1.00000, - 0.74902, 0.30588], [1.00000, 0.75294, 0.31765], [1.00000, 0.75686, - 0.32941], [1.00000, 0.76078, 0.34118], [1.00000, 0.76471, 0.35294], - [1.00000, 0.76863, 0.36471], [1.00000, 0.77255, 0.37647], [1.00000, - 0.77647, 0.38824], [1.00000, 0.78039, 0.40000], [1.00000, 0.78431, - 0.41176], [1.00000, 0.78824, 0.42353], [1.00000, 0.79216, 0.43529], - [1.00000, 0.79608, 0.44706], [1.00000, 0.80000, 0.45882], [1.00000, - 0.80392, 0.47059], [1.00000, 0.80784, 0.48235], [1.00000, 0.81176, - 0.49412], [1.00000, 0.81569, 0.50588], [1.00000, 0.81961, 0.51765], - [1.00000, 0.82353, 0.52941], [1.00000, 0.82745, 0.54118], [1.00000, - 0.83137, 0.55294], [1.00000, 0.83529, 0.56471], [1.00000, 0.83922, - 0.57647], [1.00000, 0.84314, 0.58824], [1.00000, 0.84706, 0.60000], - [1.00000, 0.85098, 0.61176], [1.00000, 0.85490, 0.62353], [1.00000, - 0.85882, 0.63529], [1.00000, 0.86275, 0.64706], [1.00000, 0.86667, - 0.65882], [1.00000, 0.87059, 0.67059], [1.00000, 0.87451, 0.68235], - [1.00000, 0.87843, 0.69412], [1.00000, 0.88235, 0.70588], [1.00000, - 0.88627, 0.71765], [1.00000, 0.89020, 0.72941], [1.00000, 0.89412, - 0.74118], [1.00000, 0.89804, 0.75294], [1.00000, 0.90196, 0.76471], - [1.00000, 0.90588, 0.77647], [1.00000, 0.90980, 0.78824], [1.00000, - 0.91373, 0.80000], [1.00000, 0.91765, 0.81176], [1.00000, 0.92157, - 0.82353], [1.00000, 0.92549, 0.83529], [1.00000, 0.92941, 0.84706], - [1.00000, 0.93333, 0.85882], [1.00000, 0.93725, 0.87059], [1.00000, - 0.94118, 0.88235], [1.00000, 0.94510, 0.89412], [1.00000, 0.94902, - 0.90588], [1.00000, 0.95294, 0.91765], [1.00000, 0.95686, 0.92941], - [1.00000, 0.96078, 0.94118], [1.00000, 0.96471, 0.95294], [1.00000, - 0.96863, 0.96471], [1.00000, 0.97255, 0.97647], [1.00000, 0.97647, - 0.98824], [1.00000, 0.98039, 1.00000], [1.00000, 0.98431, 1.00000], - [1.00000, 0.98824, 1.00000], [1.00000, 0.99216, 1.00000], [1.00000, - 0.99608, 1.00000], [1.00000, 1.00000, 1.00000], ]; - -Isophot = [ - [0.00000, 0.00000, 0.00000], [0.00000, 0.00000, 0.00000], [0.00000, - 0.00000, 0.00000], [0.00000, 0.00000, 0.00000], [0.00000, 0.00000, - 0.00000], [0.00000, 0.00000, 0.03922], [0.00000, 0.00000, 0.07843], - [0.00000, 0.00000, 0.11765], [0.00000, 0.00000, 0.15686], [0.00000, - 0.00000, 0.19608], [0.00000, 0.00000, 0.23529], [0.00000, 0.00000, - 0.27843], [0.00000, 0.00000, 0.31765], [0.00000, 0.00000, 0.35686], - [0.00000, 0.00000, 0.39608], [0.00000, 0.00000, 0.43529], [0.00000, - 0.00000, 0.47451], [0.00000, 0.00000, 0.51765], [0.00000, 0.00000, - 0.55686], [0.00000, 0.00000, 0.59608], [0.00000, 0.00000, 0.63529], - [0.00000, 0.00000, 0.67451], [0.00000, 0.00000, 0.71765], [1.00000, - 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, - 1.00000], [0.00000, 0.00000, 0.87843], [0.00000, 0.00000, 0.91765], - [0.00000, 0.00000, 0.95686], [0.00000, 0.00000, 1.00000], [0.00000, - 0.03137, 1.00000], [0.00000, 0.06275, 1.00000], [0.00000, 0.09412, - 1.00000], [0.00000, 0.12549, 1.00000], [0.00000, 0.15686, 1.00000], - [0.00000, 0.18824, 1.00000], [0.00000, 0.21961, 1.00000], [0.00000, - 0.25490, 1.00000], [0.00000, 0.28627, 1.00000], [0.00000, 0.31765, - 1.00000], [0.00000, 0.34902, 1.00000], [0.00000, 0.38039, 1.00000], - [0.00000, 0.41176, 1.00000], [0.00000, 0.44314, 1.00000], [0.00000, - 0.47843, 1.00000], [0.00000, 0.49804, 1.00000], [0.00000, 0.51765, - 1.00000], [0.00000, 0.53725, 1.00000], [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], [0.00000, - 0.61961, 1.00000], [0.00000, 0.63922, 1.00000], [0.00000, 0.65882, - 1.00000], [0.00000, 0.67843, 1.00000], [0.00000, 0.70196, 1.00000], - [0.00000, 0.72157, 1.00000], [0.00000, 0.74118, 1.00000], [0.00000, - 0.76078, 1.00000], [0.00000, 0.78431, 1.00000], [0.00000, 0.79608, - 1.00000], [0.00000, 0.81176, 1.00000], [0.00000, 0.82353, 1.00000], - [0.00000, 0.83922, 1.00000], [0.00000, 0.85490, 1.00000], [0.00000, - 0.86667, 1.00000], [0.00000, 0.88235, 1.00000], [0.00000, 0.89412, - 1.00000], [0.00000, 0.90980, 1.00000], [0.00000, 0.92549, 1.00000], - [0.00000, 0.93725, 1.00000], [0.00000, 0.95294, 1.00000], [0.00000, - 0.96863, 1.00000], [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, - 1.00000], [1.00000, 1.00000, 1.00000], [0.00000, 1.00000, 0.96078], - [0.00000, 1.00000, 0.94118], [0.00000, 1.00000, 0.92157], [0.00000, - 1.00000, 0.90196], [0.00000, 1.00000, 0.88235], [0.00000, 1.00000, - 0.86275], [0.00000, 1.00000, 0.84314], [0.00000, 1.00000, 0.82353], - [0.00000, 1.00000, 0.80392], [0.00000, 1.00000, 0.78431], [0.00000, - 1.00000, 0.76471], [0.00000, 1.00000, 0.74510], [0.00000, 1.00000, - 0.72549], [0.00000, 1.00000, 0.70588], [0.00000, 1.00000, 0.65490], - [0.00000, 1.00000, 0.60784], [0.00000, 1.00000, 0.56078], [0.00000, - 1.00000, 0.51373], [0.00000, 1.00000, 0.46667], [0.00000, 1.00000, - 0.41961], [0.00000, 1.00000, 0.37255], [0.00000, 1.00000, 0.32549], - [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], [1.00000, - 1.00000, 1.00000], [0.00000, 1.00000, 0.13725], [0.00000, 1.00000, - 0.09020], [0.00000, 1.00000, 0.04314], [0.00000, 1.00000, 0.00000], - [0.04706, 1.00000, 0.00000], [0.09412, 1.00000, 0.00000], [0.14118, - 1.00000, 0.00000], [0.18824, 1.00000, 0.00000], [0.23529, 1.00000, - 0.00000], [0.28235, 1.00000, 0.00000], [0.32941, 1.00000, 0.00000], - [0.37647, 1.00000, 0.00000], [0.42353, 1.00000, 0.00000], [0.47059, - 1.00000, 0.00000], [0.51765, 1.00000, 0.00000], [0.56471, 1.00000, - 0.00000], [0.61176, 1.00000, 0.00000], [0.65882, 1.00000, 0.00000], - [0.70588, 1.00000, 0.00000], [0.72549, 1.00000, 0.00000], [0.74510, - 1.00000, 0.00000], [0.76471, 1.00000, 0.00000], [1.00000, 1.00000, - 1.00000], [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], - [0.84314, 1.00000, 0.00000], [0.86275, 1.00000, 0.00000], [0.88235, - 1.00000, 0.00000], [0.90196, 1.00000, 0.00000], [0.92157, 1.00000, - 0.00000], [0.94118, 1.00000, 0.00000], [0.96078, 1.00000, 0.00000], - [0.98039, 1.00000, 0.00000], [1.00000, 1.00000, 0.00000], [0.99608, - 0.98039, 0.00000], [0.99608, 0.96078, 0.00000], [0.99608, 0.94118, - 0.00000], [0.99608, 0.92549, 0.00000], [0.99216, 0.90588, 0.00000], - [0.99216, 0.88627, 0.00000], [0.99216, 0.87059, 0.00000], [0.99216, - 0.85098, 0.00000], [0.98824, 0.83137, 0.00000], [0.98824, 0.81569, - 0.00000], [0.98824, 0.79608, 0.00000], [0.98824, 0.77647, 0.00000], - [0.98824, 0.76078, 0.00000], [1.00000, 1.00000, 1.00000], [1.00000, - 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], [0.98824, 0.69020, - 0.00000], [0.98824, 0.67059, 0.00000], [0.98824, 0.65490, 0.00000], - [0.98824, 0.63922, 0.00000], [0.98824, 0.61961, 0.00000], [0.99216, - 0.60392, 0.00000], [0.99216, 0.58824, 0.00000], [0.99216, 0.56863, - 0.00000], [0.99216, 0.55294, 0.00000], [0.99608, 0.53725, 0.00000], - [0.99608, 0.51765, 0.00000], [0.99608, 0.50196, 0.00000], [0.99608, - 0.48627, 0.00000], [1.00000, 0.47059, 0.00000], [1.00000, 0.43529, - 0.00000], [1.00000, 0.40392, 0.00000], [1.00000, 0.37255, 0.00000], - [1.00000, 0.34118, 0.00000], [1.00000, 0.30980, 0.00000], [1.00000, - 0.27843, 0.00000], [1.00000, 0.24706, 0.00000], [1.00000, 0.21569, - 0.00000], [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], [1.00000, 0.09020, 0.00000], [1.00000, - 0.05882, 0.00000], [1.00000, 0.02745, 0.00000], [1.00000, 0.00000, - 0.00000], [1.00000, 0.00000, 0.00000], [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], [1.00000, 0.00000, 0.00000], [1.00000, - 0.00000, 0.00000], [1.00000, 0.00000, 0.00000], [1.00000, 0.00000, - 0.00000], [1.00000, 0.00000, 0.00000], [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], [1.00000, 0.00000, 0.00000], [1.00000, - 0.00000, 0.00000], [1.00000, 0.00000, 0.00000], [1.00000, 0.00000, - 0.00000], [1.00000, 0.00000, 0.00000], [1.00000, 0.00000, 0.04706], - [1.00000, 0.00000, 0.09412], [1.00000, 0.00000, 0.14118], [1.00000, - 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, - 1.00000], [1.00000, 0.00000, 0.32941], [1.00000, 0.00000, 0.37647], - [1.00000, 0.00000, 0.42353], [1.00000, 0.00000, 0.47059], [1.00000, - 0.00000, 0.51765], [1.00000, 0.00000, 0.56471], [1.00000, 0.00000, - 0.61176], [1.00000, 0.00000, 0.65882], [1.00000, 0.00000, 0.70588], - [1.00000, 0.00000, 0.72549], [1.00000, 0.00000, 0.74902], [1.00000, - 0.00000, 0.77255], [1.00000, 0.00000, 0.79608], [1.00000, 0.00000, - 0.81569], [1.00000, 0.00000, 0.83922], [1.00000, 0.00000, 0.86275], - [1.00000, 0.00000, 0.88627], [1.00000, 0.00000, 0.90588], [1.00000, - 0.00000, 0.92941], [1.00000, 0.00000, 0.95294], [1.00000, 0.00000, - 0.97647], [1.00000, 0.00000, 1.00000], [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], [1.00000, 1.00000, 1.00000], [1.00000, - 0.14118, 1.00000], [1.00000, 0.17647, 1.00000], [1.00000, 0.21176, - 1.00000], [1.00000, 0.25098, 1.00000], [1.00000, 0.28627, 1.00000], - [1.00000, 0.32157, 1.00000], [1.00000, 0.36078, 1.00000], [1.00000, - 0.39608, 1.00000], [1.00000, 0.43137, 1.00000], [1.00000, 0.47059, - 1.00000], [1.00000, 0.48627, 1.00000], [1.00000, 0.50588, 1.00000], - [1.00000, 0.52157, 1.00000], [1.00000, 0.54118, 1.00000], [1.00000, - 0.56078, 1.00000], [1.00000, 0.57647, 1.00000], [1.00000, 0.59608, - 1.00000], [1.00000, 0.61176, 1.00000], [1.00000, 0.63137, 1.00000], - [1.00000, 0.65098, 1.00000], [1.00000, 0.66667, 1.00000], [1.00000, - 0.68627, 1.00000], [1.00000, 0.70588, 1.00000], [1.00000, 0.74510, - 1.00000], [1.00000, 0.78824, 1.00000], [1.00000, 0.83137, 1.00000], - [1.00000, 0.87059, 1.00000], [1.00000, 0.91373, 1.00000], [1.00000, - 0.95686, 1.00000], [1.00000, 1.00000, 1.00000] ]; - -Mousse = [ - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.13333], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.26667], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.46667], - [0.00000, 0.00000, 0.53333], - [0.00000, 0.00000, 0.53333], - [0.00000, 0.00000, 0.53333], - [0.00000, 0.00000, 0.53333], - [0.00000, 0.00000, 0.53333], - [0.00000, 0.00000, 0.53333], - [0.00000, 0.00000, 0.53333], - [0.00000, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.06667, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.13333, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.20000, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.26667, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.33333, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.40000, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.46667, 0.00000, 0.53333], - [0.53333, 0.00000, 0.53333], - [0.53333, 0.00000, 0.53333], - [0.53333, 0.00000, 0.53333], - [0.53333, 0.00000, 0.53333], - [0.53333, 0.00000, 0.46667], - [0.53333, 0.00000, 0.46667], - [0.53333, 0.00000, 0.46667], - [0.53333, 0.00000, 0.46667], - [0.60000, 0.00000, 0.40000], - [0.60000, 0.00000, 0.40000], - [0.60000, 0.00000, 0.40000], - [0.60000, 0.00000, 0.40000], - [0.60000, 0.00000, 0.33333], - [0.60000, 0.00000, 0.33333], - [0.60000, 0.00000, 0.33333], - [0.60000, 0.00000, 0.33333], - [0.66667, 0.00000, 0.26667], - [0.66667, 0.00000, 0.26667], - [0.66667, 0.00000, 0.26667], - [0.66667, 0.00000, 0.26667], - [0.66667, 0.00000, 0.20000], - [0.66667, 0.00000, 0.20000], - [0.66667, 0.00000, 0.20000], - [0.66667, 0.00000, 0.20000], - [0.73333, 0.00000, 0.13333], - [0.73333, 0.00000, 0.13333], - [0.73333, 0.00000, 0.13333], - [0.73333, 0.00000, 0.13333], - [0.73333, 0.00000, 0.06667], - [0.73333, 0.00000, 0.06667], - [0.73333, 0.00000, 0.06667], - [0.73333, 0.00000, 0.06667], - [0.80000, 0.00000, 0.00000], - [0.80000, 0.00000, 0.00000], - [0.80000, 0.00000, 0.00000], - [0.80000, 0.00000, 0.00000], - [0.80000, 0.00000, 0.00000], - [0.80000, 0.00000, 0.00000], - [0.80000, 0.00000, 0.00000], - [0.80000, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.86667, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [0.93333, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.06667, 0.00000], - [1.00000, 0.06667, 0.00000], - [1.00000, 0.13333, 0.00000], - [1.00000, 0.13333, 0.00000], - [1.00000, 0.20000, 0.00000], - [1.00000, 0.20000, 0.00000], - [1.00000, 0.26667, 0.00000], - [1.00000, 0.26667, 0.00000], - [1.00000, 0.33333, 0.00000], - [1.00000, 0.33333, 0.00000], - [1.00000, 0.40000, 0.00000], - [1.00000, 0.40000, 0.00000], - [1.00000, 0.46667, 0.00000], - [1.00000, 0.46667, 0.00000], - [1.00000, 0.53333, 0.00000], - [1.00000, 0.53333, 0.00000], - [1.00000, 0.60000, 0.00000], - [1.00000, 0.60000, 0.00000], - [1.00000, 0.66667, 0.00000], - [1.00000, 0.66667, 0.00000], - [1.00000, 0.73333, 0.00000], - [1.00000, 0.73333, 0.00000], - [1.00000, 0.80000, 0.00000], - [1.00000, 0.80000, 0.00000], - [1.00000, 0.86667, 0.00000], - [1.00000, 0.86667, 0.00000], - [1.00000, 0.93333, 0.00000], - [1.00000, 0.93333, 0.00000], - [1.00000, 1.00000, 0.00000], - [1.00000, 1.00000, 0.00000], - [1.00000, 1.00000, 0.00000], - [1.00000, 1.00000, 0.00000], - [1.00000, 1.00000, 0.06667], - [1.00000, 1.00000, 0.06667], - [1.00000, 1.00000, 0.13333], - [1.00000, 1.00000, 0.13333], - [1.00000, 1.00000, 0.20000], - [1.00000, 1.00000, 0.20000], - [1.00000, 1.00000, 0.26667], - [1.00000, 1.00000, 0.26667], - [1.00000, 1.00000, 0.33333], - [1.00000, 1.00000, 0.33333], - [1.00000, 1.00000, 0.40000], - [1.00000, 1.00000, 0.40000], - [1.00000, 1.00000, 0.46667], - [1.00000, 1.00000, 0.46667], - [1.00000, 1.00000, 0.53333], - [1.00000, 1.00000, 0.53333], - [1.00000, 1.00000, 0.60000], - [1.00000, 1.00000, 0.60000], - [1.00000, 1.00000, 0.66667], - [1.00000, 1.00000, 0.66667], - [1.00000, 1.00000, 0.73333], - [1.00000, 1.00000, 0.73333], - [1.00000, 1.00000, 0.80000], - [1.00000, 1.00000, 0.80000], - [1.00000, 1.00000, 0.86667], - [1.00000, 1.00000, 1.00000], -]; - -Rainbow = [ - [0.00000, 0.00000, 0.01176], - [0.00000, 0.00000, 0.02745], - [0.00000, 0.00000, 0.04314], - [0.00000, 0.00000, 0.05882], - [0.00000, 0.00000, 0.07451], - [0.00000, 0.00000, 0.09020], - [0.00000, 0.00000, 0.10588], - [0.00000, 0.00000, 0.12157], - [0.00000, 0.00000, 0.13725], - [0.00000, 0.00000, 0.15294], - [0.00000, 0.00000, 0.16863], - [0.00000, 0.00000, 0.18431], - [0.00000, 0.00000, 0.20000], - [0.00000, 0.00000, 0.21176], - [0.00000, 0.00000, 0.22745], - [0.00000, 0.00000, 0.24314], - [0.00000, 0.00000, 0.25882], - [0.00000, 0.00000, 0.27451], - [0.00000, 0.00000, 0.29020], - [0.00000, 0.00000, 0.30588], - [0.00000, 0.00000, 0.32157], - [0.00000, 0.00000, 0.33725], - [0.00000, 0.00000, 0.35294], - [0.00000, 0.00000, 0.36863], - [0.00000, 0.00000, 0.38431], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.41176], - [0.00000, 0.00000, 0.42745], - [0.00000, 0.00000, 0.44314], - [0.00000, 0.00000, 0.45882], - [0.00000, 0.00000, 0.47451], - [0.00000, 0.00000, 0.49020], - [0.00000, 0.00000, 0.50588], - [0.00000, 0.00000, 0.52157], - [0.00000, 0.00000, 0.53725], - [0.00000, 0.00000, 0.55294], - [0.00000, 0.00000, 0.56863], - [0.00000, 0.00000, 0.58431], - [0.00000, 0.00000, 0.60000], - [0.00000, 0.00000, 0.61176], - [0.00000, 0.00000, 0.62745], - [0.00000, 0.00000, 0.64314], - [0.00000, 0.00000, 0.65882], - [0.00000, 0.00000, 0.67451], - [0.00000, 0.00000, 0.69020], - [0.00000, 0.00000, 0.70588], - [0.00000, 0.00000, 0.72157], - [0.00000, 0.00000, 0.73725], - [0.00000, 0.00000, 0.75294], - [0.00000, 0.00000, 0.76863], - [0.00000, 0.00000, 0.78431], - [0.00000, 0.00000, 0.80000], - [0.00000, 0.00000, 0.81176], - [0.00000, 0.00000, 0.82745], - [0.00000, 0.00000, 0.84314], - [0.00000, 0.00000, 0.85882], - [0.00000, 0.00000, 0.87451], - [0.00000, 0.00000, 0.89020], - [0.00000, 0.00000, 0.90588], - [0.00000, 0.00000, 0.92157], - [0.00000, 0.00000, 0.93725], - [0.00000, 0.00000, 0.95294], - [0.00000, 0.00000, 0.96863], - [0.00000, 0.00000, 0.98431], - [0.00000, 0.00000, 1.00000], - [0.00000, 0.03529, 1.00000], - [0.00000, 0.07059, 1.00000], - [0.00000, 0.10980, 1.00000], - [0.00000, 0.14510, 1.00000], - [0.00000, 0.18039, 1.00000], - [0.00000, 0.21961, 1.00000], - [0.00000, 0.25490, 1.00000], - [0.00000, 0.29412, 1.00000], - [0.00000, 0.32941, 1.00000], - [0.00000, 0.36471, 1.00000], - [0.00000, 0.40392, 1.00000], - [0.00000, 0.43922, 1.00000], - [0.00000, 0.47843, 1.00000], - [0.00000, 0.50196, 1.00000], - [0.00000, 0.52549, 1.00000], - [0.00000, 0.54902, 1.00000], - [0.00000, 0.57255, 1.00000], - [0.00000, 0.59608, 1.00000], - [0.00000, 0.61961, 1.00000], - [0.00000, 0.64314, 1.00000], - [0.00000, 0.66667, 1.00000], - [0.00000, 0.69020, 1.00000], - [0.00000, 0.71373, 1.00000], - [0.00000, 0.73725, 1.00000], - [0.00000, 0.76078, 1.00000], - [0.00000, 0.78431, 1.00000], - [0.00000, 0.80000, 1.00000], - [0.00000, 0.81569, 1.00000], - [0.00000, 0.83137, 1.00000], - [0.00000, 0.84706, 1.00000], - [0.00000, 0.86667, 1.00000], - [0.00000, 0.88235, 1.00000], - [0.00000, 0.89804, 1.00000], - [0.00000, 0.91373, 1.00000], - [0.00000, 0.93333, 1.00000], - [0.00000, 0.94902, 1.00000], - [0.00000, 0.96471, 1.00000], - [0.00000, 0.98039, 1.00000], - [0.00000, 1.00000, 1.00000], - [0.00000, 1.00000, 0.97647], - [0.00000, 1.00000, 0.95294], - [0.00000, 1.00000, 0.92941], - [0.00000, 1.00000, 0.90588], - [0.00000, 1.00000, 0.88627], - [0.00000, 1.00000, 0.86275], - [0.00000, 1.00000, 0.83922], - [0.00000, 1.00000, 0.81569], - [0.00000, 1.00000, 0.79608], - [0.00000, 1.00000, 0.77255], - [0.00000, 1.00000, 0.74902], - [0.00000, 1.00000, 0.72549], - [0.00000, 1.00000, 0.70588], - [0.00000, 1.00000, 0.65098], - [0.00000, 1.00000, 0.59608], - [0.00000, 1.00000, 0.54118], - [0.00000, 1.00000, 0.48627], - [0.00000, 1.00000, 0.43137], - [0.00000, 1.00000, 0.37647], - [0.00000, 1.00000, 0.32549], - [0.00000, 1.00000, 0.27059], - [0.00000, 1.00000, 0.21569], - [0.00000, 1.00000, 0.16078], - [0.00000, 1.00000, 0.10588], - [0.00000, 1.00000, 0.05098], - [0.00000, 1.00000, 0.00000], - [0.05098, 1.00000, 0.00000], - [0.10588, 1.00000, 0.00000], - [0.16078, 1.00000, 0.00000], - [0.21569, 1.00000, 0.00000], - [0.27059, 1.00000, 0.00000], - [0.32549, 1.00000, 0.00000], - [0.37647, 1.00000, 0.00000], - [0.43137, 1.00000, 0.00000], - [0.48627, 1.00000, 0.00000], - [0.54118, 1.00000, 0.00000], - [0.59608, 1.00000, 0.00000], - [0.65098, 1.00000, 0.00000], - [0.70588, 1.00000, 0.00000], - [0.72549, 1.00000, 0.00000], - [0.74902, 1.00000, 0.00000], - [0.77255, 1.00000, 0.00000], - [0.79608, 1.00000, 0.00000], - [0.81569, 1.00000, 0.00000], - [0.83922, 1.00000, 0.00000], - [0.86275, 1.00000, 0.00000], - [0.88627, 1.00000, 0.00000], - [0.90588, 1.00000, 0.00000], - [0.92941, 1.00000, 0.00000], - [0.95294, 1.00000, 0.00000], - [0.97647, 1.00000, 0.00000], - [1.00000, 1.00000, 0.00000], - [0.99608, 0.97647, 0.00000], - [0.99608, 0.95686, 0.00000], - [0.99608, 0.93333, 0.00000], - [0.99608, 0.91373, 0.00000], - [0.99216, 0.89412, 0.00000], - [0.99216, 0.87059, 0.00000], - [0.99216, 0.85098, 0.00000], - [0.99216, 0.82745, 0.00000], - [0.98824, 0.80784, 0.00000], - [0.98824, 0.78824, 0.00000], - [0.98824, 0.76471, 0.00000], - [0.98824, 0.74510, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.70588, 0.00000], - [0.98824, 0.68627, 0.00000], - [0.98824, 0.66667, 0.00000], - [0.98824, 0.64706, 0.00000], - [0.99216, 0.62745, 0.00000], - [0.99216, 0.60784, 0.00000], - [0.99216, 0.58824, 0.00000], - [0.99216, 0.56863, 0.00000], - [0.99608, 0.54902, 0.00000], - [0.99608, 0.52941, 0.00000], - [0.99608, 0.50980, 0.00000], - [0.99608, 0.49020, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.43137, 0.00000], - [1.00000, 0.39608, 0.00000], - [1.00000, 0.36078, 0.00000], - [1.00000, 0.32549, 0.00000], - [1.00000, 0.28627, 0.00000], - [1.00000, 0.25098, 0.00000], - [1.00000, 0.21569, 0.00000], - [1.00000, 0.18039, 0.00000], - [1.00000, 0.14118, 0.00000], - [1.00000, 0.10588, 0.00000], - [1.00000, 0.07059, 0.00000], - [1.00000, 0.03529, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.05098], - [1.00000, 0.00000, 0.10588], - [1.00000, 0.00000, 0.16078], - [1.00000, 0.00000, 0.21569], - [1.00000, 0.00000, 0.27059], - [1.00000, 0.00000, 0.32549], - [1.00000, 0.00000, 0.37647], - [1.00000, 0.00000, 0.43137], - [1.00000, 0.00000, 0.48627], - [1.00000, 0.00000, 0.54118], - [1.00000, 0.00000, 0.59608], - [1.00000, 0.00000, 0.65098], - [1.00000, 0.00000, 0.70588], - [1.00000, 0.00000, 0.72549], - [1.00000, 0.00000, 0.74902], - [1.00000, 0.00000, 0.77255], - [1.00000, 0.00000, 0.79608], - [1.00000, 0.00000, 0.81569], - [1.00000, 0.00000, 0.83922], - [1.00000, 0.00000, 0.86275], - [1.00000, 0.00000, 0.88627], - [1.00000, 0.00000, 0.90588], - [1.00000, 0.00000, 0.92941], - [1.00000, 0.00000, 0.95294], - [1.00000, 0.00000, 0.97647], - [1.00000, 0.00000, 1.00000], - [1.00000, 0.03529, 1.00000], - [1.00000, 0.07059, 1.00000], - [1.00000, 0.10588, 1.00000], - [1.00000, 0.14118, 1.00000], - [1.00000, 0.18039, 1.00000], - [1.00000, 0.21569, 1.00000], - [1.00000, 0.25098, 1.00000], - [1.00000, 0.28627, 1.00000], - [1.00000, 0.32549, 1.00000], - [1.00000, 0.36078, 1.00000], - [1.00000, 0.39608, 1.00000], - [1.00000, 0.43137, 1.00000], - [1.00000, 0.47059, 1.00000], - [1.00000, 0.48627, 1.00000], - [1.00000, 0.50588, 1.00000], - [1.00000, 0.52157, 1.00000], - [1.00000, 0.54118, 1.00000], - [1.00000, 0.56078, 1.00000], - [1.00000, 0.57647, 1.00000], - [1.00000, 0.59608, 1.00000], - [1.00000, 0.61176, 1.00000], - [1.00000, 0.63137, 1.00000], - [1.00000, 0.65098, 1.00000], - [1.00000, 0.66667, 1.00000], - [1.00000, 0.68627, 1.00000], - [1.00000, 0.70588, 1.00000], - [1.00000, 0.74510, 1.00000], - [1.00000, 0.78824, 1.00000], - [1.00000, 0.83137, 1.00000], - [1.00000, 0.87059, 1.00000], - [1.00000, 0.91373, 1.00000], - [1.00000, 0.95686, 1.00000], - [1.00000, 1.00000, 1.00000], -]; - -Random = [ - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.47059, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.62745, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 0.78431, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 1.00000, 0.00392], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00392, 0.86275, 0.47059], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.78431, 0.62745], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.70588, 0.78431], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.00000, 0.62745, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.47059, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.23529, 0.00392, 1.00000], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.47059, 0.00392, 0.78431], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.62745, 0.00392, 0.62745], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.78431, 0.00392, 0.47059], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [0.90196, 0.11765, 0.23529], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.23529, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [1.00000, 0.47059, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [0.98039, 0.98039, 0.47059], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], - [1.00000, 1.00000, 1.00000], -]; - -RGB = [ - [0.00000, 0.00000, 0.00000], - [0.01176, 0.00000, 0.00000], - [0.02745, 0.00000, 0.00000], - [0.04314, 0.00000, 0.00000], - [0.05882, 0.00000, 0.00000], - [0.07451, 0.00000, 0.00000], - [0.08627, 0.00000, 0.00000], - [0.10196, 0.00000, 0.00000], - [0.11765, 0.00000, 0.00000], - [0.13333, 0.00000, 0.00000], - [0.14902, 0.00000, 0.00000], - [0.16078, 0.00000, 0.00000], - [0.17647, 0.00000, 0.00000], - [0.19216, 0.00000, 0.00000], - [0.20784, 0.00000, 0.00000], - [0.22353, 0.00000, 0.00000], - [0.23529, 0.00000, 0.00000], - [0.25098, 0.00000, 0.00000], - [0.26667, 0.00000, 0.00000], - [0.28235, 0.00000, 0.00000], - [0.29804, 0.00000, 0.00000], - [0.30980, 0.00000, 0.00000], - [0.32549, 0.00000, 0.00000], - [0.34118, 0.00000, 0.00000], - [0.35686, 0.00000, 0.00000], - [0.37255, 0.00000, 0.00000], - [0.38431, 0.00000, 0.00000], - [0.40000, 0.00000, 0.00000], - [0.41569, 0.00000, 0.00000], - [0.43137, 0.00000, 0.00000], - [0.44706, 0.00000, 0.00000], - [0.45882, 0.00000, 0.00000], - [0.47451, 0.00000, 0.00000], - [0.49020, 0.00000, 0.00000], - [0.50588, 0.00000, 0.00000], - [0.52157, 0.00000, 0.00000], - [0.53725, 0.00000, 0.00000], - [0.54902, 0.00000, 0.00000], - [0.56471, 0.00000, 0.00000], - [0.58039, 0.00000, 0.00000], - [0.59608, 0.00000, 0.00000], - [0.61176, 0.00000, 0.00000], - [0.62353, 0.00000, 0.00000], - [0.63922, 0.00000, 0.00000], - [0.65490, 0.00000, 0.00000], - [0.67059, 0.00000, 0.00000], - [0.68627, 0.00000, 0.00000], - [0.69804, 0.00000, 0.00000], - [0.71373, 0.00000, 0.00000], - [0.72941, 0.00000, 0.00000], - [0.74510, 0.00000, 0.00000], - [0.76078, 0.00000, 0.00000], - [0.77255, 0.00000, 0.00000], - [0.78824, 0.00000, 0.00000], - [0.80392, 0.00000, 0.00000], - [0.81961, 0.00000, 0.00000], - [0.83529, 0.00000, 0.00000], - [0.84706, 0.00000, 0.00000], - [0.86275, 0.00000, 0.00000], - [0.87843, 0.00000, 0.00000], - [0.89412, 0.00000, 0.00000], - [0.90980, 0.00000, 0.00000], - [0.92157, 0.00000, 0.00000], - [0.93725, 0.00000, 0.00000], - [0.95294, 0.00000, 0.00000], - [0.96863, 0.01176, 0.00000], - [0.98431, 0.02745, 0.00000], - [1.00000, 0.04314, 0.00000], - [0.98431, 0.05882, 0.00000], - [0.96863, 0.07451, 0.00000], - [0.95294, 0.09020, 0.00000], - [0.93725, 0.10588, 0.00000], - [0.92157, 0.12157, 0.00000], - [0.90196, 0.13725, 0.00000], - [0.88627, 0.15294, 0.00000], - [0.87059, 0.16863, 0.00000], - [0.85490, 0.18431, 0.00000], - [0.83922, 0.20000, 0.00000], - [0.82353, 0.21569, 0.00000], - [0.80392, 0.23137, 0.00000], - [0.78824, 0.24706, 0.00000], - [0.77255, 0.26275, 0.00000], - [0.75686, 0.27843, 0.00000], - [0.74118, 0.29412, 0.00000], - [0.72157, 0.30980, 0.00000], - [0.70588, 0.32549, 0.00000], - [0.69020, 0.34118, 0.00000], - [0.67451, 0.35686, 0.00000], - [0.65882, 0.37255, 0.00000], - [0.64314, 0.38824, 0.00000], - [0.62353, 0.40392, 0.00000], - [0.60784, 0.41961, 0.00000], - [0.59216, 0.43529, 0.00000], - [0.57647, 0.45098, 0.00000], - [0.56078, 0.46667, 0.00000], - [0.54118, 0.48235, 0.00000], - [0.52549, 0.49804, 0.00000], - [0.50980, 0.51373, 0.00000], - [0.49412, 0.52941, 0.00000], - [0.47843, 0.54510, 0.00000], - [0.46275, 0.56078, 0.00000], - [0.44314, 0.57647, 0.00000], - [0.42745, 0.59216, 0.00000], - [0.41176, 0.60784, 0.00000], - [0.39608, 0.62353, 0.00000], - [0.38039, 0.63922, 0.00000], - [0.36078, 0.65490, 0.00000], - [0.34510, 0.67059, 0.00000], - [0.32941, 0.68627, 0.00000], - [0.31373, 0.70196, 0.00000], - [0.29804, 0.71765, 0.00000], - [0.28235, 0.73333, 0.00000], - [0.26275, 0.74902, 0.00000], - [0.24706, 0.76471, 0.00000], - [0.23137, 0.78039, 0.00000], - [0.21569, 0.79608, 0.00000], - [0.20000, 0.81176, 0.00000], - [0.18039, 0.82745, 0.00000], - [0.16471, 0.84314, 0.00000], - [0.14902, 0.85882, 0.00000], - [0.13333, 0.87451, 0.00000], - [0.11765, 0.89020, 0.00000], - [0.10196, 0.90588, 0.00000], - [0.08235, 0.92157, 0.00000], - [0.06667, 0.93725, 0.00000], - [0.05098, 0.95294, 0.00000], - [0.03529, 0.96863, 0.00000], - [0.01961, 0.98431, 0.01176], - [0.00000, 1.00000, 0.02745], - [0.00000, 0.98431, 0.04314], - [0.00000, 0.96863, 0.05882], - [0.00000, 0.95294, 0.07451], - [0.00000, 0.93725, 0.09020], - [0.00000, 0.92157, 0.10588], - [0.00000, 0.90588, 0.11765], - [0.00000, 0.89020, 0.13333], - [0.00000, 0.87451, 0.14902], - [0.00000, 0.85882, 0.16471], - [0.00000, 0.84314, 0.18039], - [0.00000, 0.82745, 0.19608], - [0.00000, 0.81176, 0.21176], - [0.00000, 0.79608, 0.22353], - [0.00000, 0.78039, 0.23922], - [0.00000, 0.76471, 0.25490], - [0.00000, 0.74902, 0.27059], - [0.00000, 0.73333, 0.28627], - [0.00000, 0.71765, 0.30196], - [0.00000, 0.70196, 0.31765], - [0.00000, 0.68627, 0.33333], - [0.00000, 0.66667, 0.34510], - [0.00000, 0.65098, 0.36078], - [0.00000, 0.63529, 0.37647], - [0.00000, 0.61961, 0.39216], - [0.00000, 0.60392, 0.40784], - [0.00000, 0.58824, 0.42353], - [0.00000, 0.57255, 0.43922], - [0.00000, 0.55686, 0.45098], - [0.00000, 0.54118, 0.46667], - [0.00000, 0.52549, 0.48235], - [0.00000, 0.50980, 0.49804], - [0.00000, 0.49412, 0.51373], - [0.00000, 0.47843, 0.52941], - [0.00000, 0.46275, 0.54510], - [0.00000, 0.44706, 0.55686], - [0.00000, 0.43137, 0.57255], - [0.00000, 0.41569, 0.58824], - [0.00000, 0.40000, 0.60392], - [0.00000, 0.38431, 0.61961], - [0.00000, 0.36863, 0.63529], - [0.00000, 0.35294, 0.65098], - [0.00000, 0.33333, 0.66667], - [0.00000, 0.31765, 0.67843], - [0.00000, 0.30196, 0.69412], - [0.00000, 0.28627, 0.70980], - [0.00000, 0.27059, 0.72549], - [0.00000, 0.25490, 0.74118], - [0.00000, 0.23922, 0.75686], - [0.00000, 0.22353, 0.77255], - [0.00000, 0.20784, 0.78431], - [0.00000, 0.19216, 0.80000], - [0.00000, 0.17647, 0.81569], - [0.00000, 0.16078, 0.83137], - [0.00000, 0.14510, 0.84706], - [0.00000, 0.12941, 0.86275], - [0.00000, 0.11373, 0.87843], - [0.00000, 0.09804, 0.89020], - [0.00000, 0.08235, 0.90588], - [0.00000, 0.06667, 0.92157], - [0.00000, 0.05098, 0.93725], - [0.00000, 0.03529, 0.95294], - [0.00000, 0.01961, 0.96863], - [0.00000, 0.00000, 0.98431], - [0.00000, 0.00000, 1.00000], - [0.00000, 0.00000, 0.98431], - [0.00000, 0.00000, 0.96863], - [0.00000, 0.00000, 0.95294], - [0.00000, 0.00000, 0.93725], - [0.00000, 0.00000, 0.92157], - [0.00000, 0.00000, 0.90588], - [0.00000, 0.00000, 0.89020], - [0.00000, 0.00000, 0.87451], - [0.00000, 0.00000, 0.85882], - [0.00000, 0.00000, 0.84314], - [0.00000, 0.00000, 0.82745], - [0.00000, 0.00000, 0.81176], - [0.00000, 0.00000, 0.79608], - [0.00000, 0.00000, 0.78039], - [0.00000, 0.00000, 0.76471], - [0.00000, 0.00000, 0.74902], - [0.00000, 0.00000, 0.73333], - [0.00000, 0.00000, 0.71765], - [0.00000, 0.00000, 0.70196], - [0.00000, 0.00000, 0.68627], - [0.00000, 0.00000, 0.66667], - [0.00000, 0.00000, 0.65098], - [0.00000, 0.00000, 0.63529], - [0.00000, 0.00000, 0.61961], - [0.00000, 0.00000, 0.60392], - [0.00000, 0.00000, 0.58824], - [0.00000, 0.00000, 0.57255], - [0.00000, 0.00000, 0.55686], - [0.00000, 0.00000, 0.54118], - [0.00000, 0.00000, 0.52549], - [0.00000, 0.00000, 0.50980], - [0.00000, 0.00000, 0.49412], - [0.00000, 0.00000, 0.47843], - [0.00000, 0.00000, 0.46275], - [0.00000, 0.00000, 0.44706], - [0.00000, 0.00000, 0.43137], - [0.00000, 0.00000, 0.41569], - [0.00000, 0.00000, 0.40000], - [0.00000, 0.00000, 0.38431], - [0.00000, 0.00000, 0.36863], - [0.00000, 0.00000, 0.35294], - [0.00000, 0.00000, 0.33333], - [0.00000, 0.00000, 0.31765], - [0.00000, 0.00000, 0.30196], - [0.00000, 0.00000, 0.28627], - [0.00000, 0.00000, 0.27059], - [0.00000, 0.00000, 0.25490], - [0.00000, 0.00000, 0.23922], - [0.00000, 0.00000, 0.22353], - [0.00000, 0.00000, 0.20784], - [0.00000, 0.00000, 0.19216], - [0.00000, 0.00000, 0.17647], - [0.00000, 0.00000, 0.16078], - [0.00000, 0.00000, 0.14510], - [0.00000, 0.00000, 0.12941], - [0.00000, 0.00000, 0.11373], - [0.00000, 0.00000, 0.09804], - [0.00000, 0.00000, 0.08235], - [0.00000, 0.00000, 0.06667], - [0.00000, 0.00000, 0.05098], - [0.00000, 0.00000, 0.03529], - [0.00000, 0.00000, 0.01961], - [0.00000, 0.00000, 0.00000], -]; - -Smooth = [ - [0.00000, 0.00000, 1.00000], - [0.01569, 0.00000, 0.98431], - [0.03529, 0.00000, 0.96471], - [0.05098, 0.00000, 0.94902], - [0.06667, 0.00000, 0.93333], - [0.08627, 0.00000, 0.91373], - [0.10196, 0.00000, 0.89804], - [0.11765, 0.00000, 0.88235], - [0.13725, 0.00000, 0.86275], - [0.15294, 0.00000, 0.84706], - [0.16863, 0.00000, 0.83137], - [0.18824, 0.00000, 0.81176], - [0.20392, 0.00000, 0.79608], - [0.21961, 0.00000, 0.78039], - [0.23922, 0.00000, 0.76078], - [0.25490, 0.00000, 0.74510], - [0.27059, 0.00000, 0.72941], - [0.28627, 0.00000, 0.71373], - [0.30588, 0.00000, 0.69412], - [0.32157, 0.00000, 0.67843], - [0.33725, 0.00000, 0.66275], - [0.35686, 0.00000, 0.64314], - [0.37255, 0.00000, 0.62745], - [0.38824, 0.00000, 0.61176], - [0.40784, 0.00000, 0.59216], - [0.42353, 0.00000, 0.57647], - [0.43922, 0.00000, 0.56078], - [0.45882, 0.00000, 0.54118], - [0.47451, 0.00000, 0.52549], - [0.49020, 0.00000, 0.50980], - [0.50980, 0.00000, 0.49020], - [0.52549, 0.00000, 0.47451], - [0.54118, 0.00000, 0.45882], - [0.56078, 0.00000, 0.43922], - [0.57647, 0.00000, 0.42353], - [0.59216, 0.00000, 0.40784], - [0.61176, 0.00000, 0.38824], - [0.62745, 0.00000, 0.37255], - [0.64314, 0.00000, 0.35686], - [0.66275, 0.00000, 0.33725], - [0.67843, 0.00000, 0.32157], - [0.69412, 0.00000, 0.30588], - [0.71373, 0.00000, 0.28627], - [0.72941, 0.00000, 0.27059], - [0.74510, 0.00000, 0.25490], - [0.76078, 0.00000, 0.23922], - [0.78039, 0.00000, 0.21961], - [0.79608, 0.00000, 0.20392], - [0.81176, 0.00000, 0.18824], - [0.83137, 0.00000, 0.16863], - [0.84706, 0.00000, 0.15294], - [0.86275, 0.00000, 0.13725], - [0.88235, 0.00000, 0.11765], - [0.89804, 0.00000, 0.10196], - [0.91373, 0.00000, 0.08627], - [0.93333, 0.00000, 0.06667], - [0.94902, 0.00000, 0.05098], - [0.96471, 0.00000, 0.03529], - [0.98431, 0.00000, 0.01569], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.00000, 0.00000], - [1.00000, 0.01176, 0.00000], - [1.00000, 0.01961, 0.00000], - [1.00000, 0.03137, 0.00000], - [1.00000, 0.03922, 0.00000], - [1.00000, 0.05098, 0.00000], - [1.00000, 0.05882, 0.00000], - [1.00000, 0.07059, 0.00000], - [1.00000, 0.08235, 0.00000], - [1.00000, 0.09020, 0.00000], - [1.00000, 0.10196, 0.00000], - [1.00000, 0.10980, 0.00000], - [1.00000, 0.12157, 0.00000], - [1.00000, 0.12941, 0.00000], - [1.00000, 0.14118, 0.00000], - [0.99608, 0.15294, 0.00000], - [0.99608, 0.16078, 0.00000], - [0.99608, 0.17255, 0.00000], - [0.99608, 0.18039, 0.00000], - [0.99608, 0.19216, 0.00000], - [0.99608, 0.20392, 0.00000], - [0.99608, 0.21176, 0.00000], - [0.99608, 0.22353, 0.00000], - [0.99608, 0.23137, 0.00000], - [0.99608, 0.24314, 0.00000], - [0.99608, 0.25098, 0.00000], - [0.99608, 0.26275, 0.00000], - [0.99608, 0.27451, 0.00000], - [0.99608, 0.28235, 0.00000], - [0.99608, 0.29412, 0.00000], - [0.99608, 0.30196, 0.00000], - [0.99608, 0.31373, 0.00000], - [0.99608, 0.32157, 0.00000], - [0.99608, 0.33333, 0.00000], - [0.99608, 0.34510, 0.00000], - [0.99608, 0.35294, 0.00000], - [0.99608, 0.36471, 0.00000], - [0.99608, 0.37255, 0.00000], - [0.99608, 0.38431, 0.00000], - [0.99608, 0.39216, 0.00000], - [0.99608, 0.40392, 0.00000], - [0.99608, 0.41569, 0.00000], - [0.99608, 0.42353, 0.00000], - [0.99608, 0.43529, 0.00000], - [0.99608, 0.44314, 0.00000], - [0.99216, 0.45490, 0.00000], - [0.99216, 0.46667, 0.00000], - [0.99216, 0.47451, 0.00000], - [0.99216, 0.48627, 0.00000], - [0.99216, 0.49412, 0.00000], - [0.99216, 0.50588, 0.00000], - [0.99216, 0.51373, 0.00000], - [0.99216, 0.52549, 0.00000], - [0.99216, 0.53725, 0.00000], - [0.99216, 0.54510, 0.00000], - [0.99216, 0.55686, 0.00000], - [0.99216, 0.56471, 0.00000], - [0.99216, 0.57647, 0.00000], - [0.99216, 0.58431, 0.00000], - [0.99216, 0.59608, 0.00000], - [0.99216, 0.60000, 0.00000], - [0.99216, 0.60784, 0.00000], - [0.99216, 0.61176, 0.00000], - [0.99216, 0.61569, 0.00000], - [0.99216, 0.61961, 0.00000], - [0.99216, 0.62745, 0.00000], - [0.99216, 0.63137, 0.00000], - [0.99216, 0.63529, 0.00000], - [0.99216, 0.64314, 0.00000], - [0.98824, 0.64706, 0.00000], - [0.98824, 0.65098, 0.00000], - [0.98824, 0.65882, 0.00000], - [0.98824, 0.66275, 0.00000], - [0.98824, 0.66667, 0.00000], - [0.98824, 0.67451, 0.00000], - [0.98824, 0.67843, 0.00000], - [0.98824, 0.68235, 0.00000], - [0.98824, 0.68627, 0.00000], - [0.98824, 0.69412, 0.00000], - [0.98824, 0.69804, 0.00000], - [0.98824, 0.70196, 0.00000], - [0.98824, 0.70980, 0.00000], - [0.98824, 0.71373, 0.00000], - [0.98824, 0.71765, 0.00000], - [0.98824, 0.72549, 0.00000], - [0.98824, 0.72941, 0.00000], - [0.98824, 0.73333, 0.00000], - [0.98824, 0.73725, 0.00000], - [0.98824, 0.74510, 0.00000], - [0.98824, 0.74902, 0.00000], - [0.98431, 0.75294, 0.00000], - [0.98431, 0.76078, 0.00000], - [0.98431, 0.76471, 0.00000], - [0.98431, 0.76863, 0.00000], - [0.98431, 0.77255, 0.00000], - [0.98431, 0.78039, 0.00000], - [0.98431, 0.78431, 0.00000], - [0.98431, 0.78824, 0.00000], - [0.98431, 0.79608, 0.00000], - [0.98431, 0.80000, 0.00000], - [0.98431, 0.80392, 0.00000], - [0.98431, 0.81176, 0.00000], - [0.98431, 0.81569, 0.00000], - [0.98431, 0.81961, 0.00000], - [0.98431, 0.82745, 0.00000], - [0.98431, 0.83137, 0.00000], - [0.98431, 0.83529, 0.00000], - [0.98431, 0.83922, 0.00000], - [0.98431, 0.84706, 0.00000], - [0.98431, 0.85098, 0.00000], - [0.98039, 0.85490, 0.00000], - [0.98039, 0.86275, 0.00000], - [0.98039, 0.86667, 0.00000], - [0.98039, 0.87059, 0.00000], - [0.98039, 0.87843, 0.00000], - [0.98039, 0.88235, 0.00000], - [0.98039, 0.88627, 0.00000], - [0.98039, 0.89020, 0.00000], - [0.98039, 0.89804, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.98039, 0.90196, 0.00000], - [0.96471, 0.88627, 0.00000], - [0.94902, 0.87059, 0.00000], - [0.92941, 0.85490, 0.00000], - [0.91373, 0.83922, 0.00000], - [0.89804, 0.82745, 0.00000], - [0.88235, 0.81176, 0.00000], - [0.86275, 0.79608, 0.00000], - [0.84706, 0.78039, 0.00000], - [0.83137, 0.76471, 0.00000], - [0.81569, 0.74902, 0.00000], - [0.79608, 0.73333, 0.00000], - [0.78039, 0.71765, 0.00000], - [0.76471, 0.70196, 0.00000], - [0.74902, 0.68627, 0.00000], - [0.72941, 0.67451, 0.00000], - [0.71373, 0.65882, 0.00000], - [0.69804, 0.64314, 0.00000], - [0.68235, 0.62745, 0.00000], - [0.66275, 0.61176, 0.00000], - [0.64706, 0.59608, 0.00000], - [0.63137, 0.58039, 0.00000], - [0.61569, 0.56471, 0.00000], - [0.60000, 0.54902, 0.00000], - [0.58039, 0.53333, 0.00000], - [0.56471, 0.52157, 0.00000], - [0.54902, 0.50588, 0.00000], - [0.53333, 0.49020, 0.00000], - [0.51373, 0.47451, 0.00000], - [0.49804, 0.45882, 0.00000], - [0.48235, 0.44314, 0.00000], - [0.46667, 0.42745, 0.00000], - [0.44706, 0.41176, 0.00000], - [0.43137, 0.39608, 0.00000], - [0.41569, 0.38039, 0.00000], - [0.40000, 0.36863, 0.00000], - [0.38039, 0.35294, 0.00000], - [0.36471, 0.33725, 0.00000], - [0.34902, 0.32157, 0.00000], - [0.33333, 0.30588, 0.00000], - [0.31765, 0.29020, 0.00000], - [0.29804, 0.27451, 0.00000], - [0.28235, 0.25882, 0.00000], - [0.26667, 0.24314, 0.00000], - [0.25098, 0.22745, 0.00000], - [0.23137, 0.21569, 0.00000], - [0.21569, 0.20000, 0.00000], - [0.20000, 0.18431, 0.00000], - [0.18431, 0.16863, 0.00000], - [0.16471, 0.15294, 0.00000], - [0.14902, 0.13725, 0.00000], - [0.13333, 0.12157, 0.00000], - [0.11765, 0.10588, 0.00000], - [0.09804, 0.09020, 0.00000], - [0.08235, 0.07451, 0.00000], - [0.06667, 0.06275, 0.00000], - [0.05098, 0.04706, 0.00000], - [0.03137, 0.03137, 0.00000], - [0.01569, 0.01569, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], - [0.00000, 0.00000, 0.00000], -]; - -Staircase = [ - [0.00392, 0.00392, 0.31373], - [0.00784, 0.00784, 0.31373], - [0.01176, 0.01176, 0.31373], - [0.01569, 0.01569, 0.31373], - [0.01961, 0.01961, 0.31373], - [0.02353, 0.02353, 0.31373], - [0.02745, 0.02745, 0.31373], - [0.03137, 0.03137, 0.31373], - [0.03529, 0.03529, 0.31373], - [0.03922, 0.03922, 0.31373], - [0.04314, 0.04314, 0.31373], - [0.04706, 0.04706, 0.31373], - [0.05098, 0.05098, 0.31373], - [0.05490, 0.05490, 0.31373], - [0.05882, 0.05882, 0.31373], - [0.06275, 0.06275, 0.31373], - [0.06667, 0.06667, 0.47059], - [0.07059, 0.07059, 0.47059], - [0.07451, 0.07451, 0.47059], - [0.07843, 0.07843, 0.47059], - [0.08235, 0.08235, 0.47059], - [0.08627, 0.08627, 0.47059], - [0.09020, 0.09020, 0.47059], - [0.09412, 0.09412, 0.47059], - [0.09804, 0.09804, 0.47059], - [0.10196, 0.10196, 0.47059], - [0.10588, 0.10588, 0.47059], - [0.10980, 0.10980, 0.47059], - [0.11373, 0.11373, 0.47059], - [0.11765, 0.11765, 0.47059], - [0.12157, 0.12157, 0.47059], - [0.12549, 0.12549, 0.47059], - [0.12941, 0.12941, 0.62745], - [0.13333, 0.13333, 0.62745], - [0.13725, 0.13725, 0.62745], - [0.14118, 0.14118, 0.62745], - [0.14510, 0.14510, 0.62745], - [0.14902, 0.14902, 0.62745], - [0.15294, 0.15294, 0.62745], - [0.15686, 0.15686, 0.62745], - [0.16078, 0.16078, 0.62745], - [0.16471, 0.16471, 0.62745], - [0.16863, 0.16863, 0.62745], - [0.17255, 0.17255, 0.62745], - [0.17647, 0.17647, 0.62745], - [0.18039, 0.18039, 0.62745], - [0.18431, 0.18431, 0.62745], - [0.18824, 0.18824, 0.62745], - [0.19216, 0.19216, 0.78431], - [0.19608, 0.19608, 0.78431], - [0.20000, 0.20000, 0.78431], - [0.20392, 0.20392, 0.78431], - [0.20784, 0.20784, 0.78431], - [0.21176, 0.21176, 0.78431], - [0.21569, 0.21569, 0.78431], - [0.21961, 0.21961, 0.78431], - [0.22353, 0.22353, 0.78431], - [0.22745, 0.22745, 0.78431], - [0.23137, 0.23137, 0.78431], - [0.23529, 0.23529, 0.78431], - [0.23922, 0.23922, 0.78431], - [0.24314, 0.24314, 0.78431], - [0.24706, 0.24706, 0.78431], - [0.25098, 0.25098, 0.78431], - [0.25490, 0.25490, 0.94118], - [0.25882, 0.25882, 0.94118], - [0.26275, 0.26275, 0.94118], - [0.26667, 0.26667, 0.94118], - [0.27059, 0.27059, 0.94118], - [0.27451, 0.27451, 0.94118], - [0.27843, 0.27843, 0.94118], - [0.28235, 0.28235, 0.94118], - [0.28627, 0.28627, 0.94118], - [0.29020, 0.29020, 0.94118], - [0.29412, 0.29412, 0.94118], - [0.29804, 0.29804, 0.94118], - [0.30196, 0.30196, 0.94118], - [0.30588, 0.30588, 0.94118], - [0.30980, 0.30980, 0.94118], - [0.31373, 0.31373, 0.94118], - [0.31765, 0.31765, 0.95294], - [0.32157, 0.32157, 0.96471], - [0.32549, 0.32549, 0.97647], - [0.32941, 0.32941, 0.98824], - [0.33333, 0.33333, 1.00000], - [0.00392, 0.31373, 0.00392], - [0.00784, 0.31373, 0.00784], - [0.01176, 0.31373, 0.01176], - [0.01569, 0.31373, 0.01569], - [0.01961, 0.31373, 0.01961], - [0.02353, 0.31373, 0.02353], - [0.02745, 0.31373, 0.02745], - [0.03137, 0.31373, 0.03137], - [0.03529, 0.31373, 0.03529], - [0.03922, 0.31373, 0.03922], - [0.04314, 0.31373, 0.04314], - [0.04706, 0.31373, 0.04706], - [0.05098, 0.31373, 0.05098], - [0.05490, 0.31373, 0.05490], - [0.05882, 0.31373, 0.05882], - [0.06275, 0.31373, 0.06275], - [0.06667, 0.47059, 0.06667], - [0.07059, 0.47059, 0.07059], - [0.07451, 0.47059, 0.07451], - [0.07843, 0.47059, 0.07843], - [0.08235, 0.47059, 0.08235], - [0.08627, 0.47059, 0.08627], - [0.09020, 0.47059, 0.09020], - [0.09412, 0.47059, 0.09412], - [0.09804, 0.47059, 0.09804], - [0.10196, 0.47059, 0.10196], - [0.10588, 0.47059, 0.10588], - [0.10980, 0.47059, 0.10980], - [0.11373, 0.47059, 0.11373], - [0.11765, 0.47059, 0.11765], - [0.12157, 0.47059, 0.12157], - [0.12549, 0.47059, 0.12549], - [0.12941, 0.62745, 0.12941], - [0.13333, 0.62745, 0.13333], - [0.13725, 0.62745, 0.13725], - [0.14118, 0.62745, 0.14118], - [0.14510, 0.62745, 0.14510], - [0.14902, 0.62745, 0.14902], - [0.15294, 0.62745, 0.15294], - [0.15686, 0.62745, 0.15686], - [0.16078, 0.62745, 0.16078], - [0.16471, 0.62745, 0.16471], - [0.16863, 0.62745, 0.16863], - [0.17255, 0.62745, 0.17255], - [0.17647, 0.62745, 0.17647], - [0.18039, 0.62745, 0.18039], - [0.18431, 0.62745, 0.18431], - [0.18824, 0.62745, 0.18824], - [0.19216, 0.78431, 0.19216], - [0.19608, 0.78431, 0.19608], - [0.20000, 0.78431, 0.20000], - [0.20392, 0.78431, 0.20392], - [0.20784, 0.78431, 0.20784], - [0.21176, 0.78431, 0.21176], - [0.21569, 0.78431, 0.21569], - [0.21961, 0.78431, 0.21961], - [0.22353, 0.78431, 0.22353], - [0.22745, 0.78431, 0.22745], - [0.23137, 0.78431, 0.23137], - [0.23529, 0.78431, 0.23529], - [0.23922, 0.78431, 0.23922], - [0.24314, 0.78431, 0.24314], - [0.24706, 0.78431, 0.24706], - [0.25098, 0.78431, 0.25098], - [0.25490, 0.94118, 0.25490], - [0.25882, 0.94118, 0.25882], - [0.26275, 0.94118, 0.26275], - [0.26667, 0.94118, 0.26667], - [0.27059, 0.94118, 0.27059], - [0.27451, 0.94118, 0.27451], - [0.27843, 0.94118, 0.27843], - [0.28235, 0.94118, 0.28235], - [0.28627, 0.94118, 0.28627], - [0.29020, 0.94118, 0.29020], - [0.29412, 0.94118, 0.29412], - [0.29804, 0.94118, 0.29804], - [0.30196, 0.94118, 0.30196], - [0.30588, 0.94118, 0.30588], - [0.30980, 0.94118, 0.30980], - [0.31373, 0.94118, 0.31373], - [0.31765, 0.95294, 0.31765], - [0.32157, 0.96471, 0.32157], - [0.32549, 0.97647, 0.32549], - [0.32941, 0.98824, 0.32941], - [0.33333, 1.00000, 0.33333], - [0.31373, 0.00392, 0.00392], - [0.31373, 0.00784, 0.00784], - [0.31373, 0.01176, 0.01176], - [0.31373, 0.01569, 0.01569], - [0.31373, 0.01961, 0.01961], - [0.31373, 0.02353, 0.02353], - [0.31373, 0.02745, 0.02745], - [0.31373, 0.03137, 0.03137], - [0.31373, 0.03529, 0.03529], - [0.31373, 0.03922, 0.03922], - [0.31373, 0.04314, 0.04314], - [0.31373, 0.04706, 0.04706], - [0.31373, 0.05098, 0.05098], - [0.31373, 0.05490, 0.05490], - [0.31373, 0.05882, 0.05882], - [0.31373, 0.06275, 0.06275], - [0.47059, 0.06667, 0.06667], - [0.47059, 0.07059, 0.07059], - [0.47059, 0.07451, 0.07451], - [0.47059, 0.07843, 0.07843], - [0.47059, 0.08235, 0.08235], - [0.47059, 0.08627, 0.08627], - [0.47059, 0.09020, 0.09020], - [0.47059, 0.09412, 0.09412], - [0.47059, 0.09804, 0.09804], - [0.47059, 0.10196, 0.10196], - [0.47059, 0.10588, 0.10588], - [0.47059, 0.10980, 0.10980], - [0.47059, 0.11373, 0.11373], - [0.47059, 0.11765, 0.11765], - [0.47059, 0.12157, 0.12157], - [0.47059, 0.12549, 0.12549], - [0.62745, 0.12941, 0.12941], - [0.62745, 0.13333, 0.13333], - [0.62745, 0.13725, 0.13725], - [0.62745, 0.14118, 0.14118], - [0.62745, 0.14510, 0.14510], - [0.62745, 0.14902, 0.14902], - [0.62745, 0.15294, 0.15294], - [0.62745, 0.15686, 0.15686], - [0.62745, 0.16078, 0.16078], - [0.62745, 0.16471, 0.16471], - [0.62745, 0.16863, 0.16863], - [0.62745, 0.17255, 0.17255], - [0.62745, 0.17647, 0.17647], - [0.62745, 0.18039, 0.18039], - [0.62745, 0.18431, 0.18431], - [0.62745, 0.18824, 0.18824], - [0.78431, 0.19216, 0.19216], - [0.78431, 0.19608, 0.19608], - [0.78431, 0.20000, 0.20000], - [0.78431, 0.20392, 0.20392], - [0.78431, 0.20784, 0.20784], - [0.78431, 0.21176, 0.21176], - [0.78431, 0.21569, 0.21569], - [0.78431, 0.21961, 0.21961], - [0.78431, 0.22353, 0.22353], - [0.78431, 0.22745, 0.22745], - [0.78431, 0.23137, 0.23137], - [0.78431, 0.23529, 0.23529], - [0.78431, 0.23922, 0.23922], - [0.78431, 0.24314, 0.24314], - [0.78431, 0.24706, 0.24706], - [0.78431, 0.25098, 0.25098], - [0.94118, 0.25490, 0.25490], - [0.94118, 0.25882, 0.25882], - [0.94118, 0.26275, 0.26275], - [0.94118, 0.26667, 0.26667], - [0.94118, 0.27059, 0.27059], - [0.94118, 0.27451, 0.27451], - [0.94118, 0.27843, 0.27843], - [0.94118, 0.28235, 0.28235], - [0.94118, 0.28627, 0.28627], - [0.94118, 0.29020, 0.29020], - [0.94118, 0.29412, 0.29412], - [0.94118, 0.29804, 0.29804], - [0.94118, 0.30196, 0.30196], - [0.94118, 0.30588, 0.30588], - [0.94118, 0.30980, 0.30980], - [0.94118, 0.31373, 0.31373], - [0.94902, 0.39216, 0.39216], - [0.96078, 0.52941, 0.52941], - [0.97255, 0.66667, 0.66667], - [0.98431, 0.80392, 0.80392], - [0.99216, 0.80000, 0.80000], - [1.00000, 1.00000, 1.00000], -]; - -RGB2 = [ - [0.00000, 0.00000, 1.00000], - [0.00000, 0.00000, 0.98431], - [0.00000, 0.01961, 0.96863], - [0.00000, 0.03529, 0.95294], - [0.00000, 0.05098, 0.93725], - [0.00000, 0.06667, 0.92157], - [0.00000, 0.08235, 0.90588], - [0.00000, 0.09804, 0.89020], - [0.00000, 0.11373, 0.87843], - [0.00000, 0.12941, 0.86275], - [0.00000, 0.14510, 0.84706], - [0.00000, 0.16078, 0.83137], - [0.00000, 0.17647, 0.81569], - [0.00000, 0.19216, 0.80000], - [0.00000, 0.20784, 0.78431], - [0.00000, 0.22353, 0.77255], - [0.00000, 0.23922, 0.75686], - [0.00000, 0.25490, 0.74118], - [0.00000, 0.27059, 0.72549], - [0.00000, 0.28627, 0.70980], - [0.00000, 0.30196, 0.69412], - [0.00000, 0.31765, 0.67843], - [0.00000, 0.33333, 0.66667], - [0.00000, 0.35294, 0.65098], - [0.00000, 0.36863, 0.63529], - [0.00000, 0.38431, 0.61961], - [0.00000, 0.40000, 0.60392], - [0.00000, 0.41569, 0.58824], - [0.00000, 0.43137, 0.57255], - [0.00000, 0.44706, 0.55686], - [0.00000, 0.46275, 0.54510], - [0.00000, 0.47843, 0.52941], - [0.00000, 0.49412, 0.51373], - [0.00000, 0.50980, 0.49804], - [0.00000, 0.52549, 0.48235], - [0.00000, 0.54118, 0.46667], - [0.00000, 0.55686, 0.45098], - [0.00000, 0.57255, 0.43922], - [0.00000, 0.58824, 0.42353], - [0.00000, 0.60392, 0.40784], - [0.00000, 0.61961, 0.39216], - [0.00000, 0.63529, 0.37647], - [0.00000, 0.65098, 0.36078], - [0.00000, 0.66667, 0.34510], - [0.00000, 0.68627, 0.33333], - [0.00000, 0.70196, 0.31765], - [0.00000, 0.71765, 0.30196], - [0.00000, 0.73333, 0.28627], - [0.00000, 0.74902, 0.27059], - [0.00000, 0.76471, 0.25490], - [0.00000, 0.78039, 0.23922], - [0.00000, 0.79608, 0.22353], - [0.00000, 0.81176, 0.21176], - [0.00000, 0.82745, 0.19608], - [0.00000, 0.84314, 0.18039], - [0.00000, 0.85882, 0.16471], - [0.00000, 0.87451, 0.14902], - [0.00000, 0.89020, 0.13333], - [0.00000, 0.90588, 0.11765], - [0.00000, 0.92157, 0.10588], - [0.00000, 0.93725, 0.09020], - [0.00000, 0.95294, 0.07451], - [0.00000, 0.96863, 0.05882], - [0.00000, 0.98431, 0.04314], - [0.00000, 1.00000, 0.02745], - [0.01961, 0.98431, 0.01176], - [0.03529, 0.96863, 0.00000], - [0.05098, 0.95294, 0.00000], - [0.06667, 0.93725, 0.00000], - [0.08235, 0.92157, 0.00000], - [0.10196, 0.90588, 0.00000], - [0.11765, 0.89020, 0.00000], - [0.13333, 0.87451, 0.00000], - [0.14902, 0.85882, 0.00000], - [0.16471, 0.84314, 0.00000], - [0.18039, 0.82745, 0.00000], - [0.20000, 0.81176, 0.00000], - [0.21569, 0.79608, 0.00000], - [0.23137, 0.78039, 0.00000], - [0.24706, 0.76471, 0.00000], - [0.26275, 0.74902, 0.00000], - [0.28235, 0.73333, 0.00000], - [0.29804, 0.71765, 0.00000], - [0.31373, 0.70196, 0.00000], - [0.32941, 0.68627, 0.00000], - [0.34510, 0.67059, 0.00000], - [0.36078, 0.65490, 0.00000], - [0.38039, 0.63922, 0.00000], - [0.39608, 0.62353, 0.00000], - [0.41176, 0.60784, 0.00000], - [0.42745, 0.59216, 0.00000], - [0.44314, 0.57647, 0.00000], - [0.46275, 0.56078, 0.00000], - [0.47843, 0.54510, 0.00000], - [0.49412, 0.52941, 0.00000], - [0.50980, 0.51373, 0.00000], - [0.52549, 0.49804, 0.00000], - [0.54118, 0.48235, 0.00000], - [0.56078, 0.46667, 0.00000], - [0.57647, 0.45098, 0.00000], - [0.59216, 0.43529, 0.00000], - [0.60784, 0.41961, 0.00000], - [0.62353, 0.40392, 0.00000], - [0.64314, 0.38824, 0.00000], - [0.65882, 0.37255, 0.00000], - [0.67451, 0.35686, 0.00000], - [0.69020, 0.34118, 0.00000], - [0.70588, 0.32549, 0.00000], - [0.72157, 0.30980, 0.00000], - [0.74118, 0.29412, 0.00000], - [0.75686, 0.27843, 0.00000], - [0.77255, 0.26275, 0.00000], - [0.78824, 0.24706, 0.00000], - [0.80392, 0.23137, 0.00000], - [0.82353, 0.21569, 0.00000], - [0.83922, 0.20000, 0.00000], - [0.85490, 0.18431, 0.00000], - [0.87059, 0.16863, 0.00000], - [0.88627, 0.15294, 0.00000], - [0.90196, 0.13725, 0.00000], - [0.92157, 0.12157, 0.00000], - [0.93725, 0.10588, 0.00000], - [0.95294, 0.09020, 0.00000], - [0.96863, 0.07451, 0.00000], - [0.98431, 0.05882, 0.00000], - [1.00000, 0.04314, 0.00000], -]; - -Mirp = [ - [0.000000, 0.000000, 1.000000], - [0.000000, 0.060295, 1.000000], - [0.000000, 0.112233, 1.000000], - [0.000000, 0.157963, 1.000000], - [0.000000, 0.198902, 1.000000], - [0.000000, 0.236033, 1.000000], - [0.000000, 0.270066, 1.000000], - [0.000000, 0.301531, 1.000000], - [0.000000, 0.330834, 1.000000], - [0.000000, 0.358294, 1.000000], - [0.000000, 0.384164, 1.000000], - [0.000000, 0.408651, 1.000000], - [0.000000, 0.431924, 1.000000], - [0.000000, 0.454124, 1.000000], - [0.000000, 0.475368, 1.000000], - [0.000000, 0.495760, 1.000000], - [0.000000, 0.515384, 1.000000], - [0.000000, 0.534315, 1.000000], - [0.000000, 0.552619, 1.000000], - [0.000000, 0.570353, 1.000000], - [0.000000, 0.587566, 1.000000], - [0.000000, 0.604304, 1.000000], - [0.000000, 0.620605, 1.000000], - [0.000000, 0.636507, 1.000000], - [0.000000, 0.652040, 1.000000], - [0.000000, 0.667234, 1.000000], - [0.000000, 0.682114, 1.000000], - [0.000000, 0.696706, 1.000000], - [0.000000, 0.711030, 1.000000], - [0.000000, 0.725108, 1.000000], - [0.000000, 0.738957, 1.000000], - [0.000000, 0.752595, 1.000000], - [0.000000, 0.766038, 1.000000], - [0.000000, 0.779300, 1.000000], - [0.000000, 0.792396, 1.000000], - [0.000000, 0.805338, 1.000000], - [0.000000, 0.818139, 1.000000], - [0.000000, 0.830811, 1.000000], - [0.000000, 0.843364, 1.000000], - [0.000000, 0.855809, 1.000000], - [0.000000, 0.868156, 1.000000], - [0.000000, 0.880414, 1.000000], - [0.000000, 0.892592, 1.000000], - [0.000000, 0.904700, 1.000000], - [0.000000, 0.916746, 1.000000], - [0.000000, 0.928738, 1.000000], - [0.000000, 0.940684, 1.000000], - [0.000000, 0.952592, 1.000000], - [0.000000, 0.964470, 1.000000], - [0.000000, 0.976326, 1.000000], - [0.000000, 0.988167, 1.000000], - [0.000000, 1.000000, 1.000000], - [0.000000, 1.000000, 0.988167], - [0.000000, 1.000000, 0.976326], - [0.000000, 1.000000, 0.964470], - [0.000000, 1.000000, 0.952592], - [0.000000, 1.000000, 0.940684], - [0.000000, 1.000000, 0.928738], - [0.000000, 1.000000, 0.916746], - [0.000000, 1.000000, 0.904700], - [0.000000, 1.000000, 0.892592], - [0.000000, 1.000000, 0.880414], - [0.000000, 1.000000, 0.868156], - [0.000000, 1.000000, 0.855809], - [0.000000, 1.000000, 0.843364], - [0.000000, 1.000000, 0.830811], - [0.000000, 1.000000, 0.818139], - [0.000000, 1.000000, 0.805338], - [0.000000, 1.000000, 0.792396], - [0.000000, 1.000000, 0.779300], - [0.000000, 1.000000, 0.766038], - [0.000000, 1.000000, 0.752595], - [0.000000, 1.000000, 0.738957], - [0.000000, 1.000000, 0.725108], - [0.000000, 1.000000, 0.711030], - [0.000000, 1.000000, 0.696706], - [0.000000, 1.000000, 0.682114], - [0.000000, 1.000000, 0.667234], - [0.000000, 1.000000, 0.652040], - [0.000000, 1.000000, 0.636507], - [0.000000, 1.000000, 0.620605], - [0.000000, 1.000000, 0.604304], - [0.000000, 1.000000, 0.587566], - [0.000000, 1.000000, 0.570353], - [0.000000, 1.000000, 0.552619], - [0.000000, 1.000000, 0.534315], - [0.000000, 1.000000, 0.515384], - [0.000000, 1.000000, 0.495760], - [0.000000, 1.000000, 0.475368], - [0.000000, 1.000000, 0.454124], - [0.000000, 1.000000, 0.431924], - [0.000000, 1.000000, 0.408651], - [0.000000, 1.000000, 0.384164], - [0.000000, 1.000000, 0.358294], - [0.000000, 1.000000, 0.330834], - [0.000000, 1.000000, 0.301531], - [0.000000, 1.000000, 0.270066], - [0.000000, 1.000000, 0.236033], - [0.000000, 1.000000, 0.198902], - [0.000000, 1.000000, 0.157963], - [0.000000, 1.000000, 0.112233], - [0.000000, 1.000000, 0.060295], - [0.000000, 1.000000, 0.000000], - [0.060295, 1.000000, 0.000000], - [0.112233, 1.000000, 0.000000], - [0.157963, 1.000000, 0.000000], - [0.198902, 1.000000, 0.000000], - [0.236033, 1.000000, 0.000000], - [0.270066, 1.000000, 0.000000], - [0.301531, 1.000000, 0.000000], - [0.330834, 1.000000, 0.000000], - [0.358294, 1.000000, 0.000000], - [0.384164, 1.000000, 0.000000], - [0.408651, 1.000000, 0.000000], - [0.431924, 1.000000, 0.000000], - [0.454124, 1.000000, 0.000000], - [0.475368, 1.000000, 0.000000], - [0.495760, 1.000000, 0.000000], - [0.515384, 1.000000, 0.000000], - [0.534315, 1.000000, 0.000000], - [0.552619, 1.000000, 0.000000], - [0.570353, 1.000000, 0.000000], - [0.587566, 1.000000, 0.000000], - [0.604304, 1.000000, 0.000000], - [0.620605, 1.000000, 0.000000], - [0.636507, 1.000000, 0.000000], - [0.652040, 1.000000, 0.000000], - [0.667234, 1.000000, 0.000000], - [0.682114, 1.000000, 0.000000], - [0.696706, 1.000000, 0.000000], - [0.711030, 1.000000, 0.000000], - [0.725108, 1.000000, 0.000000], - [0.738957, 1.000000, 0.000000], - [0.752595, 1.000000, 0.000000], - [0.766038, 1.000000, 0.000000], - [0.779300, 1.000000, 0.000000], - [0.792396, 1.000000, 0.000000], - [0.805338, 1.000000, 0.000000], - [0.818139, 1.000000, 0.000000], - [0.830811, 1.000000, 0.000000], - [0.843364, 1.000000, 0.000000], - [0.855809, 1.000000, 0.000000], - [0.868156, 1.000000, 0.000000], - [0.880414, 1.000000, 0.000000], - [0.892592, 1.000000, 0.000000], - [0.904700, 1.000000, 0.000000], - [0.916746, 1.000000, 0.000000], - [0.928738, 1.000000, 0.000000], - [0.940684, 1.000000, 0.000000], - [0.952592, 1.000000, 0.000000], - [0.964470, 1.000000, 0.000000], - [0.976326, 1.000000, 0.000000], - [0.988167, 1.000000, 0.000000], - [1.000000, 1.000000, 0.000000], - [1.000000, 0.988167, 0.000000], - [1.000000, 0.976326, 0.000000], - [1.000000, 0.964470, 0.000000], - [1.000000, 0.952592, 0.000000], - [1.000000, 0.940684, 0.000000], - [1.000000, 0.928738, 0.000000], - [1.000000, 0.916746, 0.000000], - [1.000000, 0.904700, 0.000000], - [1.000000, 0.892592, 0.000000], - [1.000000, 0.880414, 0.000000], - [1.000000, 0.868156, 0.000000], - [1.000000, 0.855809, 0.000000], - [1.000000, 0.843364, 0.000000], - [1.000000, 0.830811, 0.000000], - [1.000000, 0.818139, 0.000000], - [1.000000, 0.805338, 0.000000], - [1.000000, 0.792396, 0.000000], - [1.000000, 0.779300, 0.000000], - [1.000000, 0.766038, 0.000000], - [1.000000, 0.752595, 0.000000], - [1.000000, 0.738957, 0.000000], - [1.000000, 0.725108, 0.000000], - [1.000000, 0.711030, 0.000000], - [1.000000, 0.696706, 0.000000], - [1.000000, 0.682114, 0.000000], - [1.000000, 0.667234, 0.000000], - [1.000000, 0.652040, 0.000000], - [1.000000, 0.636507, 0.000000], - [1.000000, 0.620605, 0.000000], - [1.000000, 0.604304, 0.000000], - [1.000000, 0.587566, 0.000000], - [1.000000, 0.570353, 0.000000], - [1.000000, 0.552619, 0.000000], - [1.000000, 0.534315, 0.000000], - [1.000000, 0.515384, 0.000000], - [1.000000, 0.495760, 0.000000], - [1.000000, 0.475368, 0.000000], - [1.000000, 0.454124, 0.000000], - [1.000000, 0.431924, 0.000000], - [1.000000, 0.408651, 0.000000], - [1.000000, 0.384164, 0.000000], - [1.000000, 0.358294, 0.000000], - [1.000000, 0.330834, 0.000000], - [1.000000, 0.301531, 0.000000], - [1.000000, 0.270066, 0.000000], - [1.000000, 0.236033, 0.000000], - [1.000000, 0.198902, 0.000000], - [1.000000, 0.157963, 0.000000], - [1.000000, 0.112233, 0.000000], - [1.000000, 0.060295, 0.000000], - [1.000000, 0.000000, 0.000000], - [1.000000, 0.000000, 0.060295], - [1.000000, 0.000000, 0.112233], - [1.000000, 0.000000, 0.157963], - [1.000000, 0.000000, 0.198902], - [1.000000, 0.000000, 0.236033], - [1.000000, 0.000000, 0.270066], - [1.000000, 0.000000, 0.301531], - [1.000000, 0.000000, 0.330834], - [1.000000, 0.000000, 0.358294], - [1.000000, 0.000000, 0.384164], - [1.000000, 0.000000, 0.408651], - [1.000000, 0.000000, 0.431924], - [1.000000, 0.000000, 0.454124], - [1.000000, 0.000000, 0.475368], - [1.000000, 0.000000, 0.495760], - [1.000000, 0.000000, 0.515384], - [1.000000, 0.000000, 0.534315], - [1.000000, 0.000000, 0.552619], - [1.000000, 0.000000, 0.570353], - [1.000000, 0.000000, 0.587566], - [1.000000, 0.000000, 0.604304], - [1.000000, 0.000000, 0.620605], - [1.000000, 0.000000, 0.636507], - [1.000000, 0.000000, 0.652040], - [1.000000, 0.000000, 0.667234], - [1.000000, 0.000000, 0.682114], - [1.000000, 0.000000, 0.696706], - [1.000000, 0.000000, 0.711030], - [1.000000, 0.000000, 0.725108], - [1.000000, 0.000000, 0.738957], - [1.000000, 0.000000, 0.752595], - [1.000000, 0.000000, 0.766038], - [1.000000, 0.000000, 0.779300], - [1.000000, 0.000000, 0.792396], - [1.000000, 0.000000, 0.805338], - [1.000000, 0.000000, 0.818139], - [1.000000, 0.000000, 0.830811], - [1.000000, 0.000000, 0.843364], - [1.000000, 0.000000, 0.855809], - [1.000000, 0.000000, 0.868156], - [1.000000, 0.000000, 0.880414], - [1.000000, 0.000000, 0.892592], - [1.000000, 0.000000, 0.904700], - [1.000000, 0.000000, 0.916746], - [1.000000, 0.000000, 0.928738], - [1.000000, 0.000000, 0.940684], - [1.000000, 0.000000, 0.952592], - [1.000000, 0.000000, 0.964470], - [1.000000, 0.000000, 0.976326], - [1.000000, 0.000000, 0.988167], - [1.000000, 0.000000, 1.000000], -]; diff --git a/Tigger/Images/ColormapTables/__init__.py b/Tigger/Images/ColormapTables/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Tigger/Images/Colormaps.py b/Tigger/Images/Colormaps.py deleted file mode 100644 index a0f141d..0000000 --- a/Tigger/Images/Colormaps.py +++ /dev/null @@ -1,407 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -from PyQt4.Qwt5 import * - -import math -import numpy -import numpy.ma -from scipy.ndimage import measurements - -import Kittens.utils -import copy - -_verbosity = Kittens.utils.verbosity(name="colormap"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - -class IntensityMap (object): - """An IntensityMap maps a float array into a 0...1 range.""" - def __init__ (self,dmin=None,dmax=None): - """Constructor. An optional data range may be supplied."""; - self.range = None; - if dmin is not None: - if dmax is None: - raise TypeError,"both dmin and dmax must be specified, or neither."""; - self.setDataRange(dmin,dmax); - - def copy (self): - return copy.copy(self); - - def setDataRange (self,dmin,dmax): - """Sets the data range."""; - self.range = dmin,dmax; - - def setDataSubset (self,subset,minmax=None): - """Sets the data subset."""; - self.subset = subset; - self.subset_minmax = minmax; - - def getDataSubset (self): - return self.subset,self.subset_minmax; - - def getDataRange (self,data): - """Returns the set data range, or uses data min/max if it is not set"""; - # use data min/max if no explicit ranges are set - return self.range or measurements.extrema(data)[:2]; - - def remap (self,data): - """Remaps data into 0...1 range"""; - raise RuntimeError,"remap() not implemented in "+str(type(self)); - -class LinearIntensityMap (IntensityMap): - """This scales data linearly between preset min and max values.""" - def remap (self,data): - d0,d1 = self.getDataRange(data); - dd = d1 - d0; - if dd: - return ((data-d0)/dd).clip(0,1); - else: - return numpy.zeros(data.shape,float); - -class LogIntensityMap (IntensityMap): - """This scales data linearly between preset min and max values.""" - def __init__ (self,log_cycles=6): - self.log_cycles = log_cycles; - - def remap (self,data): - # d0,d1 is current data range - d0,d1 = self.getDataRange(data); - if d0 == d1: - return numpy.zeros(data.shape,float); - dmax = d1 - d0; - data = data - d0; - dmin = dmax*(10**(-self.log_cycles)); - # clip data to between dmin and dmax, and take log - data = numpy.ma.log10(data.clip(dmin,dmax)); - # now rescale - return (data - math.log10(dmin))/(math.log10(dmax) - math.log10(dmin)); - - -class HistEqIntensityMap (IntensityMap): - def __init__ (self,nbins=256): - """Creates intensity mapper which uses histogram equalization."""; - IntensityMap.__init__(self); - self._nbins = nbins; - self._cdf = self._bins = self.subset = None; - - def setDataSubset (self,subset,minmax=None): - IntensityMap.setDataSubset(self,subset,minmax); - self._bins = None; # to recompute the CDF - - def setDataRange (self,*range): - IntensityMap.setDataRange(self,*range); - self._bins = None; # to recompute the CDF - - def _computeCDF (self,data): - """Recomputes the CDF using the current data subset and range"""; - dmin,dmax = self.getDataRange(self.subset if self.subset is not None else data); - if dmin == dmax: - self._cdf = None; - else: - dprint(1,"computing CDF for range",dmin,dmax); - # make cumulative histogram, normalize to 0...1 - hist = measurements.histogram(self.subset if self.subset is not None else data,dmin,dmax,self._nbins); - cdf = numpy.cumsum(hist); - cdf = cdf/float(cdf[-1]); - # append 0 at beginning, as left side of bin - self._cdf = numpy.zeros(len(cdf)+1,float); - self._cdf[1:] = cdf[...]; - # make array of bin edges - self._bins = dmin + (dmax-dmin)*numpy.arange(self._nbins+1)/float(self._nbins); - - def remap (self,data): - if self._bins is None: - self._computeCDF(data); - if self._cdf is None: - return numpy.zeros(data.shape,float); - values = numpy.interp(data.ravel(),self._bins,self._cdf).reshape(data.shape); - if hasattr(data,'mask'): - values = numpy.ma.masked_array(values,data.mask); - return values; - -class Colormap (QObject): - """A Colormap provides operations for turning normalized float arrays into QImages. The default implementation is a linear colormap between two colors. - """; - def __init__ (self,name,color0=QColor("black"),color1=QColor("white"),alpha=(1,1)): - QObject.__init__(self); - self.name = name; - # color is either specified as one argument (which should then be a [3,n] or [4,n] array), - # or as two QColors orstring names. - if isinstance(color0,(list,tuple)): - self._rgb = numpy.array(color0); - if self._rgb.shape[1] != 3 or self._rgb.shape[0] < 2: - raise TypeError,"expected [N,3] (N>=2) array as first argument"; - else: - if isinstance(color0,str): - color0 = QColor(color0); - if isinstance(color1,str): - color1 = QColor(color1); - self._rgb = numpy.array([[color0.red(),color0.green(),color0.blue()], - [color1.red(),color1.green(),color1.blue()]])/255.; - self._rgb_arg = numpy.arange(self._rgb.shape[0])/(self._rgb.shape[0]-1.0) - # alpha array - self._alpha = numpy.array(alpha).astype(float); - self._alpha_arg = numpy.arange(len(alpha))/(len(alpha)-1.0); - # background brush - self._brush = None; - - def makeQImage (self,width,height): - data = numpy.zeros((width,height),float); - data[...] = (numpy.arange(width)/(width-1.))[:,numpy.newaxis]; - # make brush image -- diag background, with colormap on top - img = QImage(width,height,QImage.Format_RGB32); - painter = QPainter(img); - painter.fillRect(0,0,width,height,QBrush(QColor("white"))); - painter.fillRect(0,0,width,height,QBrush(Qt.BDiagPattern)); - painter.drawImage(0,0,self.colorize(data)); - painter.end(); - return img; - - def makeQPixmap (self,width,height): - data = numpy.zeros((width,height),float); - data[...] = (numpy.arange(width)/(width-1.))[:,numpy.newaxis]; - # make brush image -- diag background, with colormap on top - img = QPixmap(width,height); - painter = QPainter(img); - painter.fillRect(0,0,width,height,QBrush(QColor("white"))); - painter.fillRect(0,0,width,height,QBrush(Qt.BDiagPattern)); - painter.drawImage(0,0,self.colorize(data)); - painter.end(); - return img; - - def makeBrush (self,width,height): - return QBrush(self.makeQImage(width,height)); - - def colorize (self,data,alpha=None): - """Converts normalized data (0...1) array into a QImage of the same dimensions. - 'alpha', if set, is a 0...1 array of the same size, which is mapped to the alpha channel - (i.e. 0 for fully transparent and 1 for fully opaque). - If data is a masked array, masked pixels will be fully transparent."""; - # setup alpha channel - if alpha is None: - alpha = numpy.interp(data.ravel(),self._alpha_arg,self._alpha).reshape(data.shape); - alpha = numpy.round(255*alpha).astype(numpy.int32).clip(0,255); - # make RGB arrays - rgbs = [ (numpy.interp(data.ravel(),self._rgb_arg,self._rgb[:,i]). - reshape(data.shape)*255).round().astype(numpy.int32).clip(0,255) - for i in range(3) ]; - # add data mask - mask = getattr(data,'mask',None); - if mask is not None and mask is not False: - alpha[mask] = 0; - for x in rgbs: - x[mask] = 0; - # do the deed - return self.QARGBImage(alpha,*rgbs); - - def makeControlWidgets (self,parent): - """Creates control widgets for the colormap's internal parameters. - "parent" is a parent widget. - Returns None if no controls are required"""; - return None; - - class QARGBImage (QImage): - """This is a QImage which is constructed from an A,R,G,B arrays."""; - def __init__ (self,a,r,g,b): - nx,ny = r.shape; - argb = (a<<24) | (r<<16) | (g<<8) | b; - # transpose array, as it is in column-major (C order), while QImages are in row-major order - dprint(5,"making qimage of size",nx,ny); - self._buffer = argb.transpose().tostring(); - QImage.__init__(self,self._buffer,nx,ny,QImage.Format_ARGB32); - -class ColormapWithControls (Colormap): - """This is a base class for a colormap with controls knobs"""; - class SliderControl (QObject): - """This class implements a slider control for a colormap"""; - def __init__ (self,name,value,minval,maxval,step,format="%s: %.1f"): - QObject.__init__(self); - self.name,self.value,self.minval,self.maxval,self.step,self.format = \ - name,value,minval,maxval,step,format; - self._default = value; - self._wlabel = None; - - def makeControlWidgets (self,parent,gridlayout,row,column): - toprow = QWidget(parent); - gridlayout.addWidget(toprow,row*2,column); - top_lo = QHBoxLayout(toprow); - top_lo.setContentsMargins(0,0,0,0); - self._wlabel = QLabel(self.format%(self.name,self.value),toprow); - top_lo.addWidget(self._wlabel); - self._wreset = QToolButton(toprow); - self._wreset.setText("reset"); - self._wreset.setToolButtonStyle(Qt.ToolButtonTextOnly); - self._wreset.setAutoRaise(True); - self._wreset.setEnabled(self.value != self._default); - QObject.connect(self._wreset,SIGNAL("clicked()"),self._resetValue); - top_lo.addWidget(self._wreset); - self._wslider = QwtSlider(parent); - # This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above - self._wslider_timer = QTimer(parent); - self._wslider_timer.setSingleShot(True); - self._wslider_timer.setInterval(500); - QObject.connect(self._wslider_timer,SIGNAL("timeout()"),self.setValue); - gridlayout.addWidget(self._wslider,row*2+1,column); - self._wslider.setRange(self.minval,self.maxval); - self._wslider.setStep(self.step); - self._wslider.setValue(self.value); - self._wslider.setTracking(False); - QObject.connect(self._wslider,SIGNAL("valueChanged(double)"),self.setValue); - QObject.connect(self._wslider,SIGNAL("sliderMoved(double)"),self._previewValue); - - def _resetValue (self): - self._wslider.setValue(self._default); - self.setValue(self._default); - - def setValue (self,value=None,notify=True): - # only update widgets if already created - self.value = value; - if self._wlabel is not None: - if value is None: - self.value = value = self._wslider.value(); - self._wreset.setEnabled(value != self._default); - self._wlabel.setText(self.format%(self.name,self.value)); - # stop timer if being called to finalize the change in value - if notify: - self._wslider_timer.stop(); - self.emit(SIGNAL("valueChanged"),self.value); - - def _previewValue (self,value): - self.setValue(notify=False); - self._wslider_timer.start(500); - self.emit(SIGNAL("valueMoved"),self.value); - - def emitChange (self,*dum): - self.emit(SIGNAL("colormapChanged")); - - def emitPreview (self,*dum): - self.emit(SIGNAL("colormapPreviewed")); - - def loadConfig (self,config): - pass; - - def saveConfig (self,config,save=True): - pass; - -class CubeHelixColormap (ColormapWithControls): - """This implements the "cubehelix" colour scheme proposed by Dave Green: - D. Green 2011, Bull. Astr. Soc. India (2011) 39, 289–295 - http://arxiv.org/pdf/1108.5083v1 - """ - def __init__(self,gamma=1,rgb=0.5,rots=-1.5,hue=1.2,name="CubeHelix"): - ColormapWithControls.__init__(self,name); - self.gamma = self.SliderControl("Gamma",gamma,0,6,.1); - self.color = self.SliderControl("Colour",rgb,0,3,.1); - self.cycles = self.SliderControl("Cycles",rots,-10,10,.1); - self.hue = self.SliderControl("Hue",hue,0,2,.1); - - def colorize (self,data,alpha=None): - """Converts normalized data (0...1) array into a QImage of the same dimensions. - 'alpha', if set, is a 0...1 array of the same size, which is mapped to the alpha channel - (i.e. 0 for fully transparent and 1 for fully opaque). - If data is a masked array, masked pixels will be fully transparent."""; - # setup alpha channel - if alpha is None: - alpha = numpy.zeros(data.shape,dtype=numpy.int32); - alpha[...] = 255; - else: - alpha = numpy.round(255*alpha).astype(numpy.int32).clip(0,255); - # make RGB arrays - dg = data**self.gamma.value; - a = self.hue.value*dg*(1-dg)/2; - phi = 2*math.pi*(self.color.value/3 + self.cycles.value*data); - cosphi = a*numpy.cos(phi); - sinphi = a*numpy.sin(phi); - r = dg - 0.14861*cosphi + 1.78277*sinphi; - g = dg - 0.29227*cosphi - 0.90649*sinphi; - b = dg + 1.97249*cosphi; - rgbs = [ (x*255).round().astype(numpy.int32).clip(0,255) for x in r,g,b ]; - # add data mask - mask = getattr(data,'mask',None); - if mask is not None and mask is not False: - alpha[mask] = 0; - for x in rgbs: - x[mask] = 0; - # do the deed - return self.QARGBImage(alpha,*rgbs); - - def makeControlWidgets (self,parent): - """Creates control widgets for the colormap's internal parameters. - "parent" is a parent widget. - Returns None if no controls are required"""; - top = QWidget(parent); - layout = QGridLayout(top); - layout.setContentsMargins(0,0,0,0); - for irow,icol,control in ((0,0,self.gamma),(0,1,self.color),(1,0,self.cycles),(1,1,self.hue)): - control.makeControlWidgets(top,layout,irow,icol); - QObject.connect(control,SIGNAL("valueChanged"),self.emitChange); - QObject.connect(control,SIGNAL("valueMoved"),self.emitPreview); - return top; - - def loadConfig (self,config): - for name in "gamma","color","cycles","hue": - control = getattr(self,name); - value = config.getfloat("cubehelix-colourmap-%s"%name,control.value); - control.setValue(value,notify=False); - - def saveConfig (self,config,save=True): - for name in "gamma","color","cycles","hue": - control = getattr(self,name); - config.set("cubehelix-colourmap-%s"%name,control.value,save=save); - - -# instantiate "static" colormaps (i.e. those that have no internal parameters, and thus can be -# shared among images without instantiating a new Colormap object for each) -GreyscaleColormap = Colormap("Greyscale"); -TransparentFuchsiaColormap = Colormap("Transparent Fuchsia",color0="fuchsia",color1="fuchsia",alpha=(0,1)); - -from ColormapTables import Karma -_karma_colormaps = [ - Colormap(cmap,getattr(Karma,cmap)) - for cmap in [ - "Background", - "Heat", - "Isophot", - "Mousse", - "Rainbow", - "RGB", - "RGB2", - "Smooth", - "Staircase", - "Mirp", - "Random" ] - ]; - -def getColormapList (): - """Returns list of Colormap instances.""" - - # Some colormaps need a unique instantiation (because they have parameters) - # For the rest, use the static objects - return [ GreyscaleColormap, - CubeHelixColormap(), - TransparentFuchsiaColormap ] + _karma_colormaps; diff --git a/Tigger/Images/ControlDialog.py b/Tigger/Images/ControlDialog.py deleted file mode 100644 index 50e4bb7..0000000 --- a/Tigger/Images/ControlDialog.py +++ /dev/null @@ -1,951 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute f and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -from PyQt4.Qwt5 import * - -import math -import numpy -import sys -import time -from scipy.ndimage import measurements - -import Kittens.utils -from Kittens.utils import curry,PersistentCurrier -from Kittens.widgets import BusyIndicator - -from Tigger.Images import SkyImage,Colormaps -from Tigger import pixmaps -from Tigger.Widgets import FloatValidator,TiggerPlotCurve,TiggerPlotMarker - -from RenderControl import RenderControl,dprint,dprintf - -DataValueFormat = "%.4g"; - -class Separator (QWidget): - def __init__ (self,parent,label,extra_widgets=[],style=QFrame.HLine+QFrame.Raised,offset=16): - QWidget.__init__(self,parent); - lo = QHBoxLayout(self); - lo.setContentsMargins(0,0,0,0); - lo.setSpacing(4); - if offset: - frame = QFrame(self); - frame.setFrameStyle(style); - frame.setMinimumWidth(offset); - lo.addWidget(frame,0); - lo.addWidget(QLabel(label,self),0); - frame = QFrame(self); - frame.setFrameStyle(style); - lo.addWidget(frame,1); - for w in extra_widgets: - lo.addWidget(w,0); - - -class ImageControlDialog (QDialog): - def __init__ (self,parent,rc,imgman): - """An ImageControlDialog is initialized with a parent widget, a RenderControl object, - and an ImageManager object"""; - QDialog.__init__(self,parent); - image = rc.image; - self.setWindowTitle("%s: Colour Controls"%image.name); - self.setWindowIcon(pixmaps.colours.icon()); - self.setModal(False); - self.image = image; - self._rc = rc; - self._imgman = imgman; - self._currier = PersistentCurrier(); - - # init internal state - self._prev_range = self._display_range = None,None; - self._hist = None; - self._geometry = None; - - # create layouts - lo0 = QVBoxLayout(self); -# lo0.setContentsMargins(0,0,0,0); - - # histogram plot - whide = self.makeButton("Hide",self.hide,width=128); - whide.setShortcut(Qt.Key_F9); - lo0.addWidget(Separator(self,"Histogram and ITF",extra_widgets=[whide])); - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - self._histplot = QwtPlot(self); - self._histplot.setAutoDelete(False); - lo1.addWidget(self._histplot,1); - lo2 = QHBoxLayout(); - lo2.setContentsMargins(0,0,0,0); - lo2.setSpacing(2); - lo0.addLayout(lo2); - lo0.addLayout(lo1); - self._wautozoom = QCheckBox("autozoom",self); - self._wautozoom.setChecked(True); - self._wautozoom.setToolTip("""

If checked, then the histrogram plot will zoom in automatically when - you narrow the current intensity range.

"""); - self._wlogy = QCheckBox("log Y",self); - self._wlogy.setChecked(True); - self._ylogscale = True; - self._wlogy.setToolTip("""

If checked, a log-scale Y axis is used for the histogram plot instead of a linear one."""); - QObject.connect(self._wlogy,SIGNAL("toggled(bool)"),self._setHistLogScale); - self._whistunzoom = self.makeButton("",self._unzoomHistogram,icon=pixmaps.full_range.icon()); - self._whistzoomout = self.makeButton("-",self._currier.curry(self._zoomHistogramByFactor,math.sqrt(.1))); - self._whistzoomin = self.makeButton("+",self._currier.curry(self._zoomHistogramByFactor,math.sqrt(10))); - self._whistzoomin.setToolTip("""

Click to zoom into the histogram plot by one step. This does not - change the current intensity range.

"""); - self._whistzoomout.setToolTip("""

Click to zoom out of the histogram plot by one step. This does not - change the current intensity range.

"""); - self._whistunzoom.setToolTip("""

Click to reset the histogram plot back to its full extent. - This does not change the current intensity range.

"""); - self._whistzoom = QwtWheel(self); - self._whistzoom.setOrientation(Qt.Horizontal); - self._whistzoom.setMaximumWidth(80); - self._whistzoom.setRange(10,0); - self._whistzoom.setStep(0.1); - self._whistzoom.setTickCnt(30); - self._whistzoom.setTracking(False); - QObject.connect(self._whistzoom,SIGNAL("valueChanged(double)"),self._zoomHistogramFinalize); - QObject.connect(self._whistzoom,SIGNAL("sliderMoved(double)"),self._zoomHistogramPreview); - self._whistzoom.setToolTip("""

Use this wheel control to zoom in/out of the histogram plot. - This does not change the current intensity range. - Note that the zoom wheel should also respond to your mouse wheel, if you have one.

"""); - # This works around a stupid bug in QwtSliders -- when using the mousewheel, only sliderMoved() signals are emitted, - # with no final valueChanged(). If we want to do a fast preview of something on sliderMoved(), and a "slow" final - # step on valueChanged(), we're in trouble. So we start a timer on sliderMoved(), and if the timer expires without - # anything else happening, do a valueChanged(). - # Here we use a timer to call zoomHistogramFinalize() w/o an argument. - self._whistzoom_timer = QTimer(self); - self._whistzoom_timer.setSingleShot(True); - self._whistzoom_timer.setInterval(500); - QObject.connect(self._whistzoom_timer,SIGNAL("timeout()"),self._zoomHistogramFinalize); - # set same size for all buttons and controls - width = 24; - for w in self._whistunzoom,self._whistzoomin,self._whistzoomout: - w.setMinimumSize(width,width); - w.setMaximumSize(width,width); - self._whistzoom.setMinimumSize(80,width); - self._wlab_histpos_text = "(hover here for help)"; - self._wlab_histpos = QLabel(self._wlab_histpos_text,self); - self._wlab_histpos.setToolTip(""" -

The plot shows a histogram of either the full image or its selected subset - (as per the "Data subset" section below).

-

The current intensity range is indicated by the grey box - in the plot.

-

Use the left mouse button to change the low intensity limit, and the right - button (on Macs, use Ctrl-click) to change the high limit.

-

Use Shift with the left mouse button to zoom into an area of the histogram, - or else use the "zoom wheel" control or the plus/minus toolbuttons above the histogram to zoom in or out. - To zoom back out to the full extent of the histogram, click on the rightmost button above the histogram.

- """); - lo2.addWidget(self._wlab_histpos,1); - lo2.addWidget(self._wautozoom); - lo2.addWidget(self._wlogy,0); - lo2.addWidget(self._whistzoomin,0); - lo2.addWidget(self._whistzoom,0); - lo2.addWidget(self._whistzoomout,0); - lo2.addWidget(self._whistunzoom,0); - self._zooming_histogram = False; - - sliced_axes = rc.slicedAxes(); - dprint(1,"sliced axes are",sliced_axes); - self._stokes_axis = None; - - # subset indication - lo0.addWidget(Separator(self,"Data subset")); - # sliced axis selectors - self._wslicers = []; - if sliced_axes: - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo1.setSpacing(2); - lo0.addLayout(lo1); - lo1.addWidget(QLabel("Current slice: ",self)); - for i,(iextra,name,labels) in enumerate(sliced_axes): - lo1.addWidget(QLabel("%s:"%name,self)); - if name == "STOKES": - self._stokes_axis = iextra; - # add controls - wslicer = QComboBox(self); - self._wslicers.append(wslicer); - wslicer.addItems(labels); - wslicer.setToolTip("""

Selects current slice along the %s axis.

"""%name); - wslicer.setCurrentIndex(self._rc.currentSlice()[iextra]); - QObject.connect(wslicer,SIGNAL("activated(int)"),self._currier.curry(self._rc.changeSlice,iextra)); - lo2 = QVBoxLayout(); - lo1.addLayout(lo2); - lo2.setContentsMargins(0,0,0,0); - lo2.setSpacing(0); - wminus = QToolButton(self); - wminus.setArrowType(Qt.UpArrow); - QObject.connect(wminus,SIGNAL("clicked()"),self._currier.curry(self._rc.incrementSlice,iextra,1)); - if i == 0: - wminus.setShortcut(Qt.SHIFT+Qt.Key_F7); - elif i == 1: - wminus.setShortcut(Qt.SHIFT+Qt.Key_F8); - wplus = QToolButton(self); - wplus.setArrowType(Qt.DownArrow); - QObject.connect(wplus,SIGNAL("clicked()"),self._currier.curry(self._rc.incrementSlice,iextra,-1)); - if i == 0: - wplus.setShortcut(Qt.Key_F7); - elif i == 1: - wplus.setShortcut(Qt.Key_F8); - wminus.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Fixed); - wplus.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Fixed); - sz = QSize(12,8); - wminus.setMinimumSize(sz); - wplus.setMinimumSize(sz); - wminus.resize(sz); - wplus.resize(sz); - lo2.addWidget(wminus); - lo2.addWidget(wplus); - lo1.addWidget(wslicer); - lo1.addSpacing(5); - lo1.addStretch(1); - # subset indicator - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo1.setSpacing(2); - lo0.addLayout(lo1); - self._wlab_subset = QLabel("Subset: xxx",self); - self._wlab_subset.setToolTip("""

This indicates the current data subset to which the histogram - and the stats given here apply. Use the "Reset to" control on the right to change the - current subset and recompute the histogram and stats.

"""); - lo1.addWidget(self._wlab_subset,1); - - self._wreset_full = self.makeButton(u"\u2192 full",self._rc.setFullSubset); - lo1.addWidget(self._wreset_full); - if sliced_axes: -# if self._stokes_axis is not None and len(sliced_axes)>1: -# self._wreset_stokes = self.makeButton(u"\u21920Stokes",self._rc.setFullSubset); - self._wreset_slice = self.makeButton(u"\u2192 slice",self._rc.setSliceSubset); - lo1.addWidget(self._wreset_slice); - else: - self._wreset_slice = None; - - # min/max controls - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo0.addLayout(lo1,0); - self._wlab_stats = QLabel(self); - lo1.addWidget(self._wlab_stats,0); - self._wmore_stats = self.makeButton("more...",self._showMeanStd); - self._wlab_stats.setMinimumHeight(self._wmore_stats.height()); - lo1.addWidget(self._wmore_stats,0); - lo1.addStretch(1); - - # intensity controls - lo0.addWidget(Separator(self,"Intensity mapping")); - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo1.setSpacing(2); - lo0.addLayout(lo1,0); - self._range_validator = FloatValidator(self); - self._wrange = QLineEdit(self),QLineEdit(self); - self._wrange[0].setToolTip("""

This is the low end of the intensity range.

"""); - self._wrange[1].setToolTip("""

This is the high end of the intensity range.

"""); - for w in self._wrange: - w.setValidator(self._range_validator); - QObject.connect(w,SIGNAL("editingFinished()"),self._changeDisplayRange); - lo1.addWidget(QLabel("low:",self),0); - lo1.addWidget(self._wrange[0],1); - self._wrangeleft0 = self.makeButton(u"\u21920",self._setZeroLeftLimit,width=32); - self._wrangeleft0.setToolTip("""

Click this to set the low end of the intensity range to 0.

"""); - lo1.addWidget(self._wrangeleft0,0); - lo1.addSpacing(8); - lo1.addWidget(QLabel("high:",self),0); - lo1.addWidget(self._wrange[1],1); - lo1.addSpacing(8); - self._wrange_full = self.makeButton(None,self._setHistDisplayRange,icon=pixmaps.intensity_graph.icon()); - lo1.addWidget(self._wrange_full); - self._wrange_full.setToolTip("""

Click this to reset the intensity range to the current extent of the histogram plot.

"""); - # add menu for display range - range_menu = QMenu(self); - wrange_menu = QToolButton(self); - wrange_menu.setText("Reset to"); - wrange_menu.setToolTip("""

Use this to reset the intensity range to various pre-defined settings.

"""); - lo1.addWidget(wrange_menu); - self._qa_range_full = range_menu.addAction(pixmaps.full_range.icon(),"Full subset",self._rc.resetSubsetDisplayRange); - self._qa_range_hist = range_menu.addAction(pixmaps.intensity_graph.icon(),"Current histogram limits",self._setHistDisplayRange); - for percent in (99.99,99.9,99.5,99,98,95): - range_menu.addAction("%g%%"%percent,self._currier.curry(self._changeDisplayRangeToPercent,percent)); - wrange_menu.setMenu(range_menu); - wrange_menu.setPopupMode(QToolButton.InstantPopup); - - lo1 = QGridLayout(); - lo1.setContentsMargins(0,0,0,0); - lo0.addLayout(lo1,0); - self._wimap = QComboBox(self); - lo1.addWidget(QLabel("Intensity policy:",self),0,0); - lo1.addWidget(self._wimap,1,0); - self._wimap.addItems(rc.getIntensityMapNames()); - QObject.connect(self._wimap,SIGNAL("currentIndexChanged(int)"),self._rc.setIntensityMapNumber); - self._wimap.setToolTip("""

Use this to change the type of the intensity transfer function (ITF).

"""); - - # log cycles control - lo1.setColumnStretch(1,1); - self._wlogcycles_label = QLabel("Log cycles: ",self); - lo1.addWidget(self._wlogcycles_label,0,1); -# self._wlogcycles = QwtWheel(self); -# self._wlogcycles.setTotalAngle(360); - self._wlogcycles = QwtSlider(self); - self._wlogcycles.setToolTip("""

Use this to change the log-base for the logarithmic intensity transfer function (ITF).

"""); - # This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above - self._wlogcycles_timer = QTimer(self); - self._wlogcycles_timer.setSingleShot(True); - self._wlogcycles_timer.setInterval(500); - QObject.connect(self._wlogcycles_timer,SIGNAL("timeout()"),self._setIntensityLogCycles); - lo1.addWidget(self._wlogcycles,1,1); - self._wlogcycles.setRange(1.,10); - self._wlogcycles.setStep(0.1); - self._wlogcycles.setTracking(False); - QObject.connect(self._wlogcycles,SIGNAL("valueChanged(double)"),self._setIntensityLogCycles); - QObject.connect(self._wlogcycles,SIGNAL("sliderMoved(double)"),self._previewIntensityLogCycles); - self._updating_imap = False; - - # lock intensity map - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo0.addLayout(lo1,0); -# lo1.addWidget(QLabel("Lock range accross",self)); - wlock = QCheckBox("Lock display range",self); - wlock.setToolTip("""

If checked, then the intensity range will be locked. The ranges of all locked images - change simultaneously.

"""); - lo1.addWidget(wlock); - wlockall = QToolButton(self); - wlockall.setIcon(pixmaps.locked.icon()); - wlockall.setText("Lock all to this"); - wlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon); - wlockall.setAutoRaise(True); - wlockall.setToolTip("""

Click this to lock together the intensity ranges of all images.

"""); - lo1.addWidget(wlockall); - wunlockall = QToolButton(self); - wunlockall.setIcon(pixmaps.unlocked.icon()); - wunlockall.setText("Unlock all"); - wunlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon); - wunlockall.setAutoRaise(True); - wunlockall.setToolTip("""

Click this to unlock the intensity ranges of all images.

"""); - lo1.addWidget(wunlockall); - wlock.setChecked(self._rc.isDisplayRangeLocked()); - QObject.connect(wlock,SIGNAL("clicked(bool)"),self._rc.lockDisplayRange); - QObject.connect(wlockall,SIGNAL("clicked()"),self._currier.curry(self._imgman.lockAllDisplayRanges,self._rc)); - QObject.connect(wunlockall,SIGNAL("clicked()"),self._imgman.unlockAllDisplayRanges); - QObject.connect(self._rc,SIGNAL("displayRangeLocked"),wlock.setChecked); - -# self._wlock_imap_axis = [ QCheckBox(name,self) for iaxis,name,labels in sliced_axes ]; -# for iw,w in enumerate(self._wlock_imap_axis): -# QObject.connect(w,SIGNAL("toggled(bool)"),self._currier.curry(self._rc.lockDisplayRangeForAxis,iw)); -# lo1.addWidget(w,0); - lo1.addStretch(1); - - # lo0.addWidget(Separator(self,"Colourmap")); - # color bar - self._colorbar = QwtPlot(self); - lo0.addWidget(self._colorbar); - self._colorbar.setAutoDelete(False); - self._colorbar.setMinimumHeight(32); - self._colorbar.enableAxis(QwtPlot.yLeft,False); - self._colorbar.enableAxis(QwtPlot.xBottom,False); - # color plot - self._colorplot = QwtPlot(self); - lo0.addWidget(self._colorplot); - self._colorplot.setAutoDelete(False); - self._colorplot.setMinimumHeight(64); - self._colorplot.enableAxis(QwtPlot.yLeft,False); - self._colorplot.enableAxis(QwtPlot.xBottom,False); - # self._colorplot.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Preferred); - self._colorbar.hide(); - self._colorplot.hide(); - # color controls - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo0.addLayout(lo1,1); - lo1.addWidget(QLabel("Colourmap:",self)); - # colormap list - ### NB: use setIconSize() and icons in QComboBox!!! - self._wcolmaps = QComboBox(self); - self._wcolmaps.setIconSize(QSize(128,16)); - self._wcolmaps.setToolTip("""

Use this to select a different colourmap.

"""); - for cmap in self._rc.getColormapList(): - self._wcolmaps.addItem(QIcon(cmap.makeQPixmap(128,16)),cmap.name); - lo1.addWidget(self._wcolmaps); - QObject.connect(self._wcolmaps,SIGNAL("activated(int)"),self._rc.setColorMapNumber); - # add widgetstack for colormap controls - self._wcolmap_control_stack = QStackedWidget(self); - self._wcolmap_control_blank = QWidget(self._wcolmap_control_stack); - self._wcolmap_control_stack.addWidget(self._wcolmap_control_blank); - lo0.addWidget(self._wcolmap_control_stack); - self._colmap_controls = []; - # add controls to stack - for index,cmap in enumerate(self._rc.getColormapList()): - if isinstance(cmap,Colormaps.ColormapWithControls): - controls = cmap.makeControlWidgets(self._wcolmap_control_stack); - self._wcolmap_control_stack.addWidget(controls); - QObject.connect(cmap,SIGNAL("colormapChanged"), - self._currier.curry(self._previewColormapParameters,index,cmap)); - QObject.connect(cmap,SIGNAL("colormapPreviewed"), - self._currier.curry(self._previewColormapParameters,index,cmap)); - self._colmap_controls.append(controls); - else: - self._colmap_controls.append(self._wcolmap_control_blank); - - # connect updates from renderControl and image - self.image.connect(SIGNAL("slice"),self._updateImageSlice); - QObject.connect(self._rc,SIGNAL("intensityMapChanged"),self._updateIntensityMap); - QObject.connect(self._rc,SIGNAL("colorMapChanged"),self._updateColorMap); - QObject.connect(self._rc,SIGNAL("dataSubsetChanged"),self._updateDataSubset); - QObject.connect(self._rc,SIGNAL("displayRangeChanged"),self._updateDisplayRange); - - # update widgets - self._setupHistogramPlot(); - self._updateDataSubset(*self._rc.currentSubset()); - self._updateColorMap(image.colorMap()); - self._updateIntensityMap(rc.currentIntensityMap(),rc.currentIntensityMapNumber()); - self._updateDisplayRange(*self._rc.displayRange()); - - def makeButton (self,label,callback=None,width=None,icon=None): - btn = QToolButton(self); -# btn.setAutoRaise(True); - label and btn.setText(label); - icon and btn.setIcon(icon); -# btn = QPushButton(label,self); - # btn.setFlat(True); - if width: - btn.setMinimumWidth(width); - btn.setMaximumWidth(width); - if icon: - btn.setIcon(icon); - if callback: - QObject.connect(btn,SIGNAL("clicked()"),callback); - return btn; - -# def closeEvent (self,ev): -# ev.ignore(); -# self.hide(); - - def hide(self): - self._geometry = self.geometry(); - QDialog.hide(self); - - def show (self): - dprint(4,"show entrypoint"); - if self._geometry: - dprint(4,"setting geometry"); - self.setGeometry(self._geometry); - if self._hist is None: - busy = BusyIndicator(); - dprint(4,"updating histogram"); - self._updateHistogram(); - dprint(4,"updating stats"); - self._updateStats(self._subset,self._subset_range); - busy = None; - dprint(4,"calling QDialog.show"); - QDialog.show(self); - - # number of bins used to compute intensity transfer function - NumItfBins = 1000; - # number of bins used for displaying histograms - NumHistBins = 500; - # number of bins used for high-res histograms - NumHistBinsHi = 10000; - # colorbar height, as fraction of plot area - ColorBarHeight = 0.1; - - class HistLimitPicker (QwtPlotPicker): - """Auguments QwtPlotPicker with functions for selecting hist min/max values"""; - def __init__ (self,plot,label,color="green",mode=QwtPicker.PointSelection,rubber_band=QwtPicker.VLineRubberBand,tracker_mode=QwtPicker.ActiveOnly,track=None): - QwtPlotPicker.__init__(self,QwtPlot.xBottom,QwtPlot.yRight,mode,rubber_band,tracker_mode,plot.canvas()); - self.plot = plot; - self.label = label; - self.track = track; - self.color = QColor(color); - self.setRubberBandPen(QPen(self.color)); - - def trackerText (self,pos): - x,y = self.plot.invTransform(QwtPlot.xBottom,pos.x()),self.plot.invTransform(QwtPlot.yLeft,pos.y()); - if self.track: - text = self.track(x,y); - if text is not None: - return text; - if self.label: - text = QwtText(self.label%dict(x=x,y=y)); - text.setColor(self.color); - return text; - return QwtText(); - - def widgetLeaveEvent (self,ev): - if self.track: - self.track(None,None); - QwtPlotPicker.widgetLeaveEvent(self,ev); - - class ColorBarPlotItem (QwtPlotItem): - def __init__ (self,y0,y1,*args): - QwtPlotItem.__init__(self,*args); - self._y0 = y1; - self._dy = y1-y0; - - def setIntensityMap (self,imap): - self.imap = imap; - - def setColorMap (self,cmap): - self.cmap = cmap; - - def draw (self,painter,xmap,ymap,rect): - """Implements QwtPlotItem.draw(), to render the colorbar on the given painter."""; - xp1,xp2,xdp,xs1,xs2,xds = xinfo = xmap.p1(),xmap.p2(),xmap.pDist(),xmap.s1(),xmap.s2(),xmap.sDist(); - yp1,yp2,ydp,ys1,ys2,yds = yinfo = ymap.p1(),ymap.p2(),ymap.pDist(),ymap.s1(),ymap.s2(),ymap.sDist(); - # xp: coordinates of pixels xp1...xp2 in data units - xp = xs1 + (xds/xdp)*(0.5+numpy.arange(int(xdp))); - # convert y0 and y1 into pixel coordinates - y0 = yp1 - (self._y0-ys1)*(ydp/yds); - dy = self._dy*(ydp/yds); - # remap into an Nx1 image - qimg = self.cmap.colorize(self.imap.remap(xp.reshape((len(xp),1)))); - # plot image - painter.drawImage(QRect(xp1,y0,xdp,dy),qimg); - - class HistogramLineMarker (object): - """Helper class implementing a line marker for a histogram plot"""; - def __init__ (self,plot,color="black",linestyle=Qt.DotLine,align=Qt.AlignBottom|Qt.AlignRight,z=90,label="",zlabel=None,linewidth=1,spacing=2, - yaxis=QwtPlot.yRight): - self.line = TiggerPlotCurve(); - self.color = color = color if isinstance(color,QColor) else QColor(color); - self.line.setPen(QPen(color,linewidth,linestyle)); - self.marker = TiggerPlotMarker(); - self.marker.setLabelAlignment(align); - try: - self.marker.setSpacing(spacing); - except AttributeError: - pass; - self.setText(label); - self.line.setZ(z); - self.marker.setZ(zlabel if zlabel is not None else z); - # set axes -- using yRight, since that is the "markup" z-axis - self.line.setAxis(QwtPlot.xBottom,yaxis); - self.marker.setAxis(QwtPlot.xBottom,yaxis); - # attach to plot - self.line.attach(plot); - self.marker.attach(plot); - - def show (self): - self.line.show(); - self.marker.show(); - - def hide (self): - self.line.hide(); - self.marker.hide(); - - def setText (self,text): - label = QwtText(text); - label.setColor(self.color); - self.marker.setLabel(label); - - - def _setupHistogramPlot (self): - self._histplot.setCanvasBackground(QColor("lightgray")); - self._histplot.setAxisFont(QwtPlot.yLeft,QApplication.font()); - self._histplot.setAxisFont(QwtPlot.xBottom,QApplication.font()); - # add histogram curves - self._histcurve1 = TiggerPlotCurve(); - self._histcurve2 = TiggerPlotCurve(); - self._histcurve1.setStyle(QwtPlotCurve.Steps); - self._histcurve2.setStyle(QwtPlotCurve.Steps); - self._histcurve1.setPen(QPen(Qt.NoPen)); - self._histcurve1.setBrush(QBrush(QColor("slategrey"))); - pen = QPen(QColor("red")); - pen.setWidth(1); - self._histcurve2.setPen(pen); - self._histcurve1.setZ(0); - self._histcurve2.setZ(100); -# self._histcurve1.attach(self._histplot); - self._histcurve2.attach(self._histplot); - # add maxbin and half-max curves - self._line_0 = self.HistogramLineMarker(self._histplot,color="grey50",linestyle=Qt.SolidLine,align=Qt.AlignTop|Qt.AlignLeft,z=90); - self._line_mean = self.HistogramLineMarker(self._histplot,color="black",linestyle=Qt.SolidLine,align=Qt.AlignBottom|Qt.AlignRight,z=91, - label="mean",zlabel=151); - self._line_std = self.HistogramLineMarker(self._histplot,color="black",linestyle=Qt.SolidLine,align=Qt.AlignTop|Qt.AlignRight,z=91, - label="std",zlabel=151); - sym = QwtSymbol(); - sym.setStyle(QwtSymbol.VLine); - sym.setSize(8); - self._line_std.line.setSymbol(sym); - self._line_maxbin = self.HistogramLineMarker(self._histplot,color="green",linestyle=Qt.DotLine,align=Qt.AlignTop|Qt.AlignRight,z=92, - label="max bin",zlabel=150); - self._line_halfmax = self.HistogramLineMarker(self._histplot,color="green",linestyle=Qt.DotLine,align=Qt.AlignBottom|Qt.AlignRight,z=90, - label="half-max",yaxis=QwtPlot.yLeft); - # add current range - self._rangebox = TiggerPlotCurve(); - self._rangebox.setStyle(QwtPlotCurve.Steps); - self._rangebox.setYAxis(QwtPlot.yRight); - self._rangebox.setPen(QPen(Qt.NoPen)); - self._rangebox.setBrush(QBrush(QColor("darkgray"))); - self._rangebox.setZ(50); - self._rangebox.attach(self._histplot); - self._rangebox2 = TiggerPlotCurve(); - self._rangebox2.setStyle(QwtPlotCurve.Sticks); - self._rangebox2.setYAxis(QwtPlot.yRight); - self._rangebox2.setZ(60); -# self._rangebox2.attach(self._histplot); - # add intensity transfer function - self._itfcurve = TiggerPlotCurve(); - self._itfcurve.setStyle(QwtPlotCurve.Lines); - self._itfcurve.setPen(QPen(QColor("blue"))); - self._itfcurve.setYAxis(QwtPlot.yRight); - self._itfcurve.setZ(120); - self._itfcurve.attach(self._histplot); - self._itfmarker = TiggerPlotMarker(); - label = QwtText("ITF"); - label.setColor(QColor("blue")); - self._itfmarker.setLabel(label); - try: - self._itfmarker.setSpacing(0); - except AttributeError: - pass; - self._itfmarker.setLabelAlignment(Qt.AlignTop|Qt.AlignRight); - self._itfmarker.setZ(120); - self._itfmarker.attach(self._histplot); - # add colorbar - self._cb_item = self.ColorBarPlotItem(1,1+self.ColorBarHeight); - self._cb_item.setYAxis(QwtPlot.yRight); - self._cb_item.attach(self._histplot); - # add pickers - self._hist_minpicker = self.HistLimitPicker(self._histplot,"low: %(x).4g"); - self._hist_minpicker.setMousePattern(QwtEventPattern.MouseSelect1,Qt.LeftButton); - QObject.connect(self._hist_minpicker,SIGNAL("selected(const QwtDoublePoint &)"),self._selectLowLimit); - self._hist_maxpicker = self.HistLimitPicker(self._histplot,"high: %(x).4g"); - self._hist_maxpicker.setMousePattern(QwtEventPattern.MouseSelect1,Qt.RightButton); - QObject.connect(self._hist_maxpicker,SIGNAL("selected(const QwtDoublePoint &)"),self._selectHighLimit); - self._hist_maxpicker1 = self.HistLimitPicker(self._histplot,"high: %(x).4g"); - self._hist_maxpicker1.setMousePattern(QwtEventPattern.MouseSelect1,Qt.LeftButton,Qt.CTRL); - QObject.connect(self._hist_maxpicker1,SIGNAL("selected(const QwtDoublePoint &)"),self._selectHighLimit); - self._hist_zoompicker = self.HistLimitPicker(self._histplot,label="zoom", - tracker_mode=QwtPicker.AlwaysOn,track=self._trackHistCoordinates,color="black", - mode=QwtPicker.RectSelection,rubber_band=QwtPicker.RectRubberBand); - self._hist_zoompicker.setMousePattern(QwtEventPattern.MouseSelect1,Qt.LeftButton,Qt.SHIFT); - QObject.connect(self._hist_zoompicker,SIGNAL("selected(const QwtDoubleRect &)"),self._zoomHistogramIntoRect); - - def _trackHistCoordinates (self,x,y): - self._wlab_histpos.setText((DataValueFormat+" %d")%(x,y) if x is not None else self._wlab_histpos_text); - return QwtText(); - - def _updateITF (self): - """Updates current ITF array."""; - # do nothing if no histogram -- means we're not visible - if self._hist is not None: - xdata = self._itf_bins; - ydata = self.image.intensityMap().remap(xdata); - self._rangebox.setData(self._rc.displayRange(),[1,1]); - self._rangebox2.setData(self._rc.displayRange(),[1,1]); - self._itfcurve.setData(xdata,ydata); - self._itfmarker.setValue(xdata[0],1); - - def _updateHistogram (self,hmin=None,hmax=None): - """Recomputes histogram. If no arguments, computes full histogram for - data subset. If hmin/hmax is specified, computes zoomed-in histogram."""; - busy = BusyIndicator(); - self._prev_range = self._display_range; - dmin,dmax = self._subset_range; - hmin0,hmax0 = dmin,dmax; - if hmin0 >= hmax0: - hmax0 = hmin0+1; - subset,mask = self.image.optimalRavel(self._subset); - # compute full-subset hi-res histogram, if we don't have one (for percentile stats) - if self._hist_hires is None: - dprint(1,"computing histogram for full subset range",hmin0,hmax0); - self._hist_hires = measurements.histogram(subset,hmin0,hmax0,self.NumHistBinsHi,labels=mask,index=None if mask is None else False); - self._hist_bins_hires = hmin0 + (hmax0-hmin0)*(numpy.arange(self.NumHistBinsHi)+0.5)/float(self.NumHistBinsHi); - self._hist_binsize_hires = (hmax0-hmin0)/self.NumHistBins; - # if hist limits not specified, then compute lo-res histogram based on the hi-res one - if hmin is None: - hmin,hmax = hmin0,hmax0; - # downsample to low-res histogram - self._hist = self._hist_hires.reshape((self.NumHistBins,self.NumHistBinsHi/self.NumHistBins)).sum(1); - else: - # zoomed-in low-res histogram - # bracket limits at subset range - hmin,hmax = max(hmin,dmin),min(hmax,dmax); - if hmin >= hmax: - hmax = hmin+1; - dprint(1,"computing histogram for",self._subset.shape,self._subset.dtype,hmin,hmax); - self._hist = measurements.histogram(subset,hmin,hmax,self.NumHistBins,labels=mask,index=None if mask is None else False); - dprint(1,"histogram computed"); - # compute bins - self._itf_bins = hmin + (hmax-hmin)*(numpy.arange(self.NumItfBins))/(float(self.NumItfBins)-1); - self._hist_bins = hmin + (hmax-hmin)*(numpy.arange(self.NumHistBins)+0.5)/float(self.NumHistBins); - # histogram range and position of peak - self._hist_range = hmin,hmax; - self._hist_min,self._hist_max,self._hist_imin,self._hist_imax = measurements.extrema(self._hist); - self._hist_peak = self._hist_bins[self._hist_imax]; - # set controls accordingly - if dmin >= dmax: - dmax = dmin+1; - zoom = math.log10((dmax-dmin)/(hmax-hmin)); - self._whistzoom.setValue(zoom); - self._whistunzoom.setEnabled(zoom>0); - self._whistzoomout.setEnabled(zoom>0); - # reset scales - self._histplot.setAxisScale(QwtPlot.xBottom,hmin,hmax); - self._histplot.setAxisScale(QwtPlot.yRight,0,1+self.ColorBarHeight); - # update curves - # call _setHistLogScale() (with current setting) to update axis scales and set data - self._setHistLogScale(self._ylogscale,replot=False); - # set plot lines - self._line_0.line.setData([0,0],[0,1]); - self._line_0.marker.setValue(0,0); - self._line_maxbin.line.setData([self._hist_peak,self._hist_peak],[0,1]); - self._line_maxbin.marker.setValue(self._hist_peak,0); - self._line_maxbin.setText(("max bin:"+DataValueFormat)%self._hist_peak); - # set half-max line - self._line_halfmax.line.setData(self._hist_range,[self._hist_max/2,self._hist_max/2]); - self._line_halfmax.marker.setValue(hmin,self._hist_max/2); - # update ITF - self._updateITF(); - - def _updateStats (self,subset,minmax): - """Recomputes subset statistics."""; - if subset.size <= (2048*2048): - self._showMeanStd(busy=False); - else: - self._wlab_stats.setText(("min: %s max: %s np: %d"%(DataValueFormat,DataValueFormat,self._subset.size))%minmax); - self._wmore_stats.show(); - - def _updateDataSubset (self,subset,minmax,desc,subset_type): - """Called when the displayed data subset is changed. Updates the histogram."""; - self._subset = subset; - self._subset_range = minmax; - self._wlab_subset.setText("Subset: %s"%desc); - self._hist = self._hist_hires = None; - self._wreset_full.setVisible(subset_type is not RenderControl.SUBSET_FULL); - self._wreset_slice and self._wreset_slice.setVisible(subset_type is not RenderControl.SUBSET_SLICE); - # hide the mean/std markers, they will only be shown when _showMeanStd() is called - self._line_mean.hide(); - self._line_std.hide(); - # if we're visibile, recompute histograms and stats - if self.isVisible(): - # if subset is sufficiently small, compute extended stats on-the-fly. Else show the "more" button to compute them later - self._updateHistogram(); - self._updateStats(subset,minmax); - self._histplot.replot(); - - def _showMeanStd (self,busy=True): - if busy: - busy = BusyIndicator(); - dmin,dmax = self._subset_range; - subset,mask = self.image.optimalRavel(self._subset); - dprint(5,"computing mean"); - mean = measurements.mean(subset,labels=mask,index=None if mask is None else False); - dprint(5,"computing std"); - std = measurements.standard_deviation(subset,labels=mask,index=None if mask is None else False); - dprint(5,"done"); - text = " ".join([ ("%s: "+DataValueFormat)%(name,value) for name,value in ("min",dmin),("max",dmax),("mean",mean),("std",std) ]+["np: %d"%self._subset.size]); - self._wlab_stats.setText(text); - self._wmore_stats.hide(); - # update markers - ypos = 0.3; - self._line_mean.line.setData([mean,mean],[0,1]); - self._line_mean.marker.setValue(mean,ypos); - self._line_mean.setText((u"\u03BC="+DataValueFormat)%mean); - self._line_mean.show(); - self._line_std.line.setData([mean-std,mean+std],[ypos,ypos]); - self._line_std.marker.setValue(mean,ypos); - self._line_std.setText((u"\u03C3="+DataValueFormat)%std); - self._line_std.show(); - self._histplot.replot(); - - def _setIntensityLogCyclesLabel (self,value): - self._wlogcycles_label.setText("Log cycles: %4.1f"%value); - - def _previewIntensityLogCycles (self,value): - self._setIntensityLogCycles(value,notify_image=False,write_config=False); - self._wlogcycles_timer.start(500); - - def _setIntensityLogCycles (self,value=None,notify_image=True,write_config=True): - if value is None: - value = self._wlogcycles.value(); - # stop timer if being called to finalize the change in value - if notify_image: - self._wlogcycles_timer.stop(); - if not self._updating_imap: - self._setIntensityLogCyclesLabel(value); - self._rc.setIntensityMapLogCycles(value,notify_image=notify_image,write_config=write_config); - self._updateITF(); - self._histplot.replot(); - - def _updateDisplayRange (self,dmin,dmax): - self._rangebox.setData([dmin,dmax],[.9,.9]); - self._wrange[0].setText(DataValueFormat%dmin); - self._wrange[1].setText(DataValueFormat%dmax); - self._wrangeleft0.setEnabled(dmin!=0); - self._display_range = dmin,dmax; - # if auto-zoom is on, zoom the histogram - # try to be a little clever about this. Zoom only if (a) both limits have changed (so that adjusting one end of the range - # does not cause endless rezooms), or (b) display range is < 1/10 of the histogram range - if self._wautozoom.isChecked() and self._hist is not None: - if (dmax - dmin)/(self._hist_range[1] - self._hist_range[0]) < .1 or (dmin != self._prev_range[0] and dmax != self._prev_range[1]): - margin = (dmax-dmin)/8; - self._updateHistogram(dmin-margin,dmax+margin); - self._updateITF(); - self._histplot.replot(); - - def _updateIntensityMap (self,imap,index): - self._updating_imap = True; - try: - self._cb_item.setIntensityMap(imap); - self._updateITF(); - self._histplot.replot(); - self._wimap.setCurrentIndex(index); - if isinstance(imap,Colormaps.LogIntensityMap): - self._wlogcycles.setValue(imap.log_cycles); - self._setIntensityLogCyclesLabel(imap.log_cycles); - self._wlogcycles.show(); - self._wlogcycles_label.show(); - else: - self._wlogcycles.hide(); - self._wlogcycles_label.hide(); - finally: - self._updating_imap = False; - - def _updateColorMap (self,cmap): - self._cb_item.setColorMap(cmap); - self._histplot.replot(); - try: - index = self._rc.getColormapList().index(cmap); - except: - return; - self._setCurrentColormapNumber(index,cmap); - - def _previewColormapParameters (self,index,cmap): - """Called to preview a new colormap parameter value"""; - self._histplot.replot(); - self._wcolmaps.setItemIcon(index,QIcon(cmap.makeQPixmap(128,16))); - - def _setCurrentColormapNumber (self,index,cmap): - self._wcolmaps.setCurrentIndex(index); - # show controls for colormap - self._wcolmap_control_stack.setCurrentWidget(self._colmap_controls[index]); - - def _changeDisplayRange (self): - """Gets display range from widgets and updates the image with it."""; - try: - newrange = [ float(str(w.text())) for w in self._wrange ]; - except ValueError: - return; - self._rc.setDisplayRange(*newrange); - - def _setHistDisplayRange (self): - self._rc.setDisplayRange(*self._hist_range); - - def _updateImageSlice (self,slice): - for i,(iextra,name,labels) in enumerate(self._rc.slicedAxes()): - self._wslicers[i].setCurrentIndex(slice[iextra]); - - def _changeDisplayRangeToPercent (self,percent): - busy = BusyIndicator(); - if self._hist is None: - self._updateHistogram(); - self._updateStats(self._subset,self._subset_range); - # delta: we need the [delta,100-delta] interval of the total distribution - delta = self._subset.size*((100.-percent)/200.); - # get F(x): cumulative sum - cumsum = numpy.zeros(len(self._hist_hires)+1,dtype=int); - cumsum[1:] = numpy.cumsum(self._hist_hires); - bins = numpy.zeros(len(self._hist_hires)+1,dtype=float); - bins[0] = self._subset_range[0]; - bins[1:] = self._hist_bins_hires + self._hist_binsize_hires/2; - # use interpolation to find value interval corresponding to [delta,100-delta] of the distribution - dprint(2,self._subset.size,delta,self._subset.size-delta); - dprint(2,cumsum,self._hist_bins_hires); - # if first bin is already > delta, then set colour range to first bin - x0,x1 = numpy.interp([delta,self._subset.size-delta],cumsum,bins); - # and change the display range (this will also cause a histplot.replot() via _updateDisplayRange above) - self._rc.setDisplayRange(x0,x1); - - def _setZeroLeftLimit (self): - self._rc.setDisplayRange(0.,self._rc.displayRange()[1]); - - def _selectLowLimit (self,pos): - self._rc.setDisplayRange(pos.x(),self._rc.displayRange()[1]); - - def _selectHighLimit (self,pos): - self._rc.setDisplayRange(self._rc.displayRange()[0],pos.x()); - - def _unzoomHistogram (self): - self._updateHistogram(); - self._histplot.replot(); - - def _zoomHistogramByFactor (self,factor): - """Changes histogram limits by specified factor"""; - # get max distance of plot limit from peak - dprint(1,"zooming histogram by",factor); - halfdist = (self._hist_range[1] - self._hist_range[0])/(factor*2); - self._updateHistogram(self._hist_peak-halfdist,self._hist_peak+halfdist); - self._histplot.replot(); - - def _zoomHistogramIntoRect (self,rect): - hmin,hmax = rect.bottomLeft().x(),rect.bottomRight().x(); - if hmax > hmin: - self._updateHistogram(rect.bottomLeft().x(),rect.bottomRight().x()); - self._histplot.replot(); - - def _zoomHistogramPreview (self,value): - dprint(2,"wheel moved to",value); - self._zoomHistogramFinalize(value,preview=True); - self._whistzoom_timer.start(); - - def _zoomHistogramFinalize (self,value=None,preview=False): - if self._zooming_histogram: - return; - self._zooming_histogram = True; - try: - if value is not None: - dmin,dmax = self._subset_range; - dist = max(dmax-self._hist_peak,self._hist_peak-dmin)/10**value; - self._preview_hist_range = max(self._hist_peak-dist,dmin),min(self._hist_peak+dist,dmax); - if preview: - self._histplot.setAxisScale(QwtPlot.xBottom,*self._preview_hist_range); - else: - dprint(2,"wheel finalized at",value); - self._whistzoom_timer.stop(); - self._updateHistogram(*self._preview_hist_range); - self._histplot.replot(); - finally: - self._zooming_histogram = False; - - def _setHistLogScale (self,logscale,replot=True): - self._ylogscale = logscale; - if logscale: - self._histplot.setAxisScaleEngine(QwtPlot.yLeft,QwtLog10ScaleEngine()); - ymax = max(1,self._hist_max); - self._histplot.setAxisScale(QwtPlot.yLeft,1,10**(math.log10(ymax)*(1+self.ColorBarHeight))); - y = self._hist.copy(); - y[y==0] = 1; - self._histcurve1.setData(self._hist_bins,y); - self._histcurve2.setData(self._hist_bins,y); - else: - self._histplot.setAxisScaleEngine(QwtPlot.yLeft,QwtLinearScaleEngine()); - self._histplot.setAxisScale(QwtPlot.yLeft,0,self._hist_max*(1+self.ColorBarHeight)); - self._histcurve1.setData(self._hist_bins,self._hist); - self._histcurve2.setData(self._hist_bins,self._hist); - if replot: - self._histplot.replot(); diff --git a/Tigger/Images/Controller.py b/Tigger/Images/Controller.py deleted file mode 100644 index dc96e6a..0000000 --- a/Tigger/Images/Controller.py +++ /dev/null @@ -1,468 +0,0 @@ -## -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -from PyQt4.Qwt5 import * -import math -import numpy -import os.path -import time -import traceback -import sys -from scipy.ndimage import measurements - -import Kittens.utils -from Kittens.utils import curry,PersistentCurrier -from Kittens.widgets import BusyIndicator - -_verbosity = Kittens.utils.verbosity(name="imagectl"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - -from Tigger.Images import SkyImage,Colormaps -from Tigger.Models import ModelClasses,PlotStyles -from Tigger.Coordinates import Projection,radec_string; -from Tigger.Models.SkyModel import SkyModel -from Tigger import pixmaps -from Tigger.Widgets import FloatValidator - -from Tigger.Images.RenderControl import RenderControl -from Tigger.Images.ControlDialog import ImageControlDialog - -class ImageController (QFrame): - """An ImageController is a widget for controlling the display of one image. - It can emit the following signals from the image: - raise raise button was clicked - center center-on-image option was selected - unload unload option was selected - slice image slice has changed, need to redraw (emitted by SkyImage automatically) - repaint image display range or colormap has changed, need to redraw (emitted by SkyImage automatically) - """; - def __init__ (self,image,parent,imgman,name=None,save=False): - QFrame.__init__(self,parent); - self.setFrameStyle(QFrame.StyledPanel|QFrame.Raised); - # init state - self.image = image; - self._imgman = imgman; - self._currier = PersistentCurrier(); - self._control_dialog = None; - # create widgets - self._lo = lo = QHBoxLayout(self); - lo.setContentsMargins(0,0,0,0); - lo.setSpacing(2); - # raise button - self._wraise = QToolButton(self); - lo.addWidget(self._wraise); - self._wraise.setIcon(pixmaps.raise_up.icon()); - self._wraise.setAutoRaise(True); - self._can_raise = False; - QObject.connect(self._wraise,SIGNAL("clicked()"),self._raiseButtonPressed); - self._wraise.setToolTip("""

Click here to raise this image above other images. Hold the button down briefly to - show a menu of image operations.

"""); - # center label - self._wcenter = QLabel(self); - self._wcenter.setPixmap(pixmaps.center_image.pm()); - self._wcenter.setToolTip("

The plot is currently centered on (the reference pixel %d,%d) of this image.

"%self.image.referencePixel()); - lo.addWidget(self._wcenter); - # name/filename label - self.name = image.name; - self._wlabel = QLabel(self.name,self); - self._number = 0; - self.setName(self.name); - self._wlabel.setToolTip("%s %s"%(image.filename,u"\u00D7".join(map(str,image.data().shape)))); - lo.addWidget(self._wlabel,1); - # if 'save' is specified, create a "save" button - if save: - self._wsave = QToolButton(self); - lo.addWidget(self._wsave); - self._wsave.setText("save"); - self._wsave.setAutoRaise(True); - self._save_dir = save if isinstance(save,str) else "."; - QObject.connect(self._wsave,SIGNAL("clicked()"),self._saveImage); - self._wsave.setToolTip("""

Click here to write this image to a FITS file.

"""); - # render control - dprint(2,"creating RenderControl"); - self._rc = RenderControl(image,self); - dprint(2,"done"); - # selectors for extra axes - self._wslicers = []; - curslice = self._rc.currentSlice(); # this may be loaded from config, so not necessarily 0 - for iextra,axisname,labels in self._rc.slicedAxes(): - if axisname.upper() not in ["STOKES","COMPLEX"]: - lbl = QLabel("%s:"%axisname,self); - lo.addWidget(lbl); - else: - lbl = None; - slicer = QComboBox(self); - self._wslicers.append(slicer); - lo.addWidget(slicer); - slicer.addItems(labels); - slicer.setToolTip("""

Selects current slice along the %s axis.

"""%axisname); - slicer.setCurrentIndex(curslice[iextra]); - QObject.connect(slicer,SIGNAL("activated(int)"),self._currier.curry(self._rc.changeSlice,iextra)); - # min/max display ranges - lo.addSpacing(5); - self._wrangelbl = QLabel(self); - lo.addWidget(self._wrangelbl); - self._minmaxvalidator = FloatValidator(self); - self._wmin = QLineEdit(self); - self._wmax = QLineEdit(self); - width = self._wmin.fontMetrics().width("1.234567e-05"); - for w in self._wmin,self._wmax: - lo.addWidget(w,0); - w.setValidator(self._minmaxvalidator); - w.setMaximumWidth(width); - w.setMinimumWidth(width); - QObject.connect(w,SIGNAL("editingFinished()"),self._changeDisplayRange); - # full-range button - self._wfullrange = QToolButton(self); - lo.addWidget(self._wfullrange,0); - self._wfullrange.setIcon(pixmaps.zoom_range.icon()); - self._wfullrange.setAutoRaise(True); - QObject.connect(self._wfullrange,SIGNAL("clicked()"),self.renderControl().resetSubsetDisplayRange); - rangemenu = QMenu(self); - rangemenu.addAction(pixmaps.full_range.icon(),"Full subset",self.renderControl().resetSubsetDisplayRange); - for percent in (99.99,99.9,99.5,99,98,95): - rangemenu.addAction("%g%%"%percent,self._currier.curry(self._changeDisplayRangeToPercent,percent)); - self._wfullrange.setPopupMode(QToolButton.DelayedPopup); - self._wfullrange.setMenu(rangemenu); - # update widgets from current display range - self._updateDisplayRange(*self._rc.displayRange()); - # lock button - self._wlock = QToolButton(self); - self._wlock.setIcon(pixmaps.unlocked.icon()); - self._wlock.setAutoRaise(True); - self._wlock.setToolTip("""

Click to lock or unlock the intensity range. When the intensity range is locked across multiple images, any changes in the intensity - range of one are propagated to the others. Hold the button down briefly for additional options.

"""); - lo.addWidget(self._wlock); - QObject.connect(self._wlock,SIGNAL("clicked()"),self._toggleDisplayRangeLock); - QObject.connect(self.renderControl(),SIGNAL("displayRangeLocked"),self._setDisplayRangeLock); - QObject.connect(self.renderControl(),SIGNAL("dataSubsetChanged"),self._dataSubsetChanged); - lockmenu = QMenu(self); - lockmenu.addAction(pixmaps.locked.icon(),"Lock all to this",self._currier.curry(imgman.lockAllDisplayRanges,self.renderControl())); - lockmenu.addAction(pixmaps.unlocked.icon(),"Unlock all",imgman.unlockAllDisplayRanges); - self._wlock.setPopupMode(QToolButton.DelayedPopup); - self._wlock.setMenu(lockmenu); - self._setDisplayRangeLock(self.renderControl().isDisplayRangeLocked()); - # dialog button - self._wshowdialog = QToolButton(self); - lo.addWidget(self._wshowdialog); - self._wshowdialog.setIcon(pixmaps.colours.icon()); - self._wshowdialog.setAutoRaise(True); - self._wshowdialog.setToolTip("""

Click for colourmap and intensity policy options.

"""); - QObject.connect(self._wshowdialog,SIGNAL("clicked()"),self.showRenderControls); - tooltip = """

You can change the currently displayed intensity range by entering low and high limits here.

- - -
Image min:%gmax:%g
"""%self.image.imageMinMax(); - for w in self._wmin,self._wmax,self._wrangelbl: - w.setToolTip(tooltip); - - # create image operations menu - self._menu = QMenu(self.name,self); - self._qa_raise = self._menu.addAction(pixmaps.raise_up.icon(),"Raise image",self._currier.curry(self.image.emit,SIGNAL("raise"))); - self._qa_center = self._menu.addAction(pixmaps.center_image.icon(),"Center plot on image",self._currier.curry(self.image.emit,SIGNAL("center"))); - self._qa_show_rc = self._menu.addAction(pixmaps.colours.icon(),"Colours && Intensities...",self.showRenderControls); - if save: - self._qa_save = self._menu.addAction("Save image...",self._saveImage); - self._menu.addAction("Export image to PNG file...",self._exportImageToPNG); - self._export_png_dialog = None; - self._menu.addAction("Unload image",self._currier.curry(self.image.emit,SIGNAL("unload"))); - self._wraise.setMenu(self._menu); - self._wraise.setPopupMode(QToolButton.DelayedPopup); - - # connect updates from renderControl and image - self.image.connect(SIGNAL("slice"),self._updateImageSlice); - QObject.connect(self._rc,SIGNAL("displayRangeChanged"),self._updateDisplayRange); - - # default plot depth of image markers - self._z_markers = None; - # and the markers themselves - self._image_border = QwtPlotCurve(); - self._image_label = QwtPlotMarker(); - - # subset markers - self._subset_pen = QPen(QColor("Light Blue")); - self._subset_border = QwtPlotCurve(); - self._subset_border.setPen(self._subset_pen); - self._subset_border.setVisible(False); - self._subset_label = QwtPlotMarker(); - text = QwtText("subset"); - text.setColor(self._subset_pen.color()); - self._subset_label.setLabel(text); - self._subset_label.setLabelAlignment(Qt.AlignRight|Qt.AlignBottom); - self._subset_label.setVisible(False); - self._setting_lmrect = False; - - self._all_markers = [ self._image_border,self._image_label,self._subset_border,self._subset_label ]; - - def close (self): - if self._control_dialog: - self._control_dialog.close(); - self._control_dialog = None; - - def __del__ (self): - self.close(); - - def __eq__ (self,other): - return self is other; - - def renderControl (self): - return self._rc; - - def getMenu (self): - return self._menu; - - def getFilename (self): - return self.image.filename; - - def setName (self,name): - self.name = name; - self._wlabel.setText("%s: %s"%(chr(ord('a')+self._number),self.name)); - - def setNumber (self,num): - self._number = num; - self._menu.menuAction().setText("%s: %s"%(chr(ord('a')+self._number),self.name)); - self._qa_raise.setShortcut(QKeySequence("Alt+"+chr(ord('A')+num))); - self.setName(self.name); - - def getNumber (self): - return self._number; - - def setPlotProjection (self,proj): - self.image.setPlotProjection(proj); - sameproj = proj == self.image.projection; - self._wcenter.setVisible(sameproj); - self._qa_center.setVisible(not sameproj); - if self._image_border: - (l0,l1),(m0,m1) = self.image.getExtents(); - path = numpy.array([l0,l0,l1,l1,l0]),numpy.array([m0,m1,m1,m0,m0]); - self._image_border.setData(*path); - if self._image_label: - self._image_label.setValue(path[0][2],path[1][2]); - - def addPlotBorder (self,border_pen,label,label_color=None,bg_brush=None): - # make plot items for image frame - # make curve for image borders - (l0,l1),(m0,m1) = self.image.getExtents(); - self._border_pen = QPen(border_pen); - self._image_border.show(); - self._image_border.setData([l0,l0,l1,l1,l0],[m0,m1,m1,m0,m0]); - self._image_border.setPen(self._border_pen); - self._image_border.setZ(self.image.z()+1 if self._z_markers is None else self._z_markers); - if label: - self._image_label.show(); - self._image_label_text = text = QwtText(" %s "%label); - text.setColor(label_color); - text.setBackgroundBrush(bg_brush); - self._image_label.setValue(l1,m1); - self._image_label.setLabel(text); - self._image_label.setLabelAlignment(Qt.AlignRight|Qt.AlignVCenter); - self._image_label.setZ(self.image.z()+2 if self._z_markers is None else self._z_markers); - - def setPlotBorderStyle (self,border_color=None,label_color=None): - if border_color: - self._border_pen.setColor(border_color); - self._image_border.setPen(self._border_pen); - if label_color: - self._image_label_text.setColor(label_color); - self._image_label.setLabel(self._image_label_text); - - def showPlotBorder (self,show=True): - self._image_border.setVisible(show); - self._image_label.setVisible(show); - - def attachToPlot (self,plot,z_markers=None): - for item in [ self.image ] + self._all_markers: - if item.plot() != plot: - item.attach(plot); - - def setImageVisible (self,visible): - self.image.setVisible(visible); - - def showRenderControls (self): - if not self._control_dialog: - dprint(1,"creating control dialog"); - self._control_dialog = ImageControlDialog(self,self._rc,self._imgman); - dprint(1,"done"); - if not self._control_dialog.isVisible(): - dprint(1,"showing control dialog"); - self._control_dialog.show(); - else: - self._control_dialog.hide(); - - def _changeDisplayRangeToPercent (self,percent): - if not self._control_dialog: - self._control_dialog = ImageControlDialog(self,self._rc,self._imgman); - self._control_dialog._changeDisplayRangeToPercent(percent); - - def _updateDisplayRange (self,dmin,dmax): - """Updates display range widgets."""; - self._wmin.setText("%.4g"%dmin); - self._wmax.setText("%.4g"%dmax); - self._updateFullRangeIcon(); - - def _changeDisplayRange (self): - """Gets display range from widgets and updates the image with it."""; - try: - newrange = float(str(self._wmin.text())),float(str(self._wmax.text())); - except ValueError: - return; - self._rc.setDisplayRange(*newrange); - - def _dataSubsetChanged (self,subset,minmax,desc,subset_type): - """Called when the data subset changes (or is reset)"""; - # hide the subset indicator -- unless we're invoked while we're actually setting the subset itself - if not self._setting_lmrect: - self._subset = None; - self._subset_border.setVisible(False); - self._subset_label.setVisible(False); - - def setLMRectSubset (self,rect): - self._subset = rect; - l0,m0,l1,m1 = rect.getCoords(); - self._subset_border.setData([l0,l0,l1,l1,l0],[m0,m1,m1,m0,m0]); - self._subset_border.setVisible(True); - self._subset_label.setValue(max(l0,l1),max(m0,m1)); - self._subset_label.setVisible(True); - self._setting_lmrect = True; - self.renderControl().setLMRectSubset(rect); - self._setting_lmrect = False; - - def currentSlice (self): - return self._rc.currentSlice(); - - def _updateImageSlice (self,slice): - dprint(2,slice); - for i,(iextra,name,labels) in enumerate(self._rc.slicedAxes()): - slicer = self._wslicers[i]; - if slicer.currentIndex() != slice[iextra]: - dprint(3,"setting widget",i,"to",slice[iextra]); - slicer.setCurrentIndex(slice[iextra]); - - def setMarkersZ (self,z): - self._z_markers = z; - for i,elem in enumerate(self._all_markers): - elem.setZ(z+i); - - def setZ (self,z,top=False,depthlabel=None,can_raise=True): - self.image.setZ(z); - if self._z_markers is None: - for i,elem in enumerate(self._all_markers): - elem.setZ(z+i+i); - # set the depth label, if any - label = "%s: %s"%(chr(ord('a')+self._number),self.name); - # label = "%s %s"%(depthlabel,self.name) if depthlabel else self.name; - if top: - label = "%s: %s"%(chr(ord('a')+self._number),self.name); - self._wlabel.setText(label); - # set hotkey - self._qa_show_rc.setShortcut(Qt.Key_F9 if top else QKeySequence()); - # set raise control - self._can_raise = can_raise; - self._qa_raise.setVisible(can_raise); - self._wlock.setVisible(can_raise); - if can_raise: - self._wraise.setToolTip("

Click here to raise this image to the top. Click on the down-arrow to access the image menu.

"); - else: - self._wraise.setToolTip("

Click to access the image menu.

"); - - def _raiseButtonPressed (self): - if self._can_raise: - self.image.emit(SIGNAL("raise")); - else: - self._wraise.showMenu(); - - def _saveImage (self): - filename = QFileDialog.getSaveFileName(self,"Save FITS file",self._save_dir,"FITS files(*.fits *.FITS *fts *FTS)"); - filename = str(filename); - if not filename: - return; - busy = BusyIndicator(); - self._imgman.showMessage("""Writing FITS image %s"""%filename,3000); - QApplication.flush(); - try: - self.image.save(filename); - except Exception,exc: - busy = None; - traceback.print_exc(); - self._imgman.showErrorMessage("""Error writing FITS image %s: %s"""%(filename,str(sys.exc_info()[1]))); - return None; - self.renderControl().startSavingConfig(filename); - self.setName(self.image.name); - self._qa_save.setVisible(False); - self._wsave.hide(); - busy = None; - - def _exportImageToPNG (self,filename=None): - if not filename: - if not self._export_png_dialog: - dialog = self._export_png_dialog = QFileDialog(self,"Export image to PNG",".","*.png"); - dialog.setDefaultSuffix("png"); - dialog.setFileMode(QFileDialog.AnyFile); - dialog.setAcceptMode(QFileDialog.AcceptSave); - dialog.setModal(True); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self._exportImageToPNG); - return self._export_png_dialog.exec_() == QDialog.Accepted; - busy = BusyIndicator(); - if isinstance(filename,QStringList): - filename = filename[0]; - filename = str(filename); - # make QPixmap - nx,ny = self.image.imageDims(); - (l0,l1),(m0,m1) = self.image.getExtents(); - pixmap = QPixmap(nx,ny); - painter = QPainter(pixmap); - # use QwtPlot implementation of draw canvas, since we want to avoid caching - xmap = QwtScaleMap(); - xmap.setPaintInterval(0,nx); - xmap.setScaleInterval(l1,l0); - ymap = QwtScaleMap(); - ymap.setPaintInterval(ny,0); - ymap.setScaleInterval(m0,m1); - self.image.draw(painter,xmap,ymap,pixmap.rect()); - painter.end(); - # save to file - try: - pixmap.save(filename,"PNG"); - except Exception,exc: - self.emit(SIGNAL("showErrorMessage"),"Error writing %s: %s"%(filename,str(exc))); - return; - self.emit(SIGNAL("showMessage"),"Exported image to file %s"%filename); - - def _toggleDisplayRangeLock (self): - self.renderControl().lockDisplayRange(not self.renderControl().isDisplayRangeLocked()); - - def _setDisplayRangeLock (self,locked): - self._wlock.setIcon(pixmaps.locked.icon() if locked else pixmaps.unlocked.icon()); - - def _updateFullRangeIcon (self): - if self._rc.isSubsetDisplayRange(): - self._wfullrange.setIcon(pixmaps.zoom_range.icon()); - self._wfullrange.setToolTip("""

The current intensity range is the full range. Hold this button down briefly for additional options.

"""); - else: - self._wfullrange.setIcon(pixmaps.full_range.icon()); - self._wfullrange.setToolTip("""

Click to reset to a full intensity range. Hold the button down briefly for additional options.

"""); diff --git a/Tigger/Images/Manager.py b/Tigger/Images/Manager.py deleted file mode 100644 index 37d7985..0000000 --- a/Tigger/Images/Manager.py +++ /dev/null @@ -1,522 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -import math -import numpy -import re -import os.path -import time -import traceback -import sys - -import Kittens.utils -pyfits = Kittens.utils.import_pyfits(); -from Kittens.utils import curry,PersistentCurrier -from Kittens.widgets import BusyIndicator - -from Tigger.Images.Controller import ImageController,dprint,dprintf - -from Tigger.Images import SkyImage -from Tigger.Images import FITS_ExtensionList - -class ImageManager (QWidget): - """An ImageManager manages a stack of images (and associated ImageControllers)""" - def __init__ (self,*args): - QWidget.__init__(self,*args); - # init layout - self._lo = QVBoxLayout(self); - self._lo.setContentsMargins(0,0,0,0); - self._lo.setSpacing(0); - # init internal state - self._currier = PersistentCurrier(); - self._z0 = 0; # z-depth of first image, the rest count down from it - self._updating_imap = False; - self._locked_display_range = False; - self._imagecons = []; - self._imagecon_loadorder = []; - self._center_image = None; - self._plot = None; - self._border_pen = None; - self._drawing_key = None; - self._load_image_dialog = None; - self._model_imagecons = set(); - # init menu and standard actions - self._menu = QMenu("&Image",self); - qag = QActionGroup(self); - # exclusive controls for plotting topmost or all images - self._qa_plot_top = qag.addAction("Display topmost image only"); - self._qa_plot_all = qag.addAction("Display all images"); - self._qa_plot_top.setCheckable(True); - self._qa_plot_all.setCheckable(True); - self._qa_plot_top.setChecked(True); - QObject.connect(self._qa_plot_all,SIGNAL("toggled(bool)"),self._displayAllImages); - self._closing = False; - - self._qa_load_clipboard = None; - self._clipboard_mode = QClipboard.Clipboard; - QObject.connect(QApplication.clipboard(),SIGNAL("changed(QClipboard::Mode)"),self._checkClipboardPath); - # populate the menu - self._repopulateMenu(); - - def close (self): - dprint(1,"closing Manager"); - self._closing = True; - for ic in self._imagecons: - ic.close(); - - def loadImage (self,filename=None,duplicate=True,to_top=True,model=None): - """Loads image. Returns ImageControlBar object. - If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True), - or else makes a new control bar. - If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling - unloadModelImages(). - """; - if filename is None: - if not self._load_image_dialog: - dialog = self._load_image_dialog = QFileDialog(self,"Load FITS image",".","FITS images (%s);;All files (*)"%(" ".join(["*"+ext for ext in FITS_ExtensionList]))); - dialog.setFileMode(QFileDialog.ExistingFile); - dialog.setModal(True); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.loadImage); - self._load_image_dialog.exec_(); - return None; - if isinstance(filename,QStringList): - filename = filename[0]; - filename = str(filename); - # report error if image does not exist - if not os.path.exists(filename): - self.showErrorMessage("""FITS image %s does not exist."""%filename); - return None; - # see if image is already loaded - if not duplicate: - for ic in self._imagecons: - if ic.getFilename() and os.path.samefile(filename,ic.getFilename()): - if to_top: - self.raiseImage(ic); - if model: - self._model_imagecons.add(id(ic)); - return ic; - # load the FITS image - busy = BusyIndicator(); - dprint(2,"reading FITS image",filename); - self.showMessage("""Reading FITS image %s"""%filename,3000); - QApplication.flush(); - try: - image = SkyImage.FITSImagePlotItem(str(filename)); - except KeyboardInterrupt: - raise; - except: - busy = None; - traceback.print_exc(); - self.showErrorMessage("""

Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer, - please send the FITS file, along with a copy of any error messages from the text console, to osmirnov@gmail.com.

"""%(filename,str(sys.exc_info()[1]))); - return None; - # create control bar, add to widget stack - ic = self._createImageController(image,"model source '%s'"%model if model else filename,model or image.name,model=model); - self.showMessage("""Loaded FITS image %s"""%filename,3000); - dprint(2,"image loaded"); - return ic; - - def showMessage (self,message,time=None): - self.emit(SIGNAL("showMessage"),message,time); - - def showErrorMessage (self,message,time=None): - self.emit(SIGNAL("showErrorMessage"),message,time); - - def setZ0 (self,z0): - self._z0 = z0; - if self._imagecons: - self.raiseImage(self._imagecons[0]); - - def enableImageBorders (self,border_pen,label_color,label_bg_brush): - self._border_pen,self._label_color,self._label_bg_brush = \ - border_pen,label_color,label_bg_brush; - - def lockAllDisplayRanges (self,rc0): - """Locks all display ranges, and sets the intensity from rc0"""; - if not self._updating_imap: - self._updating_imap = True; - rc0.lockDisplayRange(); - try: - for ic in self._imagecons: - rc1 = ic.renderControl(); - if rc1 is not rc0: - rc1.setDisplayRange(*rc0.displayRange()); - rc1.lockDisplayRange(); - finally: - self._updating_imap = False; - - def unlockAllDisplayRanges (self): - """Unlocks all display range."""; - for ic in self._imagecons: - ic.renderControl().lockDisplayRange(False); - - def _lockDisplayRange (self,rc0,lock): - """Locks or unlocks the display range of a specific controller.""" - if lock and not self._updating_imap: - self._updating_imap = True; - try: - # if something is already locked, copy display range from it - for ic in self._imagecons: - rc1 = ic.renderControl(); - if rc1 is not rc0 and rc1.isDisplayRangeLocked(): - rc0.setDisplayRange(*rc1.displayRange()); - finally: - self._updating_imap = False; - - - def _updateDisplayRange (self,rc,dmin,dmax): - """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range."""; - if not rc.isDisplayRangeLocked(): - return; - # If the display range is locked, propagate it to all images. - # but don't do it if we're already propagating (otherwise we may get called in an infinte loop) - if not self._updating_imap: - self._updating_imap = True; - try: - for ic in self._imagecons: - rc1 = ic.renderControl(); - if rc1 is not rc and rc1.isDisplayRangeLocked(): - rc1.setDisplayRange(dmin,dmax); - finally: - self._updating_imap = False; - - def getImages (self): - return [ ic.image for ic in self._imagecons ]; - - def getTopImage (self): - return (self._imagecons or None) and self._imagecons[0].image; - - def cycleImages (self): - index = self._imagecon_loadorder.index(self._imagecons[0]); - index = (index+1)%len(self._imagecon_loadorder); - self.raiseImage(self._imagecon_loadorder[index]); - - def blinkImages (self): - if len(self._imagecons)>1: - self.raiseImage(self._imagecons[1]); - - def incrementSlice (self,extra_axis,incr): - if self._imagecons: - rc = self._imagecons[0].renderControl(); - sliced_axes = rc.slicedAxes(); - if extra_axis < len(sliced_axes): - rc.incrementSlice(sliced_axes[extra_axis][0],incr); - - def setLMRectSubset (self,rect): - if self._imagecons: - self._imagecons[0].setLMRectSubset(rect); - - def getLMRectStats (self,rect): - if self._imagecons: - return self._imagecons[0].renderControl().getLMRectStats(rect); - - def unloadModelImages (self): - """Unloads images associated with model (i.e. loaded with the model=True flag)"""; - for ic in [ ic for ic in self._imagecons if id(ic) in self._model_imagecons ]: - self.unloadImage(ic); - - def unloadImage (self,imagecon): - """Unloads the given imagecon object."""; - if imagecon not in self._imagecons: - return; - # recenter if needed - self._imagecons.remove(imagecon); - self._imagecon_loadorder.remove(imagecon); - self._model_imagecons.discard(id(imagecon)); - # reparent widget and release it - imagecon.setParent(None); - imagecon.close(); - # recenter image, if unloaded the center image - if self._center_image is imagecon.image: - self.centerImage(self._imagecons[0] if self._imagecons else None,emit=False); - # emit signal - self._repopulateMenu(); - self.emit(SIGNAL("imagesChanged")); - if self._imagecons: - self.raiseImage(self._imagecons[0]); - - def getCenterImage (self): - return self._center_image; - - def centerImage (self,imagecon,emit=True): - self._center_image = imagecon and imagecon.image; - for ic in self._imagecons: - ic.setPlotProjection(self._center_image.projection); - if emit: - self.emit(SIGNAL("imagesChanged")); - - def raiseImage (self,imagecon): - # reshuffle image stack, if more than one image image - if len(self._imagecons) > 1: - busy = BusyIndicator(); - # reshuffle image stack - self._imagecons.remove(imagecon); - self._imagecons.insert(0,imagecon); - # notify imagecons - for i,ic in enumerate(self._imagecons): - label = "%d"%(i+1) if i else "1"; - ic.setZ(self._z0-i*10,top=not i,depthlabel=label,can_raise=True); - # adjust visibility - for j,ic in enumerate(self._imagecons): - ic.setImageVisible(not j or bool(self._qa_plot_all.isChecked())); - # issue replot signal - self.emit(SIGNAL("imageRaised")); - self.fastReplot(); - # else simply update labels - else: - self._imagecons[0].setZ(self._z0,top=True,depthlabel=None,can_raise=False); - self._imagecons[0].setImageVisible(True); - # update slice menus - img = imagecon.image; - axes = imagecon.renderControl().slicedAxes(); - for i,(next,prev) in enumerate(self._qa_slices): - next.setVisible(False); - prev.setVisible(False); - if i < len(axes): - iaxis,name,labels = axes[i]; - next.setVisible(True); - prev.setVisible(True); - next.setText("Show next slice along %s axis"%name); - prev.setText("Show previous slice along %s axis"%name); - # emit signasl - self.emit(SIGNAL("imageRaised"),img); - - def resetDrawKey (self): - """Makes and sets the current plot's drawing key""" - if self._plot: - key = []; - for ic in self._imagecons: - key.append(id(ic)); - key += ic.currentSlice(); - self._plot.setDrawingKey(tuple(key)); - - def fastReplot (self,*dum): - """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible."""; - if self._plot: - self.resetDrawKey(); - dprint(2,"calling replot",time.time()%60); - self._plot.replot(); - dprint(2,"replot done",time.time()%60); - - def replot (self,*dum): - """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache."""; - if self._plot: - self._plot.clearDrawCache(); - self.resetDrawKey(); - self._plot.replot(); - - def attachImagesToPlot (self,plot): - self._plot = plot; - self.resetDrawKey(); - for ic in self._imagecons: - ic.attachToPlot(plot); - - def getMenu (self): - return self._menu; - - def _displayAllImages (self,enabled): - busy = BusyIndicator(); - if enabled: - for ic in self._imagecons: - ic.setImageVisible(True); - else: - self._imagecons[0].setImageVisible(True); - for ic in self._imagecons[1:]: - ic.setImageVisible(False); - self.replot(); - - def _checkClipboardPath (self,mode=QClipboard.Clipboard): - if self._qa_load_clipboard: - self._clipboard_mode = mode; - try: - path = str(QApplication.clipboard().text(mode)); - except: - path = None; - self._qa_load_clipboard.setEnabled(bool(path and os.path.isfile(path))); - - def _loadClipboardPath (self): - try: - path = QApplication.clipboard().text(self._clipboard_mode); - except: - return; - self.loadImage(path); - - def _repopulateMenu (self): - self._menu.clear(); - self._menu.addAction("&Load image...",self.loadImage,Qt.CTRL+Qt.Key_L); - self._menu.addAction("&Compute image...",self.computeImage,Qt.CTRL+Qt.Key_M); - self._qa_load_clipboard = self._menu.addAction("Load from clipboard &path",self._loadClipboardPath,Qt.CTRL+Qt.Key_P); - self._checkClipboardPath(); - if self._imagecons: - self._menu.addSeparator(); - # add controls to cycle images and planes - for i,imgcon in enumerate(self._imagecons[::-1]): - self._menu.addMenu(imgcon.getMenu()); - self._menu.addSeparator(); - if len(self._imagecons) > 1: - self._menu.addAction("Cycle images",self.cycleImages,Qt.Key_F5); - self._menu.addAction("Blink images",self.blinkImages,Qt.Key_F6); - self._qa_slices = (( self._menu.addAction("Next slice along axis 1",self._currier.curry(self.incrementSlice,0,1),Qt.Key_F7), - self._menu.addAction("Previous slice along axis 1",self._currier.curry(self.incrementSlice,0,-1),Qt.SHIFT+Qt.Key_F7)), - ( self._menu.addAction("Next slice along axis 2",self._currier.curry(self.incrementSlice,1,1),Qt.Key_F8), - self._menu.addAction("Previous slice along axis 2",self._currier.curry(self.incrementSlice,1,-1),Qt.SHIFT+Qt.Key_F8))); - self._menu.addSeparator(); - self._menu.addAction(self._qa_plot_top); - self._menu.addAction(self._qa_plot_all); - - def computeImage (self,expression=None): - """Computes image from expression (if expression is None, pops up dialog)"""; - if expression is None: - (expression,ok) = QInputDialog.getText(self,"Compute image", - """Enter an image expression to compute. -Any valid numpy expression is supported, and -all functions from the numpy module are available (including sub-modules such as fft). -Use 'a', 'b', 'c' to refer to images. -Examples: "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc."""); -# (expression,ok) = QInputDialog.getText(self,"Compute image","""

Enter an expression to compute. -# Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the -# functions from the numpy module are available. Examples of valid expressions include "(a+b)/2", -# "cos(a)+sin(b)", "a-a.mean()", etc. -#

-# """); - expression = str(expression); - if not ok or not expression: - return; - # try to parse expression - arglist = [ (chr(ord('a')+ic.getNumber()),ic.image) for ic in self._imagecons ]; - try: - exprfunc = eval("lambda "+(",".join([ x[0] for x in arglist ]))+":"+expression, - numpy.__dict__,{}); - except Exception,exc: - self.showErrorMessage("""Error parsing expression "%s": %s."""%(expression,str(exc))); - return None; - # try to evaluate expression - self.showMessage("Computing expression \"%s\""%expression,10000); - busy = BusyIndicator(); - QApplication.flush(); - # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added, - # the result is promoted to NxMxMx1 following the numpy rules. - def trimshape (shape): - out = shape; - while out and out[-1] == 1: - out = out[:-1]; - return out; - def trimarray (array): - return array.reshape(trimshape(array.shape)); - try: - result = exprfunc(*[trimarray(x[1].data()) for x in arglist]); - except Exception,exc: - busy = None; - traceback.print_exc(); - self.showErrorMessage("""Error evaluating "%s": %s."""%(expression,str(exc))); - return None; - busy = None; - if type(result) != numpy.ma.masked_array and type(result) != numpy.ndarray: - self.showErrorMessage("""Result of "%s" is of invalid type "%s" (array expected)."""%(expression,type(result).__name__)); - return None; - # convert coomplex results to real - if numpy.iscomplexobj(result): - self.showErrorMessage("""Result of "%s" is complex. Complex images are currently - not fully supported, so we'll implicitly use the absolute value instead."""%(expression)); - expression = "abs(%s)"%expression; - result = abs(result); - # determine which image this expression can be associated with - res_shape = trimshape(result.shape); - arglist = [ x for x in arglist if hasattr(x[1],'fits_header') and trimshape(x[1].data().shape) == res_shape ]; - if not arglist: - self.showErrorMessage("""Result of "%s" has shape %s, which does not match any loaded FITS image."""%(expression,"x".join(map(str,result.shape)))); - return None; - # look for an image in the arglist with the same projection, and with a valid dirname - # (for the where-to-save hint) - template = arglist[0][1]; - # if all images in arglist have the same projection, then it doesn't matter what we use - # else ask - if len([x for x in arglist[1:] if x[1].projection == template.projection]) != len(arglist)-1: - options = [ x[0] for x in arglist ]; - (which,ok) = QInputDialog.getItem(self,"Compute image","Coordinate system to use for the result of \"%s\":"%expression,options,0,False); - if not ok: - return None; - try: - template = arglist[options.index(which)][1]; - except: - pass; - # create a FITS image - busy = BusyIndicator(); - dprint(2,"creating FITS image",expression); - self.showMessage("""Creating image for %s"""%expression,3000); - QApplication.flush(); - try: - hdu = pyfits.PrimaryHDU(result.transpose(),template.fits_header); - skyimage = SkyImage.FITSImagePlotItem(name=expression,filename=None,hdu=hdu); - except: - busy = None; - traceback.print_exc(); - self.showErrorMessage("""Error creating FITS image %s: %s"""%(expression,str(sys.exc_info()[1]))); - return None; - # get directory name for save-to hint - dirname = getattr(template,'filename',None); - if not dirname: - dirnames = [ getattr(img,'filename') for x,img in arglist if hasattr(img,'filename') ]; - dirname = dirnames[0] if dirnames else None; - # create control bar, add to widget stack - self._createImageController(skyimage,expression,expression,save=((dirname and os.path.dirname(dirname)) or ".")); - self.showMessage("Created new image for %s"%expression,3000); - dprint(2,"image created"); - - def _createImageController (self,image,name,basename,model=False,save=False): - dprint(2,"creating ImageController for",name); - ic = ImageController(image,self,self,name,save=save); - ic.setNumber(len(self._imagecons)); - self._imagecons.insert(0,ic); - self._imagecon_loadorder.append(ic); - if model: - self._model_imagecons.add(id(ic)); - self._lo.addWidget(ic); - if self._border_pen: - ic.addPlotBorder(self._border_pen,basename,self._label_color,self._label_bg_brush); - # attach appropriate signals - image.connect(SIGNAL("slice"),self.fastReplot); - image.connect(SIGNAL("repaint"),self.replot); - image.connect(SIGNAL("raise"),self._currier.curry(self.raiseImage,ic)); - image.connect(SIGNAL("unload"),self._currier.curry(self.unloadImage,ic)); - image.connect(SIGNAL("center"),self._currier.curry(self.centerImage,ic)); - QObject.connect(ic.renderControl(),SIGNAL("displayRangeChanged"),self._currier.curry(self._updateDisplayRange,ic.renderControl())); - QObject.connect(ic.renderControl(),SIGNAL("displayRangeLocked"),self._currier.curry(self._lockDisplayRange,ic.renderControl())); - self._plot = None; - # add to menus - dprint(2,"repopulating menus"); - self._repopulateMenu(); - # center and raise to top of stack - self.raiseImage(ic); - if not self._center_image: - self.centerImage(ic,emit=False); - else: - ic.setPlotProjection(self._center_image.projection); - # signal - self.emit(SIGNAL("imagesChanged")); - return ic; - diff --git a/Tigger/Images/RenderControl.py b/Tigger/Images/RenderControl.py deleted file mode 100644 index 28fcd24..0000000 --- a/Tigger/Images/RenderControl.py +++ /dev/null @@ -1,389 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -from PyQt4.Qwt5 import * -import math -import numpy -import sys -import time -import os.path -from scipy.ndimage import measurements - -import Kittens.utils -from Kittens.utils import curry,PersistentCurrier -from Kittens.widgets import BusyIndicator - -_verbosity = Kittens.utils.verbosity(name="rc"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - -from Tigger.Images import SkyImage,Colormaps -from Tigger import pixmaps,ConfigFile -from Tigger.Widgets import FloatValidator - -import Kittens.config -ImageConfigFile = Kittens.config.DualConfigParser("tigger.images.conf"); - -class RenderControl (QObject): - """RenderControl represents all the options (slices, color and intensity policy data) associated with an image. This object is shared by various GUI elements - that control the rendering of images. - """; - - SUBSET_FULL = "full"; - SUBSET_SLICE = "slice"; - SUBSET_RECT = "rect"; - - def __init__ (self,image,parent): - QObject.__init__(self,parent); - self.image = image; - self._config = Kittens.config.SectionParser(ImageConfigFile,os.path.normpath(os.path.abspath(image.filename))) if image.filename else None; - # figure out the slicing -- find extra axes with size > 1 - # self._current_slice contains all extra axis, including the size-1 ones - # self._sliced_axes is a list of (iextra,axisname,labels) tuples for size>1 axes - # where iextra is an index into self._current_slice. - self._current_slice = [0]*image.numExtraAxes(); - self._slice_dims = [1]*image.numExtraAxes(); - self._sliced_axes = []; - for i in range(image.numExtraAxes()): - iaxis,axisname,labels = image.extraAxisNumberNameLabels(i); - self._slice_dims[i] = len(labels); - if len(labels) > 1: - self._sliced_axes.append((i,axisname,labels)); - # set the full image range (i.e. mix/max) and current slice range - dprint(2,"getting data min/max"); - self._fullrange = self._slicerange = image.dataMinMax()[:2]; - dprint(2,"done"); - # create dict of intensity maps - log_cycles = self._config.getfloat("intensity-log-cycles",6) if self._config else 6; - self._imap_list = ( - ( 'Linear', Colormaps.LinearIntensityMap() ), - ( 'Histogram-equalized', Colormaps.HistEqIntensityMap() ), - ( 'log(val-min)', Colormaps.LogIntensityMap(log_cycles) ) - ); - # create list of color maps - self._cmap_list = Colormaps.getColormapList(); - default_cmap = 0; - for i,cmap in enumerate(self._cmap_list): - if isinstance(cmap,Colormaps.ColormapWithControls): - if self._config: - cmap.loadConfig(self._config); - QObject.connect(cmap,SIGNAL("colormapChanged"),self.updateColorMapParameters); - if isinstance(cmap,Colormaps.CubeHelixColormap): - default_cmap = i; - # set the initial intensity map - imap = self._config.getint("intensity-map-number",0) if self._config else 0; - cmap = self._config.getint("colour-map-number",default_cmap) if self._config else default_cmap; - imap = max(min(len(self._imap_list)-1,imap),0); - cmap = max(min(len(self._cmap_list)-1,cmap),0); - self._current_imap_index = imap; - self._current_cmap_index = cmap; - self.image.setIntensityMap(self._imap_list[imap][1]); - self.image.setColorMap(self._cmap_list[cmap]); - - # cache of min/max values for each slice, as these can be slowish to recompute when flipping slices - self._sliceranges = {}; - # This is the data subset corresponding to the current display range. When the display range is set to - # _fullrange, this is the image cube. When it is set to _slicerange, this is the current image slice. When - # setLMRectDisplayRange() or setWindowDisplayRange() is used to set the range to the specified window, - # this is the a subset of the current slice. The data subset is passed to setDataSubset() of the intensity mapper object - self._displaydata = None; - # This is a tuple of the extrema of the current data subset. This is not quite the same thing as self._displayrange below. - # When the display range is reset to cube/slice/window, _displayrange is set to _displaydata_minmax. But if - # setDisplayRange() is subsequently called (e.g. if the user manually enters new values into the Range boxes), then - # _displayrange will be set to something else until the next reset....() call. - self._displaydata_minmax = None; - # This is a low,high tuple of the current display range -- will be initialized by resetFullDisplayRange() - self._displayrange = None; - if self._config and self._config.has_option("range-min") and self._config.has_option("range-max"): - display_range = self._config.getfloat("range-min"),self._config.getfloat("range-max"); - else: - display_range = None; - self.setFullSubset(display_range,write_config=False); - # setup initial slice - if self.hasSlicing(): - if self._config and self._config.has_option("slice"): - try: - curslice = map(int,self._config.get("slice").split()); - except: - curslice = []; - if len(curslice) == len(self._current_slice): - for iaxis,i in enumerate(curslice): - naxis = len(self.image.extraAxisValues(iaxis)); - i = min(naxis-1,max(0,i)); - self._current_slice[iaxis] = i; - self.selectSlice(self._current_slice,write_config=False); - # lock display range if so configured - self._lock_display_range = self._config.getbool("lock-range",0) if self._config else False; - if self._lock_display_range: - self.lockDisplayRange(True,write_config=False); - - - def startSavingConfig(self,image_filename): - """Saves the current configuration under the specified image filename"""; - self._config = Kittens.config.SectionParser(ImageConfigFile,os.path.normpath(os.path.abspath(image_filename))); - if self._displayrange: - self._config.set("range-min",self._displayrange[0],save=False); - self._config.set("range-max",self._displayrange[1],save=False); - if self._current_slice: - self._config.set("slice"," ".join(map(str,self._current_slice)),save=False); - for cmap in self._cmap_list: - if isinstance(cmap,Colormaps.ColormapWithControls): - cmap.saveConfig(self._config,save=False); - self._config.set("intensity-map-number",self._current_imap_index,save=False); - self._config.set("colour-map-number",self._current_cmap_index,save=False); - self._config.set("lock-range",self._lock_display_range,save=True); - - - def hasSlicing (self): - """Returns True if image is a cube, and so has non-trivial slicing axes"""; - return bool(self._sliced_axes); - - def slicedAxes (self): - """Returns list of (axis_num,name,label_list) tuples per each non-trivial slicing axis"""; - return self._sliced_axes; - - def incrementSlice (self,iaxis,incr,write_config=True): - dprint(2,"incrementing slice axis",iaxis,"by",incr); - self._current_slice[iaxis] = (self._current_slice[iaxis] + incr)%self._slice_dims[iaxis]; - self._updateSlice(write_config); - - def changeSlice (self,iaxis,index,write_config=True): - dprint(2,"changing slice axis",iaxis,"to",index); - if self._current_slice[iaxis] != index: - self._current_slice[iaxis] = index; - self._updateSlice(write_config); - - def selectSlice (self,indices,write_config=True): - """Selects slice given by indices"""; - dprint(2,"selecting slice",indices); - self._current_slice = list(indices); - self._updateSlice(write_config); - - def _updateSlice (self,write_config=True): - """Common internal method called to finalize changes to _current_slice"""; - busy = BusyIndicator(); - dprint(2,"_updateSlice",self._current_slice,time.time()%60); - indices = tuple(self._current_slice); - self.image.selectSlice(*indices); - dprint(2,"image slice selected",time.time()%60); - img = self.image.image(); - self._slicerange = self._sliceranges.get(indices); - if self._slicerange is None: - self._slicerange = self._sliceranges[indices] = self.image.imageMinMax()[:2]; - dprint(2,"min/max updated",time.time()%60); - self.setSliceSubset(set_display_range=False); - if write_config and self._config: - self._config.set("slice"," ".join(map(str,indices))); - - def displayRange (self): - return self._displayrange; - - def currentSlice (self): - return self._current_slice; - - def sliceDimensions (self): - return self._slice_dims; - - def getIntensityMapNames (self): - return [ name for name,imap in self._imap_list ]; - - def currentIntensityMapNumber (self): - return self._current_imap_index; - - def currentIntensityMap (self): - return self.image.intensityMap(); - - def setIntensityMapNumber (self,index,write_config=True): - busy = BusyIndicator(); - self._current_imap_index = index; - imap = self._imap_list[index][1]; - imap.setDataSubset(self._displaydata,self._displaydata_minmax); - imap.setDataRange(*self._displayrange); - self.image.setIntensityMap(imap); - self.emit(SIGNAL("intensityMapChanged"),imap,index); - if self._config and write_config: - self._config.set("intensity-map-number",index); - - def setIntensityMapLogCycles (self,cycles,notify_image=True,write_config=True): - busy = BusyIndicator(); - imap = self.currentIntensityMap(); - if isinstance(imap,Colormaps.LogIntensityMap): - imap.log_cycles = cycles; - if notify_image: - self.image.setIntensityMap(); - self.emit(SIGNAL("intensityMapChanged"),imap,self._current_imap_index); - if self._config and write_config: - self._config.set("intensity-log-cycles",cycles); - - def lockDisplayRangeForAxis (self,iaxis,lock): - pass; - - def getColormapList (self): - return self._cmap_list; - - def updateColorMapParameters (self): - """Call this when the colormap parameters have changed"""; - busy = BusyIndicator(); - self.image.updateCurrentColorMap(); - if self._config: - self._cmap_list[self._current_cmap_index].saveConfig(self._config); - - def setColorMapNumber (self,index,write_config=True): - busy = BusyIndicator(); - self._current_cmap_index = index; - cmap = self._cmap_list[index]; - self.image.setColorMap(cmap); - self.emit(SIGNAL("colorMapChanged"),cmap); - if self._config and write_config: - self._config.set("colour-map-number",index); - - def currentSubset (self): - """Returns tuple of subset,(dmin,dmax),description for current data subset"""; - return self._displaydata,self._displaydata_minmax,self._displaydata_desc,self._displaydata_type; - - def _resetDisplaySubset (self,subset,desc,range=None,set_display_range=True,write_config=True,subset_type=None): - dprint(4,"setting display subset"); - self._displaydata = subset; - self._displaydata_desc = desc; - self._displaydata_minmax = range = range or measurements.extrema(subset)[:2]; - self._displaydata_type = subset_type; - dprint(4,"range set"); - self.image.intensityMap().setDataSubset(self._displaydata,minmax=range); - self.image.setIntensityMap(emit=False); - self.emit(SIGNAL("dataSubsetChanged"),subset,range,desc,subset_type); - if set_display_range: - self.setDisplayRange(write_config=write_config,*range); - - def setFullSubset (self,display_range=None,write_config=True): - shapedesc = u"\u00D7".join(["%d"%x for x in list(self.image.imageDims()) + [len(labels) for iaxis,name,labels in self._sliced_axes]]); - desc = "full cube" if self._sliced_axes else "full image"; - self._resetDisplaySubset(self.image.data(),desc,range=self._fullrange,subset_type=self.SUBSET_FULL, - write_config=write_config,set_display_range=False); - self.setDisplayRange(write_config=write_config,*(display_range or self._fullrange)) - - def _makeSliceDesc (self): - """Makes a description of the current slice"""; - if not self._sliced_axes: - return "full image"; - descs = []; - for iextra,name,labels in self._sliced_axes: - if name.upper() not in ["STOKES","COMPLEX"]: - descs.append("%s=%s"%(name,labels[self._current_slice[iextra]])); - else: - descs.append(labels[self._current_slice[iextra]]); - return "%s plane"%(" ".join(descs),); - - def setSliceSubset (self,set_display_range=True,write_config=True):\ - return self._resetDisplaySubset(self.image.image(),self._makeSliceDesc(),self._slicerange, - subset_type=self.SUBSET_SLICE, - set_display_range=set_display_range,write_config=write_config); - - def _setRectangularSubset (self,xx1,xx2,yy1,yy2): - descs = []; - nx,ny = self.image.imageDims(); - if xx1 or xx2 != nx: - descs.append("x=%d:%d"%(xx1,xx2)); - if yy1 or yy2 != ny: - descs.append("y=%d:%d"%(yy1,yy2)); - if descs: - descs.append("in"); - descs.append(self._makeSliceDesc()); - return self._resetDisplaySubset(self.image.image()[xx1:xx2,yy1:yy2]," ".join(descs),subset_type=self.SUBSET_RECT); - - def _lmRectToPix (self,rect): - """helper function -- converts an LM rectangle to pixel coordinates"""; - if rect.width() and rect.height(): - # convert to pixel coordinates - x1,y1,x2,y2 = rect.getCoords(); - x1,y1 = self.image.lmToPix(x1,y1); - x2,y2 = self.image.lmToPix(x2,y2); - dprint(2,x1,y1,x2,y2); - xx1,xx2 = int(math.floor(min(x1,x2))),int(math.ceil(max(x1,x2))); - yy1,yy2 = int(math.floor(min(y1,y2))),int(math.ceil(max(y1,y2))); - dprint(2,xx1,yy1,xx2,yy2); - # ensure limits - nx,ny = self.image.imageDims(); - xx1,xx2 = max(xx1,0),min(xx2,nx); - yy1,yy2 = max(yy1,0),min(yy2,ny); - dprint(2,xx1,yy1,xx2,yy2); - # check that we actually selected some valid pixels - if xx1 < xx2 and yy1 < yy2: - return xx1,xx2,yy1,yy2; - return None,None,None,None; - - def setLMRectSubset (self,rect): - xx1,xx2,yy1,yy2 = self._lmRectToPix(rect); - if xx1 is not None: - return self._setRectangularSubset(xx1,xx2,yy1,yy2); - - def getLMRectStats (self,rect): - xx1,xx2,yy1,yy2 = self._lmRectToPix(rect); - if xx1 is not None: - subset = self.image.image()[xx1:xx2,yy1:yy2]; - subset,mask = self.image.optimalRavel(subset); - mmin,mmax = measurements.extrema(subset,labels=mask,index=None if mask is None else False)[:2]; - mean = measurements.mean(subset,labels=mask,index=None if mask is None else False); - std = measurements.standard_deviation(subset,labels=mask,index=None if mask is None else False); - ssum = measurements.sum(subset,labels=mask,index=None if mask is None else False); - return xx1,xx2,yy1,yy2,mmin,mmax,mean,std,ssum,subset.size; - return None; - - def setWindowSubset (self,rect=None): - rect = rect or self.image.currentRectPix(); - if rect.width() and rect.height(): - tl = rect.topLeft(); - return self._setRectangularSubset(tl.x(),tl.x()+rect.width(),tl.y(),tl.y()+rect.height()); - - def resetSubsetDisplayRange (self): - self.setDisplayRange(*self._displaydata_minmax); - - def isSubsetDisplayRange (self): - return self._displayrange == self._displaydata_minmax; - - def setDisplayRange (self,dmin,dmax,notify_image=True,write_config=True): - if dmax < dmin: - dmin,dmax = dmax,dmin; - if (dmin,dmax) != self._displayrange: - self._displayrange = dmin,dmax; - self.image.intensityMap().setDataRange(dmin,dmax); - if notify_image: - busy = BusyIndicator(); - self.image.setIntensityMap(emit=True); - self.emit(SIGNAL("displayRangeChanged"),dmin,dmax); - if self._config and write_config: - self._config.set("range-min",dmin,save=False); - self._config.set("range-max",dmax); - - def isDisplayRangeLocked (self): - return self._lock_display_range; - - def lockDisplayRange (self,lock=True,write_config=True): - self._lock_display_range = lock; - self.emit(SIGNAL("displayRangeLocked"),lock); - if self._config and write_config: - self._config.set("lock-range",bool(lock)); - diff --git a/Tigger/Images/SkyImage.py b/Tigger/Images/SkyImage.py deleted file mode 100644 index f9ea2be..0000000 --- a/Tigger/Images/SkyImage.py +++ /dev/null @@ -1,649 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import sys -import traceback -import math -import os.path -import time - -from PyQt4.Qt import * -from PyQt4.Qwt5 import * -import numpy -import numpy.ma -from scipy.ndimage import interpolation,measurements - -import Kittens.utils -pyfits = Kittens.utils.import_pyfits(); - -from Tigger.Coordinates import Projection -from Tigger.Images import Colormaps -from Tigger.Images import FITSHeaders - -DEG = math.pi/180; - -_verbosity = Kittens.utils.verbosity(name="skyimage"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - -class SkyImagePlotItem (QwtPlotItem,QObject): - """SkyImagePlotItem is a 2D image in l,m coordimnates"""; - def __init__ (self,nx=0,ny=0,l0=0,m0=0,dl=1,dm=1,image=None): - QwtPlotItem.__init__(self); - # name, if any - self.name = self.filename = None; - # internal init - self._qo = QObject(); - self._image = self._imgminmax = None; - self._nvaluecalls = 0; - self._value_time = self._value_time0 = None; - self._lminmax = (0,0); - self._mminmax = (0,0); - self._cache_qimage = {}; - self._cache_mapping = self._cache_imap = self._cache_interp = None; - self._psfsize = 0,0,0; - # set image, if specified - if image is not None: - nx,ny = image.shape; - self.setImage(image); - # set coordinates, if specified - if nx and ny: - self.setImageCoordinates(nx,ny,l0,m0,dl,dm); - # set default colormap and intensity map - self.colormap = Colormaps.GreyscaleColormap; - self.imap = Colormaps.LinearIntensityMap(); - - def emit (self,*args): - self._qo.emit(*args); - - def connect (self,*args): - QObject.connect(self._qo,*args); - - def clearDisplayCache (self): - """Clears all display caches."""; - self._cache_qimage = {}; - self._cache_interp = self._cache_imap = None; - - def setColorMap(self,cmap=None,emit=True): - """Changes the colormap. If called with no arguments, clears colormap-dependent caches"""; - self._cache_qimage = {}; - if cmap: - self.colormap = cmap; - if emit: - self.emit(SIGNAL("repaint")); - - def updateCurrentColorMap (self): - self._cache_qimage = {}; - self.emit(SIGNAL("repaint")); - - def setIntensityMap(self,imap=None,emit=True): - """Changes the intensity map. If called with no arguments, clears intensity map-dependent caches"""; - self._cache_qimage = {}; - self._cache_imap = None; - if imap: - self.imap = imap; - if emit: - self.emit(SIGNAL("repaint")); - - def colorMap (self): - return self.colormap; - - def intensityMap (self): - return self.imap; - - def setImageCoordinates (self,nx,ny,x0,y0,l0,m0,dl,dm): - """Sets up image coordinates. Pixel x0,y0 is centered at location l0,m0 in the plot, pixel size is dl,dm, image size is (nx,ny)""" - dprint(2,"image coordinates are",nx,ny,x0,y0,l0,m0,dl,dm); - self._nx,self._ny = nx,ny; - self._l0,self._m0 = l0,m0; - self._dl,self._dm = dl,dm; - self._x0,self._y0 = x0,y0; - self._lminmax = (l0-dl*(x0+0.5),l0+(nx-x0-0.5)*dl); - if dl < 0: - self._lminmax = (self._lminmax[1],self._lminmax[0]); - self._mminmax = (m0-dm*(y0+0.5),m0+(ny-y0-0.5)*dm); - self._bounding_rect = QRectF(self._lminmax[0],self._mminmax[0],nx*abs(dl),ny*abs(dm)); - self._bounding_rect_pix = QRect(0,0,nx,ny); - dprint(2,"image extents are",self._lminmax,self._mminmax); - - def imageDims (self): - """Returns image dimensions as mx,ny"""; - return self._nx,self._ny; - - def referencePixel (self): - return self._x0,self._y0; - - def lmToPix (self,l,m): - """Converts l,m coordimnates to float (so possibly fractional) pixel coordinates.""" - return self._x0+(l-self._l0)/self._dl,self._y0+(m-self._m0)/self._dm; - - def pixToLm (self,x,y): - """Converts pixel coordinates to lm coordinates.""" - return self._l0 + (x-self._x0)*self._dl,self._m0 + (y-self._y0)*self._dm; - - def getExtents (self): - """Returns image extent, as (l0,l1),(m0,m1)"""; - return self._lminmax,self._mminmax; - - def boundingRect (self): - """Returns bouding rectangle of image, in lm coordinates."""; - return self._bounding_rect; - - def currentRect (self): - """Returns currently visible rectange, in lm coordinates. Coordinates may be outside of image range."""; - return self._current_rect; - - def currentRectPix (self): - """Returns currently visible rectange, in pixel coordinates. Pixel coordinates are bounded to 0,0 and nx-1,ny-1."""; - return self._current_rect_pix; - - def setImage (self,image,key=None,minmax=None): - """Sets image array. - If key is not None, sets this as the image key (for use with the pixmap cache.) - If minmax is not None, then stores this as the (presumably cached or precomputed) min/max values. - """; - self._image = image; - self._imgminmax = minmax; - self._image_key = key; - # clear intermediate caches - self._prefilter = self._cache_interp = self._cache_imap = None; - # if key is None, also clear QImage cache -- it only works when we have images identified by keys - if key is None: - self._cache_qimage = {}; - - def image (self): - """Returns image array."""; - return self._image; - - def imagePixel (self,x,y): - if numpy.ma.isMA(self._image): - return self._image.data[x,y],self._image.mask[x,y]; - else: - return self._image[x,y],False; - - def imageMinMax (self): - if not self._imgminmax: - dprint(3,"computing image min/max"); - rdata,rmask = self.optimalRavel(self._image); - try: - self._imgminmax = measurements.extrema(rdata,labels=rmask,index=None if rmask is None else False)[:2]; - except: - # when all data is masked, some versions of extrema() throw an exception - self._imgminmax = numpy.nan,numpy.nan; - dprint(3,self._imgminmax); - return self._imgminmax; - - def draw (self,painter,xmap,ymap,rect): - """Implements QwtPlotItem.draw(), to render the image on the given painter."""; - xp1,xp2,xdp,xs1,xs2,xds = xinfo = xmap.p1(),xmap.p2(),xmap.pDist(),xmap.s1(),xmap.s2(),xmap.sDist(); - yp1,yp2,ydp,ys1,ys2,yds = yinfo = ymap.p1(),ymap.p2(),ymap.pDist(),ymap.s1(),ymap.s2(),ymap.sDist(); - dprint(5,"draw:",rect,xinfo,yinfo); - self._current_rect = QRectF(QPointF(xs2,ys1),QSizeF(xds,yds)); - self._current_rect_pix = QRect(QPoint(*self.lmToPix(xs1,ys1)),QPoint(*self.lmToPix(xs2,ys2))).intersected(self._bounding_rect_pix); - dprint(5,"draw:",self._current_rect_pix); - # put together tuple describing current mapping - mapping = xinfo,yinfo; - # if mapping has changed w.r.t. cache (i.e. zoom has changed), discard all cached QImages - if mapping != self._cache_mapping: - dprint(2,"does not match cached mapping, cache is:",self._cache_mapping); - dprint(2,"and we have:",mapping); - self.clearDisplayCache(); - self._cache_mapping = mapping; - t0 = time.time(); - # check cached QImage for current image key. - qimg = self._cache_qimage.get(self._image_key); - if qimg: - dprint(5,"QImage found in cache, reusing"); - # else regenerate image - else: - # check for cached intensity-mapped data - if self._cache_imap is not None: - dprint(5,"intensity-mapped data found in cache, reusing"); - else: - if self._cache_interp is not None: - dprint(5,"interpolated data found in cache, reusing"); - else: - image = self._image.transpose() if self._data_fortran_order else self._image; - spline_order = 2; - xsamp = abs(xmap.sDist()/xmap.pDist())/abs(self._dl); - ysamp = abs(ymap.sDist()/ymap.pDist())/abs(self._dm); - if max(xsamp,ysamp) < .33 or min(xsamp,ysamp) > 2: - spline_order = 1; - dprint(2,"regenerating drawing cache, sampling factors are",xsamp,ysamp,"spline order is",spline_order); - self._cache_imap = None; - if self._prefilter is None and spline_order>1: - self._prefilter = interpolation.spline_filter(image,order=spline_order); - dprint(2,"spline prefiltering took",time.time()-t0,"secs"); t0 = time.time(); - # make arrays of plot coordinates - # xp[0],yp[0] corresponds to pixel 0,0, where 0,0 is the upper-left corner of the plot - # the maps are in a funny order (w.r.t. meaning of p1/p2/s1/s2), so the indices here are determined empirically - # We also adjust by half-pixel, to get the world coordinate of the pixel _center_ - xp = xmap.s1() - (xmap.sDist()/xmap.pDist())*(0.5+numpy.arange(int(xmap.pDist()))); - yp = ymap.s2() - (ymap.sDist()/ymap.pDist())*(0.5+numpy.arange(int(ymap.pDist()))); - # now convert plot coordinates into fractional image pixel coordinates - xi = self._x0 + (xp - self._l0)/self._dl; - yi = self._y0 + (yp - self._m0)/self._dm; - # interpolate image data - ### # old code for nearest-neighbour interpolation - ### # superceded by interpolation below (we simply round pixel coordinates to go to NN when oversampling) - ### xi = xi.round().astype(int); - ### oob_x = (xi<0)|(xi>=self._nx); - ### xi[oob_x] = 0; - ### yi = yi.round().astype(int); - ### oob_y = (yi<0)|(yi>=self._ny); - ### yi[oob_y] = 0; - ### idx = (xi[:,numpy.newaxis]*self._ny + yi[numpy.newaxis,:]).ravel(); - ### interp_image = self._image.ravel()[idx].reshape((len(xi),len(yi))); - ### interp_image[oob_x,:] = 0; - ### interp_image[:,oob_y] = 0; - ### self._qimage_cache = self.colormap.colorize(interp_image,self._img_range); - ### self._qimage_cache_attrs = (rect,xinfo,yinfo); - - # if either axis is oversampled by a factor of 3 or more, switch to nearest-neighbour interpolation by rounding pixel values - if xsamp < .33: - xi = xi.round(); - if ysamp < .33: - yi = yi.round(); - # make [2,nx,ny] array of interpolation coordinates - xy = numpy.zeros((2,len(xi),len(yi))); - xy[0,:,:] = xi[:,numpy.newaxis]; - xy[1,:,:] = yi[numpy.newaxis,:]; - # interpolate. Use NAN for out of range pixels... - # for fortran order, tranpose axes for extra speed (flip XY around then) - if self._data_fortran_order: - xy = xy[-1::-1,...]; - if spline_order > 1: - interp_image = interpolation.map_coordinates(self._prefilter,xy,order=spline_order,cval=numpy.nan,prefilter=False); - else: - interp_image = interpolation.map_coordinates(image,xy,order=spline_order,cval=numpy.nan); - # ...and put a mask on them (Colormap.colorize() will make these transparent). - mask = ~numpy.isfinite(interp_image); - self._cache_interp = numpy.ma.masked_array(interp_image,mask); - dprint(2,"interpolation took",time.time()-t0,"secs"); t0 = time.time(); - # ok, we have interpolated data in _cache_interp - self._cache_imap = self.imap.remap(self._cache_interp); - dprint(2,"intensity mapping took",time.time()-t0,"secs"); t0 = time.time(); - # ok, we have intensity-mapped data in _cache_imap - qimg = self.colormap.colorize(self._cache_imap); - dprint(2,"colorizing took",time.time()-t0,"secs"); t0 = time.time(); - # cache the qimage - self._cache_qimage[self._image_key] = qimg.copy(); - # now draw the image - t0 = time.time(); - painter.drawImage(xp1,yp2,qimg); - dprint(2,"drawing took",time.time()-t0,"secs"); - - def setPsfSize (self,maj,min,pa): - self._psfsize = maj,min,pa; - - def getPsfSize (self): - return self._psfsize; - -ScalePrefixes = [ "p","n",u"\u03bc","m","","K","M","G","T" ]; - -def getScalePrefix (*values): - """Helper method to get the optimal scale and SI prefix for a given range of values"""; - # take log10. If all values are zero, use prefix of 1. - log10 = numpy.ma.log10(numpy.ma.abs(values)); - if log10.mask.all(): - return 1,""; - # find appropriate prefix - # Add 1 to log10(min) (so that >=.1 unit is reported as unit), divide by 3, take floor, look up unit prefix - m = int(math.floor((log10.min()+1)/3)) + 4; - m = max(m,0); - m = min(m,len(ScalePrefixes)-1); - return 10**((m-4)*3),ScalePrefixes[m]; - -class SkyCubePlotItem (SkyImagePlotItem): - """Extends SkyImagePlotItem with a hypercube containing extra slices."""; - def __init__ (self,data=None,ndim=None): - SkyImagePlotItem.__init__(self); - # datacube (array of any rank) - self._data = self._dataminmax = None; - # current image slice (a list of indices) applied to data to make an image - self.imgslice = None; - # info about sky axes - self._skyaxes = [None,None]; - # info about other axes - self._extra_axes = []; - # set other info - if data is not None: - self.setData(data); - elif ndim: - self.setNumAxes(ndim); - - def setData (self,data,fortran_order=False): - """Sets the datacube. fortran_order is a hint, which makes iteration over - fortran-order arrays faster when computing min/max and such."""; - # Note that iteration order is absolutely critical for large cubes -- if data is in fortran - # order in memory, then that's the way we should iterate over it, period. Transposing is too - # slow. We therefore create 1D "views" of the data using numpy.ravel(x,order='F'), and use - # thse to iterate over the data for things like min/max, masking, etc. - if fortran_order: - dprint(3,"setData: computing mask (fortran order)"); - rav = numpy.ravel(data,order='F'); - rfin = numpy.isfinite(rav); - if rfin.all(): - dprint(3,"setData: phew, all finite, nothing to be masked"); - self._data = data; - else: - dprint(3,"setData: setting masked elements to 0"); - rmask = ~rfin; - rav[rmask] = 0; - dprint(3,"setData: creating masked array"); - mask = rmask.reshape(data.shape[-1::-1]).transpose(); - self._data = numpy.ma.masked_array(data,mask); - else: - dprint(3,"setData: computing mask (C order)"); - fin = numpy.isfinite(data); - if fin.all(): - dprint(3,"setData: phew, all finite, nothing to be masked"); - self._data = data; - else: - dprint(3,"setData: setting masked elements to 0"); - mask = ~fin; - data[mask] = 0; - dprint(3,"setData: creating masked array"); - self._data = numpy.ma.masked_array(data,mask); - dprint(3,"setData: wrapping up"); - self._data_fortran_order = fortran_order; - self._dataminmax = None; - self.setNumAxes(data.ndim); - ### old slow code - #dprint(3,"setData: computing mask"); - #fin = numpy.isfinite(data); - #mask = ~fin; - #dprint(3,"setData: setting masked elements to 0"); - #data[mask] = 0; - #dprint(3,"setData: creating masked array"); - #self._data = numpy.ma.masked_array(data,mask); - #dprint(3,"setData: wrapping up"); - #self._data_fortran_order = fortran_order; - #self._dataminmax = None; - #self.setNumAxes(data.ndim); - - def data (self): - """Returns datacube"""; - return self._data; - - def isDataInFortranOrder (self): - return self._data_fortran_order; - - def optimalRavel (self,array): - """Returns the "optimal ravel" corresponding to the given array, which is either FORTRAN - or C order. The optimal ravel is that over which iteration is fastest. - Returns tuple of ravarray,ravmask. If input array is not masked, then ravmask=None.""" - order = 'F' if self._data_fortran_order else 'C'; - rarr = numpy.ravel(array,order=order); - rmask = numpy.ravel(array.mask,order=order) if numpy.ma.isMA(array) else None; - return rarr,rmask; - - def dataMinMax (self): - if not self._dataminmax: - rdata,rmask = self.optimalRavel(self._data); - dprint(3,"computing data min/max"); - try: - self._dataminmax = measurements.extrema(rdata,labels=rmask,index=None if rmask is None else False); - except: - # when all data is masked, some versions of extrema() throw an exception - self._dataminmax = numpy.nan,numpy.nan; - dprint(3,self._dataminmax); - return self._dataminmax; - - def setNumAxes (self,ndim): - self.imgslice = [0]*ndim; - - def setSkyAxis (self,n,iaxis,nx,x0,dx,xpix0): - """Sets the sky axis, n=0 for RA and n=1 for Dec""" - if not self.imgslice: - raise RuntimeError,"setNumAxes() must be called first"; - # reverse axis if step is negative -# if dx<0: -# dx = -dx; -# xpix0 = nx-1-xpix0; -# self.imgslice[iaxis] = slice(-1,None,-1); -# else: - self.imgslice[iaxis] = slice(None); - self._skyaxes[n] = iaxis,nx,x0,dx,xpix0; - if iaxis == 0: - self.ra0 = x0; - else: - self.dec0 = x0; - - def getSkyAxis (self,n): - return self._skyaxes[n][:2]; - - def setExtraAxis (self,iaxis,name,labels,values,units): - """Sets additional hypercube axis. labels is an array of strings, one per each axis element, for labelled axes, or None if axis should be labelled with values/units. - values is an array of axis values, and units are the units in which values are expressed. - """; - units = units or ""; - scale,prefix = getScalePrefix(values); - units = prefix+units; - # estimate number of significant digits - valarr = numpy.array(values)/scale; - try: - ndigits = int(math.ceil(math.log10(max(abs(valarr))/abs((valarr[1:]-valarr[0:-1])).min()))); - nexp = int(abs(numpy.log10(abs(valarr))).max()); - # print ndigits,nexp; - if nexp > 4: - format = ".%de"%ndigits; - else: - format = ".%df"%ndigits; - except: - format = ".2g"; - if labels is None: - labels = [ ("%d: %"+format+" %s")%(i,val/scale,units) for i,val in enumerate(values) ]; - self._extra_axes.append((iaxis,name,labels,values,units,scale)); - - def numExtraAxes (self): - return len(self._extra_axes); - - def extraAxisNumberNameLabels (self,i): - return self._extra_axes[i][:3]; - - def extraAxisValues (self,i): - return self._extra_axes[i][3]; - - def extraAxisUnitScale (self,i): - return self._extra_axes[i][4:6]; - - def setPlotProjection (self,proj=None): - """Sets the projection of the plot. Must be called before image is drawn. If None is given, the default - projection is used. - """; - if not (self._skyaxes[0] and self._skyaxes[1]): - raise RuntimeError,"setSkyAxis() must be called for both sky axes"; - (iaxis_ra,nx,ra0,dra,i0),(iaxis_dec,ny,dec0,ddec,j0) = self._skyaxes; - proj = proj or self.projection; - # setup projection properties and get center of field - l0,m0 = proj.lm(ra0,dec0); - # find cell sizes - if proj is self.projection: - dl,dm = -self.projection.xscale,self.projection.yscale; - else: - dl = proj.offset(dra,0)[0]; - dm = proj.offset(0,ddec)[1]; - # setup image coordinates - self.setImageCoordinates(nx,ny,i0,j0,l0,m0,dl,dm); - - def setDefaultProjection (self,projection=None): - """Sets default image projection. If None is given, sets up default SinWCS projection."""; - self.projection = projection or Projection.SinWCS(ra0,dec0); - self.setPlotProjection(); - - def _setupSlice (self): - index = tuple(self.imgslice); - key = tuple([ index[iaxis] for iaxis,name,labels,values,units,scale in self._extra_axes ]); - image = self._data[index]; - self.setImage(self._data[index],key=key); - - def selectSlice (self,*indices): - if len(indices) != len(self._extra_axes): - raise ValueError,"number of indices does not match number of extra axes"""; - for i,(iaxis,name,labels,values,units,scale) in enumerate(self._extra_axes): - self.imgslice[iaxis] = indices[i]; - self._setupSlice(); - self.emit(SIGNAL("slice"),indices); - - def currentSlice (self): - return list(self.imgslice); - -class FITSImagePlotItem (SkyCubePlotItem): - - @staticmethod - def hasComplexAxis (hdr): - """Returns True if given FITS header has a complex axis (must be last axis)"""; - nax = hdr['NAXIS']; - return nax if hdr['CTYPE%d'%nax].strip() == "COMPLEX" else 0; - - @staticmethod - def addComplexAxis (header): - """Adds a complex axis to the given FITS header, returns new copy of header"""; - hdr = header.copy(); - nax = hdr['NAXIS']+1; - hdr['NAXIS'] = nax; - hdr.set('NAXIS%d'%nax,2,"complex image"); - hdr.set('CTYPE%d'%nax,"COMPLEX","complex image"); - hdr.set('CRPIX%d'%nax,1); - hdr.set('CRVAL%d'%nax,1); - hdr.set('CDELT%d'%nax,1); - return hdr; - - @staticmethod - def removeComplexAxis (header): - """Removes a complex axis from the given FITS header, returns new copy of header"""; - axis = FITSImagePlotItem.hasComplexAxis(header); - if axis: - header = header.copy(); - header['NAXIS'] = axis-1; - for name in 'NAXIS','CTYPE','CRPIX','CRVAL','CDELT': - key = "%s%d"%(name,axis); - if header.has_key(key): - del header[key]; - return header; - - def __init__ (self,filename=None,name=None,hdu=None): - SkyCubePlotItem.__init__(self); - self.name = name; - if filename or hdu: - self.read(filename,hdu); - - StokesNames = FITSHeaders.StokesNames; - ComplexNames = FITSHeaders.ComplexNames; - - def read (self,filename,hdu=None): - self.filename = filename; - self.name = self.name or os.path.basename(filename); - # read FITS file - if not hdu: - dprint(3,"opening",filename); - hdu = pyfits.open(filename)[0]; - hdu.verify('silentfix'); - hdr = self.fits_header = hdu.header; - dprint(3,"reading data"); - data = hdu.data; - # NB: all-data operations (such as getting global min/max or computing of histograms) are much faster - # (almost x2) when data is iterated - # over in the proper order. After a transpose(), data is in fortran order. Tell this to setData(). - data = numpy.transpose(data); # .copy() - dprint(3,"setting data"); - self.setData(data,fortran_order=True); - dprint(3,"reading header"); - ndim = hdr['NAXIS']; - if ndim < 2: - raise ValueError,"Cannot load a one-dimensional FITS file"; - # setup projection - # (strip out history from header, as big histories really slow down FITSWCS) - hdr1 = pyfits.Header(filter(lambda x:not str(x).startswith('HISTORY'),hdr.cards)); - proj = Projection.FITSWCS(hdr1); - nx = ny = None; - # find X and Y axes - for iaxis in range(ndim): - axs = str(iaxis+1); - npix = hdr['NAXIS'+axs]; - name = hdr.get('CTYPE'+axs,axs).strip().upper(); - # have we found the coordinate axes? - if FITSHeaders.isAxisTypeX(name): - nx = npix; - iaxis_ra = iaxis; - elif FITSHeaders.isAxisTypeY(name): - ny = npix; - iaxis_dec = iaxis; - # check that we have them - if nx is None or ny is None: - iaxis_ra,iaxis_dec = 0,1; - nx,ny = hdr.get('NAXIS1'),hdr.get('NAXIS2'); - for iaxis in range(ndim): - axs = str(iaxis+1); - # get axis description - npix = hdr['NAXIS'+axs]; - crval = hdr.get('CRVAL'+axs,0 ); - cdelt = hdr.get('CDELT'+axs,1) ; - crpix = hdr.get('CRPIX'+axs,1) -1; - name = hdr.get('CTYPE'+axs,axs).strip().upper(); - unit = hdr.get('CUNIT'+axs); - # if this is not an X/Y axis, add it to the slicers - if iaxis not in (iaxis_ra,iaxis_dec): - # values becomes a list of axis values - values = list(crval + (numpy.arange(npix) - crpix)*cdelt); - unit = unit and unit.lower().capitalize(); - # FITS knows of two enumerable axes: STOKES and COMPLEX. For these two, replace values with proper names - if name == "STOKES": - labels = [ (self.StokesNames[int(i)] if i>0 and i0 and i, -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import os.path - -FITS_ExtensionList = [ ".fts",".FTS",".fits",".FITS",".fit",".FIT" ]; - -def isFITS (filename): - return os.path.splitext(filename)[1] in FITS_ExtensionList; diff --git a/Tigger/MainWindow.py b/Tigger/MainWindow.py deleted file mode 100644 index 5cb5cad..0000000 --- a/Tigger/MainWindow.py +++ /dev/null @@ -1,684 +0,0 @@ -# -*- coding: utf-8 -*- - -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from Tigger import * - -import os -import os.path -import time -import sys -import fnmatch -import traceback - -from PyQt4.Qt import * - -import Kittens.utils -from Kittens.utils import PersistentCurrier - -from Models import ModelClasses -from Models import SkyModel -from Models.Formats import ModelHTML -import Widgets -import AboutDialog -from SkyModelTreeWidget import * -from Plot.SkyModelPlot import * -from Images.Manager import ImageManager -import Tigger.Tools.source_selector - -_verbosity = Kittens.utils.verbosity(name="mainwin"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - -class MainWindow (QMainWindow): - ViewModelColumns = [ "name","RA","Dec","type","Iapp","I","Q","U","V","RM","spi","shape" ]; - def __init__ (self,parent,hide_on_close=False): - QMainWindow.__init__(self,parent); - self.setWindowIcon(pixmaps.tigger_starface.icon()); - self._currier = PersistentCurrier(); - self.hide(); - # init column constants - for icol,col in enumerate(self.ViewModelColumns): - setattr(self,"Column%s"%col.capitalize(),icol); - # init GUI - self.setWindowTitle("Tigger"); - # self.setIcon(pixmaps.purr_logo.pm()); - cw = QWidget(self); - self.setCentralWidget(cw); - cwlo = QVBoxLayout(cw); - cwlo.setMargin(5); - # make splitter - spl1 = self._splitter1 = QSplitter(Qt.Vertical,cw); - spl1.setOpaqueResize(False); - cwlo.addWidget(spl1); - # Create listview of LSM entries - self.tw = SkyModelTreeWidget(spl1); - self.tw.hide(); - - # split bottom pane - spl2 = self._splitter2 = QSplitter(Qt.Horizontal,spl1); - spl2.setOpaqueResize(False); - self._skyplot_stack = QWidget(spl2); - self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack); - self._skyplot_stack_lo.setContentsMargins(0,0,0,0); - - # add plot - self.skyplot = SkyModelPlotter(self._skyplot_stack,self); - self.skyplot.resize(128,128); - self.skyplot.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Preferred); - self._skyplot_stack_lo.addWidget(self.skyplot,1000); - self.skyplot.hide(); - QObject.connect(self.skyplot,SIGNAL("imagesChanged"),self._imagesChanged); - QObject.connect(self.skyplot,SIGNAL("showMessage"),self.showMessage); - QObject.connect(self.skyplot,SIGNAL("showErrorMessage"),self.showErrorMessage); - - self._grouptab_stack = QWidget(spl2); - self._grouptab_stack_lo = lo =QVBoxLayout(self._grouptab_stack); - self._grouptab_stack_lo.setContentsMargins(0,0,0,0); - # add groupings table - self.grouptab = ModelGroupsTable(self._grouptab_stack); - self.grouptab.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred); - QObject.connect(self,SIGNAL("hasSkyModel"),self.grouptab.setEnabled); - lo.addWidget(self.grouptab,1000); - lo.addStretch(1); - self.grouptab.hide(); - - # add image controls -- parentless for now (setLayout will reparent them anyway) - self.imgman = ImageManager(); - self.skyplot.setImageManager(self.imgman); - QObject.connect(self.imgman,SIGNAL("imagesChanged"),self._imagesChanged); - QObject.connect(self.imgman,SIGNAL("showMessage"),self.showMessage); - QObject.connect(self.imgman,SIGNAL("showErrorMessage"),self.showErrorMessage); - - # enable status line - self.statusBar().show(); - # Create and populate main menu - menubar = self.menuBar(); - # File menu - file_menu = menubar.addMenu("&File"); - qa_open = file_menu.addAction("&Open model...",self._openFileCallback,Qt.CTRL+Qt.Key_O); - qa_merge = file_menu.addAction("&Merge in model...",self._mergeFileCallback,Qt.CTRL+Qt.SHIFT+Qt.Key_O); - QObject.connect(self,SIGNAL("hasSkyModel"),qa_merge.setEnabled); - file_menu.addSeparator(); - qa_save = file_menu.addAction("&Save model",self.saveFile,Qt.CTRL+Qt.Key_S); - QObject.connect(self,SIGNAL("isUpdated"),qa_save.setEnabled); - qa_save_as = file_menu.addAction("Save model &as...",self.saveFileAs); - QObject.connect(self,SIGNAL("hasSkyModel"),qa_save_as.setEnabled); - qa_save_selection_as = file_menu.addAction("Save selection as...",self.saveSelectionAs); - QObject.connect(self,SIGNAL("hasSelection"),qa_save_selection_as.setEnabled); - file_menu.addSeparator(); - qa_close = file_menu.addAction("&Close model",self.closeFile,Qt.CTRL+Qt.Key_W); - QObject.connect(self,SIGNAL("hasSkyModel"),qa_close.setEnabled); - qa_quit = file_menu.addAction("Quit",self.close,Qt.CTRL+Qt.Key_Q); - - # Image menu - menubar.addMenu(self.imgman.getMenu()); - # Plot menu - menubar.addMenu(self.skyplot.getMenu()); - - # LSM Menu - em = QMenu("&LSM",self); - self._qa_em = menubar.addMenu(em); - self._qa_em.setVisible(False); - QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_em.setVisible); - self._column_view_menu = QMenu("&Show columns",self); - self._qa_cv_menu = em.addMenu(self._column_view_menu); - em.addSeparator(); - em.addAction("Select &all",self._selectAll,Qt.CTRL+Qt.Key_A); - em.addAction("&Invert selection",self._selectInvert,Qt.CTRL+Qt.Key_I); - em.addAction("Select b&y attribute...",self._showSourceSelector,Qt.CTRL+Qt.Key_Y); - em.addSeparator(); - qa_add_tag = em.addAction("&Tag selection...",self.addTagToSelection,Qt.CTRL+Qt.Key_T); - QObject.connect(self,SIGNAL("hasSelection"),qa_add_tag.setEnabled); - qa_del_tag = em.addAction("&Untag selection...",self.removeTagsFromSelection,Qt.CTRL+Qt.Key_U); - QObject.connect(self,SIGNAL("hasSelection"),qa_del_tag.setEnabled); - qa_del_sel = em.addAction("&Delete selection",self._deleteSelection); - QObject.connect(self,SIGNAL("hasSelection"),qa_del_sel.setEnabled); - - # Tools menu - tm = self._tools_menu = QMenu("&Tools",self); - self._qa_tm = menubar.addMenu(tm); - self._qa_tm.setVisible(False); - QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_tm.setVisible); - - # Help menu - menubar.addSeparator(); - hm = self._help_menu = menubar.addMenu("&Help"); - hm.addAction("&About...",self._showAboutDialog); - self._about_dialog = None; - - # message handlers - self.qerrmsg = QErrorMessage(self); - - # set initial state - self.setAcceptDrops(True); - self.model = None; - self.filename = None; - self._display_filename = None; - self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None; - self.emit(SIGNAL("isUpdated"),False); - self.emit(SIGNAL("hasSkyModel"),False); - self.emit(SIGNAL("hasSelection"),False); - self._exiting = False; - - # set initial layout - self._current_layout = None; - self.setLayout(self.LayoutEmpty); - dprint(1,"init complete"); - - # layout identifiers - LayoutEmpty = "empty"; - LayoutImage = "image"; - LayoutImageModel = "model"; - LayoutSplit = "split"; - - def _getFilenamesFromDropEvent (self,event): - """Checks if drop event is valid (i.e. contains a local URL to a FITS file), and returns list of filenames contained therein."""; - dprint(1,"drop event:",event.mimeData().text()); - if not event.mimeData().hasUrls(): - dprint(1,"drop event: no urls"); - return None; - filenames = []; - for url in event.mimeData().urls(): - name = str(url.toLocalFile()); - dprint(2,"drop event: name is",name); - if name and Images.isFITS(name): - filenames.append(name); - dprint(2,"drop event: filenames are",filenames); - return filenames; - - def dragEnterEvent (self,event): - if self._getFilenamesFromDropEvent(event): - dprint(1,"drag-enter accepted"); - event.acceptProposedAction(); - else: - dprint(1,"drag-enter rejected"); - - def dropEvent (self,event): - filenames = self._getFilenamesFromDropEvent(event); - dprint(1,"dropping",filenames); - if filenames: - event.acceptProposedAction(); - busy = BusyIndicator(); - for name in filenames: - self.imgman.loadImage(name); - - def saveSizes (self): - if self._current_layout is not None: - dprint(1,"saving sizes for layout",self._current_layout); - # save main window size and splitter dimensions - sz = self.size(); - Config.set('%s-main-window-width'%self._current_layout,sz.width()); - Config.set('%s-main-window-height'%self._current_layout,sz.height()); - for spl,name in ((self._splitter1,"splitter1"),(self._splitter2,"splitter2")): - ssz = spl.sizes(); - for i,sz in enumerate(ssz): - Config.set('%s-%s-size%d'%(self._current_layout,name,i),sz); - - def loadSizes (self): - if self._current_layout is not None: - dprint(1,"loading sizes for layout",self._current_layout); - # get main window size and splitter dimensions - w = Config.getint('%s-main-window-width'%self._current_layout,0); - h = Config.getint('%s-main-window-height'%self._current_layout,0); - dprint(2,"window size is",w,h); - if not (w and h): - return None; - self.resize(QSize(w,h)); - for spl,name in (self._splitter1,"splitter1"),(self._splitter2,"splitter2"): - ssz = [ Config.getint('%s-%s-size%d'%(self._current_layout,name,i),-1) for i in 0,1 ]; - dprint(2,"splitter",name,"sizes",ssz); - if all([ sz >=0 for sz in ssz ]): - spl.setSizes(ssz); - else: - return None; - return True; - - def setLayout (self,layout): - """Changes the current window layout. Restores sizes etc. from config file."""; - if self._current_layout is layout: - return; - dprint(1,"switching to layout",layout); - # save sizes to config file - self.saveSizes(); - # remove imgman widget from all layouts - for lo in self._skyplot_stack_lo,self._grouptab_stack_lo: - if lo.indexOf(self.imgman) >= 0: - lo.removeWidget(self.imgman); - # assign it to appropriate parent and parent's layout - if layout is self.LayoutImage or layout is self.LayoutEmpty: - lo = self._skyplot_stack_lo; - else: - lo = self._grouptab_stack_lo; - self.imgman.setParent(lo.parentWidget()); - lo.addWidget(self.imgman,0); - # show/hide panels - if layout is self.LayoutEmpty: - self.tw.hide(); - self.grouptab.hide(); - self.skyplot.show(); - elif layout is self.LayoutImage: - self.tw.hide(); - self.grouptab.hide(); - self.skyplot.show(); - elif layout is self.LayoutImageModel: - self.tw.show(); - self.grouptab.show(); - self.skyplot.show(); - # reload sizes - self._current_layout = layout; - if not self.loadSizes(): - dprint(1,"no sizes loaded, setting defaults"); - if layout is self.LayoutEmpty: - self.resize(QSize(512,256)); - elif layout is self.LayoutImage: - self.resize(QSize(512,512)); - self._splitter2.setSizes([512,0]); - elif layout is self.LayoutImageModel: - self.resize(QSize(1024,512)); - self._splitter1.setSizes([256,256]); - self._splitter2.setSizes([256,256]); - - def enableUpdates (self,enable=True): - """Enables updates of the child widgets. Usually called after startup is completed (i.e. all data loaded)"""; - self.skyplot.enableUpdates(enable); - if enable: - if self.model: - self.setLayout(self.LayoutImageModel); - elif self.imgman.getImages(): - self.setLayout(self.LayoutImage); - else: - self.setLayout(self.LayoutEmpty); - self.show(); - - def _showAboutDialog (self): - if not self._about_dialog: - self._about_dialog = AboutDialog.AboutDialog(self); - self._about_dialog.show(); - - def addTool (self,name,callback): - """Adds a tool to the Tools menu"""; - self._tools_menu.addAction(name,self._currier.curry(self._callTool,callback)); - - def _callTool (self,callback): - callback(self,self.model); - - def _imagesChanged (self): - """Called when the set of loaded images has changed"""; - if self.imgman.getImages(): - if self._current_layout is self.LayoutEmpty: - self.setLayout(self.LayoutImage); - else: - if not self.model: - self.setLayout(self.LayoutEmpty); - - def _selectAll (self): - if not self.model: - return; - busy = BusyIndicator(); - for src in self.model.sources: - src.selected = True; - self.model.emitSelection(self); - - def _selectInvert (self): - if not self.model: - return; - busy = BusyIndicator(); - for src in self.model.sources: - src.selected = not src.selected; - self.model.emitSelection(self); - - def _deleteSelection (self): - unselected = [ src for src in self.model.sources if not src.selected ]; - nsel = len(self.model.sources) - len(unselected); - if QMessageBox.question(self,"Delete selection","""

Really deleted %d selected source(s)? - %d unselected sources will remain in the model.

"""%(nsel,len(unselected)), - QMessageBox.Ok|QMessageBox.Cancel,QMessageBox.Cancel) != QMessageBox.Ok: - return; - self.model.setSources(unselected); - self.showMessage("""Deleted %d sources"""%nsel); - self.model.emitUpdate(SkyModel.UpdateAll,origin=self); - - def _showSourceSelector (self): - Tigger.Tools.source_selector.show_source_selector(self,self.model); - - def _updateModelSelection (self,num,origin=None): - """Called when the model selection has been updated."""; - self.emit(SIGNAL("hasSelection"),bool(num)); - - import Tigger.Models.Formats - _formats = [ f[1] for f in Tigger.Models.Formats.listFormatsFull() ]; - - _load_file_types = [ (doc,["*"+ext for ext in extensions],load) for load,save,doc,extensions in _formats if load ]; - _save_file_types = [ (doc,["*"+ext for ext in extensions],save) for load,save,doc,extensions in _formats if save ]; - - def showMessage (self,msg,time=3000): - self.statusBar().showMessage(msg,3000); - - def showErrorMessage (self,msg,time=3000): - self.qerrmsg.showMessage(msg); - - def loadImage (self,filename): - return self.imgman.loadImage(filename); - - def setModel (self,model): - self.emit(SIGNAL("modelChanged"),model); - if model: - self.model = model; - self.emit(SIGNAL("hasSkyModel"),True); - self.emit(SIGNAL("hasSelection"),False); - self.emit(SIGNAL("isUpdated"),False); - self.model.enableSignals(); - self.model.connect("updated",self._indicateModelUpdated); - self.model.connect("selected",self._updateModelSelection); - # pass to children - self.tw.setModel(self.model); - self.grouptab.setModel(self.model); - self.skyplot.setModel(self.model); - # add items to View menu - self._column_view_menu.clear(); - self.tw.addColumnViewActionsTo(self._column_view_menu); - else: - self.model = None; - self.setWindowTitle("Tigger"); - self.emit(SIGNAL("hasSelection"),False); - self.emit(SIGNAL("isUpdated"),False); - self.emit(SIGNAL("hasSkyModel"),False); - self.tw.clear(); - self.grouptab.clear(); - self.skyplot.setModel(None); - - def _openFileCallback (self): - if not self._open_file_dialog: - filters = ";;".join([ "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._load_file_types ]); - dialog = self._open_file_dialog = QFileDialog(self,"Open sky model",".",filters); - dialog.setFileMode(QFileDialog.ExistingFile); - dialog.setModal(True); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.openFile); - self._open_file_dialog.exec_(); - return; - - def _mergeFileCallback (self): - if not self._merge_file_dialog: - filters = ";;".join([ "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._load_file_types ]); - dialog = self._merge_file_dialog = QFileDialog(self,"Merge in sky model",".",filters); - dialog.setFileMode(QFileDialog.ExistingFile); - dialog.setModal(True); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"), - self._currier.curry(self.openFile,merge=True)); - self._merge_file_dialog.exec_(); - return; - - def openFile (self,filename=None,format=None,merge=False,show=True): - from Models import ModelClasses - # check that we can close existing model - if not merge and not self._canCloseExistingModel(): - return False; - if isinstance(filename,QStringList): - filename = filename[0]; - filename = str(filename); - # try to determine the file type - filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename,format); - if import_func is None: - self.showErrorMessage("""Error loading model file %s: unknown file format"""%filename); - return; - # try to load the specified file - busy = BusyIndicator(); - self.showMessage("""Reading %s file %s"""%(filetype,filename),3000); - QApplication.flush(); - try: - model = import_func(filename); - model.setFilename(filename); - except: - busy = None; - self.showErrorMessage("""Error loading '%s' file %s: %s"""%(filetype,filename,str(sys.exc_info()[1]))); - return; - # set the layout - if show: - self.setLayout(self.LayoutImageModel); - # add to content - if merge and self.model: - self.model.addSources(model.sources); - self.showMessage("""Merged in %d sources from '%s' file %s"""%(len(model.sources),filetype,filename),3000); - self.model.emitUpdate(SkyModel.UpdateAll); - else: - self.showMessage("""Loaded %d sources from '%s' file %s"""%(len(model.sources),filetype,filename),3000); - self._display_filename = os.path.basename(filename); - self.setModel(model); - self._indicateModelUpdated(updated=False); - # only set self.filename if an export function is available for this format. Otherwise set it to None, so that trying to save - # the file results in a save-as operation (so that we don't save to a file in an unsupported format). - self.filename = filename if export_func else None; - - def closeEvent (self,event): - dprint(1,"closing"); - self._exiting = True; - self.saveSizes(); - if not self.closeFile(): - self._exiting = False; - event.ignore(); - return; - self.skyplot.close(); - self.imgman.close(); - self.emit(SIGNAL("closing")); - dprint(1,"invoking os._exit(0)"); - os._exit(0); - QMainWindow.closeEvent(self,event); - - def _canCloseExistingModel (self): - # save model if modified - if self.model and self._model_updated: - res = QMessageBox.question(self,"Closing sky model","

Model has been modified, would you like to save the changes?

", - QMessageBox.Save|QMessageBox.Discard|QMessageBox.Cancel,QMessageBox.Save); - if res == QMessageBox.Cancel: - return False; - elif res == QMessageBox.Save: - if not self.saveFile(confirm=False,overwrite=True): - return False; - # unload model images, unless we are already exiting anyway - if not self._exiting: - self.imgman.unloadModelImages(); - return True; - - def closeFile (self): - if not self._canCloseExistingModel(): - return False; - # close model - self._display_filename = None; - self.setModel(None); - # set the layout - self.setLayout(self.LayoutImage if self.imgman.getTopImage() else self.LayoutEmpty); - return True; - - def saveFile (self,filename=None,confirm=False,overwrite=True,non_native=False): - """Saves file using the specified 'filename'. If filename is None, uses current filename, if - that is not set, goes to saveFileAs() to open dialog and get a filename. - If overwrite=False, will ask for confirmation before overwriting an existing file. - If non_native=False, will ask for confirmation before exporting in non-native format. - If confirm=True, will ask for confirmation regardless. - Returns True if saving succeeded, False on error (or if cancelled by user). - """; - if isinstance(filename,QStringList): - filename = filename[0]; - filename = ( filename and str(filename) ) or self.filename; - if filename is None: - return self.saveFileAs(); - else: - warning = ''; - # try to determine the file type - filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename,None); - if export_func is None: - self.showErrorMessage("""Error saving model file %s: unsupported output format"""%filename); - return; - if os.path.exists(filename) and not overwrite: - warning += "

The file already exists and will be overwritten.

"; - if filetype != 'Tigger' and not non_native: - warning += """

Please note that you are exporting the model using the external format '%s'. - Source types, tags and other model features not supported by this - format will be omitted during the export.

"""%filetype; - # get confirmation - if confirm or warning: - dialog = QMessageBox.warning if warning else QMessageBox.question; - if dialog(self,"Saving sky model","

Save model to %s?

%s"%(filename,warning), - QMessageBox.Save|QMessageBox.Cancel,QMessageBox.Save) != QMessageBox.Save: - return False; - busy = BusyIndicator(); - try: - export_func(self.model,filename); - self.model.setFilename(filename); - except: - busy = None; - self.showErrorMessage("""Error saving model file %s: %s"""%(filename,str(sys.exc_info()[1]))); - return False; - self.showMessage("""Saved model to file %s"""%filename,3000); - self._display_filename = os.path.basename(filename); - self._indicateModelUpdated(updated=False); - self.filename = filename; - return True; - - def saveFileAs (self,filename=None): - """Saves file using the specified 'filename'. If filename is None, opens dialog to get a filename. - Returns True if saving succeeded, False on error (or if cancelled by user). - """; - if filename is None: - if not self._save_as_dialog: - filters = ";;".join([ "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._save_file_types ]); - dialog = self._save_as_dialog = QFileDialog(self,"Save sky model",".",filters); - dialog.setDefaultSuffix(ModelHTML.DefaultExtension); - dialog.setFileMode(QFileDialog.AnyFile); - dialog.setAcceptMode(QFileDialog.AcceptSave); - dialog.setConfirmOverwrite(False); - dialog.setModal(True); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.saveFileAs); - return self._save_as_dialog.exec_() == QDialog.Accepted; - # filename supplied, so save - return self.saveFile(filename,confirm=False); - - def saveSelectionAs (self,filename=None,force=False): - if not self.model: - return; - if filename is None: - if not self._save_sel_as_dialog: - filters = ";;".join([ "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._save_file_types ]); - dialog = self._save_sel_as_dialog = QFileDialog(self,"Save sky model",".",filters); - dialog.setDefaultSuffix(ModelHTML.DefaultExtension); - dialog.setFileMode(QFileDialog.AnyFile); - dialog.setAcceptMode(QFileDialog.AcceptSave); - dialog.setConfirmOverwrite(True); - dialog.setModal(True); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.saveSelectionAs); - return self._save_sel_as_dialog.exec_() == QDialog.Accepted; - # save selection - if isinstance(filename,QStringList): - filename = filename[0]; - filename= str(filename); - selmodel = self.model.copy(); - sources = [ src for src in self.model.sources if src.selected ]; - if not sources: - self.showErrorMessage("""You have not selected any sources to save."""); - return; - # try to determine the file type - filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename,None); - if export_func is None: - self.showErrorMessage("""Error saving model file %s: unsupported output format"""%filename); - return; - busy = BusyIndicator(); - try: - export_func(self.model,filename,sources=sources); - except: - busy = None; - self.showErrorMessage("""Error saving selection to model file %s: %s"""%(filename,str(sys.exc_info()[1]))); - return False; - self.showMessage("""Wrote %d selected source%s to file %s"""%(len(selmodel.sources),"" if len(selmodel.sources)==1 else "s",filename),3000); - pass; - - def addTagToSelection (self): - if not hasattr(self,'_add_tag_dialog'): - self._add_tag_dialog = Widgets.AddTagDialog(self,modal=True); - self._add_tag_dialog.setTags(self.model.tagnames); - self._add_tag_dialog.setValue(True); - if self._add_tag_dialog.exec_() != QDialog.Accepted: - return; - tagname,value = self._add_tag_dialog.getTag(); - if tagname is None or value is None: - return None; - dprint(1,"tagging selected sources with",tagname,value); - # tag selected sources - for src in self.model.sources: - if src.selected: - src.setAttribute(tagname,value); - # If tag is not new, set a UpdateSelectionOnly flag on the signal - dprint(1,"adding tag to model"); - self.model.addTag(tagname); - dprint(1,"recomputing totals"); - self.model.getTagGrouping(tagname).computeTotal(self.model.sources); - dprint(1,"emitting update signal"); - what = SkyModel.UpdateSourceContent+SkyModel.UpdateTags+SkyModel.UpdateSelectionOnly; - self.model.emitUpdate(what,origin=self); - - def removeTagsFromSelection (self): - if not hasattr(self,'_remove_tag_dialog'): - self._remove_tag_dialog = Widgets.SelectTagsDialog(self,modal=True,caption="Remove Tags",ok_button="Remove"); - # get set of all tags in selected sources - tags = set(); - for src in self.model.sources: - if src.selected: - tags.update(src.getTagNames()); - if not tags: - return; - tags = list(tags); - tags.sort(); - # show dialog - self._remove_tag_dialog.setTags(tags); - if self._remove_tag_dialog.exec_() != QDialog.Accepted: - return; - tags = self._remove_tag_dialog.getSelectedTags(); - if not tags: - return; - # ask for confirmation - plural = (len(tags)>1 and "s") or ""; - if QMessageBox.question(self,"Removing tags","

Really remove the tag%s '%s' from selected sources?

"%(plural,"', '".join(tags)), - QMessageBox.Yes|QMessageBox.No,QMessageBox.Yes) != QMessageBox.Yes: - return; - # remove the tags - for src in self.model.sources: - if src.selected: - for tag in tags: - src.removeAttribute(tag); - # update model - self.model.scanTags(); - self.model.initGroupings(); - # emit signal - what = SkyModel.UpdateSourceContent+SkyModel.UpdateTags+SkyModel.UpdateSelectionOnly; - self.model.emitUpdate(what,origin=self); - - def _indicateModelUpdated (self,what=None,origin=None,updated=True): - """Marks model as updated."""; - self._model_updated = updated; - self.emit(SIGNAL("isUpdated"),updated); - if self.model: - self.setWindowTitle("Tigger - %s%s"%((self._display_filename or "(unnamed)"," (modified)" if updated else ""))); - diff --git a/Tigger/Plot/MouseModes.py b/Tigger/Plot/MouseModes.py deleted file mode 100644 index e0cd5f7..0000000 --- a/Tigger/Plot/MouseModes.py +++ /dev/null @@ -1,209 +0,0 @@ -from Tigger import * -from PyQt4.Qt import * -import Kittens.utils -from Kittens.utils import curry,PersistentCurrier - -_verbosity = Kittens.utils.verbosity(name="mmod"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - - -_Contexts = dict(image=1,model=2); - -WHEELUP = "WheelUp"; -WHEELDOWN = "WheelDown"; - -MM_ZWIN = "zoom-window"; -MM_ZUNDO = "zoom-undo"; -MM_ZREDO = "zoom-redo"; -MM_UNZOOM = "unzoom"; -MM_MEAS = "measure"; -MM_STATS = "stats"; -MM_SELSRC = "select-source"; -MM_SELWIN = "select-window"; -MM_SELWINPLUS = "select-window-plus"; -MM_DESEL = "deselect-window"; - -_AllFuncs = [ MM_ZWIN,MM_ZUNDO,MM_ZREDO,MM_UNZOOM, - MM_MEAS,MM_STATS,MM_SELSRC,MM_SELWIN,MM_SELWINPLUS,MM_DESEL ]; - -FuncDoc = { - MM_ZWIN: "Zoom into window (click-drag) or zoom in at point (double-click)", - MM_ZUNDO: "Zoom out to previous view", - MM_ZREDO: "Zoom in to previous view", - MM_UNZOOM: "Zoom fully out", - MM_MEAS: "Measuring ruler (click and drag)", - MM_STATS: "Stats in box (click and drag)", - MM_SELSRC: "Select nearest source", - MM_SELWIN: "Select sources in window", - MM_SELWINPLUS: "Extend selection with sources in window", - MM_DESEL: "Deselect sources in window" -}; - -_DefaultModes = "Mouse3,Mouse2,Mouse1"; -_DefaultInitialMode = "Mouse3"; - -class MouseModeManager (QObject): - class MouseMode (object): - def __init__ (self,mid): - self.id = mid; - self.name = self.icon = self.tooltip = None; - self.contexts = []; - self.submodes = []; - self.patterns = {}; - self.qa = None; - - def addAction (self,menu,qag,callback,toolbar=None): - self.qa = menu.addAction(self.name,callback); - icon = self.icon and getattr(pixmaps,self.icon,None); - icon and self.qa.setIcon(icon.icon()); - self.qa.setCheckable(True); - qag.addAction(self.qa); - toolbar and toolbar.addAction(self.qa); - - def __init__ (self,parent,menu,toolbar): - QObject.__init__(self,parent); - self._currier = PersistentCurrier(); - # get list of mouse modes from config - modelist = []; - for mid in Config.get("mouse-modes",_DefaultModes).split(","): - if not ConfigFile.has_section(mid): - print "ERROR: unknown mouse-mode '%s', skipping. Check your %s."%(mid,ConfigFileName); - else: - modelist.append(self._readModeConfig(mid)); - self._modes = dict([ (mode.id,mode) for mode in modelist ]); - self._qag_mode = QActionGroup(self); - self._qag_submode = QActionGroup(self); - self._all_submodes = []; - # make entries for main modes - for mode in modelist: - mode.addAction(menu,self._qag_mode,callback=self._currier.curry(self._setMode,mode.id)); - if mode.submodes: - self._all_submodes += list(mode.submodes); - # make entries for submodes - self._qa_submode_sep = menu.addSeparator(); - self._modes.update([ (mode.id,mode) for mode in self._all_submodes ]); - for mode in self._all_submodes: - mode.addAction(menu,self._qag_submode,toolbar=toolbar,callback=self._currier.curry(self._setSubmode,mode.id)); - # other init - self._current_context = None; - self._available_submodes = []; - # set initial mode - initmode = Config.get("current-mouse-mode",_DefaultInitialMode); - if initmode not in self._modes: - initmode = modelist[0].id; - self._modes[initmode].qa.setChecked(True); - self._setMode(initmode,write_config=False); - - def currentMode (self): - return self._current_submode or self._current_mode; - - def setContext (self,has_image,has_model): - self._current_context = (has_image and _Contexts['image'])|(has_model and _Contexts['model']); - self._ensureValidSubmodes(); - - def _ensureValidSubmodes (self): - current = None; - self._valid_submodes = []; - # accumulate list of valid submodes, and find the checked-on one - for mode in self._available_submodes: - if not mode.contexts or not self._current_context or self._current_context&mode.contexts: - self._valid_submodes.append(mode); - mode.qa.setVisible(True); - if mode.qa.isChecked(): - current = mode.id; - else: - mode.qa.setVisible(False); - if self._valid_submodes: - self._setSubmode(current or self._valid_submodes[0].id); - - def _setMode (self,mid,write_config=True): - """Called when the mouse mode changes"""; - if write_config: - Config.set("current-mouse-mode",mid); - self._current_mode = mode = self._modes[mid]; - # hide submodes if any - for mm in self._all_submodes: - mm.qa.setVisible(False); - self._qa_submode_sep.setVisible(bool(mode.submodes)); - self._current_submode = None; - self._available_submodes = mode.submodes; - # make relevant submodes visible, and make sure one is enabled - if mode.submodes: - self._ensureValidSubmodes(); - else: - self.emit(SIGNAL("setMouseMode"),mode); - - def _setSubmode (self,mid): - """Called when the mouse submode changes"""; - self._current_submode = mode = self._modes[mid]; - mode.qa.setChecked(True); - # hide submodes if any - for mm in self._all_submodes: - mm.qa.setShortcuts([]); - # set F4 shortcut to next submode - if len(self._valid_submodes) > 1: - for i,mm in enumerate(self._valid_submodes): - if mm is mode: - self._valid_submodes[(i+1)%len(self._valid_submodes)].qa.setShortcut(Qt.Key_F4); - break; - self.emit(SIGNAL("setMouseMode"),mode); - - def _readModeConfig (self,section,main_tooltip=None): - """Reads the given config section (and uses the supplied defaults dict) - and returns a dict of mouse_patterns,key_patterns per each function.""" - # read basic stuff - mode = self.MouseMode(section); - config = Kittens.config.SectionParser(ConfigFile,section); - mode.name = config.get("name",section); - mode.icon = config.get("icon","") or None; - mode.contexts = sum([ _Contexts.get(x,0) for x in config.get("contexts","").split(",") ]); - submodes = config.get("submodes","") or None; - # eiher a mode with submodes, or a main mode - if submodes: - mode.tooltip = "

Your current mouse scheme is \"%s\".

"%mode.name; - for mid in submodes.split(","): - if ConfigFile.has_section(mid): - mode.submodes.append(self._readModeConfig(mid,main_tooltip=mode.tooltip)); - else: - print "ERROR: unknown submode '%s' in mode config section '%s', skipping/ Check your %s."%(mid,section,ConfigFileName); - else: - if main_tooltip: - mode.tooltip = main_tooltip +"""

In this scheme, available mouse functions depend on the selected mode. - The current mode is %s. Use F4 to cycle through other modes.

"""%mode.name; - else: - mode.tooltip = "

Your current mouse scheme is: \"%s\".

"%mode.name; - mode.tooltip += """

The following mouse functions are available:


\n"""; - patterns = {}; - # get basic patterns - for func in _AllFuncs: - # get pattern - pattern = config.get(func,""); - if not pattern: - continue; - mouse_pattern = key_pattern = None; - for pat in pattern.split(";"): - pat = pat.strip(); - if pat and pat.lower() != "none": - # split by "+" and lookup each identifier in the Qt namespace - scomps = pat.split("+"); - try: - comps = [ x if x in (WHEELUP,WHEELDOWN) else getattr(Qt,x) for x in scomps ]; - except AttributeError: - print "WARNING: can't parse '%s' for function '%s' in mode config section '%s', disabling. Check your %s."%(pat,func,section,ConfigFileName); - continue; - # append key/button code and sum of modifiers to the key or keyboard pattern list - if scomps[-1].startswith("Key_"): - if key_pattern: - print "WARNING: more than one key pattern for function '%s' in mode config section '%s', ignoring. Check your %s."%(func,section,ConfigFileName); - else: - key_pattern = comps[-1],sum(comps[:-1]); - else: - if mouse_pattern: - print "WARNING: more than one mouse pattern for function '%s' in mode config section '%s', ignoring. Check your %s."%(func,section,ConfigFileName); - else: - mouse_pattern = comps[-1],sum(comps[:-1]); - mode.tooltip += "\n"%(pattern,FuncDoc[func]); - mode.patterns[func] = (mouse_pattern or (0,0),key_pattern or (0,0)); - mode.tooltip += "
%s:  %s

"; - return mode; diff --git a/Tigger/Plot/SkyModelPlot.py b/Tigger/Plot/SkyModelPlot.py deleted file mode 100644 index 0953048..0000000 --- a/Tigger/Plot/SkyModelPlot.py +++ /dev/null @@ -1,1874 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -from PyQt4.Qwt5 import * -import math -import os.path -import re -import time -import numpy - -import Kittens.utils - -from Kittens.utils import curry,PersistentCurrier -from Kittens.widgets import BusyIndicator - -_verbosity = Kittens.utils.verbosity(name="plot"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - -from Tigger import pixmaps,Config,ConfigFile -from Tigger.Models import ModelClasses,PlotStyles -from Tigger import Coordinates -from Tigger.Coordinates import Projection -from Tigger.Models.SkyModel import SkyModel -from Tigger.Widgets import TiggerPlotCurve,TiggerPlotMarker -from Tigger.Plot import MouseModes - -# plot Z depths for various classes of objects -Z_Image = 1000; -Z_Grid = 9000; -Z_Source = 10000; -Z_SelectedSource = 10001; -Z_CurrentSource = 10002; -Z_Markup = 10010; - -# default stepping of grid circles -DefaultGridStep_ArcSec = 30*60; - -DEG = math.pi/180; - -class SourceMarker (object): - """SourceMarker implements a source marker corresponding to a SkyModel source. - The base class implements a marker at the centre. - """; - QwtSymbolStyles = dict( none=QwtSymbol.NoSymbol, - cross=QwtSymbol.XCross, - plus=QwtSymbol.Cross, - dot=QwtSymbol.Ellipse, - circle=QwtSymbol.Ellipse, - square=QwtSymbol.Rect, - diamond=QwtSymbol.Diamond, - triangle=QwtSymbol.Triangle, - dtriangle=QwtSymbol.DTriangle, - utriangle=QwtSymbol.UTriangle, - ltriangle=QwtSymbol.LTriangle, - rtriangle=QwtSymbol.RTriangle, - hline=QwtSymbol.HLine, - vline=QwtSymbol.VLine, - star1=QwtSymbol.Star1, - star2=QwtSymbol.Star2, - hexagon=QwtSymbol.Hexagon ); - - def __init__ (self,src,l,m,size,model): - self.src = src; - self._lm,self._size = (l,m),size; - self.plotmarker = TiggerPlotMarker(); - self.plotmarker.setValue(l,m); - self._symbol = QwtSymbol(); - self._font = QApplication.font(); - self._model = model; - self.resetStyle(); - - def lm (self): - """Returns plot coordinates of marker, as an l,m tuple"""; - return self._lm; - - def lmQPointF (self): - """Returns plot coordinates of marker, as a QPointF"""; - return self.plotmarker.value(); - - def source (self): - """Returns model source associated with marker"""; - return self.src; - - def attach (self,plot): - """Attaches to plot"""; - self.plotmarker.attach(plot); - - def isVisible (self): - return self.plotmarker.isVisible(); - - def setZ (self,z): - self.plotmarker.setZ(z); - - def resetStyle (self): - """Sets the source style based on current model settings""" - self.style,self.label = self._model.getSourcePlotStyle(self.src); - self._selected = getattr(self.src,'selected',False); - # setup marker components - self._setupMarker(self.style,self.label); - # setup depth - if self._model.currentSource() is self.src: - self.setZ(Z_CurrentSource); - elif self._selected: - self.setZ(Z_SelectedSource); - else: - self.setZ(Z_Source); - - def _setupMarker (self,style,label): - """Sets up the plot marker (self.plotmarker) based on style object and label string. - If style=None, makes marker invisible."""; - if not style: - self.plotmarker.setVisible(False); - return; - self.plotmarker.setVisible(True); - self._symbol.setStyle(self.QwtSymbolStyles.get(style.symbol,QwtSymbol.Cross)); - self._font.setPointSize(style.label_size); - symbol_color = QColor(style.symbol_color); - label_color = QColor(style.label_color); - # dots have a fixed size - if style.symbol == "dot": - self._symbol.setSize(2); - else: - self._symbol.setSize(self._size); - self._symbol.setPen(QPen(symbol_color,style.symbol_linewidth)); - self._symbol.setBrush(QBrush(Qt.NoBrush)); - lab_pen = QPen(Qt.NoPen); - lab_brush = QBrush(Qt.NoBrush); - self._label = label or ""; - self.plotmarker.setSymbol(self._symbol); - txt = QwtText(self._label); - txt.setColor(label_color); - txt.setFont(self._font); - txt.setBackgroundPen(lab_pen); - txt.setBackgroundBrush(lab_brush); - self.plotmarker.setLabel(txt); - self.plotmarker.setLabelAlignment(Qt.AlignBottom|Qt.AlignRight); - - def checkSelected (self): - """Checks the src.selected attribute, resets marker if it has changed. - Returns True is something has changed."""; - sel = getattr(self.src,'selected',False); - if self._selected == sel: - return False; - self._selected = sel; - self.resetStyle(); - return True; - - def changeStyle (self,group): - if group.func(self.src): - self.resetStyle(); - return True; - return False; - -class ImageSourceMarker (SourceMarker): - """This auguments SourceMarker with a FITS image.""" - def __init__ (self,src,l,m,size,model,imgman): - # load image if needed - self.imgman = imgman; - dprint(2,"loading Image source",src.shape.filename); - self.imagecon = imgman.loadImage(src.shape.filename,duplicate=False,to_top=False,model=src.name); - # this will return None if the image fails to load, in which case we still produce a marker, - # but nothing else - if self.imagecon: - self.imagecon.setMarkersZ(Z_Source); - # init base class - SourceMarker.__init__(self,src,l,m,size,model); - - def attach (self,plot): - SourceMarker.attach(self,plot); - if self.imagecon: - self.imagecon.attachToPlot(plot); - - def _setupMarker (self,style,label): - SourceMarker._setupMarker(self,style,label); - if not style: - return; - symbol_color = QColor(style.symbol_color); - label_color = QColor(style.label_color); - if self.imagecon: - self.imagecon.setPlotBorderStyle(border_color=symbol_color,label_color=label_color); - -def makeSourceMarker (src,l,m,size,model,imgman): - """Creates source marker based on source type"""; - shape = getattr(src,'shape',None); -# print type(shape),isinstance(shape,ModelClasses.FITSImage),shape.__class__,ModelClasses.FITSImage; - if isinstance(shape,ModelClasses.FITSImage): - return ImageSourceMarker(src,l,m,size,model,imgman); - else: - return SourceMarker(src,l,m,size,model); - -def makeDualColorPen (color1,color2,width=3): - c1,c2 = QColor(color1).rgb(),QColor(color2).rgb(); - texture = QImage(2,2,QImage.Format_RGB32); - texture.setPixel(0,0,c1); - texture.setPixel(1,1,c1); - texture.setPixel(0,1,c2); - texture.setPixel(1,0,c2); - return QPen(QBrush(texture),width); - -class ToolDialog (QDialog): - def __init__ (self,parent,configname,menuname,show_shortcut=None): - QDialog.__init__(self,parent); - self.setModal(False); - self.setFocusPolicy(Qt.NoFocus); - self.hide(); - self._configname = configname; - self._geometry = None; - # make hide/show qaction - self._qa_show = qa = QAction("Show %s"%menuname.replace("&","&&"),self); - if show_shortcut: - qa.setShortcut(show_shortcut); - qa.setCheckable(True); - qa.setChecked(Config.getbool("%s-show"%configname,False)); - qa.setVisible(False); - qa.setToolTip("""

The quick zoom & cross-sections window shows a zoom of the current image area - under the mose pointer, and X/Y cross-sections through that area.

"""); - QObject.connect(qa,SIGNAL("triggered(bool)"),self.setVisible); - self._closing = False; - self._write_config = curry(Config.set,"%s-show"%configname); - QObject.connect(qa,SIGNAL("triggered(bool)"),self._write_config); - QObject.connect(self,SIGNAL("isVisible"),qa.setChecked); - - def getShowQAction (self): - return self._qa_show; - - def makeAvailable (self,available=True): - """Makes the tool available (or unavailable)-- shows/hides the "show" control, and shows/hides the dialog according to this control."""; - self._qa_show.setVisible(available); - self.setVisible(self._qa_show.isChecked() if available else False); - - def initGeometry (self): - x0 = Config.getint('%s-x0'%self._configname,0); - y0 = Config.getint('%s-y0'%self._configname,0); - w = Config.getint('%s-width'%self._configname,0); - h = Config.getint('%s-height'%self._configname,0); - if w and h: - self.resize(w,h); - self.move(x0,y0); - return True; - return False; - - def _saveGeometry (self): - Config.set('%s-x0'%self._configname,self.pos().x()); - Config.set('%s-y0'%self._configname,self.pos().y()); - Config.set('%s-width'%self._configname,self.width()); - Config.set('%s-height'%self._configname,self.height()); - - def close (self): - self._closing = True; - QDialog.close(self); - - def closeEvent (self,event): - QDialog.closeEvent(self,event); - if not self._closing: - self._write_config(False); - - def moveEvent (self,event): - self._saveGeometry(); - QDialog.moveEvent(self,event); - - def resizeEvent (self,event): - self._saveGeometry(); - QDialog.resizeEvent(self,event); - - def setVisible (self,visible,emit=True): - if not visible: - self._geometry = self.geometry(); - else: - if self._geometry: - self.setGeometry(self._geometry); - if emit: - self.emit(SIGNAL("isVisible"),visible); - QDialog.setVisible(self,visible); - -class LiveImageZoom (ToolDialog): - def __init__ (self,parent,radius=10,factor=12): - ToolDialog.__init__(self,parent,configname="livezoom",menuname="live zoom & cross-sections",show_shortcut=Qt.Key_F2); - self.setWindowTitle("Zoom & Cross-sections"); - radius = Config.getint("livezoom-radius",radius); - # add plots - self._lo0 = lo0 = QVBoxLayout(self); - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo1.setSpacing(0); - lo0.addLayout(lo1); - # control checkboxes - self._showzoom = QCheckBox("show zoom",self); - self._showcs = QCheckBox("show cross-sections",self); - self._showzoom.setChecked(True); - self._showcs.setChecked(True); - QObject.connect(self._showzoom,SIGNAL("toggled(bool)"),self._showZoom); - QObject.connect(self._showcs,SIGNAL("toggled(bool)"),self._showCrossSections); - lo1.addWidget(self._showzoom,0); - lo1.addSpacing(5); - lo1.addWidget(self._showcs,0); - lo1.addStretch(1); - self._smaller = QToolButton(self); - self._smaller.setIcon(pixmaps.window_smaller.icon()); - QObject.connect(self._smaller,SIGNAL("clicked()"),self._shrink); - self._larger = QToolButton(self); - self._larger.setIcon(pixmaps.window_larger.icon()); - QObject.connect(self._larger,SIGNAL("clicked()"),self._enlarge); - lo1.addWidget(self._smaller); - lo1.addWidget(self._larger); - self._has_zoom = self._has_xcs = self._has_ycs = False; - # setup zoom plot - font = QApplication.font(); - self._zoomplot = QwtPlot(self); -# self._zoomplot.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed); - self._zoomplot.setMargin(0); - self._zoomplot.setTitle(""); - axes = { QwtPlot.xBottom: "X pixel coordinate", - QwtPlot.yLeft:"Y pixel coordinate", - QwtPlot.xTop:"X cross-section value", - QwtPlot.yRight:"Y cross-section value" }; - for axis,title in axes.iteritems(): - self._zoomplot.enableAxis(True); - self._zoomplot.setAxisScale(axis,0,1); - self._zoomplot.setAxisFont(axis,font); - self._zoomplot.setAxisMaxMajor(axis,3); - self._zoomplot.axisWidget(axis).setMinBorderDist(16,16); - self._zoomplot.axisWidget(axis).show(); - text = QwtText(title); - text.setFont(font); - self._zoomplot.axisWidget(axis).setTitle(text); - self._zoomplot.setAxisLabelRotation(QwtPlot.yLeft,-90); - self._zoomplot.setAxisLabelAlignment(QwtPlot.yLeft,Qt.AlignVCenter); - self._zoomplot.setAxisLabelRotation(QwtPlot.yRight,90); - self._zoomplot.setAxisLabelAlignment(QwtPlot.yRight,Qt.AlignVCenter); - self._zoomplot.plotLayout().setAlignCanvasToScales(True); - lo0.addWidget(self._zoomplot,0); - # setup ZoomItem for zoom plot - self._zi = self.ImageItem(); - self._zi.attach(self._zoomplot); - self._zi.setZ(0); - # setup targeting reticule for zoom plot - self._reticules = TiggerPlotCurve(),TiggerPlotCurve(); - for curve in self._reticules: - curve.setPen(QPen(QColor("green"))); - curve.setStyle(QwtPlotCurve.Lines); - curve.attach(self._zoomplot); - curve.setZ(1); - # setup cross-section curves - pen = makeDualColorPen("navy","yellow"); - self._xcs = TiggerPlotCurve(); - self._ycs = TiggerPlotCurve(); - self._xcs.setPen(makeDualColorPen("navy","yellow")); - self._ycs.setPen(makeDualColorPen("black","cyan")); - for curve in self._xcs,self._ycs: - curve.setStyle(QwtPlotCurve.Steps); - curve.attach(self._zoomplot); - curve.setZ(2); - self._xcs.setXAxis(QwtPlot.xBottom); - self._xcs.setYAxis(QwtPlot.yRight); - self._ycs.setXAxis(QwtPlot.xTop); - self._ycs.setYAxis(QwtPlot.yLeft); - self._ycs.setCurveType(QwtPlotCurve.Xfy); - # make QTransform for flipping images upside-down - self._xform = QTransform(); - self._xform.scale(1,-1); - # init geometry - self.setPlotSize(radius,factor); - self.initGeometry(); - - - def _showZoom (self,show): - if not show: - self._zi.setVisible(False); - - def _showCrossSections (self,show): - self._zoomplot.enableAxis(QwtPlot.xTop,show); - self._zoomplot.enableAxis(QwtPlot.yRight,show); - if not show: - self._xcs.setVisible(False); - self._ycs.setVisible(False); - - def _enlarge (self): - self.setPlotSize(self._radius*2,self._magfac); - - def _shrink (self): - self.setPlotSize(self._radius/2,self._magfac); - - def setPlotSize (self,radius,factor): - Config.set('livezoom-radius',radius); - self._radius = radius; - # enable smaller/larger buttons based on radius - self._smaller.setEnabled(radius>5); - self._larger.setEnabled(radius<40); - # compute other sizes - self._npix = radius*2+1; - self._magfac = factor; - width = height = self._npix*self._magfac; - self._zoomplot.setMinimumHeight(height+80); - self._zoomplot.setMinimumWidth(width+80); - # set data array - self._data = numpy.ma.masked_array(numpy.zeros((self._npix,self._npix),float),numpy.zeros((self._npix,self._npix),bool)); - # reset window size - self._lo0.update(); - self.resize(self._lo0.minimumSize()); - - def _getZoomSlice (self,ix,nx): - ix0,ix1 = ix - self._radius,ix + self._radius + 1; - zx0 = -min(ix0,0); - ix0 = max(ix0,0); - zx1 = self._npix - max(ix1,nx-1) + (nx-1) - ix1 = min(ix1,nx-1); - return ix0,ix1,zx0,zx1; - - class ImageItem (QwtPlotItem): - """ImageItem subclass used by LiveZoomer to display zoomed-in images"""; - def __init__ (self): - QwtPlotItem.__init__(self); - self._qimg = None; - - def setImage (self,qimg): - self._qimg = qimg; - - def draw (self,painter,xmap,ymap,rect): - """Implements QwtPlotItem.draw(), to render the image on the given painter."""; - self._qimg and painter.drawImage(QRect(xmap.p1(),ymap.p2(),xmap.pDist(),ymap.pDist()),self._qimg); - - - def trackImage (self,image,ix,iy): - if not self.isVisible(): - return; - # update zoomed image - # find overlap of zoom window with image, mask invisible pixels - nx,ny = image.imageDims(); - ix0,ix1,zx0,zx1 = self._getZoomSlice(ix,nx); - iy0,iy1,zy0,zy1 = self._getZoomSlice(iy,ny); - if ix0 < nx and ix1 >=0 and iy0 < ny and iy1 >= 0: - if self._showzoom.isChecked(): - self._data.mask[...] = False; - self._data.mask[:zx0,...] = True; - self._data.mask[zx1:,...] = True; - self._data.mask[...,:zy0] = True; - self._data.mask[...,zy1:] = True; - # copy & colorize region - self._data[zx0:zx1,zy0:zy1] = image.image()[ix0:ix1,iy0:iy1]; - intensity = image.intensityMap().remap(self._data); - self._zi.setImage(image.colorMap().colorize(image.intensityMap().remap(self._data)).transformed(self._xform)); - self._zi.setVisible(True); - # set cross-sections - if self._showcs.isChecked(): - if iy >=0 and iy < ny and ix1>ix0: - xcs = [ float(x) for x in image.image()[ix0:ix1,iy] ]; - self._xcs.setData(numpy.arange(ix0-1,ix1)+.5,[xcs[0]]+xcs); - self._xcs.setVisible(True); - self._zoomplot.setAxisAutoScale(QwtPlot.yRight); - self._has_xcs = True; - else: - self._xcs.setVisible(False); - self._zoomplot.setAxisScale(QwtPlot.yRight,0,1); - if ix >=0 and ix < nx and iy1>iy0: - ycs = [ float(y) for y in image.image()[ix,iy0:iy1] ]; - self._ycs.setData([ycs[0]]+ycs,numpy.arange(iy0-1,iy1)+.5); - self._ycs.setVisible(True); - self._zoomplot.setAxisAutoScale(QwtPlot.xTop); - self._has_ycs = True; - else: - self._ycs.setVisible(False); - self._zoomplot.setAxisScale(QwtPlot.xTop,0,1); - else: - for plotitem in self._zi,self._xcs,self._ycs: - plotitem.setVisible(False); - # set zoom plot scales - x0,x1 = ix-self._radius-.5,ix+self._radius+.5; - y0,y1 = iy-self._radius-.5,iy+self._radius+.5 - self._reticules[0].setData([ix,ix],[y0,y1]); - self._reticules[1].setData([x0,x1],[iy,iy]); - self._zoomplot.setAxisScale(QwtPlot.xBottom,x0,x1); - self._zoomplot.setAxisScale(QwtPlot.yLeft,y0,y1); - self._zoomplot.enableAxis(QwtPlot.xTop,self._showcs.isChecked()); - # update plots - self._zoomplot.replot(); - -class LiveProfile (ToolDialog): - def __init__ (self,parent): - ToolDialog.__init__(self,parent,configname="liveprofile",menuname="profiles",show_shortcut=Qt.Key_F3); - self.setWindowTitle("Profiles"); - # add plots - lo0 = QVBoxLayout(self); - lo0.setSpacing(0); - lo1 = QHBoxLayout(); - lo1.setContentsMargins(0,0,0,0); - lo0.addLayout(lo1); - lab = QLabel("Axis: ",self); - self._wprofile_axis = QComboBox(self); - QObject.connect(self._wprofile_axis,SIGNAL("activated(int)"),self.selectAxis); - lo1.addWidget(lab,0); - lo1.addWidget(self._wprofile_axis,0); - lo1.addStretch(1); - # add profile plot - self._font = font = QApplication.font(); - self._profplot = QwtPlot(self); - self._profplot.setMargin(0); - self._profplot.enableAxis(QwtPlot.xBottom); - self._profplot.enableAxis(QwtPlot.yLeft); - self._profplot.setAxisFont(QwtPlot.xBottom,font); - self._profplot.setAxisFont(QwtPlot.yLeft,font); -# self._profplot.setAxisMaxMajor(QwtPlot.xBottom,3); - self._profplot.setAxisAutoScale(QwtPlot.yLeft); - self._profplot.setAxisMaxMajor(QwtPlot.yLeft,3); - self._profplot.axisWidget(QwtPlot.yLeft).setMinBorderDist(16,16); - self._profplot.setAxisLabelRotation(QwtPlot.yLeft,-90); - self._profplot.setAxisLabelAlignment(QwtPlot.yLeft,Qt.AlignVCenter); - self._profplot.plotLayout().setAlignCanvasToScales(True); - lo0.addWidget(self._profplot,0); - self._profplot.setMinimumHeight(192); - self._profplot.setMinimumWidth(256); - self._profplot.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum); - # and profile curve - self._profcurve = TiggerPlotCurve(); - self._ycs = TiggerPlotCurve(); - self._profcurve.setPen(QPen(QColor("black"))); - self._profcurve.setStyle(QwtPlotCurve.Lines); - self._profcurve.attach(self._profplot); - # config geometry - if not self.initGeometry(): - self.resize(300,192); - self._axes = []; - self._lastsel = None; - self._image_id = None; - - def setImage (self,image): - if id(image) == self._image_id: - return; - self._image_id = id(image); - # build list of axes -- first X and Y - self._axes = []; - for n,label in enumerate(("X","Y")): - iaxis,np = image.getSkyAxis(n); - self._axes.append((label,iaxis,range(np),"pixels")); - self._xaxis = self._axes[0][1]; - self._yaxis = self._axes[1][1]; - # then, extra axes - for i in range(image.numExtraAxes()): - iaxis,name,labels = image.extraAxisNumberNameLabels(i); - if len(labels) > 1 and name.upper() not in ("STOKES","COMPLEX"): - values = image.extraAxisValues(i); - unit,scale = image.extraAxisUnitScale(i); - self._axes.append((name,iaxis,[x/scale for x in values],unit)); - # put them into the selector - names = [ name for name,iaxis,vals,unit in self._axes ]; - self._wprofile_axis.addItems(names); - if self._lastsel in names: - axis = names.index(self._lastsel); - elif len(self._axes) > 2: - axis = 2; - else: - axis = 0; - self._wprofile_axis.setCurrentIndex(axis); - self.selectAxis(axis,remember=False); - - def selectAxis (self,i,remember=True): - if i < len(self._axes): - name,iaxis,values,unit = self._axes[i]; - self._selaxis = iaxis,values; - self._profplot.setAxisScale(QwtPlot.xBottom,min(values),max(values)); - title = QwtText("%s, %s"%(name,unit) if unit else name); - title.setFont(self._font); - self._profplot.setAxisTitle(QwtPlot.xBottom,title); - # save selection - if remember: - self._lastsel = name; - - def trackImage (self,image,ix,iy): - if not self.isVisible(): - return; - nx,ny = image.imageDims(); - inrange = ix < nx and ix >=0 and iy < ny and iy >= 0; - if inrange: - # check if image has changed - self.setImage(image); - # make profile slice - iaxis,xval = self._selaxis; - slicer = image.currentSlice(); - slicer[self._xaxis] = ix; - slicer[self._yaxis] = iy; - slicer[iaxis] = slice(None); - yval = image.data()[tuple(slicer)]; - i0,i1 = 0,len(xval); - # if X or Y profile, set axis scale to match that of window - if iaxis == 0: - rect = image.currentRectPix(); - i0 = rect.topLeft().x(); - i1 = i0 + rect.width(); - self._profplot.setAxisScale(QwtPlot.xBottom,xval[i0],xval[i1-1]); - elif iaxis == 1: - rect = image.currentRectPix(); - i0 = rect.topLeft().y(); - i1 = i0 + rect.height(); - self._profplot.setAxisScale(QwtPlot.xBottom,xval[i0],xval[i1-1]); - self._profcurve.setData(xval[i0:i1],yval[i0:i1]); - self._profcurve.setVisible(inrange); - # update plots - self._profplot.replot(); - -class SkyModelPlotter (QWidget): - # Selection modes for the various selector functions below. - # Default is usually Clear+Add - SelectionClear = 1; # clear previous selection - SelectionAdd = 2; # add to selection - SelectionRemove = 4; # remove from selection - # Mouse pointer modes - MouseZoom = 0; - MouseMeasure = 1; - MouseSubset = 2; - MouseSelect = 3; - MouseDeselect = 4; - - class Plot (QwtPlot): - """Auguments QwtPlot with additional functions, including a cache of QPoints thatr's cleared whenever a plot layout is - updated of the plot is zoomed"""; - def __init__ (self,mainwin,skymodelplotter,parent): - QwtPlot.__init__(self,parent); - self._skymodelplotter = skymodelplotter; - self.setAcceptDrops(True); - self.clearCaches(); - self._mainwin = mainwin; - self._drawing_key = None; - - def dragEnterEvent (self,event): - return self._mainwin.dragEnterEvent(event); - - def dropEvent (self,event): - return self._mainwin.dropEvent(event); - - def lmPosToScreen (self,fpos): - return QPoint(self.transform(QwtPlot.xBottom,fpos.x()),self.transform(QwtPlot.yLeft,fpos.y())); - - def lmRectToScreen (self,frect): - return QRect(self.lmPosToScreen(frect.topLeft()),self.lmPosToScreen(frect.bottomRight())); - - def screenPosToLm (self,pos): - return QPointF(self.invTransform(QwtPlot.xBottom,pos.x()),self.invTransform(QwtPlot.yLeft,pos.y())); - - def screenRectToLm(self,rect): - return QRectF(self.screenPosToLm(rect.topLeft()),self.screenPosToLm(rect.bottomRight())); - - def getMarkerPosition (self,marker): - """Returns QPoint associated with the given marker. Caches coordinate conversion by marker ID."""; - mid = id(marker); - pos = self._coord_cache.get(mid); - if pos is None: - self._coord_cache[mid] = pos = self.lmPosToScreen(marker.lmQPointF()); - return pos; - - def drawCanvas (self,painter): - dprint(5,"drawCanvas",time.time()%60); - if self._drawing_key is None: - dprint(5,"drawCanvas: key not set, redrawing"); - return QwtPlot.drawCanvas(self,painter); - else: - dprint(5,"drawCanvas: current key is",self._drawing_key); - pm = self._draw_cache.get(self._drawing_key); - if pm: - dprint(5,"drawCanvas: found pixmap in cache, drawing"); - else: - width,height = painter.device().width(),painter.device().height() - dprint(5,"drawCanvas: not in cache, redrawing %dx%d pixmap"%(width,height)); - self._draw_cache[self._drawing_key] = pm = QPixmap(width,height); - pm.fill(self.canvasBackground()); - QwtPlot.drawCanvas(self,QPainter(pm)); - painter.drawPixmap(0,0,pm); - dprint(5,"drawCanvas done",time.time()%60); - return; - - def clear (self): - """Override clear() to provide a saner interface."""; - self.clearCaches(); - self.detachItems(QwtPlotItem.Rtti_PlotItem,False); - - def updateLayout (self): - # if an update event is pending, skip our internal stuff - if self._skymodelplotter.isUpdatePending(): - dprint(5,"updateLayout: ignoring, since a plot update is pending"); - QwtPlot.updateLayout(self); - else: - dprint(5,"updateLayout"); - self.clearCaches(); - QwtPlot.updateLayout(self); - self.emit(SIGNAL("updateLayout")); - - def setDrawingKey (self,key=None): - """Sets the current drawing key. If key is set to not None, then drawCanvas() will look in the draw cache - for a pixmap matching the key, instead of redrawing the canvas. It will also cache the results of the draw. - """; - dprint(2,"setting drawing key",key); - self._drawing_key = key; - - def clearCaches (self): - dprint(2,"clearing plot caches"); - self._coord_cache = {}; - self._draw_cache = {}; - - def clearDrawCache (self): - self._draw_cache = {}; - - class PlotZoomer (QwtPlotZoomer): - def __init__(self,canvas,track_callback=None,label=None): - QwtPlotZoomer.__init__(self, canvas); - self.setMaxStackDepth(1000); - self._use_wheel = True; - self._track_callback = track_callback; - if label: - self._label = QwtText(label); - else: - self._label = QwtText(""); - self._fixed_aspect = False; - self._dczoom_button = self._dczoom_modifiers = None; - # maintain a separate stack of "desired" (as opposed to actual) zoom rects. When a resize of the plot happens, - # we recompute the actual zoom rect based on the aspect ratio and the desired rect. - self._zoomrects = []; - # watch plot for changes: if resized, aspect ratios need to be checked - QObject.connect(self.plot(),SIGNAL("updateLayout"),self._checkAspects); - - def isFixedAspect (self): - return self._fixed_aspect; - - def setFixedAspect (self,fixed): - self._fixed_aspect = fixed; - self._checkAspects(); - - def setDoubleClickZoom (self,button,modifiers): - self._dczoom_button,self._dczoom_modifiers = button,modifiers; - - def _checkAspects (self): - """If fixed-aspect mode is in effect, goes through zoom rects and adjusts them to the plot aspect"""; - if self._fixed_aspect: - dprint(2,"plot canvas size is",self.plot().size()); - dprint(2,"zoom rects are",self._zoomrects); - self._resetZoomStack(self.zoomRectIndex()); - - def setZoomStack (self,stack,index=0): - self._zoomrects = stack; - self._resetZoomStack(index); - - def _resetZoomStack (self,index): - stack = map(self.adjustRect,self._zoomrects); - if stack: - zs = stack[index]; - dprint(2,"resetting plot limits to",zs); - self.plot().setAxisScale(QwtPlot.yLeft,zs.top(),zs.bottom()); - self.plot().setAxisScale(QwtPlot.xBottom,zs.right(),zs.left()); - self.plot().axisScaleEngine(QwtPlot.xBottom).setAttribute(QwtScaleEngine.Inverted, True); - QwtPlotZoomer.setZoomBase(self); - dprint(2,"reset limits, zoom stack is now",self.zoomStack()); - dprint(2,"setting zoom stack",stack,index); - QwtPlotZoomer.setZoomStack(self,stack,index); - dprint(2,"zoom stack is now",self.zoomStack(),self.maxStackDepth()); - - def adjustRect (self,rect): - """Adjusts rectangle w.r.t. aspect ratio settings. That is, if a fixed aspect ratio is in effect, adjusts the rectangle to match - the aspect ratio of the plot canvas. Returns adjusted version.""" - if self._fixed_aspect: - dprint(2,"adjusting rect to canvas size:",self.canvas().size(),rect); - aspect0 = self.canvas().width()/float(self.canvas().height()) if self.canvas().height() else 1; - aspect = rect.width()/float(rect.height()); - # increase rectangle, if needed to match the aspect - if aspect < aspect0: - dx = rect.width()*(aspect0/aspect-1)/2; - return rect.adjusted(-dx,0,dx,0); - elif aspect0 and aspect > aspect0: - dy = rect.height()*(aspect/aspect0-1)/2; - return rect.adjusted(0,-dy,0,dy); - return rect; - - def rescale (self): - self.plot().clearCaches(); - return QwtPlotZoomer.rescale(self); - - def zoom (self,rect): - if not isinstance(rect,int): - rect = rect.intersected(self.zoomBase()); - # check that it's not too small, ignore if it is - x1,y1,x2,y2 = rect.getCoords(); - x1 = self.plot().transform(self.xAxis(),x1); - y1 = self.plot().transform(self.yAxis(),y1); - x2 = self.plot().transform(self.xAxis(),x2); - y2 = self.plot().transform(self.yAxis(),y2); - dprint(2,"zoom by",abs(x1-x2),abs(y1-y2)); - if abs(x1-x2)<=20 and abs(y1-y2)<=20: - return; - if isinstance(rect,int) or rect.isValid(): - dprint(2,"zoom",rect); - if not isinstance(rect,int): - self._zoomrects[self.zoomRectIndex()+1:] = [ QRectF(rect) ]; - rect = self.adjustRect(rect); - dprint(2,"zooming to",rect); - QwtPlotZoomer.zoom(self,rect); - dprint(2,"zoom stack is now",self.zoomStack()); - else: - dprint(2,"invalid zoom selected, ignoring",rect); - - def trackerText (self,pos): - return (self._track_callback and self._track_callback(pos)) or (self._label if self.isActive() else QwtText("")); - - def enableWheel (self,enable): - self._use_wheel = enable; - - def widgetMouseDoubleClickEvent (self,ev): - x = self.plot().invTransform(self.xAxis(),ev.x()); - y = self.plot().invTransform(self.yAxis(),ev.y()); - if int(ev.button()) == self._dczoom_button and int(ev.modifiers()) == self._dczoom_modifiers: - self.emit(SIGNAL("provisionalZoom"),x,y,1,10); - - def widgetWheelEvent (self,ev): - x = self.plot().invTransform(self.xAxis(),ev.x()); - y = self.plot().invTransform(self.yAxis(),ev.y()); - dprint(3,"zoomer wheel",ev.x(),ev.y(),ev.delta(),x,y,self._use_wheel); - if self._use_wheel: - self.emit(SIGNAL("provisionalZoom"),x,y,(1 if ev.delta() > 0 else -1),200); -# if ev.delta() < 0: -# if self.zoomRectIndex() > 0: -# self.zoom(-1); -# else: -# dprint(0,"zoomed all the way out, wheel event ignored"); -# else: -# x1,y1,x2,y2 = self.zoomRect().getCoords(); -# w = (x2-x1)/2; -# h = (y2-y1)/2; -# # self.zoom(QRectF(x-w/2,y-h/2,w,h)); -# self.emit(SIGNAL("provisionalZoom"),x,y,1); - QwtPlotPicker.widgetWheelEvent(self,ev); - - class PlotPicker (QwtPlotPicker): - """Auguments QwtPlotPicker with functions for selecting objects"""; - def __init__ (self,canvas,label,color="red",select_callback=None,track_callback=None, - mode=QwtPicker.RectSelection,rubber_band=QwtPicker.RectRubberBand,text_bg=None): - QwtPlotPicker.__init__(self,QwtPlot.xBottom,QwtPlot.yLeft,mode,rubber_band,QwtPicker.ActiveOnly,canvas); - # setup appearance - self._text = QwtText(label); - self._color = None; -# self._text_inactive = QwtText(); - self.setLabel(label,color); - if isinstance(text_bg,QColor): - text_bg = QBrush(text_bg); - self._text_bg = text_bg; - if text_bg: - self._text.setBackgroundBrush(text_bg); - self._text_inactive.setBackgroundBrush(text_bg); - # setup callbacks - self._track_callback = track_callback; - self._select_callback = select_callback; - if select_callback: - if mode == QwtPicker.RectSelection: - QObject.connect(self,SIGNAL("selected(const QwtDoubleRect &)"),select_callback); - elif mode == QwtPicker.PointSelection: - QObject.connect(self,SIGNAL("selected(const QwtDoublePoint &)"),select_callback); - elif mode == QwtPicker.PolygonSelection: - QObject.connect(self,SIGNAL("selected(const QwtPolygon &)"),select_callback); - - def setLabel (self,label,color=None): - if color: - self.setRubberBandPen(makeDualColorPen(color,"white")); - self._color = QColor(color); - self._text.setColor(self._color); - self._label = label; - self._text.setText(label); - - def trackerText (self,pos): - text = self._track_callback and self._track_callback(pos); - if text is None: - self._text.setText(self._label); - return self._text; # if self.isActive() else self._text_inactive; - else: - if not isinstance(text,QwtText): - if self._label: - text = "%s %s"%(self._label,text); - text = QwtText(text); - self._text.setText(self._label); - text = self._text; - if self._text_bg: - text.setBackgroundBrush(self._text_bg); - if self._color is not None: - text.setColor(self._color); - return text; - - class PlotRuler (PlotPicker): - """This is an ugly kludge to get a QwtPicker in PolygonSelection mode (with a PolygonRubberBand) - to act as a DragSelection. By default, it is impossible to display a "ruler" that acts like - a RecSelection-style DragSelection: rulers are only available with a PolygonSelection, - which does not support drag, but rather requires two clicks. By intercepting the mouse release - event here and faking a second mouse press, we achieve DragSelection-like behaviour.""" - def widgetLeaveEvent (self,event): - self.reset(); - def transition (self,event): - SkyModelPlotter.PlotPicker.transition(self,event); - if event.type() == QEvent.MouseButtonRelease: - ev1 = QMouseEvent(QEvent.MouseButtonPress,event.pos(),event.button(),event.buttons(),event.modifiers()); - SkyModelPlotter.PlotPicker.transition(self,ev1); - ev2 = QMouseEvent(QEvent.MouseButtonRelease,event.pos(),event.button(),event.buttons(),event.modifiers()); - SkyModelPlotter.PlotPicker.transition(self,ev2); - self.reset(); - - def __init__ (self,parent,mainwin,*args): - QWidget.__init__(self,parent,*args); - # plot update logic -- handle updates via the event looop - self._updates_enabled = False; # updates ignored until this is True - self._update_pending = 0; # serial number of most recently posted update event - self._update_done = 0; # serial number of most recently processed update event - self._update_what = 0; # mask of updates ('what' arguments to _updateLayout) accumulated since last update was done - # create currier - self._currier = PersistentCurrier(); - # init widgetry - lo = QHBoxLayout(self); - lo.setSpacing(0); - lo.setContentsMargins(0,0,0,0); - self.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding); - self.plot = self.Plot(mainwin,self,self); - self.plot.setAutoDelete(False); - self.plot.setEnabled(False); - self.plot.enableAxis(QwtPlot.yLeft,False); - self.plot.enableAxis(QwtPlot.xBottom,False); - lo.addWidget(self.plot); - # setup plot groupings - self._bg_color = QColor("#808080"); - self.plot.setCanvasBackground(self._bg_color); - self._bg_brush = QBrush(self._bg_color); - color = QColor("white"); - color.setAlpha(128); - self._coord_bg_brush = QBrush(color); - self._grid_color= QColor("navy"); - self._grid_pen = QPen(self._grid_color); - self._grid_pen.setStyle(Qt.DotLine); - self._image_pen = QPen(self._grid_color); - self._image_pen.setStyle(Qt.DashLine); - # init plot pickers - self._initPickers(); - # init markup symbols and colors and pens - self._plot_markup = []; - self._stats_color = QColor("red"); - self._stats_pen = QPen(self._stats_color,1); -# self._stats_pen.setStyle(Qt.DotLine); - self._subset_color = QColor("lightblue"); - self._subset_pen = QPen(self._subset_color,1); - self._markup_color = QColor("cyan"); - self._markup_pen = QPen(self._markup_color,1); - self._markup_brush = QBrush(Qt.NoBrush); - self._markup_xsymbol = QwtSymbol(QwtSymbol.XCross,self._markup_brush,self._markup_pen,QSize(16,16)); - self._markup_absymbol = QwtSymbol(QwtSymbol.Ellipse,self._markup_brush,self._markup_pen,QSize(4,4)); - self._markup_a_label = QwtText("A"); - self._markup_a_label.setColor(self._markup_color); - self._markup_b_label = QwtText("B"); - self._markup_b_label.setColor(self._markup_color); - # init live zoomers - self._livezoom = LiveImageZoom(self); - self._liveprofile = LiveProfile(self); - # other internal init - self.model = None; - self.projection = None; - self._zoomrect = None; - self._text_no_source = QwtText(""); - self._text_no_source.setColor(QColor("red")); - # image controller - self._imgman = self._image = None; - self._markers = {}; - self._source_lm = {}; - self._export_png_dialog = None; - # menu and toolbar - self._menu = QMenu("&Plot",self); - self._wtoolbar = QToolBar(self); - self._wtoolbar.setIconSize(QSize(16,16)); - self._wtoolbar.setOrientation(Qt.Vertical); - lo.insertWidget(0,self._wtoolbar); - self._qag_mousemode = QActionGroup(self); - self._qa_unzoom = self._wtoolbar.addAction(pixmaps.zoom_out.icon(),"Unzoom plot",self._currier.curry(self._zoomer.zoom,0)); - self._qa_unzoom.setToolTip("""

Click to unzoom the plot all the way out to its full size.

"""); - self._qa_unzoom.setShortcut(Qt.ALT+Qt.Key_Minus); - self._wtoolbar.addSeparator(); - self._menu.addAction(self._qa_unzoom); - # mouse mode controls - mouse_menu = self._menu.addMenu("Mouse mode"); - # init top of menu - mouse_menu.addAction("Show quick mouse reference",self._showMouseModeTooltip,Qt.Key_F1); - self._qa_mwzoom = qa = mouse_menu.addAction("Use mouse wheel zoom"); - qa.setCheckable(True); - QObject.connect(qa,SIGNAL("toggled(bool)"),self._zoomer.enableWheel); - QObject.connect(qa,SIGNAL("triggered(bool)"),self._currier.curry(Config.set,"mouse-wheel-zoom")); - qa.setChecked(Config.getbool("mouse-wheel-zoom",True)); - self._zoomer.enableWheel(qa.isChecked()); - mouse_menu.addSeparator(); - self._mousemodes = MouseModes.MouseModeManager(self,mouse_menu,self._wtoolbar); - QObject.connect(self._mousemodes,SIGNAL("setMouseMode"),self._setMouseMode); - self._setMouseMode(self._mousemodes.currentMode()); - self._qa_colorzoom = self._wtoolbar.addAction(pixmaps.zoom_colours.icon(),"Zoom colourmap into subset", - self._colourZoomIntoSubset); - self._qa_colorzoom.setShortcut(Qt.SHIFT+Qt.Key_F4); - self._qa_colorzoom.setVisible(False); - self._menu.addAction(self._qa_colorzoom); - # hide/show tools - self._menu.addAction(self._liveprofile.getShowQAction()); - self._menu.addAction(self._livezoom.getShowQAction()); - # fixed aspect - qa = self._menu.addAction("Fix aspect ratio"); - qa.setCheckable(True); - qa.setChecked(Config.getbool("fix-aspect-ratio",True)); - QObject.connect(qa,SIGNAL("toggled(bool)"),self._zoomer.setFixedAspect); - QObject.connect(qa,SIGNAL("triggered(bool)"),self._currier.curry(Config.set,"fix-aspect-ratio")); - self._zoomer.setFixedAspect(qa.isChecked()); - qa.setToolTip("""

Enable this to maintain a fixed aspect ratio in the plot.

"""); - # beam - self._qa_show_psf = self._menu.addAction("Show PSF (aka beam)"); - self._qa_show_psf.setCheckable(True); - self._qa_show_psf.setChecked(True); - self._psf_marker = TiggerPlotCurve(); - self._psf_marker.setPen(QPen(QColor("lightgreen"))); - self._psf_marker.setZ(Z_Grid); - QObject.connect(self._qa_show_psf,SIGNAL("toggled(bool)"),self._showPsfMarker); - # grid stepping - self._grid_step_arcsec = DefaultGridStep_ArcSec; - gridmenu = self._menu.addMenu("Show grid circles"); - qag = QActionGroup(gridmenu); - gridsteps = [ None,1,2,5,10,30,60,120,300,600 ]; - for step in gridsteps: - if step is None: - text = "None"; - elif step < 60: - text = "%d'"%step; - else: - text = u"%d\u00B0"%(step/60); - qa = gridmenu.addAction(text,self._currier.curry(self._setGridCircleStepping,step and step*60)); - qa.setCheckable(True); - qa.setChecked(step==self._grid_step_arcsec); - qag.addAction(qa); - qa = self._qa_custom_grid = gridmenu.addAction("Custom...",self._setCustomGridCircleStepping); - qa.setCheckable(True); - qag.addAction(qa); - self._grid_step_arcsec_str = ""; - if self._grid_step_arcsec/60 not in gridsteps: - self._setCustomGridCircleSteppingLabel(); - qa.setChecked(True); - # save as PNG file - self._menu.addAction("Export plot to PNG file...",self._exportPlotToPNG,Qt.CTRL+Qt.Key_F12); - - def close (self): - self._menu.clear(); - self._wtoolbar.clear(); - self._livezoom.close(); - self._liveprofile.close(); - - def getMenu (self): - return self._menu; - - def enableUpdates (self,enable=True): - self._updates_enabled = enable; - if enable: - self.postUpdateEvent(); - - # extra flag for updateContents() -- used when image content or projection has changed - UpdateImages = 1<<16; - - def setImageManager (self, im): - """Attaches an image manager.""" - self._imgman = im; - im.setZ0(Z_Image); - im.enableImageBorders(self._image_pen,self._grid_color,self._bg_brush); - QObject.connect(im,SIGNAL("imagesChanged"),self._currier.curry(self.postUpdateEvent,self.UpdateImages)); - QObject.connect(im,SIGNAL("imageRaised"),self._imageRaised); - - class UpdateEvent (QEvent): - def __init__ (self,serial): - QEvent.__init__(self,QEvent.User); - self.serial = serial; - - def isUpdatePending (self): - return self._update_pending > self._update_done; - - def postUpdateEvent (self,what=SkyModel.UpdateAll,origin=None): - """Posts an update event. Since plot updates are somewhat expensive, and certain operations can cause multiple updates, - we handle them through the event loop.""" - dprintf(3,"postUpdateEvent(what=%x,origin=%s)\n",what,origin); - self._update_what |= what; - self._update_pending += 1; - dprintf(3,"posting update event, serial %d, new mask %x\n",self._update_pending,self._update_what); - QCoreApplication.postEvent(self,self.UpdateEvent(self._update_pending)); - - def event (self,ev): - if isinstance(ev,self.UpdateEvent): - if ev.serial < self._update_pending: - dprintf(3,"ignoring update event %d since a more recent one is already posted\n",ev.serial); - else: - dprintf(3,"received update event %d, updating contents with mask %x\n",ev.serial,self._update_what); - self._updateContents(self._update_what); - self._update_what = 0; - self._update_done = ev.serial; - return QWidget.event(self,ev); - - def _initPickers (self): - """Called from __init__ to create the various plot pickers for support of mouse modes."""; - # this picker is invisible -- it is just there to make sure _trackCoordinates is always called - self._tracker = self.PlotPicker(self.plot.canvas(),"",mode=QwtPicker.PointSelection,track_callback=self._trackCoordinates); - self._tracker.setTrackerMode(QwtPicker.AlwaysOn); - # zoom picker - self._zoomer = self.PlotZoomer(self.plot.canvas(),label="zoom"); - self._zoomer_pen = makeDualColorPen("navy","yellow"); - self._zoomer.setRubberBandPen(self._zoomer_pen); - self._zoomer.setTrackerPen(QColor("yellow")); - QObject.connect(self._zoomer,SIGNAL("zoomed(const QwtDoubleRect &)"),self._plotZoomed); - QObject.connect(self._zoomer,SIGNAL("provisionalZoom"),self._plotProvisionalZoom); - self._zoomer_box = TiggerPlotCurve(); - self._zoomer_box.setPen(self._zoomer_pen); - self._zoomer_label = TiggerPlotMarker(); - self._zoomer_label_text = QwtText(""); - self._zoomer_label_text.setColor(QColor("yellow")); - self._zoomer_label.setLabel(self._zoomer_label_text); - self._zoomer_label.setLabelAlignment(Qt.AlignBottom|Qt.AlignRight); - for item in self._zoomer_label,self._zoomer_box: - item.setZ(Z_Markup); - self._provisional_zoom_timer = QTimer(self); - self._provisional_zoom_timer.setSingleShot(True); - QObject.connect(self._provisional_zoom_timer,SIGNAL("timeout()"),self._finalizeProvisionalZoom); - self._provisional_zoom = None; - -# self._zoomer.setStateMachine(QwtPickerDragRectMachine()); - self._zoomer.setSelectionFlags(QwtPicker.RectSelection|QwtPicker.DragSelection); - # ruler picker for measurement mode - self._ruler = self.PlotRuler(self.plot.canvas(),"measure","cyan",self._measureRuler, - mode=QwtPicker.PolygonSelection, - rubber_band=QwtPicker.PolygonRubberBand, - track_callback=self._trackRuler); - # this is the initial position of the ruler -- None if ruler is not tracking - self._ruler_pos0 = None; - # stats picker - self._picker_stats = self.PlotPicker(self.plot.canvas(),"stats","red",self._selectRectStats); - # model selection pickers - self._picker1 = self.PlotPicker(self.plot.canvas(),"select","green",self._selectRect); - self._picker2 = self.PlotPicker(self.plot.canvas(),"+select","green",curry(self._selectRect,mode=self.SelectionAdd)); - self._picker3 = self.PlotPicker(self.plot.canvas(),"-select","red",curry(self._selectRect,mode=self.SelectionRemove)); - self._picker4 = self.PlotPicker(self.plot.canvas(),"","green",self._selectNearestSource,mode=QwtPicker.PointSelection); - for picker in self._zoomer,self._ruler,self._picker1,self._picker2,self._picker3,self._picker4: - for sel in QwtEventPattern.MouseSelect1,QwtEventPattern.MouseSelect2,QwtEventPattern.MouseSelect3,QwtEventPattern.MouseSelect4: - picker.setMousePattern(sel,0,0); - picker.setTrackerMode(QwtPicker.ActiveOnly); -# for picker in self._ruler,self._picker1,self._picker2,self._picker3: -# QObject.connect(picker,SIGNAL("wheelEvent"),self._zoomer.widgetWheelEvent); - - def _showMouseModeTooltip (self): - tooltip = self._mousemodes.currentMode().tooltip; - if self._qa_mwzoom.isChecked(): - tooltip += """

You also have mouse-wheel zoom enabled. Rolling the wheel up will zoom in at the current zoom point. - Rolling the wheel down will zoom back out.

"""; - QMessageBox.information(self,"Quick mouse reference",tooltip); -# self._showCoordinateToolTip(self._mousemodes.currentMode().tooltip,rect=False); - - @staticmethod - def _setPickerPattern (picker,patt,func,mousemode,auto_disable=True): - """Helper function, sets mouse/key pattern for picker from the mode patterns dict"""; - mpat,kpat = mousemode.patterns.get(func,((0,0),(0,0))); - if auto_disable: - picker.setEnabled(mpat[0] or kpat[0]); - elif mpat[0] or kpat[0]: - picker.setEnabled(True); - picker.setMousePattern(patt,*mpat); - picker.setKeyPattern(patt,*kpat); - - def _setMouseMode (self,mode): - """Sets the current mouse mode from patterns (see MouseModes), updates action shortcuts. - 'mode' is MouseModes.MouseModeManager.MouseMode object. This has a patterns dict. - For each MM_xx function defined in MouseModes, patterns[MM_xx] = (mouse_patt,key_patt) - Each pattern is either None, or a (button,state) pair. If MM_xx is not in the dict, then thatfunction is - disabled."""; - dprint(1,"setting mouse mode",mode.id); - self._mouse_mode = mode.id; - # remove markup - self._removePlotMarkup(); - # disable/enable pickers accordingly - self._zoomer.setEnabled(True); - self._setPickerPattern(self._zoomer,QwtEventPattern.MouseSelect1,MouseModes.MM_ZWIN,mode,auto_disable=False); - if MouseModes.MM_ZWIN in mode.patterns: - self._zoomer.setDoubleClickZoom(*mode.patterns[MouseModes.MM_ZWIN][0]); - else: - self._zoomer.setDoubleClickZoom(0,0); - self._setPickerPattern(self._zoomer,QwtEventPattern.MouseSelect2,MouseModes.MM_UNZOOM,mode,auto_disable=False); - self._setPickerPattern(self._zoomer,QwtEventPattern.MouseSelect3,MouseModes.MM_ZUNDO,mode,auto_disable=False); - self._setPickerPattern(self._zoomer,QwtEventPattern.MouseSelect6,MouseModes.MM_ZREDO,mode,auto_disable=False); - self._setPickerPattern(self._ruler,QwtEventPattern.MouseSelect1,MouseModes.MM_MEAS,mode); - self._setPickerPattern(self._picker_stats,QwtEventPattern.MouseSelect1,MouseModes.MM_STATS,mode); - self._setPickerPattern(self._picker1,QwtEventPattern.MouseSelect1,MouseModes.MM_SELWIN,mode); - self._setPickerPattern(self._picker2,QwtEventPattern.MouseSelect1,MouseModes.MM_SELWINPLUS,mode); - self._setPickerPattern(self._picker3,QwtEventPattern.MouseSelect1,MouseModes.MM_DESEL,mode); - self._setPickerPattern(self._picker4,QwtEventPattern.MouseSelect1,MouseModes.MM_SELSRC,mode); - dprint(2,"picker4 pattern:",mode.patterns.get(MouseModes.MM_SELSRC,None)); - - def findNearestSource (self,pos,world=True,range=10): - """Returns source object nearest to the specified point (within range, in pixels), or None if nothing is in range. - 'pos' is a QPointF/QwtDoublePoint object in lm coordinates if world=True, else a QPoint object.""" - if world: - pos = self.plot.lmPosToScreen(pos); - dists = [ ((pos-self.plot.getMarkerPosition(marker)).manhattanLength(),marker) for marker in self._markers.itervalues() if marker.isVisible() ]; - if dists: - mindist = min(dists,key=lambda x:x[0]); - if mindist[0] < 10: - return mindist[1].src; - return None; - - def _convertCoordinates (self,pos): - # get ra/dec coordinates of point - pos = self.plot.screenPosToLm(pos); - l,m = pos.x(),pos.y(); - ra,dec = self.projection.radec(l,m); - rh,rm,rs = ModelClasses.Position.ra_hms_static(ra); - dsign,dd,dm,ds = ModelClasses.Position.dec_sdms_static(dec); - dist,pa = Coordinates.angular_dist_pos_angle(self.projection.ra0,self.projection.dec0,ra,dec); - Rd,Rm,Rs = ModelClasses.Position.dec_dms_static(dist); - PAd = pa*180/math.pi; - if PAd < 0: - PAd += 360; - # if we have an image, add pixel coordinates - x = y = val = flag = None; - image = self._imgman and self._imgman.getTopImage(); - if image: - x,y = map(int,map(round,image.lmToPix(l,m))); - nx,ny = image.imageDims(); - if x>=0 and x=0 and y 1: - lmpos = self.plot.screenPosToLm(pos); - ra,dec = self.projection.radec(lmpos.x(),lmpos.y()); - dist,pa = Coordinates.angular_dist_pos_angle(*(self._ruler_radec0 + [ra,dec])); - Rd,Rm,Rs = ModelClasses.Position.dec_dms_static(dist); - pa *= 180/math.pi; - pa += 360*(pa<0); - msgtext = u"%d\u00B0%02d'%05.2f\" PA=%.2f\u00B0"%(Rd,Rm,Rs,pa); - # self._ruler1.setLabel(""); - return QwtText(msgtext); - - def _measureRuler (self,polygon): - self._ruler_pos0 = None; - if not self.projection or polygon.size() < 2: - return; - # get distance between points, if <=1, report coordinates rather than a measurement - markup_items = []; - pos0,pos1 = polygon.point(0),polygon.point(1); - l,m,ra,dec,dist,pa,rh,rm,rs,dsign,dd,dm,ds,Rd,Rm,Rs,PAd,x,y,val,flag = self._convertCoordinates(pos0); - if (pos0-pos1).manhattanLength() <= 1: - # make tooltip text with HTML, make console (and cliboard) text w/o HTML - tiptext = ""; - msgtext = ""; - if self.projection.has_projection(): - tiptext += "X: %02dh%02dm%05.2fs %s%02d°%02d'%05.2f\"   r0=%d°%02d'%05.2f\"   PA0=%06.2f°"%(rh,rm,rs,dsign,dd,dm,ds,Rd,Rm,Rs,PAd); - msgtext += u"X: %2dh%02dm%05.2fs %s%02d\u00B0%02d'%05.2f\" (%.6f\u00B0 %.6f\u00B0) r=%d\u00B0%02d'%05.2f\" (%.6f\u00B0) PA=%6.2f\u00B0"%( - rh,rm,rs,dsign,dd,dm,ds,ra*180/math.pi,dec*180/math.pi,Rd,Rm,Rs,dist*180/math.pi,PAd); - if x is not None: - tiptext += "   x=%d y=%d value=blank"%(x,y) if flag else "   x=%d y=%d value=%g"%(x,y,val); - msgtext += " x=%d y=%d value=blank"%(x,y) if flag else " x=%d y=%d value=%g"%(x,y,val); - tiptext += ""; - # make marker - marker = TiggerPlotMarker(); - marker.setValue(l,m); - marker.setSymbol(self._markup_xsymbol); - markup_items.append(marker); - else: - l1,m1,ra1,dec1,dist1,pa1,rh1,rm1,rs1,dsign1,dd1,dm1,ds1,Rd1,Rm1,Rs1,PAd1,x1,y1,val1,flag1 = self._convertCoordinates(pos1); - # make tooltip text with HTML, and console/clipboard text without HTML - tiptext = ""; - msgtext = ""; - if self.projection.has_projection(): - tiptext += "A: %02dh%02dm%05.2fs %s%02d°%02d'%05.2f\"   r0=%d°%02d'%05.2f\"   PA0=%06.2f°"%(rh,rm,rs,dsign,dd,dm,ds,Rd,Rm,Rs,PAd); - msgtext += u"A: %2dh%02dm%05.2fs %s%02d\u00B0%02d'%05.2f\" (%.6f\u00B0 %.6f\u00B0) r=%d\u00B0%02d'%05.2f\" (%.6f\u00B0) PA=%06.2f\u00B0"%( - rh,rm,rs,dsign,dd,dm,ds,ra*180/math.pi,dec*180/math.pi,Rd,Rm,Rs,dist*180/math.pi,PAd); - if x is not None: - tiptext += "   x=%d y=%d value=blank"%(x,y) if flag else "   x=%d y=%d value=%g"%(x,y,val); - msgtext += u" x=%d y=%d value=blank"%(x,y) if flag else " x=%d y=%d value=%g"%(x,y,val); - tiptext += "
"; - if self.projection.has_projection(): - tiptext += "B: %02dh%02dm%05.2fs %s%02d°%02d'%05.2f\"   r0=%d°%02d'%05.2f\"   PA0=%06.2f°"%(rh1,rm1,rs1,dsign1,dd1,dm1,ds1,Rd1,Rm1,Rs1,PAd1) ; - msgtext += u"\nB: %2dh%02dm%05.2fs %s%02d\u00B0%02d'%05.2f\" (%.6f\u00B0 %.6f\u00B0) r=%d\u00B0%02d'%05.2f\" (%.6f\u00B0) PA=%6.2f\u00B0"%( - rh1,rm1,rs1,dsign1,dd1,dm1,ds1,ra1*180/math.pi,dec1*180/math.pi,Rd1,Rm1,Rs1,dist1*180/math.pi,PAd1); - if x1 is not None: - tiptext += "   x=%d y=%d value=blank"%(x1,y1) if flag1 else "   x=%d y=%d value=%g"%(x1,y1,val1); - msgtext += u" x=%d y=%d value=blank"%(x1,y1) if flag1 else " x=%d y=%d value=%g"%(x1,y1,val1); - tiptext += "
"; - # distance measurement - dist2,pa2 = Coordinates.angular_dist_pos_angle(ra,dec,ra1,dec1); - Rd2,Rm2,Rs2 = ModelClasses.Position.dec_dms_static(dist2); - pa2 *= 180/math.pi; - pa2 += 360*(pa2<0); - tiptext += "|AB|=%d°%02d'%05.2f\"   PAAB=%06.2f°"%(Rd2,Rm2,Rs2,pa2); - msgtext += u"\n|AB|=%d\u00B0%02d'%05.2f\" (%.6f\u00B0) PA=%6.2f\u00B0"%(Rd2,Rm2,Rs2,dist2*180/math.pi,pa2); - # make markers - marka,markb = TiggerPlotMarker(),TiggerPlotMarker(); - marka.setValue(l,m); - markb.setValue(l1,m1); - marka.setLabel(self._markup_a_label); - markb.setLabel(self._markup_b_label); - marka.setSymbol(self._markup_absymbol); - markb.setSymbol(self._markup_absymbol); - # work out optimal label alignment - aligna = Qt.AlignRight if pos0.x() > pos1.x() else Qt.AlignLeft; - alignb = Qt.AlignLeft if pos0.x() > pos1.x() else Qt.AlignRight; - aligna |= Qt.AlignBottom if pos0.y() > pos1.y() else Qt.AlignTop; - alignb |= Qt.AlignTop if pos0.y() > pos1.y() else Qt.AlignBottom; - marka.setLabelAlignment(aligna); - markb.setLabelAlignment(alignb); - marka.setSpacing(0); - markb.setSpacing(0); - line = TiggerPlotCurve(); - line.setData([l,l1],[m,m1]); - line.setBrush(self._markup_brush); - line.setPen(self._markup_pen); - markup_items = [ marka,markb,line ]; - # since this is going to hide the stats box, hide the colour zoom button too - self._qa_colorzoom.setVisible(False); - # calling QToolTip.showText() directly from here doesn't work, so set a timer on it - QTimer.singleShot(10,self._currier.curry(self._showCoordinateToolTip,tiptext)); - # same deal for markup items - for item in markup_items: - item.setZ(Z_Markup); - QTimer.singleShot(10,self._currier.curry(self._addPlotMarkup,markup_items)); - print msgtext; - QApplication.clipboard().setText(msgtext+"\n"); - QApplication.clipboard().setText(msgtext+"\n",QClipboard.Selection); - - def _showCoordinateToolTip (self,text,rect=True): - dprint(2,text); - if rect: - QToolTip.showText(self.plot.mapToGlobal(QPoint(0,0)),text,self.plot,self.plot.rect()); - else: - QToolTip.showText(self.plot.mapToGlobal(QPoint(0,0)),text); - - def _imageRaised (self): - """This is called when an image is raised to the top"""; - self._updatePsfMarker(None,replot=True); - self._removePlotMarkup(); - self._image_subset = None; - - def _showPsfMarker (self,show): - self._psf_marker.setVisible(show); - self.plot.clearDrawCache(); - self.plot.replot(); - - def _updatePsfMarker (self,rect=None,replot=False): - # show PSF if asked to - topimage = self._imgman and self._imgman.getTopImage(); - pmaj,pmin,ppa = topimage.getPsfSize() if topimage else (0,0,0); - self._qa_show_psf.setVisible(bool(topimage and pmaj!=0)); - self._psf_marker.setVisible(bool(topimage and pmaj!=0 and self._qa_show_psf.isChecked())); - if self._qa_show_psf.isVisible(): - rect = rect or self._zoomer.zoomBase(); - rect &= topimage.boundingRect(); - dprint(1,"updating PSF for zoom rect",rect); - lm = rect.bottomLeft(); - l00 = lm.x() + pmaj/1.2; - m00 = lm.y() - pmaj/1.2; - dprint(1,"drawing PSF at",l00,m00,"z",self._psf_marker.z()); - arg = numpy.arange(0,1.02,.02)*math.pi*2; - mp0,lp0 = pmaj*numpy.cos(arg)/2,pmin*numpy.sin(arg)/2; # angle 0 is m direction - c,s = numpy.cos(ppa),numpy.sin(ppa); - lp = lp0*c + mp0*s; - mp = - lp0*s + mp0*c; - self._psf_marker.setData(lp+l00,mp+m00); - if replot and self._psf_marker.isVisible(): - self._replot(); - - def _replot (self): - dprint(1,"replot"); - self.plot.clearDrawCache(); - self.plot.replot(); - - def _addPlotMarkup (self,items): - """Adds a list of QwtPlotItems to the markup"""; - self._removePlotMarkup(replot=False); - for item in items: - item.attach(self.plot); - self._plot_markup = items; - self._replot(); - - def _removePlotMarkup (self,replot=True): - """Removes all markup items, and refreshes the plot if replot=True"""; - for item in self._plot_markup: - item.detach(); - if self._plot_markup and replot: - QToolTip.showText(QPoint(0,0),""); - self._replot(); - self._plot_markup = []; - - def _trackCoordinates (self,pos): - if not self.projection: - return None; - # if Ctrl is pushed, get nearest source and make it "current" - if QApplication.keyboardModifiers()&(Qt.ControlModifier|Qt.ShiftModifier): - src = self.findNearestSource(pos,world=False,range=range); - if src: - self.model.setCurrentSource(src); - # get ra/dec coordinates of point - l,m,ra,dec,dist,pa,rh,rm,rs,dsign,dd,dm,ds,Rd,Rm,Rs,PAd,x,y,val,flag = self._convertCoordinates(pos); -# text = "

%2dh%02dm%05.2fs %+2d°%02d'%05.2f\""%(rh,rm,rs,dd,dm,ds); - # emit message as well - msgtext = ""; - if self.projection.has_projection(): - msgtext = u"%02dh%02dm%05.2fs %s%02d\u00B0%02d'%05.2f\" r=%d\u00B0%02d'%05.2f\" PA=%.2f\u00B0"%(rh,rm,rs,dsign,dd,dm,ds,Rd,Rm,Rs,PAd); - # if we have an image, add pixel coordinates - image = self._imgman and self._imgman.getTopImage(); - if image and x is not None: - msgtext += " x=%d y=%d value=blank"%(x,y) if flag else " x=%d y=%d value=%g"%(x,y,val); - self._livezoom.trackImage(image,x,y); - self._liveprofile.trackImage(image,x,y); - self.emit(SIGNAL("showMessage"),msgtext,3000); - return None; - - def _selectSources (self,sources,mode): - """Helper function to select sources in list"""; - # turn list into set of ids - subset = set(map(id,sources)); - updated = False; - for src in self.model.sources: - newsel = src.selected; - if id(src) in subset: - dprint(3,"selecting",src.name); - if mode&self.SelectionAdd: - newsel = True; - elif mode&self.SelectionRemove: - newsel = False; - elif mode&self.SelectionClear: - newsel = False; - updated |= (newsel != src.selected); - src.selected = newsel; - # emit signal if changed - if updated: - self.model.emitSelection(origin=self); - - def _selectNearestSource (self,pos,world=True,range=10,mode=SelectionAdd): - """Selects or deselects source object nearest to the specified point (within range, in pixels). - Note that _mouse_mode == MouseDeselect will force mode=SelectionRemove. - 'pos' is a QPointF/QwtDoublePoint object in lm coordinates if world=True, else a QPoint object.""" - dprint(1,"selectNearestSource",pos); - # deselect mouse mode implies removing from selection, in all other modes we add - if self._mouse_mode == self.MouseDeselect: - mode = self.SelectionRemove; - src = self.findNearestSource(pos,world=world,range=range); - if src: - self._selectSources([src],mode); - - def _makeRectMarker (self,rect,pen): - x1,y1,x2,y2 = rect.getCoords(); - line = TiggerPlotCurve(); - line.setData([x1,x1,x2,x2,x1],[y1,y2,y2,y1,y1]); -# line.setBrush(self._stats_brush); - line.setPen(pen); - label = TiggerPlotMarker(); - label.setValue(max(x1,x2),max(y1,y2)); - text = QwtText("stats"); - text.setColor(pen.color()); - label.setLabel(text); - label.setLabelAlignment(Qt.AlignBottom|Qt.AlignRight); - return [ line,label ]; - - def _selectImageSubset (self,rect,image=None): - # make zoom button visible if subset is selected - self._qa_colorzoom.setVisible(bool(rect)); - self._image_subset = rect; - if rect is None: - self._removePlotMarkup(); - else: - # get image stats - busy = BusyIndicator(); - stats = self._imgman.getLMRectStats(self._image_subset); - busy = None; - if stats is None: - self._removePlotMarkup(); - self._image_subset = None; - return; - # make tooltip - DataValueFormat = "%.4g"; - stats = list(stats); - stats1 = tuple(stats[:4] + [ DataValueFormat%s for s in stats[4:9] ] + stats[9:]); - msgtext = "[%d:%d,%d:%d] min %s, max %s, mean %s, std %s, sum %s, np %d"%stats1; - tiptext = """

Region: [%d:%d,%d:%d]
- Stats: min %s, max %s, mean %s, std %s, sum %s, np %d
- Use the "Colour zoom" button on the left (or press Shift+F4) to set the current data subset and - intensity range to this image region.

"""%stats1; - # make markup on plot to indicate current subset - markup_items = self._makeRectMarker(rect,self._stats_pen); - # calling QToolTip.showText() directly from here doesn't work, so set a timer on it - QTimer.singleShot(10,self._currier.curry(self._showCoordinateToolTip,tiptext)); - # same deal for markup items - for item in markup_items: - item.setZ(Z_Markup); - QTimer.singleShot(10,self._currier.curry(self._addPlotMarkup,markup_items)); - print msgtext; - QApplication.clipboard().setText(msgtext+"\n"); - QApplication.clipboard().setText(msgtext+"\n",QClipboard.Selection); - - def _colourZoomIntoSubset (self): - # zoom into current image subset (if any), and hide the zoom button - dprint(1,self._image_subset); - if self._image_subset is not None: - self._imgman.setLMRectSubset(self._image_subset); - self._removePlotMarkup(); - self._image_subset = None; - self._qa_colorzoom.setVisible(False); - - def _selectRectStats (self,rect): - image = self._imgman and self._imgman.getTopImage(); - dprint(1,"subset selection",rect,"image:",image and image.boundingRect()); - if not image or not rect.intersects(image.boundingRect()): - self._selectImageSubset(None); - return; - zoomrect = image.boundingRect().intersected(rect); - dprint(1,"selecting image subset",zoomrect); - self._selectImageSubset(zoomrect,image); - - def _selectRect (self,rect,world=True,mode=SelectionClear|SelectionAdd): - """Selects sources within the specified rectangle. For meaning of 'mode', see flags above. - 'rect' is a QRectF/QwtDoubleRect object in lm coordinates if world=True, else a QRect object in screen coordinates.""" - dprint(1,"selectRect",rect); - if not world: - rect = self.plot.screenRectToLm(rect); - sources = [ marker.source() for marker in self._markers.itervalues() if marker.isVisible() and rect.contains(marker.lmQPointF()) ]; - if sources: - self._selectSources(sources,mode); - - def _finalizeProvisionalZoom(self): - if self._provisional_zoom is not None: - self._zoomer.zoom(self._provisional_zoom); - - def _plotProvisionalZoom (self,x,y,level,timeout=200): - """Called when mouse wheel is used to zoom in our out"""; - self._provisional_zoom_level += level; - self._zoomer_box.setVisible(False); - self._zoomer_label.setVisible(False); - if self._provisional_zoom_level > 0: - # make zoom box of size 2^level smaller than current screen - x1,y1,x2,y2 = self._zoomer.zoomRect().getCoords(); - w = (x2-x1)/2**self._provisional_zoom_level; - h = (y2-y1)/2**self._provisional_zoom_level; - self._provisional_zoom = QRectF(x-w/2,y-h/2,w,h); - x1,y1,x2,y2 = self._provisional_zoom.getCoords(); - self._zoomer_box.setData([x1,x2,x2,x1,x1],[y1,y1,y2,y2,y1]); - self._zoomer_label.setValue(max(x1,x2),max(y1,y2)); - self._zoomer_label_text.setText("zoom"); - self._zoomer_label.setLabel(self._zoomer_label_text); - self._zoomer_box.setVisible(True); - self._zoomer_label.setVisible(True); - else: - maxout = -self._zoomer.zoomRectIndex(); - self._provisional_zoom_level = level = max(self._provisional_zoom_level,maxout); - if self._provisional_zoom_level < 0: - self._zoomer_label.setValue(x,y); - self._zoomer_label_text.setText("zoom out %d"%abs(level) if level != maxout else "zoom out full"); - self._zoomer_label.setLabel(self._zoomer_label_text); - self._zoomer_label.setVisible(True); - self._provisional_zoom = int(self._provisional_zoom_level); - else: - self._provisional_zoom = None; - QTimer.singleShot(5,self._replot); - self._provisional_zoom_timer.start(timeout); - - def _plotZoomed (self,rect): - dprint(2,"zoomed to",rect); - self._zoomer_box.setVisible(False); - self._zoomer_label.setVisible(False); - self._provisional_zoom = None; - self._provisional_zoom_level = 0; - self._zoomrect = QRectF(rect); # make copy - self._qa_unzoom.setEnabled(rect != self._zoomer.zoomBase()); - self._updatePsfMarker(rect,replot=True); - - def _setGridCircleStepping (self,arcsec=DefaultGridStep_ArcSec): - """Changes the visible grid circles. None to disable."""; - self._grid_step_arcsec = arcsec; - self._updateContents(); - - def _setCustomGridCircleStepping (self): - """Opens dialog to get a custom grid step."""; - text,ok = QInputDialog.getText(self,"Set custom grid step","""

- Specify a custom grid stepping as a value and a unit string.
Recognized unit strings are - d or deg, ' (single quote) or arcmin, and " (double quote) or arcsec.
Default is arcmin.

""", - text=self._grid_step_arcsec_str); - if text: - match = re.match("([-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)(d|deg|['\"]|arcmin)?$",text,re.I); - try: - value = float(match.group(1)); - except: - QMessageBox.warning(self,"Invalid input","Invalid input: \"%s\""%text); - return; - if round(value) == value: - value = int(value); - unit = match.group(5); - if unit in ("d","deg"): - value *= 3600; - elif not unit or unit in ("'","arcmin"): - value *= 60; - self._setGridCircleStepping(value or None); - self._setCustomGridCircleSteppingLabel(); - - def _setCustomGridCircleSteppingLabel (self): - """Changes the label of the custom grid step action."""; - step = self._grid_step_arcsec; - if not step: - self._grid_step_arcsec_str = ""; - elif step < 60: - self._grid_step_arcsec_str = ("%f\"" if isinstance(step,float) else "%d\"")%step; - elif step < 3600: - self._grid_step_arcsec_str = ("%f'" if step%60 else "%d'")%(step/60.); - else: - self._grid_step_arcsec_str = ("%fdeg" if step%3600 else "%ddeg")%(step/3600.); - if self._grid_step_arcsec_str: - self._qa_custom_grid.setText("Custom (%s)..."%self._grid_step_arcsec_str); - else: - self._qa_custom_grid.setText("Custom..."); - - def _updateContents (self,what=SkyModel.UpdateAll,origin=None): - # do nothing if updates are disabled (this is possible on startup, or when multiple - # things are being loaded), or if update is of no concern to us - if not self._updates_enabled or not what&(SkyModel.UpdateSourceList|SkyModel.UpdateSourceContent|self.UpdateImages): - return; - # clear any plot markup - dprint(2,"clearing plot markup"); - for item in self._plot_markup: - item.detach(); - self._plot_markup = []; - self._image_subset = None; - # clear plot, but do not delete items - self.projection = None; - self.plot.clear(); - self._psf_marker.attach(self.plot); - self._zoomer_box.attach(self.plot); - self._zoomer_label.attach(self.plot); - self._zoomer_box.setVisible(False); - self._zoomer_label.setVisible(False); - # get current image (None if no images) - self._image = self._imgman and self._imgman.getCenterImage(); - # show/hide live zoomer with image - if self._image: - for tool in self._livezoom,self._liveprofile: - tool.makeAvailable(bool(self._image)); - # enable or disable mouse modes as appropriate - self._mousemodes.setContext(has_image=bool(self._image),has_model=bool(self.model)); - # do nothing if no image and no model - if not self._image and not self.model: - self.plot.setEnabled(False); - return; - self.plot.setEnabled(True); - # Use projection of first image, or 'Sin' by default - if self._image: - self.projection = self._image.projection; - dprint(1,"using projection from image",self._image.name); - ra,dec = self.projection.radec(0,0); - else: - self.projection = Projection.SinWCS(*self.model.fieldCenter()); - dprint(1,"using default Sin projection"); - # compute lm: dict from source ID to l,m tuple - if self.model: - self._source_lm = dict([(id(src),self.projection.lm(src.pos.ra,src.pos.dec)) for src in self.model.sources]); - # now find plot extents - extent= [[0,0],[0,0]]; - for iext in 0,1: - if self._source_lm: - xmin = extent[iext][0] = min([lm[iext] for lm in self._source_lm.itervalues()]); - xmax = extent[iext][1] = max([lm[iext] for lm in self._source_lm.itervalues()]); - # add 5% on either side - margin = .05*(xmax - xmin); - extent[iext][0] -= margin; - extent[iext][1] += margin; - dprint(2,"plot extents for model",extent); - # account for bounding rects of images - # TODO: check that images are visible - for img in ((self._imgman and self._imgman.getImages()) or []): - ext = img.getExtents(); - dprint(2,"image extents",ext); - for i in 0,1: - extent[i][0] = min(extent[i][0],ext[i][0]); - extent[i][1] = max(extent[i][1],ext[i][1]); - # if margins still not set, force them to 1x1 degree - for i in 0,1: - if extent[i][0] == extent[i][1]: - extent[i] = [-DEG*0.5,DEG*0.5 ]; - dprint(2,"plot extents for model & images",extent); - (lmin,lmax),(mmin,mmax) = extent; - # adjust plot limits, if a fixed ratio is in effect, and set the zoom base - zbase = QRectF(QPointF(lmin,mmin),QPointF(lmax,mmax)); -# zbase = self._zoomer.adjustRect(zbase); - zooms = [ zbase ]; - dprint(2,"zoom base, adjusted for aspect:",zbase); - # zooms = [ self._zoomer.adjustRect(zbase) ]; - # if previously set zoom rect intersects the zoom base at all (and is not a superset), try to restore it - dprint(2,"previous zoom area:",self._zoomrect); - if self._zoomrect and self._zoomrect.intersects(zbase): - rect = self._zoomrect.intersected(zbase); -# rect = self._zoomer.adjustRect(self._zoomrect.intersected(zbase)); - if rect != zbase: - dprint(2,"will restore zoomed area",rect); - zooms.append(rect); - self._qa_unzoom.setEnabled(len(zooms)>1); - self._provisional_zoom_level = 0; -# dprint(2,"adjusted for aspect ratio",lmin,lmax,mmin,mmax); - # reset plot limits -- X axis inverted (L increases to left) -# lmin,lmax,mmin,mmax = zbase.left(),zbase.right(),zbase.top(),zbase.bottom(); -# self.plot.setAxisScale(QwtPlot.yLeft,mmin,mmax); -# self.plot.setAxisScale(QwtPlot.xBottom,lmax,lmin); -# self.plot.axisScaleEngine(QwtPlot.xBottom).setAttribute(QwtScaleEngine.Inverted, True); -# dprint(2,"setting zoom base",zbase); -# self._zoomer.setZoomBase(zbase); - dprint(5,"drawing grid"); - # add grid lines & circles - circstep = self._grid_step_arcsec; - if circstep: - self._grid = [ TiggerPlotCurve(),TiggerPlotCurve() ]; - self._grid[0].setData([lmin,lmax],[0,0]); - self._grid[1].setData([0,0],[mmin,mmax]); - # see how many units (of arcminute) fit in max diagonal direction - maxr = int(round(math.sqrt(lmax**2+mmax**2)/(DEG/3600))); - # cache sines and cosines of curve argument - angles = numpy.array(range(0,361,5))*DEG; - sines = numpy.sin(angles); - cosines = numpy.cos(angles); - # make circles - for r in numpy.arange(circstep,maxr,circstep): - # find radius in each direction, by projecting a point - rl ,dum= self.projection.offset(r*DEG/3600,0); - dum,rm = self.projection.offset(0,r*DEG/3600); - # make curve - curve = TiggerPlotCurve(); - x ,y = rl*cosines,rm*sines; - curve.setData(x,y); - curve.setCurveAttribute(QwtPlotCurve.Fitted,True); - self._grid.append(curve); - # make a text label and marker - marker = TiggerPlotMarker(); - m,s = divmod(r,60); - d,m = divmod(m,60); - if d: - label = "%d°%02d'%02d\""%(d,m,s) if s else ("%d°%02d'"%(d,m) if m else "%d°"%d); - elif m: - label = "%d'%02d\""%(m,s) if s else "%d'"%m; - else: - label = "%d\""%s; - text = QwtText(label,QwtText.RichText); - text.setColor(self._grid_color); - marker.setValue(x[0],y[0]); - marker.setLabel(text); - marker.setLabelAlignment(Qt.AlignRight|Qt.AlignBottom); - marker.setZ(Z_Grid); - marker.attach(self.plot); - for gr in self._grid: - gr.setPen(self._grid_pen); - gr.setZ(Z_Grid); - gr.attach(self.plot); - # make a new set of source markers, since either the image or the model may have been updated - if self.model: - dprint(5,"making skymodel markers"); - # compute min/max brightness - # brightnesses <=1e-20 are specifically excluded (as they're probably "dummy" sources, etc.) - b = [ abs(src.brightness()) for src in self.model.sources if abs(src.brightness()) > 1e-20 ]; - self._min_bright = min(b) if b else 0; - self._max_bright = max(b) if b else 0; - # make items for every object in the model - self._markers = {}; - for isrc,src in enumerate(self.model.sources): - l,m = self._source_lm[id(src)]; - self._markers[src.name] = marker = makeSourceMarker(src,l,m,self.getSymbolSize(src),self.model,self._imgman); - # now (re)attach the source markers, since the plot has been cleared - for marker in self._markers.itervalues(): - marker.attach(self.plot); - # attach images to plot - if self._imgman: - dprint(5,"attaching images"); - self._imgman.attachImagesToPlot(self.plot); - # update the PlotZoomer with our set of zooms. This implictly causes a plot update - dprint(5,"updating zoomer"); - self._zoomer.setZoomStack(zooms,len(zooms)-1); - self._updatePsfMarker(None,replot=True); - # self.plot.replot(); - - def setModel (self,model): - self._source_lm = {}; - self._markers = {}; - self.model = model; - dprint(2,"setModel",model); - if model : - # connect signals - self.model.connect("updated",self.postUpdateEvent); - self.model.connect("selected",self.updateModelSelection); - self.model.connect("changeCurrentSource",self.setCurrentSource); - self.model.connect("changeGroupingStyle",self.changeGroupingStyle); - # update plot - self.postUpdateEvent(SkyModel.UpdateAll); - - def _exportPlotToPNG (self,filename=None): - if not filename: - if not self._export_png_dialog: - dialog = self._export_png_dialog = QFileDialog(self,"Export plot to PNG",".","*.png"); - dialog.setDefaultSuffix("png"); - dialog.setFileMode(QFileDialog.AnyFile); - dialog.setAcceptMode(QFileDialog.AcceptSave); - dialog.setModal(True); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self._exportPlotToPNG); - return self._export_png_dialog.exec_() == QDialog.Accepted; - busy = BusyIndicator(); - if isinstance(filename,QStringList): - filename = filename[0]; - filename = str(filename); - # make QPixmap - pixmap = QPixmap(self.plot.width(),self.plot.height()); - pixmap.fill(self._bg_color); - painter = QPainter(pixmap); - # use QwtPlot implementation of draw canvas, since we want to avoid caching - QwtPlot.drawCanvas(self.plot,painter); - painter.end(); - # save to file - try: - pixmap.save(filename,"PNG"); - except Exception,exc: - self.emit(SIGNAL("showErrorMessage"),"Error writing %s: %s"%(filename,str(exc))); - return; - self.emit(SIGNAL("showMessage"),"Exported plot to file %s"%filename); - - def setCurrentSource (self,src,src0=None,origin=None): - dprint(2,"setCurrentSource",src and src.name,src0 and src0.name,origin); - if self.model and self.model.curgroup.style.apply: - for s in src,src0: - marker = s and self._markers.get(s.name); - marker and marker.resetStyle(); - self.plot.clearDrawCache(); - self.plot.replot(); - - def updateModelSelection (self,nsel=0,origin=None): - """This is callled when something changes the set of selected model sources"""; - # call checkSelected() on all plot markers, replot if any return True - if filter(lambda marker:marker.checkSelected(),self._markers.itervalues()): - self.plot.clearDrawCache(); - self.plot.replot(); - - def changeGroupingStyle (self,group,origin=None): - # call changeStyle() on all plot markers, replot if any return True - if filter(lambda marker:marker.changeStyle(group),self._markers.itervalues()): - self.plot.clearDrawCache(); - self.plot.replot(); - - def getSymbolSize (self,src): - return (max(math.log10(abs(src.brightness())) - math.log10(self._min_bright)+1,1))*3; diff --git a/Tigger/Plot/__init__.py b/Tigger/Plot/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Tigger/SkyModelTreeWidget.py b/Tigger/SkyModelTreeWidget.py deleted file mode 100644 index 055c89c..0000000 --- a/Tigger/SkyModelTreeWidget.py +++ /dev/null @@ -1,693 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import math -from PyQt4.Qt import * - -import Kittens.widgets -import Kittens.utils -from Kittens.utils import PersistentCurrier -from Kittens.widgets import BusyIndicator - -from Tigger.Models import ModelClasses,PlotStyles -from Tigger.Models.SkyModel import SkyModel - -_verbosity = Kittens.utils.verbosity(name="tw"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - -ViewColumns = [ "name","RA","RA err","Dec","Dec err","r","type", - "Iapp","I","I err","Q","Q err","U","U err","V","V err","RM","RM err","spi","spi err", - "shape","shape err","tags" ]; - -for icol,col in enumerate(ViewColumns): - globals()["Column%s"%col.capitalize().replace(" ","_")] = icol; -NumColumns = len(ViewColumns); - -DEG = math.pi/180; - -# Qt-4.6 and up (PyQt 4.7 and up) has very slow QTreeWidgetItem updates, determine version here -from PyQt4 import QtCore -_SLOW_QTREEWIDGETITEM = QtCore.PYQT_VERSION_STR >= '4.7'; - -class SkyModelTreeWidget (Kittens.widgets.ClickableTreeWidget): - """This implements a QTreeWidget for sky models"""; - def __init__ (self,*args): - Kittens.widgets.ClickableTreeWidget.__init__(self,*args); - self._currier = PersistentCurrier(); - self.model = None; - # insert columns - self.setHeaderLabels(ViewColumns); - self.headerItem().setText(ColumnIapp,"I(app)"); - self.header().setMovable(False); - self.header().setClickable(True); - self.setSortingEnabled(True); - self.setRootIsDecorated(False); - self.setEditTriggers(QAbstractItemView.AllEditTriggers); - self.setMouseTracking(True); - # set column width modes - for icol in range(NumColumns-1): - self.header().setResizeMode(icol,QHeaderView.ResizeToContents); - self.header().setStretchLastSection(True); - ## self.setTextAlignment(ColumnR,Qt.AlignRight); - ## self.setTextAlignment(ColumnType,Qt.AlignHCenter); - # _column_enabled[i] is True if column is available in the model. - # _column_show[i] is True if column is currently being shown (via a view control) - self._column_enabled = [True]*NumColumns; - self._column_shown = [True]*NumColumns; - # other listview init - self.header().show(); - self.setSelectionMode(QTreeWidget.ExtendedSelection); - self.setAllColumnsShowFocus(True); - ## self.setShowToolTips(True); - self._updating_selection = False; - self.setRootIsDecorated(False); - # connect signals to track selected sources - QObject.connect(self,SIGNAL("itemSelectionChanged()"),self._selectionChanged); - QObject.connect(self,SIGNAL("itemEntered(QTreeWidgetItem*,int)"),self._itemHighlighted); - # add "View" controls for different column categories - self._column_views = []; - self._column_widths = {}; - self.addColumnCategory("Position",[ColumnRa,ColumnDec]); - self.addColumnCategory("Position errors",[ColumnRa_err,ColumnDec_err],False); - self.addColumnCategory("Type",[ColumnType]); - self.addColumnCategory("Flux",[ColumnIapp,ColumnI]); - self.addColumnCategory("Flux errors",[ColumnI_err],False); - self.addColumnCategory("Polarization",[ColumnQ,ColumnU,ColumnV,ColumnRm]); - self.addColumnCategory("Polarization errors",[ColumnQ_err,ColumnU_err,ColumnV_err,ColumnRm_err],False); - self.addColumnCategory("Spectrum",[ColumnSpi]); - self.addColumnCategory("Spectrum errors",[ColumnSpi_err],False); - self.addColumnCategory("Shape",[ColumnShape]); - self.addColumnCategory("Shape errors",[ColumnShape_err],False); - self.addColumnCategory("Tags",[ColumnTags]); - - def _showColumn (self,col,show=True): - """Shows or hides the specified column. - (When hiding, saves width of column to internal array so that it can be restored properly.)""" - hdr = self.header(); - hdr.setSectionHidden(col,not show) - if show: - if not hdr.sectionSize(col): - hdr.resizeSection(col,self._column_widths[col]); - hdr.setResizeMode(col,QHeaderView.ResizeToContents); - else: - if hdr.sectionSize(col): - self._column_widths[col] = hdr.sectionSize(col); - - def _enableColumn (self,column,enable=True): - busy = BusyIndicator(); - self._column_enabled[column] = enable; - self._showColumn(column,enable and self._column_shown[column]); - - def _showColumnCategory (self,columns,show): - busy = BusyIndicator(); - for col in columns: - self._column_shown[col] = show; - self._showColumn(col,self._column_enabled[col] and show); - - def _selectionChanged (self): - if self._updating_selection: - return; - for item in self.iterator(): - item._src.select(item.isSelected()); - self.model.emitSelection(origin=self); - - def _itemHighlighted (self,item,col): - dprint(3,"highlighting",item._src.name); - self.model.setCurrentSource(item._src,origin=self); - - def viewportEvent (self,event): - if event.type() in (QEvent.Leave,QEvent.FocusOut) and self.model: - self.model.setCurrentSource(None,origin=self); - return QTreeWidget.viewportEvent(self,event); - - def addColumnCategory (self,name,columns,visible=True): - qa = QAction(name,self); - qa.setCheckable(True); - qa.setChecked(visible); - if not visible: - self._showColumnCategory(columns,False) - QObject.connect(qa,SIGNAL("toggled(bool)"),self._currier.curry(self._showColumnCategory,columns)); - self._column_views.append((name,qa,columns)); - - def clear (self): - Kittens.widgets.ClickableTreeWidget.clear(self); - self.model = None; - self._itemdict = {}; - - def setModel (self,model): - self.model = model; - self._refreshModel(SkyModel.UpdateAll); - self.model.connect("changeCurrentSource",self._updateCurrentSource); - self.model.connect("changeGroupingVisibility",self.changeGroupingVisibility); - self.model.connect("selected",self._updateModelSelection); - self.model.connect("updated",self._refreshModel); - - def _refreshModel (self,what=SkyModel.UpdateAll,origin=None): - if origin is self or not what&(SkyModel.UpdateSourceList|SkyModel.UpdateSourceContent): - return; - # if only selection was changed, take shortcut - if what&SkyModel.UpdateSelectionOnly: - dprint(2,"model update -- selection only"); - return self._refreshSelectedItems(origin); - busy = BusyIndicator(); - # else repopulate widget completely - dprint(2,"model update -- complete"); - Kittens.widgets.ClickableTreeWidget.clear(self); - dprint(2,"creating model items"); - items = [ SkyModelTreeWidgetItem(src) for src in self.model.sources ]; - self._itemdict = dict(zip([src.name for src in self.model.sources],items)); - dprint(2,"adding to tree widget"); - self.addTopLevelItems(items); - self.header().updateGeometry(); - # show/hide columns based on tag availability - self._enableColumn(ColumnIapp,'Iapp' in self.model.tagnames); - self._enableColumn(ColumnR,'r' in self.model.tagnames); - dprint(2,"re-sorting"); - self.sortItems(('Iapp' in self.model.tagnames and ColumnIapp) or ColumnI,Qt.DescendingOrder); - busy = None; - - def addColumnViewActionsTo (self,menu): - for name,qa,columns in self._column_views: - menu.addAction(qa); - - def _updateCurrentSource (self,src,src0=None,origin=None): - # if origin is self: - # return; - # dehighlight old item - item = src0 and self._itemdict.get(src0.name); - if item: - item.setHighlighted(False); - # scroll to new item, if found - item = src and self._itemdict.get(src.name); - if item: - item.setHighlighted(True,origin is not self); - if origin is not self: - self.scrollToItem(item); - - def _updateModelSelection (self,nsel,origin=None): - """This is called when some other widget (origin!=self) changes the set of selected model sources"""; - if origin is self: - return; - self._updating_selection = True; -## this is very slow because of setSelected() -# for item in self.iterator(): -# item.setSelected(item._src.selected); - selection = QItemSelection(); - for item in self.iterator(): - if item._src.selected: - selection.append(QItemSelectionRange(self.indexFromItem(item,0),self.indexFromItem(item,self.columnCount()-1))); - self.selectionModel().select(selection,QItemSelectionModel.ClearAndSelect); - self.changeGroupingVisibility(None,origin=origin); - self._updating_selection = False; - - def _refreshSelectedItems (self,origin=None): - busy = BusyIndicator(); - dprint(3,"refreshing selected items"); - for item in self.iterator(): - if item.isSelected(): - dprint(4,"resetting item",item._src.name); - item.setSource(item._src); - dprint(3,"refreshing selected items done"); - busy = None; - - def changeGroupingVisibility (self,group,origin=None): - if origin is self: - return; - for item in self.iterator(): - # collect show_list values from groupings to which this source belongs (default group excepted) - show = [ group.style.show_list for group in self.model.groupings if group is not self.model.defgroup and group.func(item._src) ]; - # if at least one group is showing explicitly, show - # else if at least one group is hiding explicitly, hide - # else use default setting - if max(show) == PlotStyles.ShowAlways: - visible = True; - elif min(show) == PlotStyles.ShowNot: - visible = False; - else: - visible = bool(self.model.defgroup.style.show_list); - # set visibility accordingly - item.setHidden(not visible); - - TagsWithOwnColumn = set(["Iapp","r"]); - -class SkyModelTreeWidgetItem (QTreeWidgetItem): - - _fonts = None; - @staticmethod - def _initFonts (): - """Initializes fonts on fitrst call"""; - if SkyModelTreeWidgetItem._fonts is None: - stdfont = QApplication.font(); - boldfont = QFont(stdfont); - boldfont.setBold(True); - SkyModelTreeWidgetItem._fonts = [ stdfont,boldfont ]; - SkyModelTreeWidgetItem._fontmetrics = QFontMetrics(boldfont); - - def __init__ (self,src,*args): - QTreeWidgetItem.__init__(self,*args); - self._src = src; - # fonts - self._initFonts(); - # array of actual (i.e. numeric) column values - self._values = [None]*NumColumns; - # set text alignment - for icol in range(NumColumns): - self.setTextAlignment(icol,Qt.AlignLeft); - self.setTextAlignment(ColumnR,Qt.AlignRight); - self.setTextAlignment(ColumnType,Qt.AlignHCenter); - # setup source - self._highlighted = self._highlighted_visual = False; - self.setSource(src); - - def setHighlighted (self,highlighted=True,visual=False): -# global _SLOW_QTREEWIDGETITEM; -# if 1: # not _SLOW_QTREEWIDGETITEM: - visual = True; - dprint(3,self._src.name,"highlighted",highlighted,visual); - if highlighted != self._highlighted: -# brush = QApplication.palette().alternateBase() if highlighted else QApplication.palette().base(); -# for col in range(self.columnCount()): -# self.setBackground(col,brush); - if highlighted and visual: - self.setFont(0,self._fonts[1]); - elif not highlighted and self._highlighted_visual: - self.setFont(0,self._fonts[0]); - self._highlighted = highlighted; - self._highlighted_visual = visual; - - @staticmethod - def _angErrToStr (value): - """helper method: converts angular error to string representation in deg or arcmin or arcsec"""; - arcsec = (value/DEG)*3600; - if arcsec < 60: - return unichr(0xB1)+"%.2g\""%arcsec; - elif arcsec < 3600: - return unichr(0xB1)+"%.2f'"%(arcsec*60); - else: - return unichr(0xB1)+"%.2f%s"%(arcsec*3600,unichr(0xB0)); - - - def setSource (self,src): - # name - dprint(3,"setSource 1",src.name); - self.setColumn(ColumnName,src.name); - self.setSizeHint(0,QSize(self._fontmetrics.width("x"+src.name),0)); - # coordinates - self.setColumn(ColumnRa,src.pos.ra,"%2dh%02dm%05.2fs"%src.pos.ra_hms()); - self.setColumn(ColumnDec,src.pos.dec,("%s%2d"+unichr(0xB0)+"%02d'%05.2f\"")% - src.pos.dec_sdms()); - if src.pos.ra_err is not None: - self.setColumn(ColumnRa_err,src.pos.ra_err,self._angErrToStr(src.pos.ra_err)); - if src.pos.dec_err is not None: - self.setColumn(ColumnDec_err,src.pos.dec_err,self._angErrToStr(src.pos.dec_err)); - if hasattr(src,'r'): - self.setColumn(ColumnR,src.r,"%.1f'"%(src.r*180*60/math.pi)); - # type - self.setColumn(ColumnType,src.typecode); - # flux - if hasattr(src,'Iapp'): - self.setColumn(ColumnIapp,src.Iapp,"%.3g"%src.Iapp); - for stokes in "IQUV": - stk = getattr(src.flux,stokes,None); - stk_err = getattr(src.flux,stokes+"_err",None); - if stk is not None: - self.setColumn(globals()['Column'+stokes],stk,"%.3g"%stk); - if stk_err is not None: - self.setColumn(globals()['Column'+stokes+"_err"],stk_err,unichr(0xB1)+"%.2g"%stk_err); - if hasattr(src.flux,'rm'): - self.setColumn(ColumnRm,src.flux.rm,"%.2f"%src.flux.rm); - if hasattr(src.flux,'rm_err'): - self.setColumn(ColumnRm_err,src.flux.rm_err,unichr(0xB1)+"%.2f"%src.flux.rm); - # spi - if isinstance(src.spectrum,ModelClasses.SpectralIndex): - spi = getattr(src.spectrum,'spi',0); - if not isinstance(spi,(list,tuple)): - spi = [spi]; - spi = ",".join([ "%.2f"%x for x in spi]); - self.setColumn(ColumnSpi,src.spectrum.spi,spi ); - spierr = getattr(src.spectrum,'spi_err',None); - if spierr is not None: - if not isinstance(spierr,(list,tuple)): - spierr = [spierr]; - spierr = ",".join([ "%.2f"%x for x in spierr]); - self.setColumn(ColumnSpi_err,src.spectrum.spi_err,unichr(0xB1)+spierr); - # shape - shape = getattr(src,'shape',None); - if isinstance(shape,ModelClasses.ModelItem): - shapeval = shape.getShape(); - shapestr = shape.strDesc(delimiters=('"',unichr(0xD7),unichr(0x21BA),unichr(0xB0))); - self.setColumn(ColumnShape,shapeval,shapestr); - errval = shape.getShapeErr(); - if errval: - errstr = shape.strDescErr(delimiters=('"',unichr(0xD7),unichr(0x21BA),unichr(0xB0))); - self.setColumn(ColumnShape_err,errval,unichr(0xB1)+errstr); - dprint(3,"setSource 3",src.name); - # Tags. Tags are all extra attributes that do not have a dedicated column (i.e. not Iapp or r), and do not start - # with "_" (which is reserved for internal attributes) - - ## the complexity below seems entirely unnecessary, since sorting the tag strings automatically puts "_" first, - ## "-" second, and alphabet afterwards - - #truetags = []; - #falsetags = []; - #othertags = []; - #for attr,val in src.getExtraAttributes(): - #if attr[0] != "_" and attr not in SkyModelTreeWidget.TagsWithOwnColumn: - #if val is False: - #falsetags.append("-"+attr); - #elif val is True: - #truetags.append("+"+attr); - #else: - #othertags.append("%s=%s"%(attr,str(val))); - #for tags in truetags,falsetags,othertags: - #tags.sort(); - #self.setColumn(ColumnTags,tags," ".join(truetags+falsetags+othertags)); - - # so instead: - tags = [ "+"+attr if val is True else "-"+attr if val is False else "%s=%s"%(attr,str(val)) - for attr,val in src.getExtraAttributes() - if attr[0] != "_" and attr not in SkyModelTreeWidget.TagsWithOwnColumn ]; - tagstr = " ".join(sorted(tags)); - dprint(3,"setSource 4",src.name); - self.setColumn(ColumnTags,tags,tagstr); - dprint(3,"setSource 5",src.name); - dprint(3,"setSource done",src.name); - - def setColumn (self,icol,value,text=None): - """helper function to set the value of a column"""; - if text is None: - text = str(value); - self.setText(icol,text); - self._values[icol] = value; - - def __lt__ (self,other): - icol = self.treeWidget().sortColumn(); - if isinstance(other,SkyModelTreeWidgetItem): - return self._values[icol] < other._values[icol]; - else: - return self.text(icol) < other.text(icol); - - def __ge__ (self,other): - return other < self; - - -class ModelGroupsTable (QWidget): - EditableAttrs = [ attr for attr in PlotStyles.StyleAttributes if attr in PlotStyles.StyleAttributeOptions ]; - ColList = 3; - ColPlot = 4; - ColApply = 5; - AttrByCol = dict([(i+6,attr) for i,attr in enumerate(EditableAttrs)]); - - def __init__ (self,parent,*args): - QWidget.__init__(self,parent,*args); - self.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding); - lo = QVBoxLayout(self); - lo.setContentsMargins(0,0,0,0); - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - lbl = QLabel(QString("Source groupings:"),self); - lo1.addWidget(lbl,0); - lo1.addStretch(1); - # add show/hide button - self._showattrbtn = QPushButton(self); - self._showattrbtn.setMinimumWidth(256); - lo1.addWidget(self._showattrbtn,0); - lo1.addStretch(); - QObject.connect(self._showattrbtn,SIGNAL("clicked()"),self._togglePlotControlsVisibility); - # add table - self.table = QTableWidget(self); - lo.addWidget(self.table); - QObject.connect(self.table,SIGNAL("cellChanged(int,int)"),self._valueChanged); - self.table.setSelectionMode(QTableWidget.NoSelection); - # setup basic columns - self.table.setColumnCount(6+len(self.EditableAttrs)); - for i,label in enumerate(("grouping","total","selection","list","plot","style")): - self.table.setHorizontalHeaderItem(i,QTableWidgetItem(label)); - self.table.horizontalHeader().setSectionHidden(self.ColApply,True); - # setup columns for editable grouping attributes - for i,attr in self.AttrByCol.iteritems(): - self.table.setHorizontalHeaderItem(i,QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr])); - self.table.horizontalHeader().setSectionHidden(i,True); - self.table.verticalHeader().hide(); - # other internal init - self._attrs_shown = False; - self._togglePlotControlsVisibility(); - self.model = None; - self._setting_model = False; - self._currier = PersistentCurrier(); - # row of 'selected' grouping - self._irow_selgroup = 0; - - def clear (self): - self.table.setRowCount(0); - self.model = None; - - # setup mappings from the group.show_plot attribute to check state - ShowAttrToCheckState = { PlotStyles.ShowNot:Qt.Unchecked, - PlotStyles.ShowDefault:Qt.PartiallyChecked, - PlotStyles.ShowAlways:Qt.Checked }; - CheckStateToShowAttr = dict([(val,key) for key,val in ShowAttrToCheckState.iteritems ()]); - - def _makeCheckItem (self,name,group,attr): - item = QTableWidgetItem(name); - if group is self.model.defgroup: - item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable); - item.setCheckState(Qt.Checked if getattr(group.style,attr) else Qt.Unchecked); - else: - item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsTristate); - item.setCheckState(self.ShowAttrToCheckState[getattr(group.style,attr)]); - return item; - - def _updateModel (self,what=SkyModel.UpdateAll,origin=None): - if origin is self or not what&(SkyModel.UpdateTags|SkyModel.UpdateGroupStyle): - return; - model = self.model; - self._setting_model= True; # to ignore cellChanged() signals (in valueChanged()) - # _item_cb is a dict (with row,col keys) containing the widgets (CheckBoxes ComboBoxes) per each cell - self._item_cb = {}; - # lists of "list" and "plot" checkboxes per each grouping (excepting the default grouping); each entry is an (row,col,item) tuple. - # used as argument to self._showControls() - self._list_controls = []; - self._plot_controls = []; - # list of selection callbacks (to which signals are connected) - self._callbacks = []; - # set requisite number of rows,and start filling - self.table.setRowCount(len(model.groupings)); - for irow,group in enumerate(model.groupings): - self.table.setItem(irow,0,QTableWidgetItem(group.name)); - if group is model.selgroup: - self._irow_selgroup = irow; - # total # source in group: skip for "current" - if group is not model.curgroup: - self.table.setItem(irow,1,QTableWidgetItem(str(group.total))); - # selection controls: skip for current and selection - if group not in (model.curgroup,model.selgroup): - btns = QWidget(); - lo = QHBoxLayout(btns); - lo.setContentsMargins(0,0,0,0); - lo.setSpacing(0); - # make selector buttons (depending on which group we're in) - if group is model.defgroup: - Buttons = ( - ("+",lambda src,grp=group:True,"select all sources"), - ("-",lambda src,grp=group:False,"unselect all sources") ); - else: - Buttons = ( - ("=",lambda src,grp=group:grp.func(src),"select only this grouping"), - ("+",lambda src,grp=group:src.selected or grp.func(src),"add grouping to selection"), - ("-",lambda src,grp=group:src.selected and not grp.func(src),"remove grouping from selection"), - ("&&",lambda src,grp=group:src.selected and grp.func(src),"intersect selection with grouping")); - lo.addStretch(1); - for label,predicate,tooltip in Buttons: - btn = QToolButton(btns); - btn.setText(label); - btn.setMinimumWidth(24); - btn.setMaximumWidth(24); - btn.setToolTip(tooltip); - lo.addWidget(btn); - # add callback - QObject.connect(btn,SIGNAL("clicked()"),self._currier.curry(self.selectSources,predicate)); - lo.addStretch(1); - self.table.setCellWidget(irow,2,btns); - # "list" checkbox (not for current and selected groupings: these are always listed) - if group not in (model.curgroup,model.selgroup): - item = self._makeCheckItem("",group,"show_list"); - self.table.setItem(irow,self.ColList,item); - item.setToolTip("""

If checked, sources in this grouping will be listed in the source table. If un-checked, sources will be - excluded from the table. If partially checked, then the default list/no list setting of "all sources" will be in effect. -

"""); - # "plot" checkbox (not for the current grouping, since that's always plotted) - if group is not model.curgroup: - item = self._makeCheckItem("",group,"show_plot"); - self.table.setItem(irow,self.ColPlot,item); - item.setToolTip("""

If checked, sources in this grouping will be included in the plot. If un-checked, sources will be - excluded from the plot. If partially checked, then the default plot/no plot setting of "all sources" will be in effect. -

"""); - # custom style control - # for default, current and selected, this is just a text label - if group is model.defgroup: - item = QTableWidgetItem("default:"); - item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter); - item.setToolTip("""

This is the default plot style used for all sources for which a custom grouping style is not selected.

"""); - self.table.setItem(irow,self.ColApply,item); - elif group is model.curgroup: - item = QTableWidgetItem(""); - item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter); - item.setToolTip("""

This is the plot style used for the highlighted source, if any.

"""); - self.table.setItem(irow,self.ColApply,item); - elif group is model.selgroup: - item = QTableWidgetItem(""); - item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter); - item.setToolTip("""

This is the plot style used for the currently selected sources.

"""); - self.table.setItem(irow,self.ColApply,item); - # for the rest, a combobox with custom priorities - else: - cb = QComboBox(); - cb.addItems(["default"]+["custom %d"%p for p in range(1,10)]); - index = max(0,min(group.style.apply,9)); -# dprint(0,group.name,"apply",index); - cb.setCurrentIndex(index); - QObject.connect(cb,SIGNAL("activated(int)"),self._currier.xcurry(self._valueChanged,(irow,self.ColApply))); - self.table.setCellWidget(irow,self.ColApply,cb); - cb.setToolTip("""

This controls whether sources within this group are plotted with a customized - plot style. Customized styles have numeric priority; if a source belongs to multiple groups, then - the style with the lowest priority takes precedence.

"""); - # attribute comboboxes - for icol,attr in self.AttrByCol.iteritems(): - # get list of options for this style attribute. If dealing with first grouping (i==0), which is - # the "all sources" grouping, then remove the "default" option (which is always first in the list) - options = PlotStyles.StyleAttributeOptions[attr]; - if irow == 0: - options = options[1:]; - # make combobox - cb = QComboBox(); - cb.addItems(map(str,options)); - # the "label" option is also editable - if attr == "label": - cb.setEditable(True); - try: - index = options.index(getattr(group.style,attr)); - cb.setCurrentIndex(index); - except ValueError: - cb.setEditText(str(getattr(group.style,attr))); - slot = self._currier.xcurry(self._valueChanged,(irow,icol)); - QObject.connect(cb,SIGNAL("activated(int)"),slot); - QObject.connect(cb,SIGNAL("editTextChanged(const QString &)"),slot); - cb.setEnabled(group is model.defgroup or group.style.apply); - self.table.setCellWidget(irow,icol,cb); - label = attr; - if irow: - cb.setToolTip("""

This is the %s used to plot sources in this group, when a "custom" style for the group - is enabled via the style control.

"""%label); - else: - cb.setToolTip("

This is the default %s used for all sources for which a custom style is not specified below.

"%label); - self.table.resizeColumnsToContents(); - # re-enable processing of cellChanged() signals - self._setting_model= False; - - def setModel (self,model): - self.model = model; - self.model.connect("updated",self._updateModel); - self.model.connect("selected",self.updateModelSelection); - self._updateModel(SkyModel.UpdateAll); - - def _valueChanged (self,row,col): - """Called when a cell has been edited"""; - if self._setting_model: - return; - group = self.model.groupings[row]; - item = self.table.item(row,col); - if col == self.ColList: - if group is not self.model.defgroup: - # tri-state items go from unchecked to checked when user clicks them. Make them partially checked instead. - if group.style.show_list == PlotStyles.ShowNot and item.checkState() == Qt.Checked: - item.setCheckState(Qt.PartiallyChecked); - group.style.show_list = self.CheckStateToShowAttr[item.checkState()]; - self.model.emitChangeGroupingVisibility(group,origin=self); - return; - elif col == self.ColPlot: - if group is not self.model.defgroup: - # tri-state items go from unchecked to checked by default. Make them partially checked instead. - if group.style.show_plot == PlotStyles.ShowNot and item.checkState() == Qt.Checked: - item.setCheckState(Qt.PartiallyChecked); - group.style.show_plot = self.CheckStateToShowAttr[item.checkState()]; - elif col == self.ColApply: - group.style.apply = self.table.cellWidget(row,col).currentIndex(); - # enable/disable editable cells - for j in self.AttrByCol.keys(): - item1 = self.table.item(row,j); - if item1: - fl = item1.flags()&~Qt.ItemIsEnabled; - if group.style.apply: - fl |= Qt.ItemIsEnabled; - item1.setFlags(fl); - cw = self.table.cellWidget(row,j); - cw and cw.setEnabled(group.style.apply); - elif col in self.AttrByCol: - cb = self.table.cellWidget(row,col); - txt = str(cb.currentText()); - attr = self.AttrByCol[col]; - if txt == "default": - setattr(group.style,attr,PlotStyles.DefaultValue); - else: - setattr(group.style,attr,PlotStyles.StyleAttributeTypes.get(attr,str)(txt)); - # all other columns: return so we don't emit a signal - else: - return; - # in all cases emit a signal - self.model.emitChangeGroupingStyle(group,origin=self); - - def selectSources (self,predicate): - """Selects sources according to predicate(src)""" - busy = BusyIndicator(); - for src in self.model.sources: - src.selected = predicate(src); - self.model.emitSelection(origin=self); - busy = None; - - def updateModelSelection (self,nsel,origin=None): - """This is called when some other widget changes the set of selected model sources"""; - self.table.clearSelection(); - if self.model: - self.table.item(self._irow_selgroup,1).setText(str(nsel)); - - def _togglePlotControlsVisibility (self): - if self._attrs_shown: - self._attrs_shown = False; - self.table.hideColumn(self.ColApply); - for col in self.AttrByCol.iterkeys(): - self.table.hideColumn(col); - self._showattrbtn.setText("Show plot styles >>"); - else: - self._attrs_shown = True; - self.table.showColumn(self.ColApply); - for col in self.AttrByCol.iterkeys(): - self.table.showColumn(col); - self._showattrbtn.setText("<< Hide plot styles"); - - diff --git a/Tigger/Images/FITSHeaders.py b/Tigger/Tools/FITSHeaders.py similarity index 100% rename from Tigger/Images/FITSHeaders.py rename to Tigger/Tools/FITSHeaders.py diff --git a/Tigger/Tools/Imaging.py b/Tigger/Tools/Imaging.py index 777d967..0c47455 100644 --- a/Tigger/Tools/Imaging.py +++ b/Tigger/Tools/Imaging.py @@ -31,8 +31,8 @@ import math import numpy -from Tigger.Coordinates import Projection,radec_string -from Tigger.Images import FITSHeaders +from Tigger.Coordinates import Projection +import FITSHeaders from scipy.ndimage.filters import convolve from scipy.ndimage.interpolation import map_coordinates import astLib.astWCS diff --git a/Tigger/Tools/__init__.py b/Tigger/Tools/__init__.py index 05fd51a..52e8a1c 100644 --- a/Tigger/Tools/__init__.py +++ b/Tigger/Tools/__init__.py @@ -22,12 +22,3 @@ # or write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # - -_registered_tools = []; - -def getRegisteredTools (): - return _registered_tools; - -def registerTool (name,callback): - _registered_tools.append((name,callback)); - diff --git a/Tigger/Tools/add_brick.py b/Tigger/Tools/add_brick.py deleted file mode 100644 index ad1bf79..0000000 --- a/Tigger/Tools/add_brick.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -import math -import Kittens.utils -pyfits = Kittens.utils.import_pyfits(); -import os.path - -from Kittens.widgets import BusyIndicator -from Tigger.Widgets import FileSelector -from Tigger.Models import SkyModel,ModelClasses -from Tigger.Tools import Imaging - -DEG = math.pi/180; - -from astLib.astWCS import WCS - -class AddBrickDialog (QDialog): - def __init__ (self,parent,modal=True,flags=Qt.WindowFlags()): - QDialog.__init__(self,parent,flags); - self.setModal(modal); - self.setWindowTitle("Add FITS brick"); - lo = QVBoxLayout(self); - lo.setMargin(10); - lo.setSpacing(5); - # file selector - self.wfile = FileSelector(self,label="FITS filename:",dialog_label="FITS file",default_suffix="fits",file_types="FITS files (*.fits *.FITS)",file_mode=QFileDialog.ExistingFile); - lo.addWidget(self.wfile); - # overwrite or add mode - lo1 = QGridLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - lo1.addWidget(QLabel("Padding factor:",self),0,0); - self.wpad = QLineEdit("2",self); - self.wpad.setValidator(QDoubleValidator(self)); - lo1.addWidget(self.wpad,0,1); - lo1.addWidget(QLabel("Assign source name:",self),1,0); - self.wname = QLineEdit(self); - lo1.addWidget(self.wname,1,1); - # OK/cancel buttons - lo.addSpacing(10); - lo2 = QHBoxLayout(); - lo.addLayout(lo2); - lo2.setContentsMargins(0,0,0,0); - lo2.setMargin(5); - self.wokbtn = QPushButton("OK",self); - self.wokbtn.setMinimumWidth(128); - QObject.connect(self.wokbtn,SIGNAL("clicked()"),self.accept); - self.wokbtn.setEnabled(False); - cancelbtn = QPushButton("Cancel",self); - cancelbtn.setMinimumWidth(128); - QObject.connect(cancelbtn,SIGNAL("clicked()"),self.reject); - lo2.addWidget(self.wokbtn); - lo2.addStretch(1); - lo2.addWidget(cancelbtn); - self.setMinimumWidth(384); - # signals - QObject.connect(self.wfile,SIGNAL("filenameSelected"),self._fileSelected); - # internal state - self.qerrmsg = QErrorMessage(self); - - def setModel (self,model): - self.model = model; - if model.filename(): - self._model_dir = os.path.dirname(os.path.abspath(model.filename())); - else: - self._model_dir = os.path.abspath('.'); - self.wfile.setDirectory(self._model_dir); - self._fileSelected(self.wfile.filename(),quiet=True); - - def _fileSelected (self,filename,quiet=False): - self.wokbtn.setEnabled(False); - if not filename: - return None; - # check that filename matches model - if not os.path.samefile(self._model_dir,os.path.dirname(filename)): - self.wfile.setFilename(''); - if not quiet: - QMessageBox.warning(self,"Directory mismatch","""

The FITS file must reside in the same directory - as the current sky model.

"""); - self.wfile.setDirectory(self._model_dir); - return None; - # if filename is not in model already, enable the "add to model" control - for src in self.model.sources: - if isinstance(getattr(src,'shape',None),ModelClasses.FITSImage): - if os.path.exists(src.shape.filename) and os.path.samefile(src.shape.filename,filename): - if not quiet: - QMessageBox.warning(self,"Already in model","This FITS brick is already present in the model."); - self.wfile.setFilename(''); - return None; - if not str(self.wname.text()): - self.wname.setText(os.path.splitext(os.path.basename(str(filename)))[0]); - self.wokbtn.setEnabled(True); - return filename; - - def accept (self): - """Tries to add brick, and closes the dialog if successful."""; - filename = self.wfile.filename(); - # read fits file - busy = BusyIndicator(); - try: - input_hdu = pyfits.open(filename)[0]; - except Exception,err: - busy = None; - QMessageBox.warning(self,"Error reading FITS","Error reading FITS file %s: %s"%(filename,str(err))); - return; - # check name - srcname = str(self.wname.text()) or os.path.splitext(os.path.basename(str(filename)))[0]; - if srcname in set([src.name for src in self.model.sources]): - QMessageBox.warning(self,"Already in model","

The model already contains a source named '%s'. Please select a different name.

"%srcname); - return; - # get image parameters - hdr = input_hdu.header; - max_flux = float(input_hdu.data.max()); - wcs = WCS(hdr,mode='pyfits'); - # Get reference pixel coordinates - # wcs.getCentreWCSCoords() doesn't work, as that gives us the middle of the image - # So scan the header to get the CRPIX values - ra0 = dec0 = 1; - for iaxis in range(hdr['NAXIS']): - axs = str(iaxis+1); - name = hdr.get('CTYPE'+axs,axs).upper(); - if name.startswith("RA"): - ra0 = hdr.get('CRPIX'+axs,1)-1; - elif name.startswith("DEC"): - dec0 = hdr.get('CRPIX'+axs,1)-1; - # convert pixel to degrees -# print ra0,dec0; - ra0,dec0 = wcs.pix2wcs(ra0,dec0); - ra0 *= DEG; - dec0 *= DEG; -# print ModelClasses.Position.ra_hms_static(ra0); -# print ModelClasses.Position.dec_sdms_static(dec0); - sx,sy = wcs.getHalfSizeDeg(); - sx *= DEG; - sy *= DEG; - nx,ny = input_hdu.data.shape[-1:-3:-1]; - pos = ModelClasses.Position(ra0,dec0); - flux = ModelClasses.Flux(max_flux); - shape = ModelClasses.FITSImage(sx,sy,0,os.path.basename(filename),nx,ny,pad=float(str(self.wpad.text()) or "1")); - img_src = SkyModel.Source(srcname,pos,flux,shape=shape); - self.model.setSources(self.model.sources + [img_src]); - self.model.emitUpdate(SkyModel.SkyModel.UpdateAll,origin=self); - busy = None; - return QDialog.accept(self); - -def add_brick (mainwin,model): - dialog = getattr(mainwin,'_add_brick_dialog',None); - if not dialog: - dialog = mainwin._add_brick_dialog = AddBrickDialog(mainwin); - dialog.setModel(model); - # show dialog - return dialog.exec_(); - -from Tigger.Tools import registerTool -registerTool("Add FITS brick to model...",add_brick); \ No newline at end of file diff --git a/Tigger/Tools/export_karma.py b/Tigger/Tools/export_karma.py deleted file mode 100644 index 3540e5f..0000000 --- a/Tigger/Tools/export_karma.py +++ /dev/null @@ -1,150 +0,0 @@ -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -import math -import os.path -from Kittens.widgets import BusyIndicator -from Tigger.Widgets import FileSelector - -DEG = math.pi/180; - - -class ExportKarmaDialog (QDialog): - def __init__ (self,parent,modal=True,flags=Qt.WindowFlags()): - QDialog.__init__(self,parent,flags); - self.setModal(modal); - self.setWindowTitle("Export Karma annotations"); - lo = QVBoxLayout(self); - lo.setMargin(10); - lo.setSpacing(5); - # file selector - self.wfile = FileSelector(self,label="Filename:",dialog_label="Karma annotations filename",default_suffix="ann",file_types="Karma annotations (*.ann)"); - lo.addWidget(self.wfile); - # selected sources checkbox - self.wsel = QCheckBox("selected sources only",self); - lo.addWidget(self.wsel); - # OK/cancel buttons - lo.addSpacing(10); - lo2 = QHBoxLayout(); - lo.addLayout(lo2); - lo2.setContentsMargins(0,0,0,0); - lo2.setMargin(5); - self.wokbtn = QPushButton("OK",self); - self.wokbtn.setMinimumWidth(128); - QObject.connect(self.wokbtn,SIGNAL("clicked()"),self.accept); - self.wokbtn.setEnabled(False); - cancelbtn = QPushButton("Cancel",self); - cancelbtn.setMinimumWidth(128); - QObject.connect(cancelbtn,SIGNAL("clicked()"),self.reject); - lo2.addWidget(self.wokbtn); - lo2.addStretch(1); - lo2.addWidget(cancelbtn); - self.setMinimumWidth(384); - # signals - QObject.connect(self.wfile,SIGNAL("valid"),self.wokbtn.setEnabled); - # internal state - self.qerrmsg = QErrorMessage(self); - self._model_filename = None; - - def setModel (self,model): - self.model = model; - # set the default annotations filename, whenever a new model filename is set - filename = self.model.filename(); - if filename and filename != self._model_filename: - self._model_filename = filename; - self.wfile.setFilename(os.path.splitext(filename)[0] + ".ann"); - - def accept (self): - """Tries to export annotations, and closes the dialog if successful."""; - try: - filename = self.wfile.filename(); - if os.path.exists(filename) and QMessageBox.question(self,"Exporting Karma annotations","

Overwrite the file %s?

"%filename, - QMessageBox.Yes|QMessageBox.No,QMessageBox.Yes) != QMessageBox.Yes: - return; - f = file(self.wfile.filename(),"wt"); - f.write('COORD W\nPA STANDARD\nCOLOR GREEN\nFONT hershey12\n'); - # source list - if self.wsel.isChecked(): - sources = [ src for src in self.model.sources if src.selected ]; - else: - sources = self.model.sources; - # calculate basis size for crosses (TODO: replace min_size with something more sensible, as this value is in degrees) - brightnesses = [ abs(src.brightness()) for src in sources if src.brightness() != 0 ]; - min_bright = brightnesses and min(brightnesses) ; - min_size = 0.01; - # loop over sources - busy = BusyIndicator(); - for src in sources: - ra = src.pos.ra/DEG; - dec = src.pos.dec/DEG; - # figure out source size - if src.brightness() and min_bright: - ysize = (math.log10(abs(src.brightness())) - math.log10(min_bright) + 1)*min_size; - else: - ysize = min_size; - xsize = ysize/(math.cos(src.pos.dec) or 1); - # figure out source style - style,label = self.model.getSourcePlotStyle(src); - if style: - f.write('# %s\n'%src.name); - # write symbol for source - f.write('COLOR %s\n'%style.symbol_color); - if style.symbol == "plus": - f.write('CROSS %.12f %.12f %f %f\n'%(ra,dec,xsize,ysize)); - elif style.symbol == "cross": - f.write('CROSS %.12f %.12f %f %f 45\n'%(ra,dec,ysize,ysize)); - elif style.symbol == "circle": - f.write('CIRCLE %.12f %.12f %f\n'%(ra,dec,ysize)); - elif style.symbol == "dot": - f.write('DOT %.12f %.12f\n'%(ra,dec)); - elif style.symbol == "square": - f.write('CBOX %.12f %.12f %f %f\n'%(ra,dec,xsize,ysize)); - elif style.symbol == "diamond": - f.write('CBOX %.12f %.12f %f %f 45\n'%(ra,dec,xsize,ysize)); - # write label - if label: - f.write('FONT hershey%d\n'%(style.label_size*2)); - f.write('COLOR %s\n'%style.label_color); - f.write('TEXT %.12f %.12f %s\n'%(ra,dec,label)); - f.close(); - except IOError,err: - busy = None; - self.qerrmsg.showMessage("Error writing Karma annotations file %s: %s"%(filename,str(err))); - return; - busy = None; - self.parent().showMessage("Wrote Karma annotations for %d sources to file %s"%(len(sources),filename)); - return QDialog.accept(self); - -def export_karma_annotations (mainwin,model): - dialog = getattr(mainwin,'_export_karma_dialog',None); - if not dialog: - dialog = mainwin._export_karma_dialog = ExportKarmaDialog(mainwin); - dialog.setModel(model); - # show dialog - return dialog.exec_(); - -from Tigger.Tools import registerTool -registerTool("Export Karma annotations...",export_karma_annotations); \ No newline at end of file diff --git a/Tigger/Tools/make_brick.py b/Tigger/Tools/make_brick.py deleted file mode 100644 index 3ec6f6c..0000000 --- a/Tigger/Tools/make_brick.py +++ /dev/null @@ -1,313 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -import math -from math import * -import Kittens.utils -pyfits = Kittens.utils.import_pyfits(); -import os.path -import traceback - -from Kittens.widgets import BusyIndicator -from Tigger.Widgets import FileSelector -from Tigger.Models import SkyModel,ModelClasses -from Tigger.Tools import Imaging - -DEG = math.pi/180; - -from astLib.astWCS import WCS - -class MakeBrickDialog (QDialog): - def __init__ (self,parent,modal=True,flags=Qt.WindowFlags()): - QDialog.__init__(self,parent,flags); - self.setModal(modal); - self.setWindowTitle("Convert sources to FITS brick"); - lo = QVBoxLayout(self); - lo.setMargin(10); - lo.setSpacing(5); - # file selector - self.wfile = FileSelector(self,label="FITS filename:",dialog_label="Output FITS file",default_suffix="fits", - file_types="FITS files (*.fits *.FITS)",file_mode=QFileDialog.ExistingFile); - lo.addWidget(self.wfile); - # reference frequency - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - label = QLabel("Frequency, MHz:",self); - lo1.addWidget(label); - tip = """

If your sky model contains spectral information (such as spectral indices), then a brick may be generated - for a specific frequency. If a frequency is not specified here, the reference frequency of the model sources will be assumed.

"""; - self.wfreq = QLineEdit(self); - self.wfreq.setValidator(QDoubleValidator(self)); - label.setToolTip(tip); - self.wfreq.setToolTip(tip); - lo1.addWidget(self.wfreq); - # beam gain - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - self.wpb_apply = QCheckBox("Apply primary beam expression:",self); - self.wpb_apply.setChecked(True); - lo1.addWidget(self.wpb_apply); - tip = """

If this option is specified, a primary power beam gain will be applied to the sources before inserting - them into the brick. This can be any valid Python expression making use of the variables 'r' (corresponding - to distance from field centre, in radians) and 'fq' (corresponding to frequency.)

"""; - self.wpb_exp = QLineEdit(self); - self.wpb_apply.setToolTip(tip); - self.wpb_exp.setToolTip(tip); - lo1.addWidget(self.wpb_exp); - # overwrite or add mode - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - self.woverwrite = QRadioButton("overwrite image",self); - self.woverwrite.setChecked(True); - lo1.addWidget(self.woverwrite); - self.waddinto = QRadioButton("add into image",self); - lo1.addWidget(self.waddinto); - # add to model - self.wadd = QCheckBox("Add resulting brick to sky model as a FITS image component",self); - lo.addWidget(self.wadd); - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - self.wpad = QLineEdit(self); - self.wpad.setValidator(QDoubleValidator(self)); - self.wpad.setText("1.1"); - lab = QLabel("...with padding factor:",self); - lab.setToolTip("""

The padding factor determines the amount of null padding inserted around the image during - the prediction stage. Padding alleviates the effects of tapering and detapering in the uv-brick, which can show - up towards the edges of the image. For a factor of N, the image will be padded out to N times its original size. - This increases memory use, so if you have no flux at the edges of the image anyway, then a pad factor of 1 is - perfectly fine.

"""); - self.wpad.setToolTip(lab.toolTip()); - QObject.connect(self.wadd,SIGNAL("toggled(bool)"),self.wpad.setEnabled); - QObject.connect(self.wadd,SIGNAL("toggled(bool)"),lab.setEnabled); - self.wpad.setEnabled(False); - lab.setEnabled(False); - lo1.addStretch(1); - lo1.addWidget(lab,0); - lo1.addWidget(self.wpad,1); - self.wdel = QCheckBox("Remove from the sky model sources that go into the brick",self); - lo.addWidget(self.wdel); - # OK/cancel buttons - lo.addSpacing(10); - lo2 = QHBoxLayout(); - lo.addLayout(lo2); - lo2.setContentsMargins(0,0,0,0); - lo2.setMargin(5); - self.wokbtn = QPushButton("OK",self); - self.wokbtn.setMinimumWidth(128); - QObject.connect(self.wokbtn,SIGNAL("clicked()"),self.accept); - self.wokbtn.setEnabled(False); - cancelbtn = QPushButton("Cancel",self); - cancelbtn.setMinimumWidth(128); - QObject.connect(cancelbtn,SIGNAL("clicked()"),self.reject); - lo2.addWidget(self.wokbtn); - lo2.addStretch(1); - lo2.addWidget(cancelbtn); - self.setMinimumWidth(384); - # signals - QObject.connect(self.wfile,SIGNAL("filenameSelected"),self._fileSelected); - # internal state - self.qerrmsg = QErrorMessage(self); - - def setModel (self,model): - self.model = model; - pb = self.model.primaryBeam(); - if pb: - self.wpb_exp.setText(pb); - else: - self.wpb_apply.setChecked(False); - self.wpb_exp.setText(""); - if model.filename(): - self._model_dir = os.path.dirname(os.path.abspath(model.filename())); - else: - self._model_dir = os.path.abspath('.'); - self.wfile.setDirectory(self._model_dir); - self._fileSelected(self.wfile.filename(),quiet=True); - - def _fileSelected (self,filename,quiet=False): - self.wokbtn.setEnabled(False); - if not filename: - return None; - # check that filename matches model - if not os.path.samefile(self._model_dir,os.path.dirname(filename)): - self.wfile.setFilename(''); - if not quiet: - QMessageBox.warning(self,"Directory mismatch","""

The FITS file must reside in the same directory - as the current sky model.

"""); - self.wfile.setDirectory(self._model_dir); - return None; - # read fits file - busy = BusyIndicator(); - try: - input_hdu = pyfits.open(filename)[0]; - hdr = input_hdu.header; - # get frequency, if specified - for axis in range(1,hdr['NAXIS']+1): - if hdr['CTYPE%d'%axis].upper() == 'FREQ': - self.wfreq.setText(str(hdr['CRVAL%d'%axis]/1e+6)); - break; - except Exception,err: - busy = None; - self.wfile.setFilename(''); - if not quiet: - QMessageBox.warning(self,"Error reading FITS","Error reading FITS file %s: %s"%(filename,str(err))); - return None; - self.wokbtn.setEnabled(True); - # if filename is not in model already, enable the "add to model" control - for src in self.model.sources: - if isinstance(getattr(src,'shape',None),ModelClasses.FITSImage) \ - and os.path.exists(src.shape.filename) and os.path.exists(filename) \ - and os.path.samefile(src.shape.filename,filename): - self.wadd.setChecked(True); - self.wadd.setEnabled(False); - self.wadd.setText("image already in sky model"); - break; - else: - self.wadd.setText("add image to sky model"); - return filename; - - def accept (self): - """Tries to make a brick, and closes the dialog if successful."""; - sources = [ src for src in self.model.sources if src.selected and src.typecode == 'pnt' ]; - filename = self.wfile.filename(); - if not self._fileSelected(filename): - return; - # get PB expression - pbfunc = None; - if self.wpb_apply.isChecked(): - pbexp = str(self.wpb_exp.text()); - try: - pbfunc = eval("lambda r,fq:"+pbexp); - except Exception,err: - QMessageBox.warning(self,"Error parsing PB experssion", - "Error parsing primary beam expression %s: %s"%(pbexp,str(err))); - return; - # get frequency - freq = str(self.wfreq.text()); - freq = float(freq)*1e+6 if freq else None; - # get pad factor - pad = str(self.wpad.text()); - pad = max(float(pad),1) if pad else 1; - # read fits file - busy = BusyIndicator(); - try: - input_hdu = pyfits.open(filename)[0]; - except Exception,err: - busy = None; - QMessageBox.warning(self,"Error reading FITS","Error reading FITS file %s: %s"%(filename,str(err))); - return; - # reset data if asked to - if self.woverwrite.isChecked(): - input_hdu.data[...] = 0; - # insert sources - Imaging.restoreSources(input_hdu,sources,0,primary_beam=pbfunc,freq=freq); - # save fits file - try: - # pyfits seems to produce an exception: - # TypeError: formatwarning() takes exactly 4 arguments (5 given) - # when attempting to overwrite a file. As a workaround, remove the file first. - if os.path.exists(filename): - os.remove(filename); - input_hdu.writeto(filename); - except Exception,err: - traceback.print_exc(); - busy = None; - QMessageBox.warning(self,"Error writing FITS","Error writing FITS file %s: %s"%(filename,str(err))); - return; - changed = False; - sources = self.model.sources; - # remove sources from model if asked to - if self.wdel.isChecked(): - sources = [ src for src in sources if not (src.selected and src.typecode == 'pnt') ]; - changed = True; - # add image to model if asked to - if self.wadd.isChecked(): - hdr = input_hdu.header; - # get image parameters - max_flux = float(input_hdu.data.max()); - wcs = WCS(hdr,mode='pyfits'); - # Get reference pixel coordinates - # wcs.getCentreWCSCoords() doesn't work, as that gives us the middle of the image - # So scan the header to get the CRPIX values - ra0 = dec0 = 1; - for iaxis in range(hdr['NAXIS']): - axs = str(iaxis+1); - name = hdr.get('CTYPE'+axs,axs).upper(); - if name.startswith("RA"): - ra0 = hdr.get('CRPIX'+axs,1)-1; - elif name.startswith("DEC"): - dec0 = hdr.get('CRPIX'+axs,1)-1; - # convert pixel to degrees - ra0,dec0 = wcs.pix2wcs(ra0,dec0); - ra0 *= DEG; - dec0 *= DEG; - sx,sy = wcs.getHalfSizeDeg(); - sx *= DEG; - sy *= DEG; - nx,ny = input_hdu.data.shape[-1:-3:-1]; - # check if this image is already contained in the model - for src in sources: - if isinstance(getattr(src,'shape',None),ModelClasses.FITSImage) and os.path.samefile(src.shape.filename,filename): - # update source parameters - src.pos.ra,src.pos.dec = ra0,dec0; - src.flux.I = max_flux; - src.shape.ex,src.shape.ey = sx,sy; - src.shape.nx,src.shape.ny = nx,ny; - src.shape.pad = pad; - break; - # not contained, make new source object - else: - pos = ModelClasses.Position(ra0,dec0); - flux = ModelClasses.Flux(max_flux); - shape = ModelClasses.FITSImage(sx,sy,0,os.path.basename(filename),nx,ny,pad=pad); - img_src = SkyModel.Source(os.path.splitext(os.path.basename(filename))[0],pos,flux,shape=shape); - sources.append(img_src); - changed = True; - if changed: - self.model.setSources(sources); - self.model.emitUpdate(SkyModel.SkyModel.UpdateAll,origin=self); - self.parent().showMessage("Wrote %d sources to FITS file %s"%(len(sources),filename)); - busy = None; - return QDialog.accept(self); - -def make_brick (mainwin,model): - # check that something is selected - if not [ src for src in model.sources if src.selected ]: - mainwin.showErrorMessage("Cannot make FITS brick without a source selection. Please select some sources first."); - return; - dialog = getattr(mainwin,'_make_brick_dialog',None); - if not dialog: - dialog = mainwin._make_brick_dialog = MakeBrickDialog(mainwin); - dialog.setModel(model); - # show dialog - return dialog.exec_(); - -from Tigger.Tools import registerTool -registerTool("Make FITS brick from selected sources...",make_brick); diff --git a/Tigger/Tools/restore_image.py b/Tigger/Tools/restore_image.py deleted file mode 100644 index 4b27bae..0000000 --- a/Tigger/Tools/restore_image.py +++ /dev/null @@ -1,212 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -import math -import Kittens.utils -pyfits = Kittens.utils.import_pyfits(); -import os.path - -from Kittens.widgets import BusyIndicator -from Tigger.Widgets import FileSelector -from Tigger.Models import SkyModel,ModelClasses -from Tigger.Tools import Imaging - -DEG = math.pi/180; - -from astLib.astWCS import WCS - -class RestoreImageDialog (QDialog): - def __init__ (self,parent,modal=True,flags=Qt.WindowFlags()): - QDialog.__init__(self,parent,flags); - self.setModal(modal); - self.setWindowTitle("Restore model into image"); - lo = QVBoxLayout(self); - lo.setMargin(10); - lo.setSpacing(5); - # file selector - self.wfile_in = FileSelector(self,label="Input FITS file:",dialog_label="Input FITS file",default_suffix="fits",file_types="FITS files (*.fits *.FITS)",file_mode=QFileDialog.ExistingFile); - lo.addWidget(self.wfile_in); - self.wfile_out = FileSelector(self,label="Output FITS file:",dialog_label="Output FITS file",default_suffix="fits",file_types="FITS files (*.fits *.FITS)",file_mode=QFileDialog.AnyFile); - lo.addWidget(self.wfile_out); - # beam size - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - lo1.addWidget(QLabel("Restoring beam FWHM, major axis:",self)); - self.wbmaj = QLineEdit(self); - lo1.addWidget(self.wbmaj); - lo1.addWidget(QLabel("\" minor axis:",self)); - self.wbmin = QLineEdit(self); - lo1.addWidget(self.wbmin); - lo1.addWidget(QLabel("\" P.A.:",self)); - self.wbpa = QLineEdit(self); - lo1.addWidget(self.wbpa); - lo1.addWidget(QLabel(u"\u00B0",self)); - for w in self.wbmaj,self.wbmin,self.wbpa: - w.setValidator(QDoubleValidator(self)); - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); - self.wfile_psf = FileSelector(self,label="Set restoring beam by fitting PSF image:",dialog_label="PSF FITS file",default_suffix="fits",file_types="FITS files (*.fits *.FITS)",file_mode=QFileDialog.ExistingFile); - lo1.addSpacing(32); - lo1.addWidget(self.wfile_psf); - # selection only - self.wselonly = QCheckBox("restore selected model sources only",self); - lo.addWidget(self.wselonly ); - # OK/cancel buttons - lo.addSpacing(10); - lo2 = QHBoxLayout(); - lo.addLayout(lo2); - lo2.setContentsMargins(0,0,0,0); - lo2.setMargin(5); - self.wokbtn = QPushButton("OK",self); - self.wokbtn.setMinimumWidth(128); - QObject.connect(self.wokbtn,SIGNAL("clicked()"),self.accept); - self.wokbtn.setEnabled(False); - cancelbtn = QPushButton("Cancel",self); - cancelbtn.setMinimumWidth(128); - QObject.connect(cancelbtn,SIGNAL("clicked()"),self.reject); - lo2.addWidget(self.wokbtn); - lo2.addStretch(1); - lo2.addWidget(cancelbtn); - self.setMinimumWidth(384); - # signals - QObject.connect(self.wfile_in,SIGNAL("filenameSelected"),self._fileSelected); - QObject.connect(self.wfile_in,SIGNAL("filenameSelected"),self._inputFileSelected); - QObject.connect(self.wfile_out,SIGNAL("filenameSelected"),self._fileSelected); - QObject.connect(self.wfile_psf,SIGNAL("filenameSelected"),self._psfFileSelected); - # internal state - self.qerrmsg = QErrorMessage(self); - - def setModel (self,model): - nsel = len([ src for src in model.sources if src.selected ]); - self.wselonly.setVisible(nsel>0 and nsel 0 and len(sel_sources) < len(sources) and self.wselonly.isChecked(): - sources = sel_sources; - if not sources: - self.qerrmsg.showMessage("No sources to restore."); - return; - busy = BusyIndicator(); - # get filenames - infile = self.wfile_in.filename(); - outfile = self.wfile_out.filename(); - self.parent().showMessage("Restoring %d model sources to image %s, writing to %s"%(len(sources),infile,outfile)); - # read fits file - try: - input_hdu = pyfits.open(infile)[0]; - except Exception,err: - busy = None; - self.qerrmsg.showMessage("Error reading FITS file %s: %s"%(infile,str(err))); - return; - # get beam sizes - try: - bmaj = float(str(self.wbmaj.text())); - bmin = float(str(self.wbmin.text())); - pa = float(str(self.wbpa.text()) or "0"); - except Exception,err: - busy = None; - self.qerrmsg.showMessage("Invalid beam size specified"); - return; - bmaj = bmaj/(Imaging.FWHM*3600)*DEG; - bmin = bmin/(Imaging.FWHM*3600)*DEG; - pa = pa*DEG; - # restore - try: - Imaging.restoreSources(input_hdu,sources,bmaj,bmin,pa); - except Exception,err: - busy = None; - self.qerrmsg.showMessage("Error restoring model into image: %s"%str(err)); - return; - # save fits file - try: - input_hdu.writeto(outfile,clobber=True); - except Exception,err: - busy = None; - self.qerrmsg.showMessage("Error writing FITS file %s: %s"%(outfile,str(err))); - return; - self.parent().loadImage(outfile); - busy = None; - return QDialog.accept(self); - -def restore_into_image (mainwin,model): - dialog = getattr(mainwin,'_restore_into_image_dialog',None); - if not dialog: - dialog = mainwin._restore_into_image_dialog = RestoreImageDialog(mainwin); - dialog.setModel(model); - # show dialog - return dialog.exec_(); - -from Tigger.Tools import registerTool -registerTool("Restore model into image...",restore_into_image); diff --git a/Tigger/Tools/source_selector.py b/Tigger/Tools/source_selector.py deleted file mode 100644 index d85d889..0000000 --- a/Tigger/Tools/source_selector.py +++ /dev/null @@ -1,297 +0,0 @@ -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -from PyQt4.Qt import * -import math -import os.path -import traceback - -from Kittens.widgets import SIGNAL,BusyIndicator -from Kittens.utils import curry -from Tigger.Widgets import FileSelector -from Tigger.Models import SkyModel,ModelClasses -from Tigger import SkyModelTreeWidget - -import Kittens.utils - -_verbosity = Kittens.utils.verbosity(name="source_selector"); -dprint = _verbosity.dprint; -dprintf = _verbosity.dprintf; - - -# list of standard tags to be sorted by -StandardTags = [ "ra","dec","r","Iapp","I","Q","U","V","rm","spi" ]; - -# dict of accessors for nested source attributes -TagAccessors = dict(); -for tag in "ra","dec": - TagAccessors[tag] = lambda src,t=tag:getattr(src.pos,t); -for tag in list("IQUV")+["rm"]: - TagAccessors[tag] = lambda src,t=tag:getattr(src.flux,t); -for tag in ["spi"]: - TagAccessors[tag] = lambda src,t=tag:getattr(src.spectrum,t); - -# tags for which sorting is not available -NonSortingTags = set(["name","typecode"]); - - -class SourceSelectorDialog (QDialog): - def __init__ (self,parent,flags=Qt.WindowFlags()): - QDialog.__init__(self,parent,flags); - self.setModal(False); - self.setWindowTitle("Select sources by..."); - lo = QVBoxLayout(self); - lo.setMargin(10); - lo.setSpacing(5); - # select by - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setContentsMargins(0,0,0,0); -# lab = QLabel("Select:"); -# lo1.addWidget(lab); - self.wselby = QComboBox(self); - lo1.addWidget(self.wselby,0); - QObject.connect(self.wselby,SIGNAL("activated(const QString &)"),self._setup_selection_by); - # under/over - self.wgele = QComboBox(self); - lo1.addWidget(self.wgele,0); - self.wgele.addItems([">",">=","<=","<","sum<=","sum>"]); - QObject.connect(self.wgele,SIGNAL("activated(const QString &)"),self._select_threshold); - # threshold value - self.wthreshold = QLineEdit(self); - QObject.connect(self.wthreshold,SIGNAL("editingFinished()"),self._select_threshold); - lo1.addWidget(self.wthreshold,1); - # min and max label - self.wminmax = QLabel(self); - lo.addWidget(self.wminmax); - # selection slider - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - self.wpercent = QSlider(self); - self.wpercent.setTracking(False); - QObject.connect(self.wpercent,SIGNAL("valueChanged(int)"),self._select_percentile); - QObject.connect(self.wpercent,SIGNAL("sliderMoved(int)"),self._select_percentile_threshold); - self.wpercent.setRange(0,100); - self.wpercent.setOrientation(Qt.Horizontal); - lo1.addWidget(self.wpercent); - self.wpercent_lbl = QLabel("0%",self); - self.wpercent_lbl.setMinimumWidth(64); - lo1.addWidget(self.wpercent_lbl); -# # hide button -# lo.addSpacing(10); -# lo2 = QHBoxLayout(); -# lo.addLayout(lo2); -# lo2.setContentsMargins(0,0,0,0); -# hidebtn = QPushButton("Close",self); -# hidebtn.setMinimumWidth(128); -# QObject.connect(hidebtn,SIGNAL("clicked()"),self.hide); -# lo2.addStretch(1); -# lo2.addWidget(hidebtn); -# lo2.addStretch(1); -# self.setMinimumWidth(384); - self._in_select_threshold = False; - self._sort_index = None; - self.qerrmsg = QErrorMessage(self); - - def resetModel (self): - """Resets dialog based on current model."""; - if not self.model: - return; - # getset of model tags, and remove the non-sorting tags - alltags = set(self.model.tagnames); - alltags -= NonSortingTags; - # make list of tags from StandardTags that are present in model - self.sorttags = [ tag for tag in StandardTags if tag in alltags or tag in TagAccessors ]; - # append model tags that were not in StandardTags - self.sorttags += list(alltags - set(self.sorttags)); - # set selector - self.wselby.clear(); - self.wselby.addItems(self.sorttags); - for tag in "Iapp","I": - if tag in self.sorttags: - self.wselby.setCurrentIndex(self.sorttags.index(tag)); - break; - self._setup_selection_by(self.wselby.currentText()); - - def _reset_percentile (self): - self.wthreshold.setText(""); - self.wpercent.setValue(50); - self.wpercent_lbl.setText("--%"); - - def _setup_selection_by (self,tag): - tag = str(tag); # may be QString - # clear threshold value and percentiles - self._reset_percentile(); - # get min/max values, and sort indices - # _sort_index will be an array of (value,src,cumsum) tuples, sorted by tag value (high to low), - # where src is the source, and cumsum is the sum of all values in the list from 0 up to and including the current one - self._sort_index = []; - minval = maxval = None; - for isrc,src in enumerate(self.model.sources): - try: - if hasattr(src,tag): - value = float(getattr(src,tag)); - else: - value = float(TagAccessors[tag](src)); - # skip source if failed to access this tag as a float - except: - traceback.print_exc(); - continue; - self._sort_index.append([value,src,0]); - minval = min(minval,value) if minval is not None else value; - maxval = max(maxval,value) if maxval is not None else value; - # add label - if minval is None: - self._range = None; - self.wminmax.setText("'%s' is not a numeric attribute"%tag); - for w in self.wgele,self.wthreshold,self.wpercent,self.wpercent_lbl: - w.setEnabled(False); - else: - self._range = (minval,maxval); - self.wminmax.setText("min: %g max: %g"%self._range); - for w in self.wgele,self.wthreshold,self.wpercent,self.wpercent_lbl: - w.setEnabled(True); - # sort index by descending values - self._sort_index.sort(reverse=True); - # generate cumulative sums - cumsum = 0.; - for entry in self._sort_index: - cumsum += entry[0]; - entry[2] = cumsum; - - - # Maps comparison operators to callables. Used in _select_threshold. - # Each callable takes two arguments: e is a tuple of (value,src,cumsum) (see _sort_index above), and x is a threshold - # Second argument is a flag: if False, selection is inverted w.r.t. operator - Operators = { - "<" : ((lambda e,x:e[0]>=x),False), - "<=" : ((lambda e,x:e[0]>x),False), - ">" : ((lambda e,x:e[0]>x),True), - ">=" : ((lambda e,x:e[0]>=x),True), - "sum<=": ((lambda e,x:e[2]<=x),True), - "sum>": ((lambda e,x:e[2]<=x),False) - }; - - def _select_threshold (self,*dum): - dprint(1,"select_threshold",dum); - self._in_select_threshold = True; - busy = BusyIndicator(); - try: - # get threshold, ignore if not set - threshold = str(self.wthreshold.text()); - if not threshold: - self._reset_percentile(); - return; - # try to parse threshold, ignore if invalid - try: - threshold = float(threshold); - except: - self._reset_percentile(); - return; - # get comparison operator - op,select = self.Operators[str(self.wgele.currentText())]; - # apply to initial segment (that matches operator) - for num,entry in enumerate(self._sort_index): - if not op(entry,threshold): - break; - entry[1].selected = select; - else: - num = len(self._sort_index); - # apply to remaining segment - for val,src,cumsum in self._sort_index[num:]: - src.selected = not select; - # set percentile - percent = round(float(num*100)/len(self._sort_index)); - if not select: - percent = 100-percent; - self.wpercent.setValue(percent); - self.wpercent_lbl.setText("%3d%%"%percent); - # emit signal - self.model.emitSelection(self); - finally: - self._in_select_threshold = False; - busy = None; - - def _select_percentile (self,percent): - self._select_percentile_threshold(percent,do_select=True); - - def _select_percentile_threshold (self,percent,do_select=False): - # ignore if no sort index set up, or if _select_threshold() is being called - if self._sort_index is None or self._in_select_threshold: - return; - dprint(1,"select_precentile_threshold",percent); - busy = BusyIndicator(); - # number of objects to select - nsrc = len(self._sort_index); - nsel = int(math.ceil(nsrc*float(percent)/100)); - # get comparison operator - opstr = str(self.wgele.currentText()); - op,select = self.Operators[opstr]; - # select head or tail of list, depending on direction of operator - if select: - thr = self._sort_index[min(nsel,nsrc-1)]; - slc1 = slice(0,nsel); - slc2 = slice(nsel,None); - else: - thr = self._sort_index[-min(nsel+1,nsrc)]; - slc1 = slice(nsrc-nsel,None); - slc2 = slice(0,nsrc-nsel); - if do_select: - for val,src,cumsum in self._sort_index[slc1]: - src.selected = True; - for val,src,cumsum in self._sort_index[slc2]: - src.selected = False; - self.model.emitSelection(self); - self.wpercent_lbl.setText("%3d%%"%percent); - self.wthreshold.setText("%g"%(thr[2] if opstr.startswith("sum") else thr[0])); - return nsel; - - def setModel (self,model): - """Sets the current model. If dialog is visible, applies the changes"""; - self.model = model; - if self.isVisible(): - self.resetModel(); - if not model: - self.hide(); - - def show (self): - """Shows dialog, resetting the model if it was invisible."""; - if not self.isVisible(): - self.resetModel(); - QDialog.show(self); - -def show_source_selector (mainwin,model): - dialog = getattr(mainwin,'_source_selector_dialog',None); - if not dialog: - dialog = mainwin._source_selector_dialog = SourceSelectorDialog(mainwin); - QObject.connect(mainwin,SIGNAL("modelChanged"),dialog.setModel); - QObject.connect(mainwin,SIGNAL("closing"),dialog.close); - dialog.setModel(model); - # show dialog - dialog.show(); - dialog.raise_(); - -#from Tigger.Tools import registerTool -#registerTool("Source selector...",show_source_selector); diff --git a/Tigger/Widgets.py b/Tigger/Widgets.py deleted file mode 100644 index 84d23e7..0000000 --- a/Tigger/Widgets.py +++ /dev/null @@ -1,310 +0,0 @@ -# -*- coding: utf-8 -*- -# -#% $Id$ -# -# -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import sys -import math -import traceback -import re - -from PyQt4.Qt import * -from PyQt4.Qwt5 import * - -class TiggerPlotCurve (QwtPlotCurve): - """Wrapper around QwtPlotCurve to make it compatible with numpy float types""" - def setData (self,x,y): - return QwtPlotCurve.setData(self,map(float,x),map(float,y)); - -class TiggerPlotMarker (QwtPlotMarker): - """Wrapper around QwtPlotCurve to make it compatible with numpy float types""" - def setValue (self,x,y): - return QwtPlotMarker.setValue(self,float(x),float(y)); - - -class FloatValidator (QValidator): - """QLineEdit validator for float items in standard or scientific notation"""; - re_intermediate = re.compile("^-?([0-9]*)\.?([0-9]*)([eE]([+-])?[0-9]*)?$"); - def validate (self,input,pos): - input = str(input); - try: - x = float(input); - return QValidator.Acceptable,pos; - except: - pass; - if not input or self.re_intermediate.match(input): - return QValidator.Intermediate,pos; - return QValidator.Invalid,pos; - -class ValueTypeEditor (QWidget): - ValueTypes = (bool,int,float,complex,str); - def __init__ (self,*args): - QWidget.__init__(self,*args); - lo = QHBoxLayout(self); - lo.setContentsMargins(0,0,0,0); - lo.setSpacing(5); - # type selector - self.wtypesel = QComboBox(self); - for i,tp in enumerate(self.ValueTypes): - self.wtypesel.addItem(tp.__name__); - QObject.connect(self.wtypesel,SIGNAL("activated(int)"),self._selectTypeNum); - typesel_lab = QLabel("&Type:",self); - typesel_lab.setBuddy(self.wtypesel); - lo.addWidget(typesel_lab,0); - lo.addWidget(self.wtypesel,0); - self.wvalue = QLineEdit(self); - self.wvalue_lab = QLabel("&Value:",self); - self.wvalue_lab.setBuddy(self.wvalue); - self.wbool = QComboBox(self); - self.wbool.addItems(["false","true"]); - self.wbool.setCurrentIndex(1); - lo.addWidget(self.wvalue_lab,0); - lo.addWidget(self.wvalue,1); - lo.addWidget(self.wbool,1); - self.wvalue.hide(); - # make input validators - self._validators = {int:QIntValidator(self),float:QDoubleValidator(self) }; - # select bool type initially - self._selectTypeNum(0); - - def _selectTypeNum (self,index): - tp = self.ValueTypes[index]; - self.wbool.setShown(tp is bool); - self.wvalue.setShown(tp is not bool); - self.wvalue_lab.setBuddy(self.wbool if tp is bool else self.wvalue); - self.wvalue.setValidator(self._validators.get(tp,None)); - - def setValue (self,value): - """Sets current value"""; - for i,tp in enumerate(self.ValueTypes): - if isinstance(value,tp): - self.wtypesel.setCurrentIndex(i); - self._selectTypeNum(i); - if tp is bool: - self.wbool.setCurrentIndex(1 if value else 0); - else: - self.wvalue.setText(str(value)); - return; - # unknown value: set bool - self.setValue(True); - - def getValue (self): - """Returns current value, or None if no legal value is set"""; - tp = self.ValueTypes[self.wtypesel.currentIndex()]; - if tp is bool: - return bool(self.wbool.currentIndex()); - else: - try: - return tp(self.wvalue.text()); - except: - print "Error converting input to type ",tp.__name__; - traceback.print_exc(); - return None; - - -class FileSelector (QWidget): - """A FileSelector is a one-line widget for selecting a file."""; - def __init__ (self,parent,label,filename=None,dialog_label=None,file_types=None,default_suffix=None,file_mode=QFileDialog.AnyFile): - QWidget.__init__(self,parent); - lo = QHBoxLayout(self); - lo.setContentsMargins(0,0,0,0); - lo.setSpacing(5); - # label - lab = QLabel(label,self); - lo.addWidget(lab,0); - # text field - self.wfname = QLineEdit(self); - self.wfname.setReadOnly(True); - self.setFilename(filename); - lo.addWidget(self.wfname,1); - # selector - wsel = QToolButton(self); - wsel.setText("Choose..."); - QObject.connect(wsel,SIGNAL("clicked()"),self._chooseFile); - lo.addWidget(wsel,0); - # other init - self._file_dialog = None; - self._dialog_label = dialog_label or label; - self._file_types = file_types or "All files (*)"; - self._file_mode = file_mode; - self._default_suffix = default_suffix; - self._dir = None; - - def _chooseFile (self): - if self._file_dialog is None: - dialog = self._file_dialog = QFileDialog(self,self._dialog_label,".",self._file_types); - if self._default_suffix: - dialog.setDefaultSuffix(self._default_suffix); - dialog.setFileMode(self._file_mode); - dialog.setModal(True); - if self._dir is not None: - dialog.setDirectory(self._dir); - QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.setFilename); - return self._file_dialog.exec_(); - - def setFilename (self,filename): - if isinstance(filename,QStringList): - filename = filename[0]; - filename = (filename and str(filename) ) or ''; - self.wfname.setText(filename); - self.emit(SIGNAL("valid"),bool(filename)); - self.emit(SIGNAL("filenameSelected"),filename); - - def setDirectory (self,directory): - self._dir = directory; - if self._file_dialog is not None: - self._file_dialog.setDirectory(directory); - - def filename (self): - return str(self.wfname.text()); - - def isValid (self): - return bool(self.filename()); - - - -class AddTagDialog (QDialog): - def __init__ (self,parent,modal=True,flags=Qt.WindowFlags()): - QDialog.__init__(self,parent,flags); - self.setModal(modal); - self.setWindowTitle("Add Tag"); - lo = QVBoxLayout(self); - lo.setMargin(10); - lo.setSpacing(5); - # tag selector - lo1 = QHBoxLayout(); - lo.addLayout(lo1); - lo1.setSpacing(5); - self.wtagsel = QComboBox(self); - self.wtagsel.setEditable(True); - wtagsel_lbl = QLabel("&Tag:",self); - wtagsel_lbl.setBuddy(self.wtagsel); - lo1.addWidget(wtagsel_lbl,0); - lo1.addWidget(self.wtagsel,1); - QObject.connect(self.wtagsel,SIGNAL("activated(int)"),self._check_tag); - QObject.connect(self.wtagsel,SIGNAL("editTextChanged(const QString &)"),self._check_tag_text); - # value editor - self.valedit = ValueTypeEditor(self); - lo.addWidget(self.valedit); - # buttons - lo.addSpacing(10); - lo2 = QHBoxLayout(); - lo.addLayout(lo2); - lo2.setContentsMargins(0,0,0,0); - lo2.setMargin(5); - self.wokbtn = QPushButton("OK",self); - self.wokbtn.setMinimumWidth(128); - QObject.connect(self.wokbtn,SIGNAL("clicked()"),self.accept); - self.wokbtn.setEnabled(False); - cancelbtn = QPushButton("Cancel",self); - cancelbtn.setMinimumWidth(128); - QObject.connect(cancelbtn,SIGNAL("clicked()"),self.reject); - lo2.addWidget(self.wokbtn); - lo2.addStretch(1); - lo2.addWidget(cancelbtn); - self.setMinimumWidth(384); - - def setTags (self,tagnames): - self.wtagsel.clear(); - self.wtagsel.addItems(list(tagnames)); - self.wtagsel.addItem(""); - self.wtagsel.setCurrentIndex(len(tagnames)); - - def setValue (self,value): - self.valedit.setValue(value); - - def _check_tag (self,tag): - self.wokbtn.setEnabled(True); - - def _check_tag_text (self,text): - self.wokbtn.setEnabled(bool(str(text)!="")); - - def accept (self): - """When dialog is accepted with a default (bool) tag type, - check if the user hasn't entered a name=value entry in the tag name field. - This is a common mistake, and should be treated as a shortcut for setting string tags."""; - if isinstance(self.valedit.getValue(),bool): - tagval = str(self.wtagsel.currentText()).split("=",1); - if len(tagval) > 1: -# print tagval; - if QMessageBox.warning(self, - "Set a string tag instead?","""

You have included an "=" sign in the tag name. - Perhaps you actually mean to set tag "%s" to the string value "%s"?

"""%tuple(tagval), - QMessageBox.Yes|QMessageBox.No,QMessageBox.Yes) == QMessageBox.No: - return; - self.wtagsel.setEditText(tagval[0]); - self.valedit.setValue(tagval[1]); - return QDialog.accept(self); - - def getTag (self): - return str(self.wtagsel.currentText()),self.valedit.getValue(); - -class SelectTagsDialog (QDialog): - def __init__ (self,parent,modal=True,flags=Qt.WindowFlags(),caption="Select Tags",ok_button="Select"): - QDialog.__init__(self,parent,flags); - self.setModal(modal); - self.setWindowTitle(caption); - lo = QVBoxLayout(self); - lo.setMargin(10); - lo.setSpacing(5); - # tag selector - self.wtagsel = QListWidget(self); - lo.addWidget(self.wtagsel); -# self.wtagsel.setColumnMode(QListBox.FitToWidth); - self.wtagsel.setSelectionMode(QListWidget.MultiSelection); - QObject.connect(self.wtagsel,SIGNAL("itemSelectionChanged()"),self._check_tag); - # buttons - lo.addSpacing(10); - lo2 = QHBoxLayout(); - lo.addLayout(lo2); - lo2.setContentsMargins(0,0,0,0); - lo2.setMargin(5); - self.wokbtn = QPushButton(ok_button,self); - self.wokbtn.setMinimumWidth(128); - QObject.connect(self.wokbtn,SIGNAL("clicked()"),self.accept); - self.wokbtn.setEnabled(False); - cancelbtn = QPushButton("Cancel",self); - cancelbtn.setMinimumWidth(128); - QObject.connect(cancelbtn,SIGNAL("clicked()"),self.reject); - lo2.addWidget(self.wokbtn); - lo2.addStretch(1); - lo2.addWidget(cancelbtn); - self.setMinimumWidth(384); - self._tagnames = []; - - def setTags (self,tagnames): - self._tagnames = tagnames; - self.wtagsel.clear(); - self.wtagsel.insertItems(0,list(tagnames)); - - def _check_tag (self): - for i in range(len(self._tagnames)): - if self.wtagsel.item(i).isSelected(): - self.wokbtn.setEnabled(True); - return; - else: - self.wokbtn.setEnabled(False); - - def getSelectedTags (self): - return [ tag for i,tag in enumerate(self._tagnames) if self.wtagsel.item(i).isSelected() ]; diff --git a/Tigger/__init__.py b/Tigger/__init__.py index c50bc2a..c6aecbe 100644 --- a/Tigger/__init__.py +++ b/Tigger/__init__.py @@ -28,10 +28,8 @@ from Tigger.Models.Formats import load, save, listFormats import Kittens.config -import os.path - -__version__ = "1.3.8" +__version__ = "1.4" release_string = __version__ svn_revision_string = __version__ @@ -39,25 +37,11 @@ matplotlib_nuked = False -# initializes GUI-related globals. Only called from the viewer -def init_gui(): - from Kittens.widgets import BusyIndicator - import Kittens.pixmaps - import Kittens.utils - global pixmaps, Config, ConfigFile, ConfigFileName - pixmaps = Kittens.pixmaps.PixmapCache("Tigger") - ConfigFileName = ".tigger.conf" - ConfigFile = Kittens.config.DualConfigParser("tigger.conf",["/usr/lib/Tigger", os.path.dirname(__file__)]) - Config = Kittens.config.SectionParser(ConfigFile,"Tigger") - - startup_dprint = startup_dprintf = lambda *dum:None _verbosity = Kittens.utils.verbosity(name="tigger") dprint = _verbosity.dprint dprintf = _verbosity.dprintf - - def import_pyfits (): """Helper function to import pyfits and return it. Provides a workaround for pyfits-2.3, which is actually arrogant enough (fuck you with a bargepole, pyfits!) @@ -87,6 +71,7 @@ def nuke_matplotlib (): into the sys.modules dict. Call nuke_matplotlib() once, and all further attempts to import matplotlib by any other code will be cheerfully ignored. """ + global matplotlib_nuked if 'pylab' not in sys.modules: # replace the modules referenced by astLib by dummy_module objects, which return a dummy callable for every attribute class dummy_module (object): @@ -94,8 +79,3 @@ def __getattr__ (self,name): return 'nowhere' if name == '__file__' else (lambda *args,**kw:True) sys.modules['pylab'] = sys.modules['matplotlib'] = sys.modules['matplotlib.patches'] = dummy_module() matplotlib_nuked = True - - - - - diff --git a/Tigger/bin/tigger b/Tigger/bin/tigger deleted file mode 120000 index 70d214d..0000000 --- a/Tigger/bin/tigger +++ /dev/null @@ -1 +0,0 @@ -../tigger \ No newline at end of file diff --git a/Tigger/bin/tigger-make-brick b/Tigger/bin/tigger-make-brick index b35bbcf..1e6a5be 100755 --- a/Tigger/bin/tigger-make-brick +++ b/Tigger/bin/tigger-make-brick @@ -26,13 +26,10 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -import sys -import pyfits -import re import os.path import pyfits +import Tigger import math -import numpy from math import * from astLib.astWCS import WCS @@ -42,22 +39,10 @@ NATIVE = "Tigger"; if __name__ == '__main__': import Kittens.utils - from Kittens.utils import curry _verbosity = Kittens.utils.verbosity(name="convert-model"); dprint = _verbosity.dprint; dprintf = _verbosity.dprintf; - # find Tigger - try: - import Tigger - except ImportError: - sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))); - try: - import Tigger - except: - print "Unable to import the Tigger package. Please check your installation and PYTHONPATH."; - sys.exit(1); - Tigger.nuke_matplotlib(); # don't let the door hit you in the ass, sucka from Tigger.Tools import Imaging diff --git a/Tigger/icons/astron_logo.png b/Tigger/icons/astron_logo.png deleted file mode 100644 index 793fe6bd69bf3730b666a0e5cce3e2d690ca5a3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7373 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 diff --git a/Tigger/icons/big_plus.png b/Tigger/icons/big_plus.png deleted file mode 100644 index 1e03be9bc896c5f6d8a943ff9d153b42b67e6b16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)8Z)0JRB|3DF3=|YfF7jDEZ>IVpdRI~vVL~%?rHj{Mb-aA4?aN$HkpY6+e zIJ}%A@;qlRND8>Ne);a1hpoZcM@ORNcT0CDpm2lpt8;$t;5m`HA+Mts_V&s9W~>cf zetj;y8B(ff-fq3o6i^ss-tluMK}bpJ9RCvtKmqnoV4WvU2M7e2b^n@kK&}DWEA7Qj z+`01X%TpaFO`sJG5esISB#+&7>@2gVH>sP%}a+DuI`p-7R~pI$1Y5x z9J%rcDSiulyep{dI#Oy%ghc%waq@#~ax=Pj`qhjEu-38}b?Q6+eS$+ zxgkjtHlh#o1_P#oDl|xq z)$=3opuA-AQ-@kiO_W*dZ1Abw6Jy@#*R^Fg!5P*sA&3jgil^7k&X>BCo8zhBfN|0000j diff --git a/Tigger/icons/center_image.png b/Tigger/icons/center_image.png deleted file mode 100644 index 1dc4efd4ef3a107b44c33c26cb1117bc20586cb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 735 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJbFq_W2nPqp?T7vkfLzW3kH}&M z2FBeW%xLxI@gtz1WQl7;NpOBzNqJ&XDnogBxn5>oc5!lIL8@MUQTpt6Hc~* zfr5#Ni9n56Sy@1pxw*MO6AB9pfhtQ&OUug2fNlla4zv_#2+-b+j*kBR{s|K%%$YN1 z$BrGlcJ11?Z{N9d=Pq2haQX7(D_5>uy?XT;3&%B6%bRSRH*em&C9ibL)#LW<+jl}E z?%ut7&)n+1uEBjvoBOs7_Y;yIa&SGgw0`vH(PJ68=iEHcEv;X?c=7V(%U7>nz1BB) zJ#WDqAbRuW?c$|xSFC#b_U-%k?>{V9_+iJ+k9+rj{P^+Hr%#_hfBwqP|LxniU%!6+ z{{8#UuU~)u{Q3L$@4sKafFW6LdsYM(`idn%e!&ckK%fQ$K;IRD{nEG<2)1o|4g^30 zzD?sg>%TN_vv}raHT~$3@AZo7_g;FSlO`+?`!mwE zogpDR%%iBAf#brjbb+Sq4koAH;!(LJF7Y zJnmO@Q~C4^jn!w)t?s^M9$~%n^EAEPZq@t=vHLF1)tqv0+tRSg*n900IxL<&EGU}t zE_;?z$MKuLs=qH;=IV4tVZ!ru&pK4LvmUUIT%7Y`wYAV&pcffDUHx3vIVCg!0Gv{2 ACjbBd diff --git a/Tigger/icons/colours.png b/Tigger/icons/colours.png deleted file mode 100644 index 5a05fa48df66de304d39d3804991b47ee97e910f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 495 zcmVV5RYPi+ulaS zte_KiJZoMciX!xudf*8Mk2$Pg^D*{wHuae@Al88V2E;GG-NixBKHu@62=|wVT>vW? zAT*F@fc^l+FOS~p`X*E&@N{!l$|GlgVT@Am1Y>JxP&({1ACxNP&=r;EdyT zbPH=%RvE>B>gg?za5;q&iDuHogLpuZim>W?d%?xlmX{KtUajU?iBAA%z#x#I9O#ex z7#c%h5m?a6dTunl(%KplDG}7`dDesvpy(A!zCn(rNo{6lm!UMkG!V5~Flx2zF(TK+ zPto`t%m~38Af?>RrlRl#*m_yrQYj$E@fBluaoqreLG}kub~l2nW5)*=`Zvn~(9Qbg lqjF%Lnm#nJ+}8bnuTK`U*?x�oMQk002ovPDHLkV1mwa*^mGL diff --git a/Tigger/icons/full_range.png b/Tigger/icons/full_range.png deleted file mode 100644 index 6b06d7951cd5a5f699271245a471c48fa3524891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 557 zcmV+|0@D47P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iXP& z4G0XHc^FFo00FH@L_t(I%dL~YYZGA@$3M^gNUoPw3nEx5E{&6eQx{33ouz^m*U(ND zRH!-@v{Q$wAVny4>fE9Kf@m_up$39WGDk6x#5Q-Ac(AYK!j& zT)Aj-c>-4FMc#cDC*u3h5Il{0p%yhhZD6Er$%{y_f2`N{AExR_2aj*d9~XDF z{49pl99!~Y7)1a>1irnQESaX6P1)vd((>xOXp&R5xoeq5wz57^I=^2SMXCy_s)a&9 z|K|iS%H=OFeBTeM)!N$h^tF%GYHjT-d~on}1Hfpty6HHM(v3#bot@2pZZw+iS-90Y zbO2;$XYZBkj`xDVb3{;8B$LVXJa5Q=xbAq*_qz_9a=#x;E-x=Wu_~4F)i{ow;oa|t v8A?ug7*a$)6;V|z%Q8n^!i9zVqi^yr79k&_mgv%S00000NkvXXu0mjf(~X4n`{S`6%(=t z+pK$VKlu2De|C1h|NjSOpd?8^PavQ(P=EjD*&K0-kc);7oI^u{KgbvY6)5ZfbspvF zz}oyw&DLt5ud~_y_WbnN=AUxE04h+{tIuD*1;tNH8&s%$$JxDddoo;F5+i} zMDA*CV13@##6ZDprU6ihASnwF3c`CkrJ5RuMfM+kY;SC{lHy+_Lb9%SN2@h85M1&- zWYfIKO0vF63}s#S4%d>;Y?>K}23N~1B-3nSC~I^(@T8~dfhXUG5kUy?;`oO*A-~%$ zmpzjRUXBqT{#kao=6B@P01brF*-x!q;z_U~V{@efD5D~HWwfUypLYl)#!oFKBwE#e ziR3eIiSXP=oyYlXvZSiv+kMBY0q}n)gahwgDaz|o%}A(r0($0MZuhL^PtxvUigw*0 zOmiY;Oaf-jveGP1G`_=BJhmkk18IS`q>?J}J}Qp12@z%XDK(A~gaUGBW_q#3CU00000NkvXXu0mjf&|M^m diff --git a/Tigger/icons/locked.png b/Tigger/icons/locked.png deleted file mode 100644 index 04a05c1cb1fec55b1d037f11b7340d8d24f9b969..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 750 zcmVsmTOMf>H=7Ly%CAwRb5*>sq*K2!wDGwwax7f*>fu zMXOv`(8`V0A~f(jlp#%LhH>6F&-d=U?-tXHQ<7(O?mhqi?_AEgb^H_4({CefZQ+Nl zt<8^`n(D6s$g-NsW~G^2uK0R#@_|&Vch?@Kre3v#!=Xex-v4Z9s5csmwbRwrLU(t2 zb0FZmolfW7@%a7K#l?AX4v_JRTV6 z3rC~P3pr#qW2szP^{qWTcnL}KA-1` z5L&fy6`&|8hM^;bAd|_Tn5MZ}sm;zNva+mbnpQ&BbreOd&aFzid1*6*==2I{AFrA9 zOb*U%0ouXUQnRmynpc7Kn&0|}rGU^Uq{>J8Q>tjN`oP-7@?A{9g=Ag|YM z=vajMo0`Kx^;9W&3VX|jP+tr@+4$@M$V%LckKGz=>c|1dfD?q^P%R*a1?0hT0>zz< zJ73@Legn#YqnwSU?m#ieh2a2_z}k>F%7xBmaF}y23M%-t3(O7_OK@<4Avql8oHIba z=wKB!@QS|;m9CZziVnvr7#3E^MZQ=Aa2!B2z^{R;4r~WZyKFRV&@Ct#VAx<-pu4O5 z>KZZZGD-g-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0IO0G|-o z|NsA&l~o))db+fTOYzWwmw!^e*wKYjWHbj*(*KYsoC1$0vVos0EA?+BCx`2{m< z-MSwNK7Rc7@89<57HObdm#2$kh{WaOgaZsdb7bD+tkLNyVAP9KP7e0AOKWN9o!Pl` ztL~(R=DCgY)@^E z5fT*U8qAO<9?-gHSKBg%J9@Q}p}xMpxs2Y$D;W5XL^*Ohr(9=f+$%bX!QP`~zT|fC bhYSqgJ_)>L?|Z@ybQOcAtDnm{r-UW|N6Se@5LDlse4e%$#$+Z_fFCerB%|0j50-0L)l0Hvt#kO)JPX2ym?ti8qOA zHwUX9mMzWK8|6R%gPjLD8N+{>$VrZXU>^P~*Q5*&BRwk%3Q#T(PV;&uCmvrba;#hQ z1yN9W?CMeDgPSnr5%iE`wN&5 zg9=Xk^bp>BQf}nAT&9$?G=%AJpVtr12WVBG{1jwh1|dq%keei+EMF(70?K>^ znV>U9GlYvPkHSz0IHLEe1f|(yxSi;ta`EAayi^Z2kBz~YoD4pdcDxBu4BtG!`h3#B zs*#(roiQ<$^(`%XMp|3r#cej-5&@-UWWdKYkQIcIsYO6U&SC?h%U=8eLkR%_XuD8z>6S5X)#Ml=Njnsdd&JMro%x{2sz8a&bQQl=@th19YIKvq!Fl= z$-Ves39hwQ;IZoby_@GOZT0L7&AI68arUTo2X?8Fa9^kXI3yhRIPQ_^s>@temftCS rKeUtGkp-N)(o2KO^0XN7M}PqU40%7)NFIND00000NkvXXu0mjfqPko! diff --git a/Tigger/icons/subset_range.png b/Tigger/icons/subset_range.png deleted file mode 100644 index 89459cce3e33c1140dc38cd1044576c97d2448eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJbFq_W2nPqp?T7vkfLzW3kH})6 z^6MbX=%g{b0w^e1;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6i|_OfKQ0) z|Ns9bGd@V9zm-UTS6W`TdF$Sait4tut|?PyE?l^5&6gEk{n0<(1p}wrK)y zM5g_Hv)OUSvm^ltHpXYQBF}z@o!RKhnPst7=!(I8j;cHV|8&`E?_ts3d~OfOCk&ph KelF{r5}E+IQH{_5 diff --git a/Tigger/icons/tigger_logo.png b/Tigger/icons/tigger_logo.png deleted file mode 100644 index 2121fd3aa93627223be75d8ef87b53032343f4bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19456 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

$o5_hC^%Bc7RX;QG(vhcg@aQlB3zb= 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 diff --git a/Tigger/icons/tigger_starface.png b/Tigger/icons/tigger_starface.png deleted file mode 100644 index 2121fd3aa93627223be75d8ef87b53032343f4bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19456 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 diff --git a/Tigger/icons/window_larger.png b/Tigger/icons/window_larger.png deleted file mode 100644 index a8c35c4eda3514f5bc0942d6d0defdbd605de813..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 785 zcmV+s1Md8ZP)q1azL6lnT zrh&HLA6zIJSBeX9A#6erHF})4Ou%yf-y?5y9Ad zcpkxc1W^LC01vQpK-PgL1HfXxY_+vKsL$(>lzuWccS60ti+U?}lPPHG&^SiIkg#NV zG-bGa)A&HZg@;p<&5I*C@ll^f>o;j~{f5@nX)SdcXHFw^I{EgUCf|Ikja8qh?%1in z-F>U9`|1YJ>tih>3HtJ1BnXjZ%Lwpts|v<}R;twIFQVh`e^5%))7dzlisA&!!S4|3 zBfIvJJoppwKotaNeg6Z&nO}(y9HO~lBid-Rhw5we0lwacSE=BJwj;N1WBOL00-`yj zvGsE@QA)!nnNI4srx`ex18@%Mu2L3|!7b?E7S!eIQ$QSa4Q)e)wt+&aIv=%3P6m7h zTwzDGxWugeb}i1P=x5}qXOYPWL5Y9AL;c7x(l|m)0kiPo<|vKBM{(k4MlqVDU`*kl zx&TH-m>z#g`PMDWbc5*2FQ}|ri)8t$!RQ}!pB`o+OX-ZK@OB>uYl|6RP#`=tOxE`) z(bX$d)~`id?M-5T{*j3+rRUr)n1MC8{o!ncz(CO5O?l-2;i83fzJ850U_I~*cphk6 zqbrFp1FNyUy_8EfCj6O; P00000NkvXXu0mjfXQyoH diff --git a/Tigger/icons/window_sigma.png b/Tigger/icons/window_sigma.png deleted file mode 100644 index e1a4a4a547ee2168703daaef2854404386099107..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693 zcmV;m0!safP)#uRTRd5Ywvx|y}37vkVLSO*k~md;-d&MID@T1 z2DA{dQ;drKAy%16s~`v_Q44MIAXUKFh=qbhOrlZLc;}JaJNLZy`Yg`LIFc;*ioMsj zUf*8Z_|J@cEC3EX1&@6Xj|+|(d{)8Mr-CEcn{T|z^&2-oSY3Jc{pn<~Vt|kCdGgWm^uZ2`$wI`e zN82p8_1m``E?Po}be;PP&m9Y=zqwkP*}+6!5Xe5CZ!(&UBn3A`kkiz++4}QG+Gfu5 zp+`VY8Xfcp4nGkN9olg~sj0ScZ>_DiY3r%EdwBZTTldNVocsK34on}Wtu{F@QUFM8 zHAA_kn*YUe63ATxz@;xgBzF~9KoV=~8x-XP_l9P60&hWwtp#cj~Ka zN5`WuPeqx^qO`Isttbi$G4#Ri?(WJtXEPiBa&vJ-(!%KftbhWT3>Xh6dc|mf8mNHX b0S&;v7x6)PmL88)00000NkvXXu0mjfGHpYH diff --git a/Tigger/icons/window_smaller.png b/Tigger/icons/window_smaller.png deleted file mode 100644 index 9310a4713f11321df6698e0dba4afbedef51ed5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 772 zcmV+f1N;1mP)< z`)|VKt60F{@gSn4qi1Lqio{cs)K893?e$=?xdN?J&^?0$Q&Y(N z0`l7xyo(V)6$nGj+;if|3)F^&k%@7{+LpUu)fhY=vyWl>c4(CFAFM+)^Gg+y-abSX zQ?2sqf zBjqw`T51gB9zCQuGmS44DSXu3HZU`>*g#+R{B0; z{V$VLIy&I!cMt>_AeI!{^9kYJy)?ca#SQeL)z^e+*4>05weu6?yVq0cEJFVda&gph z1MKa^X4g`2BYxz2#OBb4Aeu*?ylNR{@a{wO?-Y)V5ycVr|MOs-12BAkWE?L&grP%3 zv)A*E2*Ygt$QfSD++*>DEP~0|IUhdKs+#XoRGWO zZ%8SMQwjMH6N2C^9=7iNHRTm;r4Ixc5D{;rwPT`66Swby*9u<13p2_w&Rh|P@__OH z9tfVJlz`gTjY}hriYfhB3efFBtBFznY#l|7m@yPelCD5_l{H4?j@T5;w3dam~to zhQyr~NaLT}i=F~T!FwT41%4NE=Tedq&$oR(hR3g{6g;`m#ucIeh*}Wy8b283mCPsk z$iMlO(Wg5MuG~Nq1f1X>03YCk-s^mAdxNcmBE$a;7eN7Sh`Z9yH(uZQ0^_X0yqGz; z78Sb?jM<=G0Obd{4M7J4u0aq8gu(H{oO!IhaT>Q$s!~Ke(GYlS8!Dv?fH?u;s@9WP zu$%NDFw zYZ+hz6fh_vF2Ducl}JZzvrp5>eZ|F2ZKS`v}u!MWMo{>zuGdX z&1ls7c9E)GKpUv#+!%Rd&z?OyP+wm!#l^*9G#X3wckz=>Ktvch#M*8})ayr-Xo>&Uj$P*fFQm+1uRQEEN?MVltW5=p+)NSug4$98)ts z5g8gg=5RQ+=j7y&mzT%l#fuNO}4N+kNoz<;(7_ zt}d1>TecZUCal7^Yv7a-5Rtyl&d#c~wl<7LV;3OsTTjq0!2vzk)Ml$4aT&1f|C j4h{~U0W=W_j*a;nNf=4vr))`B00000NkvXXu0mjf{m*Xg diff --git a/Tigger/icons/zoom-out.png b/Tigger/icons/zoom-out.png deleted file mode 100644 index b56a0bb79fc0403afca890a53945c9feba7a2092..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1866 zcmV-Q2etT#P)bM zK~#9!&6jOxTjv$WpXa{l>fM$l+mdBlwq;9BY`KwJyG??@!$~)?Eo=wS+$`Jf^m9p|#Hkx||n=&Pv>s;qJ=%+WPn3eDjU5lS1(5P&aPe80rpp zkKO-t(>y*U8{xi?8XD+UBZDDg^LkW=x31!A_UJZ`t0IY#HoI2FvCV{eAmYE;)z$g% z(W6K4lK=<^2z8?Q3tMFyY{Nr+w|J4zovC(tAGFerRagh<19AR zS(qRR$__=zGUf_)6K`k;%PQis08x@3!Sz)Az1WMq>jKcQqh~xJu&KXEBIl2Ow%F?#@B(4YQoz9ox zmQe`fxByOY|GA|DN-YiUoubXYwM#hNE}2v;2x^_c+eC&~6~KlOpkSRyrc9AH#GJ*N zvo_Vc;;B?qOVAvx=BjcY8$c9ASFwUsbGs47G$*7)MPmAs##-*sTi3^OWQGqp3P3fTF2tq0S*#Rte!1M%8cr{MDN^ zHc_#QoCdqc5V#=0aHCEcS0PN04Q47-3~4`+vok`u`WlmWwl0T(zC=P?9>bDhUBR4L1hsT7JsT}Pc?ki_}9{)dA3OQyqm#M|1BMvjd5LYsom{F-E ztq@x-DZ5IG+rcYta`~*MY{3{moSmJ0ud%Ta9~>O8le`Pq`uh5UAPW2o=Ym5Q0`AV; zSW!x3HMK+$pVg@OKsM~Dbh$Pk%lK;Jd*@&25JrN|f-RLw;gQRi!$TJ@4n-o7VaWAh zadA;QE*md$EG{nGg$a7Rv9a-5a(`cHZf!-~XU=rAG&f(W zt*w19IXU^IO9_7ooC~7q@K{wAV(0Kk5GWW7+M1i2f8Ej1@d~^V@i(qsMd5Ha+S%UP zynXxjOQWNsQ!r&;P~^XhL6jl(A!3KIe|U|dLuIpB9ai~AVB!XNsr4@}En%zGiaJA~ z+U`*3+ch;c^Sisdt6r@YlYt**p!hsMe3cMl+1OY|CdrI@dV6}?+uNT9 zz1aAd9jrPxSWoeht2`9BZqqhy!zm!r^up-+%849{mpm&l_!SZpJE= z%3lED5*X19CPccXA?Sh^JaL{!PeHL*Y#%%^4iLj$5SSCeUQl5yf6Sw&qw)*<|+#@3+U-$d{82vx!Ul0{#Wq9p+B>(^b07*qoM6N<$ Eg7rj$G5`Po diff --git a/Tigger/icons/zoom_colours.png b/Tigger/icons/zoom_colours.png deleted file mode 100644 index 05f6c39ba2ddcb416d09d8658daa9a1a6c7b33d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 933 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 L^>bP0l+XkKS(umU diff --git a/Tigger/tigger b/Tigger/tigger deleted file mode 100755 index 9b98f86..0000000 --- a/Tigger/tigger +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright (C) 2002-2011 -# The MeqTree Foundation & -# ASTRON (Netherlands Foundation for Research in Astronomy) -# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see , -# or write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -if __name__ == "__main__": - from optparse import OptionParser - import traceback - import Tigger - import Kittens.utils - from PyQt4.Qt import * - - Tigger.init_gui() - - _verbosity = Kittens.utils.verbosity(name="startup") - dprint = _verbosity.dprint - dprintf = _verbosity.dprintf - - # parse options is the first thing we should do - - usage = "usage: %prog [options] " - parser = OptionParser(usage=usage) - parser.add_option("-d", "--debug",dest="verbose",type="string",action="append",metavar="Context=Level", - help="(for debugging Python code) sets verbosity level of the named Python context. May be used multiple times.") - parser.add_option("-T", "--timestamps",action="store_true", - help="(for debugging Python code) enable timestamps in debug output") - (options, rem_args) = parser.parse_args() - - if options.timestamps: - try: - Kittens.utils.verbosity.enable_timestamps() - except: - pass - dprint(1,"starting up") - - # setup include path - import sys - import os.path - # Tigger lives here for now. Add it to include path so that "import Tigger" works - sys.modules['TiggerMain'] = __name__ - - Tigger.nuke_matplotlib(); # don't let the door hit you in the ass, sucka - - print "Welcome to Tigger "+Tigger.release_string - print "Please wait a second while the GUI starts up." - - ## ugly hack to get around UGLY FSCKING ARROGNAT (misspelling fully intentional) pyfits-2.3 bug - Kittens.utils.import_pyfits() - ## (we don't really need pyfits yet, but better import it quickly to activate the workaround) - - - dprint(1,"imported Tigger") - Tigger.startup_dprint = dprint - Tigger.startup_dprintf = dprintf - - - dprint(1,"imported Qt4") - app = QApplication(sys.argv) - app.setDesktopSettingsAware(True) - from Tigger import pixmaps - app.setWindowIcon(pixmaps.tigger_starface.icon()) - # Need for PlotStyles colors -- ignore error on non-X11 platforms like the Mac - try: - QColor.setAllowX11ColorNames(True) - except AttributeError: - pass - - dprint(1,"created QApplication") - #splash = QSplashScreen(Tigger.pixmaps.tigger_splash.pm()) - #splash.showMessage("Welcome to Tigger!",Qt.AlignHCenter|Qt.AlignBottom) - #splash.show() - - import Tigger.Images - import Tigger.MainWindow - dprint(1,"imported Tigger.MainWindow") - import Tigger.Tools - from Tigger.Tools import export_karma,add_brick,make_brick,restore_image - dprint(1,"imported Tigger.Tools") - - - mainwin = Tigger.MainWindow.MainWindow(None) - dprint(1,"created main window") - - # add optional tools - for name,callback in Tigger.Tools.getRegisteredTools(): - mainwin.addTool(name,callback) - dprint(1,"added optional tools") - - # parse remaining args - images = [ arg for arg in rem_args if Tigger.Images.isFITS(arg) ] - models = [ arg for arg in rem_args if arg not in images ] - - if len(models) > 1: - parser.error("Only one model should be specified at the command line.") - - # load images first - for img in images: - print "Loading image",img - #splash.showMessage("Loading image %s"%img,Qt.AlignHCenter|Qt.AlignBottom) - mainwin.loadImage(img) - dprint(1,"loaded image",img) - - # load model, if specified - for mod in models: - print "Loading model",mod - try: - mainwin.openFile(mod,show=False) - except: - traceback.print_exc() - print "Error loading model %s"%mod - os._exit(0) - - #splash.showMessage("Loading model %s"%mod,Qt.AlignHCenter|Qt.AlignBottom) - dprint(1,"loaded model",mod) - - # start updating the plot - mainwin.enableUpdates() - dprint(1,"started plot updates") - - # flush app event queue, so windows get resized , etc. - app.processEvents() - - # handle SIGINT - def sigint_handler (sig,stackframe): - print "Caught Ctrl+C, exiting..." - mainwin.close() - - - import signal - signal.signal(signal.SIGINT,sigint_handler) - dprint(1,"added signal handler") - - #splash.finish(mainwin) - - app.exec_() \ No newline at end of file diff --git a/Tigger/tigger.conf b/Tigger/tigger.conf deleted file mode 100644 index 0ff77f0..0000000 --- a/Tigger/tigger.conf +++ /dev/null @@ -1,107 +0,0 @@ -[Tigger] -mouse-modes = Mouse3,Mouse3a,Mouse2,Mouse1 -current-mouse-mode = Mouse3 - -# available keyboard modifiers: SHIFT, CTRL, ALT, META -[Mouse3] -name = Three-button mouse: zoom/unzoom/measure -icon = -contexts = -submodes = -zoom-window = LeftButton -zoom-undo = MidButton -zoom-redo = SHIFT+MidButton -unzoom = CTRL+MidButton -measure = SHIFT+RightButton -stats = RightButton -select-source = CTRL+LeftButton -select-window = SHIFT+LeftButton -select-window-plus = SHIFT+CTRL+LeftButton -deselect-window = SHIFT+ALT+LeftButton - -[Mouse3a] -name = Three-button mouse: zoom/measure/unzoom -icon = -contexts = -submodes = -zoom-window = LeftButton -zoom-undo = RightButton -zoom-redo = SHIFT+RightButton -unzoom = CTRL+RightButton -measure = SHIFT+MidButton -stats = MidButton -select-source = CTRL+LeftButton -select-window = SHIFT+LeftButton -select-window-plus = SHIFT+CTRL+LeftButton -deselect-window = ALT+LeftButton - -[Mouse2] -name = Two-button mouse -icon = -contexts = -submodes = -zoom-window = LeftButton -zoom-undo = RightButton -zoom-redo = SHIFT+RightButton -unzoom = CTRL+RightButton -measure = META+LeftButton -stats = META+RightButton -select-source = CTRL+LeftButton -select-window = SHIFT+LeftButton -select-window-plus = SHIFT+CTRL+LeftButton -deselect-window = SHIFT+ALT+LeftButton - -[Mouse1] -name = Single-button mouse -icon = -contexts = -submodes = Mouse1_zoom,Mouse1_measure,Mouse1_select - -[Mouse1_zoom] -name = Zoom -icon = zoom_in -contexts = -submodes = -zoom-window = LeftButton -zoom-undo = CTRL+LeftButton -zoom-redo = SHIFT+LeftButton -unzoom = SHIFT+CTRL+LeftButton -measure = -stats = -select-source = -select-window = -select-window-plus = -deselect-window = - -[Mouse1_measure] -name = Measure -icon = ruler -contexts = -submodes = -zoom-window = -zoom-undo = -zoom-redo = -unzoom = -measure = SHIFT+LeftButton -stats = LeftButton -select-source = -select-window = -select-window-plus = -deselect-window = - -[Mouse1_select] -name = Select -icon = big_plus -contexts = model -submodes = -zoom-window = LeftButton -zoom-undo = -zoom-redo = -unzoom = -measure = -stats = -select-source = CTRL+LeftButton -select-window = SHIFT+LeftButton -select-window-plus = SHIFT+CTRL+LeftButton -deselect-window = ALT+CTRL+LeftButton - diff --git a/setup.py b/setup.py index 35ce9ec..37bc708 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,9 @@ #!/usr/bin/env python from __future__ import print_function -import os -import sys from setuptools import setup, find_packages -import warnings -msg = """ -Could not detect PyQt4. Install PyQt4 system wide. If you are in -a virtualenv install the vext.pyqt4 package: - - $ pip install astro-tigger[venv] - -""" - -try: - import PyQt4 -except ImportError: - warnings.warn(msg) - - -__version__ = "1.3.10" +__version__ = "1.4" requirements = ['astro_kittens', 'numpy', 'scipy', 'astlib', 'pyfits'] @@ -29,16 +12,12 @@ 'Tigger/bin/tigger-make-brick', 'Tigger/bin/tigger-restore', 'Tigger/bin/tigger-tag', - 'Tigger/tigger', ] -package_data = {'Tigger': [ - 'icons/*.png', - 'tigger.conf', -] } +package_data = { +} extras_require = { - 'venv': ['vext.pyqt4'], }