veusz-1.21.1/0000775000175000017500000000000012376130063011133 5ustar jssjssveusz-1.21.1/Documents/0000775000175000017500000000000012376130063013074 5ustar jssjssveusz-1.21.1/Documents/manual.txt0000644000175000017500000024376512275421421015130 0ustar jssjssVeusz - a scientific plotting package Jeremy Sanders Copyright 2014 This document is licensed under the GNU General Public License, version 2 or greater. Please see the file COPYING for details, or see http://www.gnu.org/ licenses/gpl-2.0.html. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Table of Contents 1. Introduction Veusz Terminology Widget Settings: properties and formatting Text Measurements Axis numeric scales Installation The main window My first plot 2. Reading data Standard text import Data types in text import Descriptors CSV files HDF5 files Error bars Slices 2D data ranges Dates 2D text or CSV format FITS files Reading other data formats Manipulating datasets Using dataset plugins Using expressions to create new datasets Linking datasets to expressions Splitting data Defining new constants or functions Dataset plugins Capturing data 3. Command line interface Introduction Commands Action Add AddCustom AddImportPath CloneWidget Close CreateHistogram DatasetPlugin EnableToolbar Export ForceUpdate Get GetChildren GetClick GetData GetDataType GetDatasets GPL ImportFile ImportFile2D ImportFileCSV ImportFileHDF5 ImportFilePlugin ImportFITSFile ImportString ImportString2D IsClosed List Load MoveToPage ReloadData Rename Remove ResizeWindow Save Set SetAntiAliasing SetData SetDataExpression SetDataRange SetData2D SetData2DExpression SetData2DExpressionXYZ SetData2DXYFunc SetDataDateTime SetDataText SetToReference SetUpdateInterval SetVerbose StartSecondView TagDatasets To Quit WaitForClose Zoom Security 4. Using Veusz from other programs Non-Qt Python programs Older path-based interface New-style object interface Translating old to new style PyQt4 programs Non Python programs C, C++ and Fortran Chapter 1. Introduction Table of Contents Veusz Terminology Widget Settings: properties and formatting Text Measurements Axis numeric scales Installation The main window My first plot Veusz Veusz is a scientific plotting package. It was designed to be easy to use, easily extensible, but powerful. The program features a graphical user interface, which works under Unix/Linux, Windows or Mac OS X. It can also be easily scripted (the saved file formats are similar to Python scripts) or used as module inside Python. Veusz reads data from a number of different types of data file, it can be manually entered, or constructed from other datasets. In Veusz the document is built in an object-oriented fashion, where a document is built up by a number of widgets in a hierarchy. For example, multiple function or xy widgets can be placed inside a graph widget, and many graphs can be placed in a grid widget. The technologies behind Veusz include PyQt (a very easy to use Python interface to Qt, which is used for rendering and the graphical user interface, GUI) and numpy (a package for Python which makes the handling of large datasets easy). Veusz can be extended by the user easily by adding plugins. Support for different data file types can be added with import plugins. Dataset plugins automate the manipulation of datasets. Tools plugins automate the manipulation of the document. Terminology Here we define some terminology for future use. Widget A document and its graphs are built up from widgets. These widgets can often by placed within each other, depending on the type of the widget. A widget has children (those widgets placed within it) and its parent. The widgets have a number of different settings which modify their behaviour. These settings are divided into properties, which affect what is plotted and how it is plotted. These would include the dataset being plotted or whether an axis is logarithmic. There are also formatting settings, including the font to be used and the line thickness. In addition they have actions, which perform some sort of activity on the widget or its children, like "fit" for a fit widget. As an aside, using the scripting interface, widgets are specified with a "path", like a file in Unix or Windows. These can be relative to the current widget (do not start with a slash), or absolute (do not start with a slash). Examples of paths include, "/page1/graph1/x", "x" and ".". The widget types include 1. document - representing a complete document. A document can contain pages. In addition it contains a setting giving the page size for the document. 2. page - representing a page in a document. One or more graphs can be placed on a page, or a grid. 3. graph - defining an actual graph. A graph can be placed on a page or within a grid. Contained within the graph are its axes and plotters. A graph can be given a background fill and a border if required. It also has a margin, which specifies how far away from the edge of its parent widget to plot the body of the graph. A graph can contain several axes, at any position on the plot. In addition a graph can use axes defined in parent widgets, shared with other graphs. More than one graph can be placed within in a page. The margins can be adjusted so that they lie within or besides each other. 4. grid - containing one or more graphs. A grid plots graphs in a gridlike fashion. You can specify the number of rows and columns, and the plots are automatically replotted in the chosen arrangement. A grid can contain graphs or axes. If an axis is placed in a grid, it can be shared by the graphs in the grid. 5. axis - giving the scale for plotting data. An axis translates the coordinates of the data to the screen. An axis can be linear or logarithmic, it can have fixed endpoints, or can automatically get them from the plotted data. It also has settings for the axis labels and lines, tick labels, and major and minor tick marks. An axis may be "horizontal" or "vertical" and can appear anywhere on its parent graph or grid. If an axis appears within a grid, then it can be shared by all the graphs which are contained within the grid. The axis-broken widget is an axis sub-type. It is an axis type where there are jumps in the scale of the axis. The axis-function widget allows the user to create an axis where the values are scaled by a monotonic function, allowing non-linear and non-logarithmic axis scales. The widget can also be linked to a different axis via the function. 6. plotters - types of widgets which plot data or add other things on a graph. There is no actual plotter widget which can be added, but several types of plotters listed below. Plotters typically take an axis as a setting, which is the axis used to plot the data on the graph (default x and y). a. function - a plotter which plots a function on the graph. Functions can be functions of x or y (parametric functions are not done yet!), and are defined in Python expression syntax, which is very close to most other languages. For example "3*x**2 + 2*x - 4". A number of functions are available (e.g. sin, cos, tan, exp, log...). Technically, Veusz imports the numpy package when evaluating, so numpy functions are available. As well as the function setting, also settable is the line type to plot the function, and the number of steps to evaluate the function when plotting. Filling is supported above/below/left/right of the function. b. xy - a plotter which plots scatter, line, or stepped plots. This versatile plotter takes an x and y dataset, and plots (optional) points, in a chosen marker and colour, connecting them with (optional) lines, and plotting (optional) error bars. An xy plotter can also plot a stepped line, allowing histograms to be plotted (note that it doesn't yet do the binning of the data). The settings for the xy widget are the various attibutes for the points, line and error bars, the datasets to plot, and the axes to plot on. The xy plotter can plot a label next to each dataset, which is either the same for each point or taken from a text dataset. If you wish to leave gaps in a plot, the input value "nan" can be specified in the numeric dataset. c. fit - fit a function to data. This plotter is a like the function plotter, but allows fitting of the function to data. This is achived by clicking on a "fit" button, or using the "fit" action of the widget. The fitter takes a function to fit containing the unknowns, e.g. "a*x**2 + b*x + c", and initial values for the variables (here a, b and c). It then fits the data (note that at the moment, the fit plotter fits all the data, not just the data that can be seen on the graph) by minimising the chi-squared. In order to fit properly, the y data (or x, if fitting as a function of x) must have a properly defined, preferably symmetric error. If there is none, Veusz assumes the same fractional error everywhere, or symmetrises asymmetric errors. Note that more work is required in this widget, as if a parameter is not well defined by the data, the matrix inversion in the fit will fail. In addition Veusz does not supply estimates for the errors or the final chi-squared in a machine readable way. If the fitting parameters vary significantly from 1, then it is worth "normalizing" them by adding in a factor in the fit equation to bring them to of the order of 1. d. bar - a bar chart which plots sets of data as horizontal or vertical bars. Multiple datasets are supported. In "grouped" mode the bars are placed side-by-side for each dataset. In "stacked" mode the bars are placed on top of each other (in the appropriate direction according to the sign of the dataset). Bars are placed on coordinates given, or in integer values from 1 upward if none are given. Error bars are plotted for each of the datasets. Different fill styles can be given for each dataset given. A separate key value can be given for each dataset. e. key - a box which describes the data plotted. If a key is added to a plot, the key looks for "key" settings of the other data plotted within a graph. If there any it builds up a box containing the symbol and line for the plotter, and the text in the "key" setting of the widget. This allows a key to be very easily added to a plot. The key may be placed in any of the corners of the plot, in the centre, or manually placed. Depending on the ordering of the widgets, the key will be placed behind or on top of the widget. The key can be filled and surrounded by a box, or not filled or surrounded. f. label - a text label places on a graph. The alignment can be adjusted and the font changed. The position of the label can be specified in fractional terms of the current graph, or using axis coordinates. g. rect, ellipse - these draw a rectangle or ellipse, respectively, of size and rotation given. These widgets can be placed directly on the page or on a graph. The centre can be given in axis coordinates or fractional coordinates. h. imagefile - draw an external graphs file on the graph or page, with size and rotation given. The centre can be given in axis coordinates or fractional coordinates. i. line - draw a line with optional arrowheads on the graph or page. One end can be given in axis coordinates or fractional coordinates. j. contour - plot contours of a 2D dataset on the graph. Contours are automatically calculated between the minimum and maximum values of the graph or chosen manually. The line style of the contours can be chosen individually and the region between contours can be filled with shading or color. 2D datasets currently consist of a regular grid of values between minimum and maximum positions in x and y. They can be constructed from three 1D datasets of x, y and z if they form a regular x, y grid. k. image - plot a 2D dataset as a colored image. Different color schemes can be chosen. The scaling between the values and the image can be specified as linear, logarithmic, square-root or square. l. polygon - plot x and y points from datasets as a polygon. The polygon can be placed directly on the page or within a graph. Coordinates are either plotted using the axis or as fractions of the width and height of the containing widget. m. boxplot - plot distribution of points in a dataset. n. polar - plot polar data or functions. This is a non-orthogonal plot and is placed directly on the page rather than in a graph. o. ternary - plot data of three variables which add up to 100 per cent.This is a non-orthogonal plot and is placed directly on the page rather than in a graph. Settings: properties and formatting The various settings of the widgets come in a number of types, including integers (e.g. 10), floats (e.g. 3.14), dataset names ("mydata"), expressions ("x+y"), text ("hi there!"), distances (see above), options ("horizontal" or "vertical" for axes). Veusz performs type checks on these parameters. If they are in the wrong format the control to edit the setting will turn red. In the command line, a TypeError exception is thrown. In the GUI, the current page is replotted if a setting is changed when enter is pressed or the user moves to another setting. The settings are split up into formatting settings, controlling the appearance of the plot, or properties, controlling what is plotted and how it is plotted. Default settings, including the default font and line style, and the default settings for any graph widget, can be modified in the "Default styles" dialog box under the "Edit" menu. Default settings are set on a per-document basis, but can be saved into a separate file and loaded. A default default settings file can be given to use for new documents (set in the preferences dialog). Text Veusz understands a limited set of LaTeX-like formatting for text. There are some differences (for example, "10^23" puts the 2 and 3 into superscript), but it is fairly similar. You should also leave out the dollar signs. Veusz supports superscripts ("^"), subscripts ("_"), brackets for grouping attributes are "{" and "}". Supported LaTeX symbols include: \AA, \Alpha, \Beta, \Chi, \Delta, \Epsilon, \ Eta, \Gamma, \Iota, \Kappa, \Lambda, \Mu, \Nu, \Omega, \Omicron, \Phi, \Pi, \ Psi, \Rho, \Sigma, \Tau, \Theta, \Upsilon, \Xi, \Zeta, \alpha, \approx, \ast, \ asymp, \beta, \bowtie, \bullet, \cap, \chi, \circ, \cup, \dagger, \dashv, \ ddagger, \deg, \delta, \diamond, \divide, \doteq, \downarrow, \epsilon, \equiv, \eta, \gamma, \ge, \gg, \in, \infty, \int, \iota, \kappa, \lambda, \le, \ leftarrow, \lhd, \ll, \models, \mp, \mu, \neq, \ni, \nu, \odot, \omega, \ omicron, \ominus, \oplus, \oslash, \otimes, \parallel, \perp, \phi, \pi, \pm, \ prec, \preceq, \propto, \psi, \rhd, \rho, \rightarrow, \sigma, \sim, \simeq, \ sqrt, \sqsubset, \sqsubseteq, \sqsupset, \sqsupseteq, \star, \stigma, \subset, \subseteq, \succ, \succeq, \supset, \supseteq, \tau, \theta, \times, \umid, \ unlhd, \unrhd, \uparrow, \uplus, \upsilon, \vdash, \vee, \wedge, \xi, \zeta. Please request additional characters if they are required (and exist in the unicode character set). Special symbols can be included directly from a character map. Other LaTeX commands are supported. "\\" breaks a line. This can be used for simple tables. For example "{a\\b} {c\\d}" shows "a c" over "b d". The command "\frac{a}{b}" shows a vertical fraction a/b. Also supported are commands to change font. The command "\font{name}{text}" changes the font text is written in to name. This may be useful if a symbol is missing from the current font, e.g. "\font{symbol}{g}" should produce a gamma. You can increase, decrease, or set the size of the font with "\size{+2}{text}", "\size{-2}{text}", or "\size{20}{text}". Numbers are in points. Various font attributes can be changed: for example, "\italic{some italic text} " (or use "\textit" or "\emph"), "\bold{some bold text}" (or use "\textbf") and "\underline{some underlined text}". Example text could include "Area / \pi (10^{-23} cm^{-2})", or "\pi\bold{g}". Veusz plots these symbols with Qt's unicode support. You can also include special characters directly, by copying and pasting from a character map application. If your current font does not contain these symbols then you may get a box character. Measurements Distances, widths and lengths in Veusz can be specified in a number of different ways. These include absolute distances specified in physical units, e.g. 1cm, 0.05m, 10mm, 5in and 10pt, and relative units, which are relative to the largest dimension of the page, including 5%, 1/20, 0.05. Axis numeric scales The way in which numbers are formatted in axis scales is chosen automatically. For standard numerical axes, values are shown with the "%Vg" formatting (see below). For date axes, an appropriate date formatting is used so that the interval shown is correct. A format can be given for an axis in the axis number formatting panel can be given to explicitly choose a format. Some examples are given in the drop down axis menu. Hold the mouse over the example for detail. C-style number formatting is used with a few Veusz specific extensions. Text can be mixed with format specifiers, which start with a "%" sign. Examples of C-style formatting include: "%.2f" (decimal number with two decimal places, e.g. 2.01), "%.3e" (scientific formatting with three decimal places, e.g. 2.123e-02), "%g" (general formatting, switching between "%f" and "%e" as appropriate). See http://opengroup.org/onlinepubs/007908799/xsh/fprintf.html for details. Veusz extensions include "%Ve", which is like "%e" except it displays scientific notation as written, e.g. 1.2x10^23, rather than 1.2e+23. "%Vg" switches between standard numbers and Veusz scientific notation for large and small numbers. "%VE" using engineering SI suffixes to represent large or small numbers (e.g. 1000 is 1k). Veusz allows dates and times to be formatted using "%VDX" where "X" is one of the formatting characters for strftime (see http://opengroup.org/onlinepubs/ 007908799/xsh/strftime.html for details). These include "a" for an abbreviated weekday name, "A" for full weekday name, "b" for abbreviated month name, "B" for full month name, "c" date and time representaiton, "d" day of month 01..31, "H" hour as 00..23, "I" hour as 01..12, "j" as day of year 001..366, "m" as month 01..12, "M" minute as 00..59, "p" AM/PM, "S" second 00..61, "U" week number of year 00..53 (Sunday as first day of week), "w" weekday as decimal number 0..6, "W" week number of year (Monday as first day of week), "x" date representation, "X" time representation, "y" year without century 00..99 and "Y" year. "%VDVS" is a special Veusz addon format which shows seconds and fractions of seconds (e.g. 12.2). Installation Please look at the Installation notes (INSTALL) for details on installing Veusz. The main window You should see the main window when you run Veusz (you can just type the veusz command in Unix). [mainwindow] The Veusz window is split into several sections. At the top is the menu bar and tool bar. These work in the usual way to other applications. Sometimes options are disabled (greyed out) if they do not make sense to be used. If you hold your mouse over a button for a few seconds, you will usually get an explanation for what it does called a "tool tip". Below the main toolbar is a second toolbar for constructing the graph by adding widgets (on the left), and some editing buttons. The add widget buttons add the request widget to the currently selected widget in the selection window. The widgets are arranged in a tree-like structure. Below these toolbars and to the right is the plot window. This is where the current page of the current document is shown. You can adjust the size of the plot on the screen (the zoom factor) using the "View" menu or the zoom tool bar button (the magnifying glass). Initially you will not see a plot in the plot window, but you will see the Veusz logo. At the moment you cannot do much else with the window. In the future you will be able to click on items in the plot to modify them. To the left of the plot window is the selection window, and the properties and formatting windows. The properties window lets you edit various aspects of the selected widget (such as the minimum and maximum values on an axis). Changing these values should update the plot. The formatting lets you modify the appearance of the selected widget. There are a series of tabs for choosing what aspect to modify. The various windows can be "dragged" from the main window to "float" by themselves on the screen. To the bottom of the window is the console. This window is not shown by default, but can be enabled in the View menu. The console is a Veusz and Python command line console. To read about the commands available see Commands. As this is a Python console, you can enter mathematical expressions (e.g. "1+2.0*cos(pi/4)") here and they will be evaluated when you press Enter. The usual special functions and the operators are supported. You can also assign results to variables (e.g. "a=1+2") for use later. The console also supports command history like many Unix shells. Press the up and down cursor keys to browse through the history. Command line completion is not available yet! There also exists a dataset browsing window, by default to the right of the screen. This window allows you to view the datasets currently loaded, their dimensions and type. Hovering a mouse over the size of the dataset will give you a preview of the data. My first plot After opening Veusz, on the left of the main window, you will see a Document, containing a Page, which contains a Graph with its axes. The Graph is selected in the selection window. The toolbar above adds a new widget to the selected widget. If a widget cannot be added to a selected widget it is disabled. On opening a new document Veusz automatically adds a new Page and Graph (with axes) to the document. You will see something like this: [winwithgra] Select the x axis which has been added to the document (click on "x" in the selection window). In the properties window you will see a variety of different properties you can modify. For instance you can enter a label for the axis by writing "Area (cm^{2})" in the box next to label and pressing enter. Veusz supports text in LaTeX-like form (without the dollar signs). Other important parameters is the "log" switch which switches between linear and logarithmic axes, and "min" and "max" which allow the user to specify the minimum and maximum values on the axes. The formatting dialog lets you edit various aspects of the graph appearance. For instance the "Line" tab allows you to edit the line of the axis. Click on "Line", then you can then modify its colour. Enter "green" instead of "black" and press enter. Try making the axis label bold. Now you can try plotting a function on the graph. If the graph, or its children are selected, you will then be able to click the "function" button at the top (a red curve on a graph). You will see a straight line (y=x) added to the plot. If you select "function1", you will be able to edit the functional form plotted and the style of its line. Change the function to "x**2" (x-squared). We will now try plotting data on the graph. Go to your favourite text editor and save the following data as test.dat: 1 0.1 -0.12 1.1 0.1 2.05 0.12 -0.14 4.08 0.12 2.98 0.08 -0.1 2.9 0.11 4.02 0.04 -0.1 15.3 1.0 The first three columns are the x data to plot plus its asymmetric errors. The final two columns are the y data plus its symmetric errors. In Veusz, go to the "Data" menu and select "Import". Type the filename into the filename box, or use the "Browse..." button to search for the file. You will see a preview of the data pop up in the box below. Enter "x,+,- y,+-" into the descriptors edit box (note that commas and spaces in the descriptor are almost interchangeable in Veusz 1.6 or newer). This describes the format of the data which describes dataset "x" plus its asymmetric errors, and "y" with its symmetric errors. If you now click "Import", you will see it has imported datasets "x" and "y". To plot the data you should now click on "graph1" in the tree window. You are now able to click on the "xy" button (which looks like points plotted on a graph). You will see your data plotted on the graph. Veusz plots datasets "x" and "y" by default, but you can change these in the properties of the "xy" plotter. You are able to choose from a variety of markers to plot. You can remove the plot line by choosing the "Plot Line" subsetting, and clicking on the "hide" option. You can change the colour of the marker by going to the "Marker Fill" subsetting, and entering a new colour (e.g. red), into the colour property. Chapter 2. Reading data Table of Contents Standard text import Data types in text import Descriptors CSV files HDF5 files Error bars Slices 2D data ranges Dates 2D text or CSV format FITS files Reading other data formats Manipulating datasets Using dataset plugins Using expressions to create new datasets Linking datasets to expressions Splitting data Defining new constants or functions Dataset plugins Capturing data Currently Veusz supports reading data from files with text, CSV, HDF5, FITS, 2D text or CSV, QDP, binary and NPY/NPZ formats. Use the Data → Import dialog to read data, or the importing commands in the API can be used. In addition, the user can load or write import plugins in Python which load data into Veusz in an arbitrary format. At the moment QDP, binary and NPY/NPZ files are supported with this method. The HDF5 file format is the most sophisticated, and is recommended for complex datasets. By default, data are "linked" to the file imported from. This means that the data are not stored in the Veusz saved file and are reloaded from the original data file when opening. In addition, the user can use the Data → Reload menu option to reload data from linked files. Unselect the linked option when importing to remove the association with the data file and to store the data in the Veusz saved document. Note that a prefix and suffix can be given when importing. These are added to the front or back of each dataset name imported. They are convenient for grouping data together. [importdial] We list the various types of import below. Standard text import The default text import operates on simple text files. The data are assumed to be in columns separated by whitespace. Each column corresponds to dataset (or its error bars). Each row is an entry in the dataset. The way the data are read is goverened by a simple "descriptor". This can simply be a list of dataset names separated by spaces. If no descriptor is given, the columns are treated as separate datasets and are given names col1, col2, etc. Veusz attempts to automatically determine the type of the data. When reading in data, Veusz treats any whitespace as separating columns. The columns do not actually need to be aligned. Furthermore a "\" symbol can be placed at the end of a line to mark a continuation. Veusz will read the next line as if it were placed at the end of the current line. In addition comments and blank lines are ignored (unless in block mode). Comments start with a "#", ";", "!" or "%", and continue until the end of the line. The special value "nan" can be used to specify a break in a dataset. If the option to read data in blocks is enabled, Veusz treats blank lines (or lines starting with the word "no") as block separators. For each dataset in the descriptor, separate datasets are created for each block, using a numeric suffix giving the block number, e.g. _1, _2. Data types in text import Veusz supports reading in several types of data. The type of data can be added in round brackets after the name in the descriptor. Veusz will try to guess the type of data based on the first value, so you should specify it if there is any form of ambiguity (e.g. is 3 text or a number). Supported types are numbers (use numeric in brackets) and text (use text in brackets). An example descriptor would be "x(numeric) +- y(numeric) + - label(text)" for an x dataset followed by its symmetric errors, a y dataset followed by two columns of asymmetric errors, and a final column of text for the label dataset. A text column does not need quotation unless it contains space characters or escape characters. However make sure you deselect the "ignore text" option in the import dialog. This ignores lines of text to ease the import of data from other applications. Quotation marks are recommended around text if you wish to avoid ambiguity. Text is quoted according to the Python rules for text. Double or single quotation marks can be used, e.g. "A 'test'", 'A second "test"'. Quotes can be escaped by prefixing them with a backslash, e.g. "A new \"test\ "". If the data are generated from a Python script, the repr function provides the text in a suitable form. Dates and times are also supported with the syntax "dataset(date)". Dates must be in ISO format YYYY-MM-DD. Times are in 24 hour format hh:mm:ss.ss. Dates with times are written YYYY-MM-DDThh:mm:ss.ss (this is a standard ISO format, see http://www.w3.org/TR/NOTE-datetime). Dates are stored within Veusz as a number which is the number of seconds since the start of January 1st 2009. Veusz also supports dates and times in the local format, though take note that the same file and data may not work on a system in a different location. Descriptors A list of datasets, or a "Descriptor", is given in the Import dialog to describe how the data are formatted in the import file. The descriptor at its simplest is a space or comma-separated list of the names of the datasets to import. These are columns in the file. Following a dataset name the text "+", "-", or "+-" can be given to say that the following column is a positive error bar, negative error bar or symmetric error bar for the previous (non error bar) dataset. These symbols should be separated from the dataset name or previous symbol with a space or a comma symbol. In addition, if multiple numbered columns should be imported, the dataset name can be followed by square brackets containing a range in the form "[a:b]" to number columns a to b, or [:] to number remaining columns. See below for examples of this use. Dataset names can contain virtually any character, even unicode characters. If the name contains non alpha-numeric characters (characters outside of A-Z, a-z and 0-9), then the dataset name should be contained within back-tick characters. An example descriptor is `length data (m)`,+- `speed (mps)`,+,-, for two datasets with spaces and brackets in their names. Instead of specifying the descriptor in the Import dialog, the descriptor can be placed in the data file using a descriptor statement on a separate line, consisting of "descriptor" followed by the descriptor. Multiple descriptors can be placed in a single file, for example: # here is one section descriptor x,+- y,+,- 1 0.5 2 0.1 -0.1 2 0.3 4 0.2 -0.1 # my next block descriptor alpha beta gamma 1 2 3 4 5 6 7 8 9 # etc... Descriptor examples 1. x y two columns are present in the file, they will be read in as datasets "x" and "y". 2. x,+- y,+,- or x +- y + - two datasets are in the file. Dataset "x" consists of the first two columns. The first column are the values and the second are the symmetric errors. "y" consists of three columns (note the comma between + and -). The first column are the values, the second positive asymmetric errors, and the third negative asymmetric errors. Suppose the input file contains: 1.0 0.3 2 0.1 -0.2 1.5 0.2 2.3 2e-2 -0.3E0 2.19 0.02 5 0.1 -0.1 Then x will contain "1+-0.3", "1.5+-0.2", "2.19+-0.02". y will contain "2 +0.1 -0.2", "2.3 +0.02 -0.3", "5 +0.1 -0.1". 3. x[1:2] y[:] the first column is the data "x_1", the second "x_2". Subsequent columns are read as "y[1]" to "y[n]". 4. y[:]+- read each pair of columns as a dataset and its symmetric error, calling them "y[1]" to "y[n]". 5. foo,,+- read the first column as the foo dataset, skip a column, and read the third column as its symmetric error. CSV files CVS (comma separated variable) files are often written from other programs, such as spreadsheets, including Excel and Gnumeric. Veusz supports reading from these files. In the import dialog choose "CSV", then choose a filename to import from. In the CSV file the user should place the data in either rows or columns. Veusz will use a name above a column or to the left of a row to specify what the dataset name should be. The user can use new names further down in columns or right in rows to specify a different dataset name. Names do not have to be used, and Veusz will assign default "col" and "row" names if not given. You can also specify a prefix which is prepended to each dataset name read from the file. To specify symmetric errors for a column, put "+-" as the dataset name in the next column or row. Asymmetric errors can be stated with "+" and "-" in the columns. The data type in CSV files are automatically detected unless specified. The data type can be given in brackets after the column name, e.g. "name (text)", where the data type is "date", "numeric" or "text". Explicit data types are needed if the data look like a different data type (e.g. a text item of "1.23"). The date format in CSV files can be specified in the import dialog box - see the examples given. In addition CSV files support numbers in European format (e.g. 2,34 rather than 2.34), depending on the setting in the dialog box. HDF5 files HDF5 is a flexible data format. Datasets and tables can be stored in a hierarchical arrangements of groups within a file. Veusz supports reading 1D numeric, text, date-time or 2D numeric data from HDF files. The h5py Python module must be installed to use HDF5 files (included in binary releases). In the import dialog box, choose which individual datasets to import, or selecting a group to import all the datasets within the group. If selecting a group, datasets in the group incompatible with Veusz are ignored. A name can be provided for each dataset imported by entering one under "Import as". If one is not given, the dataset or column name is used. The name can also be specified by setting the HDF5 dataset attribute vsz_name to the name. Note that for compound datasets (tables), vsz_ attributes for columns are given by appending the suffix _columnname to the attribute. Error bars Error bars are supported in two ways. The first way is to combine 1D datasets. For the datasets which are error bars, use a name which is the same as the main dataset but with the suffix "(+-)", "(+)" or "(-)", for symmetric, postive or negative error bars, respectively. The second method is to use a 2D dataset with two or three columns, for symmetric or asymmetric error bars, respectively. Click on the dataset in the dialog and choose the option to import as a 1D dataset. This second method can also be enabled by adding an HDF5 attribute vsz_twod_as_oned set to a non-zero value for the dataset. Slices As Veusz only supports 1D and 2D datasets, you may wish to reduce the dimensions of a dataset before importing by slicing. You can also give a slice to import a subset of a dataset. When importing, in the slice column you can give a slice expression. This should have the same number of entries as the dataset has dimensions, separated by commas. An entry can be a single number, to select a particular row or column. Alternatively it could be an expression like a:b:c or a:b, where a is the starting index, b is one beyond the stopping index and optionally c is the step size. A slice can also be specified by providing an HDF5 attribute vsz_slice for the dataset. 2D data ranges 2D data have an associated X and Y range. By default the number of pixels of the image are used to give this range. A range can be specified by clicking on the dataset and entering a minimum and maximum X and Y coordinates. Alternatively, provide the HDF5 attribute for the dataset vsz_range, which should be set to an array of four values (minimum x, minimum y, maximum x, maximum y). Dates Date/time datasets can be made from a 1D numeric dataset or from a text dataset. For the 1D dataset, use the number of seconds relative to the start of the year 2009 (this is Veusz format) or the year 1970 (this is Unix format). In the import dialog, click on the name of the dataset and choose the date option. To specify a date format in the HDF5 file, set the attribute vsz_convert_datetime to either veusz or unix. For text datasets, dates must be given in the right format, selected in the import dialog after clicking on the dataset name. As in other file formats, by default Veusz uses ISO 8601 format, which looks like "2013-12-22T21:08:07", where the date and time parts are optional. The T is also optional. You can also provide your own format when importing by giving a date expression using YYYY, MM, DD, hh, mm and ss (e.g. "YYYY-MM-DD|T|hh:mm:ss"), where vertical bars mark optional parts of the expression. To automate this, set the attribute vsz_convert_datetime to the format expression or iso to specify ISO format. 2D text or CSV format Veusz can import 2D data from standard text or CSV files. In this case the data should consist of a matrix of data values, with the columns separated by one or more spaces or tabs and the rows on different lines. In addition to the data the file can contain lines at the top which affect the import. Such specifiers are used, for example, to change the coordinates of the pixels in the file. By default the first pixels coordinates is between 0 and 1, with the centre at 0.5. Subsequent pixels are 1 greater. When using specifiers in CSV files, put the different parts (separated by spaces) in separate columns. Below are listed the specifiers: 1. xrange A B - make the 2D dataset span the coordinate range A to B in the x-axis (where A and B are numbers). Note that the range is inclusive, so a 1 pixel wide image with A=0 and B=1 would have the pixel centre at 0.5. The pixels are assumed to have the same spacing. Do not use this as the same time as the xedge or xcent options. 2. yrange A B - make the 2D dataset span the coordinate range A to B in the y-axis (where A and B are numbers). 3. xedge A B C... - rather than assume the pixels have the same spacing, give the coordinates of the edges of the pixels in the x-axis. The numbers should be space-separated and there should be one more number than pixels. Do not give xrange or xcent if this is given. 4. yedge A B C... - rather than assume the pixels have the same spacing, give the coordinates of the edges of the pixels in the y-axis. 5. xcent A B C... - rather than give a total range or pixel edges, give the centres of the pixels. There should be the same number of values as pixels in the image. Do not give xrange or xedge if this is given. 6. ycent A B C... - rather than give a total range or pixel edges, give the centres of the pixels. 7. invertrows - invert the rows after reading the data. 8. invertcols - invert the columns after reading the data. 9. transpose - swap rows and columns after importing data. 10. gridatedge - the first row and leftmost column give the positions of the centres of the pixels. This is also an option in the import dialog. The values should be increasing. FITS files 1D or 2D data can be read from FITS files. 1D data, with optional errors bars, can be read from table extensions, and 2D data from image or primary extensions. Note that pyfits or astropy must be installed to get FITS support. To read 1D data, choose a tabular HDU for to import from, enter the name to give the imported data, and choose the columns to assign to the data. Multiple sets of data can be read by repeatedly importing. For 2D data, choose an image HDU. Enter the name of the dataset. The data are imported with pixel coordinates by default (i.e. the pixels are numbered with integers). Other modes can be selected under Image WCS mode. These include fractional, where the pixels are numbered between 0 and 1. Pixel (WCS) assigns the pixel coordinate calculated relative to the CRPIX1/2 header keywords. Linear (WCS) uses linear coordinates where the Pixel (WCS) coordinates are multiplied by the respective CDELT1/2 values and added to the CRVAL1/2 values. Reading other data formats As mentioned above, a user may write some Python code to read a data file or set of data files. To write a plugin which is incorportated into Veusz, see http://barmag.net/veusz-wiki/ImportPlugins You can also include Python code in an input file to read data, which we describe here. Suppose an input file "in.dat" contains the following data: 1 2 2 4 3 9 4 16 Of course this data could be read using the ImportFile command. However, you could also read it with the following Veusz script (which could be saved to a file and loaded with execfile or Load. The script also places symmetric errors of 0.1 on the x dataset. x = [] y = [] for line in open("in.dat"): parts = [float(i) for i in line.split()] x.append(parts[0]) y.append(parts[1]) SetData('x', x, symerr=0.1) SetData('y', y) Manipulating datasets Imported datasets can easily be modified in the Data Editor dialog box. This dialog box can also be used to create new datasets from scratch by typing them in. The Data Create dialog box is used to new datasets as a numerical sequence, parametrically or based on other datasets given expressions. If you want to plot a function of a dataset, you often do not have to create a new dataset. Veusz allows to enter expressions directly in many places. Using dataset plugins Dataset plugins can be used to perform arbitrary manipulation of datasets. Veusz includes several plugins for mathematical operation of data and other dataset manipulations, such as concatenation or splitting. If you wish to write your own plugins look at http://barmag.net/veusz-wiki/DatasetPlugins. Using expressions to create new datasets For instance, if the user has already imported dataset d, then they can create d2 which consists of d**2. Expressions are in Python numpy syntax and can include the usual mathematical functions. [createdata] Expressions for error bars can also be given. By appending _data, _serr, _perr or _nerr to the name of the dataset in the expression, the user can base their expression on particular parts of the given dataset (the main data, symmetric errors, positive errors or negative errors). Otherwise the program uses the same parts as is currently being specified. If a dataset name contains non alphanumeric characters, its name should be quoted in the expression in back-tick characters, e.g. `length (cm)`*2. The numpy functionality is particularly useful for doing more complicated expressions. For instance, a conditional expression can be written as where(x Use a dataset plugin. pluginname: name of plugin to use fields: dict of input values to plugin datasetnames: dict mapping old names to new names of datasets if they are renamed. The new name None means dataset is deleted EnableToolbar EnableToolbar(enable=True) Enable/disable the zooming toolbar in the plotwindow. This command is only supported in embedded mode or from veusz_listen. Export Export(filename, color=True, page=0 dpi=100, antialias=True, quality=85, backcolor='#ffffff00', pdfdpi=150, svgtextastext=False) Export the page given to the filename given. The filename must end with the correct extension to get the right sort of output file. Currrenly supported extensions are '.eps', '.pdf', '.svg', '.jpg', '.jpeg', '.bmp' and '.png'. If color is True, then the output is in colour, else greyscale. page is the page number of the document to export (starting from 0 for the first page!). dpi is the number of dots per inch for bitmap output files. antialias - antialiases output if True. quality is a quality parameter for jpeg output. backcolor is the background color for bitmap files, which is a name or a #RRGGBBAA value (red, green, blue, alpha). pdfdpi is the dpi to use when exporting EPS or PDF files. svgtextastext says whether to export SVG text as text, rather than curves. ForceUpdate ForceUpdate() Force the window to be updated to reflect the current state of the document. Often used when periodic updates have been disabled (see SetUpdateInterval). This command is only supported in embedded mode or from veusz_listen. Get Get('settingpath') Returns: The value of the setting given by the path. >>> Get('/page1/graph1/x/min') 'Auto' GetChildren GetChildren(where='.') Returns: The names of the widgets which are children of the path given GetClick GetClick() Waits for the user to click on a graph and returns the position of the click on appropriate axes. Command only works in embedded mode. Returns: A list containing tuples of the form (axispath, val) for each axis for which the click was in range. The value is the value on the axis for the click. GetData GetData(name) Returns: For a 1D dataset, a tuple containing the dataset with the name given. The value is (data, symerr, negerr, poserr), with each a numpy array of the same size or None. data are the values of the dataset, symerr are the symmetric errors (if set), negerr and poserr and negative and positive asymmetric errors (if set). If a text dataset, return a list of text elements. If the dataset is a date-time dataset, return a list of Python datetime objects. If the dataset is a 2D dataset return the tuple (data, rangex, rangey), where data is a 2D numpy array and rangex/y are tuples giving the range of the x and y coordinates of the data. data = GetData('x') SetData('x', data[0]*0.1, *data[1:]) GetDataType GetDataType(name) Get type of dataset with name given. Returns '1d' for a 1d dataset, '2d' for a 2d dataset, 'text' for a text dataset and 'datetime' for a datetime dataset. GetDatasets GetDatasets() Returns: The names of the datasets in the current document. GPL GPL() Print out the GNU Public Licence, which Veusz is licenced under. ImportFile ImportFile('filename', 'descriptor', linked=False, prefix='', suffix='', encoding='utf_8', renames={}) Imports data from a file. The arguments are the filename to load data from and the descriptor. The format of the descriptor is a list of variable names representing the columns of the data. For more information see Descriptors. If the linked parameter is set to True, if the document is saved, the data imported will not be saved with the document, but will be reread from the filename given the next time the document is opened. The linked parameter is optional. If prefix and/or suffix are set, then the prefix and suffix are added to each dataset name. If set, renames maps imported dataset names to final dataset names after import. Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset. Changed in version 0.5: A tuple is returned rather than just the number of imported variables. ImportFile2D ImportFile2D('filename', datasets, xrange=(a,b), yrange=(c,d), invertrows=True/ False, invertcols=True/False, transpose=True/False, prefix='', suffix='', linked=False, encoding='utf8', renames={}) Imports two-dimensional data from a file. The required arguments are the filename to load data from and the dataset name, or a list of names to use. filename is a string which contains the filename to use. datasets is either a string (for a single dataset), or a list of strings (for multiple datasets). The xrange parameter is a tuple which contains the range of the X-axis along the two-dimensional dataset, for example (-1., 1.) represents an inclusive range of -1 to 1. The yrange parameter specifies the range of the Y-axis similarly. If they are not specified, (0, N) is the default, where N is the number of datapoints along a particular axis. invertrows and invertcols if set to True, invert the rows and columns respectively after they are read by Veusz. transpose swaps the rows and columns. If prefix and/or suffix are set, they are prepended or appended to imported dataset names. If set, renames maps imported dataset names to final dataset names after import. If the linked parameter is True, then the datasets are linked to the imported file, and are not saved within a saved document. The file format this command accepts is a two-dimensional matrix of numbers, with the columns separated by spaces or tabs, and the rows separated by new lines. The X-coordinate is taken to be in the direction of the columns. Comments are supported (use "#", "!" or "%"), as are continuation characters (" \"). Separate datasets are deliminated by using blank lines. In addition to the matrix of numbers, the various optional parameters this command takes can also be specified in the data file. These commands should be given on separate lines before the matrix of numbers. They are: 1. xrange A B 2. yrange C D 3. invertrows 4. invertcols 5. transpose ImportFileCSV ImportFileCSV('filename', readrows=False, dsprefix='', dssuffix='', linked= False, encoding='utf_8', renames={}) This command imports data from a CSV format file. Data are read from the file using the dataset names given at the top of the files in columns. Please see the reading data section of this manual for more information. dsprefix is prepended to each dataset name and dssuffix is added (the prefix option is deprecated and also addeds an underscore to the dataset name). linked specifies whether the data will be linked to the file. renames, if set, provides new names for datasets after import. ImportFileHDF5 ImportFileHDF5(filename, items, namemap={}, slices={}, twodranges={}, twod_as_oned=set([]), convert_datetime={}, prefix='', suffix='', renames={}, linked=False) Import data from a HDF5 file. items is a list of groups and datasets which can be imported. If a group is imported, all child datasets are imported. namemap maps an input dataset to a veusz dataset name. Special suffixes can be used on the veusz dataset name to indicate that the dataset should be imported specially. 'foo (+)': import as +ve error for dataset foo 'foo (-)': import as -ve error for dataset foo 'foo (+-)': import as symmetric error for dataset foo slices is an optional dict specifying slices to be selected when importing. For each dataset to be sliced, provide a tuple of values, one for each dimension. The values should be a single integer to select that index, or a tuple (start, stop, step), where the entries are integers or None. twodranges is an optional dict giving data ranges for 2d datasets. It maps names to (minx, miny, maxx, maxy). twod_as_oned: optional set containing 2d datasets to attempt to read as 1d convert_datetime should be a dict mapping hdf name to specify date/time importing. For a 1d numeric dataset: if this is set to 'veusz', this is the number of seconds since 2009-01-01, if this is set to 'unix', this is the number of seconds since 1970-01-01. For a text dataset, this should give the format of the date/time, e.g. 'YYYY-MM-DD|T|hh:mm:ss' or 'iso' for iso format. renames is a dict mapping old to new dataset names, to be renamed after importing. linked specifies that the dataset is linked to the file. Attributes can be used in datasets to override defaults: 'vsz_name': set to override name for dataset in veusz 'vsz_slice': slice on importing (use format "start:stop:step,...") 'vsz_range': should be 4 item array to specify x and y ranges: [minx, miny, maxx, maxy] 'vsz_twod_as_oned': treat 2d dataset as 1d dataset with errors 'vsz_convert_datetime': treat as date/time, set to one of the values above. For compound datasets these attributes can be given on a per-column basis using attribute names vsz_attributename_columnname. Returns: list of imported datasets ImportFilePlugin ImportFilePlugin('pluginname', 'filename', **pluginargs, linked=False, encoding ='utf_8', prefix='', suffix='', renames={}) Import data from file using import plugin 'pluginname'. The arguments to the plugin are given, plus optionally a text encoding, and prefix and suffix to prepend or append to dataset names. renames, if set, provides new names for datasets after import. ImportFITSFile ImportFITSFile(datasetname, filename, hdu, datacol='A', symerrcol='B', poserrcol='C', negerrcol='D', linked=True/False, renames={}) This command does a simple import from a FITS file. The FITS format is used within the astronomical community to transport binary data. For a more powerful FITS interface, you can use PyFITS within your scripts. The datasetname is the name of the dataset to import, the filename is the name of the FITS file to import from. The hdu parameter specifies the HDU to import data from (numerical or a name). If the HDU specified is a primary HDU or image extension, then a two-dimensional dataset is loaded from the file. The optional parameters (other than linked) are ignored. Any WCS information within the HDU are used to provide a suitable xrange and yrange. If the HDU is a table, then the datacol parameter must be specified (and optionally symerrcol, poserrcol and negerrcol). The dataset is read in from the named column in the table. Any errors are read in from the other specified columns. If linked is True, then the dataset is not saved with a saved document, but is reread from the data file each time the document is loaded. renames, if set, provides new names for datasets after import. ImportString ImportString('descriptor', 'data') Like, ImportFile, but loads the data from the specfied string rather than a file. This allows data to be easily embedded within a document. The data string is usually a multi-line Python string. Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset. Changed in version 0.5: A tuple is returned rather than just the number of imported variables. ImportString('x y', ''' 1 2 2 5 3 10 ''') ImportString2D ImportString2D(datasets, string) Imports a two-dimensional dataset from the string given. This is similar to the ImportFile2D command, with the same dataset format within the string. This command, however, does not currently take any optional parameters. The various controlling parameters can be set within the string. See the ImportFile2D section for details. IsClosed IsClosed() Returns a boolean value telling the caller whether the plotting window has been closed. Note: this command is only supported in the embedding interface. List List(where='.') List the widgets which are contained within the widget with the path given, the type of widgets, and a brief description. Load Load('filename.vsz') Loads the veusz script file given. The script file can be any Python code. The code is executed using the Veusz interpreter. Note: this command is only supported at the command line and not in a script. Scripts may use the python execfile function instead. MoveToPage MoveToPage(pagenum) Updates window to show the page number given of the document. Note: this command is only supported in the embedding interface or veusz_listen. ReloadData ReloadData() Reload any datasets which have been linked to files. Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset. Rename Remove('widgetpath', 'newname') Rename the widget at the path given to a new name. This command does not move widgets. See To for a description of the path syntax. '.' can be used to select the current widget. Remove Remove('widgetpath') Remove the widget selected using the path. See To for a description of the path syntax. ResizeWindow ResizeWindow(width, height) Resizes window to be width by height pixels. Note: this command is only supported in the embedding interface or veusz_listen. Save Save('filename.vsz') Save the current document under the filename given. Set Set('settingpath', val) Set the setting given by the path to the value given. If the type of val is incorrect, an InvalidType exception is thrown. The path to the setting is the optional path to the widget the setting is contained within, an optional subsetting specifier, and the setting itself. Set('page1/graph1/x/min', -10.) SetAntiAliasing SetAntiAliasing(on) Enable or disable anti aliasing in the plot window, replotting the image. SetData SetData(name, val, symerr=None, negerr=None, poserr=None) Set the dataset name with the values given. If None is given for an item, it will be left blank. val is the actual data, symerr are the symmetric errors, negerr and poserr and the getative and positive asymmetric errors. The data can be given as lists or numpys. SetDataExpression SetDataExpression(name, val, symerr=None, negerr=None, poserr=None, linked= False, parametric=None) Create a new dataset based on the expressions given. The expressions are Python syntax expressions based on existing datasets. If linked is True, the dataset will change as the datasets in the expressions change. Parametric can be set to a tuple of (minval, maxval, numitems). t in the expression will iterate from minval to maxval in numitems values. SetDataRange SetDataRange(name, numsteps, val, symerr=None, negerr=None, poserr=None, linked =False) Set dataset to be a range of values with numsteps steps. val is tuple made up of (minimum value, maximum value). symerr, negerr and poserr are optional tuples for the error bars. If linked is True, the dataset can be saved in a document as a SetDataRange, otherwise it is expanded to the values which would make it up. SetData2D SetData2D('name', val, xrange=(A,B), yrange=(C,D), xgrid=[1,2,3...], ygrid= [4,5,6...]) Creates a two-dimensional dataset with the name given. val is either a two-dimensional numpy array, or is a list of lists, with each list in the list representing a row. Do not give xrange if xgrid is set and do not give yrange if ygrid is set, and vice versa. xrange and yrange are optional tuples giving the inclusive range of the X and Y coordinates of the data. xgrid and ygrid are optional lists, tuples or arrays which give the coordinates of the edges of the pixels. There should be one more item in each array than pixels. SetData2DExpression SetData2DExpression('name', expr, linked=False) Create a 2D dataset based on expressions. name is the new dataset name expr is an expression which should return a 2D array linked specifies whether to permanently link the dataset to the expressions. SetData2DExpressionXYZ SetData2DExpressionXYZ('name', 'xexpr', 'yexpr', 'zexpr', linked=False) Create a 2D dataset based on three 1D expressions. The x, y expressions need to evaluate to a grid of x, y points, with the z expression as the 2D value at that point. Currently only linear fixed grids are supported. This function is intended to convert calculations or measurements at fixed points into a 2D dataset easily. Missing values are filled with NaN. SetData2DXYFunc SetData2DXYFunc('name', xstep, ystep, 'expr', linked=False) Construct a 2D dataset using a mathematical expression of "x" and "y". The x values are specified as (min, max, step) in xstep as a tuple, the y values similarly. If linked remains as False, then a real 2D dataset is created, where values can be modified and the data are stored in the saved file. SetDataDateTime SetDataDateTime('name', vals) Creates a datetime dataset of name given. vals is a list of Python datetime objects. SetDataText SetDataText(name, val) Set the text dataset name with the values given. val must be a type that can be converted into a Python list. SetDataText('mylabel', ['oranges', 'apples', 'pears', 'spam']) SetToReference SetToReference(setting, refval) Set setting to match other setting refval always.. SetUpdateInterval SetUpdateInterval(interval) Tells window to update every interval milliseconds at most. The value 0 disables updates until this function is called with a non-zero. The value -1 tells Veusz to update the window every time the document has changed. This will make things slow if repeated changes are made to the document. Disabling updates and using the ForceUpdate command will allow the user to control updates directly. Note: this command is only supported in the embedding interface or veusz_listen. SetVerbose SetVerbose(v=True) If v is True, then extra information is printed out by commands. StartSecondView StartSecondView(name = 'window title') In the embedding interface, this method will open a new Embedding interface onto the same document, returning the object. This new window provides a second view onto the document. It can, for instance, show a different page to the primary view. name is a window title for the new window. Note: this command is only supported in the embedding interface. TagDatasets TagDatasets('tag', ['ds1', 'ds2'...]) Adds the tag to the list of datasets given.. To To('widgetpath') The To command takes a path to a widget and moves to that widget. For example, this may be "/", the root widget, "graph1", "/page1/graph1/x", "../x". The syntax is designed to mimic Unix paths for files. "/" represents the base widget (where the pages reside), and ".." represents the widget next up the tree. Quit Quit() Quits Veusz. This is only supported in veusz_listen. WaitForClose WaitForClose() Wait until the plotting window has been closed. Note: this command is only supported in the embedding interface. Zoom Zoom(factor) Sets the plot zoom factor, relative to a 1:1 scaling. factor can also be "width", "height" or "page", to zoom to the page width, height or page, respectively. This is only supported in embedded mode or veusz_listen. Security With the 1.0 release of Veusz, input scripts and expressions are checked for possible security risks. Only a limited subset of Python functionality is allowed, or a dialog box is opened allowing the user to cancel the operation. Specifically you cannot import modules, get attributes of Python objects, access globals() or locals() or do any sort of file reading or manipulation. Basically anything which might break in Veusz or modify a system is not supported. In addition internal Veusz functions which can modify a system are also warned against, specifically Print(), Save() and Export(). If you are running your own scripts and do not want to be bothered by these dialogs, you can run veusz with the --unsafe-mode option. Chapter 4. Using Veusz from other programs Table of Contents Non-Qt Python programs Older path-based interface New-style object interface Translating old to new style PyQt4 programs Non Python programs C, C++ and Fortran Non-Qt Python programs Veusz can be used as a Python module for plotting data. There are two ways to use the module: (1) with an older path-based Veusz commands, used in Veusz saved document files or (2) using an object-oriented interface. With the old style method the user uses a unix-path inspired API to navigate the widget tree and add or manipulate widgets. With the new style interface, the user navigates the tree with attributes of the Root object to access Nodes. The new interface is likely to be easier to use unless you are directly translating saved files. Older path-based interface """An example embedding program. Veusz needs to be installed into the Python path for this to work (use setup.py) This animates a sin plot, then finishes """ import time import numpy import veusz.embed as veusz # construct a Veusz embedded window # many of these can be opened at any time g = veusz.Embedded('window title') g.EnableToolbar() # construct the plot g.To( g.Add('page') ) g.To( g.Add('graph') ) g.Add('xy', marker='tiehorz', MarkerFill__color='green') # this stops intelligent axis extending g.Set('x/autoExtend', False) g.Set('x/autoExtendZero', False) # zoom out g.Zoom(0.8) # loop, changing the values of the x and y datasets for i in range(10): x = numpy.arange(0+i/2., 7.+i/2., 0.05) y = numpy.sin(x) g.SetData('x', x) g.SetData('y', y) # wait to animate the graph time.sleep(2) # let the user see the final result print "Waiting for 10 seconds" time.sleep(10) print "Done!" # close the window (this is not strictly necessary) g.Close() The embed interface has the methods listed in the command line interface listed in the Veusz manual http://home.gna.org/veusz/docs/manual.html Multiple Windows are supported by creating more than one Embedded object. Other useful methods include: • WaitForClose() - wait until window has closed • GetClick() - return a list of (axis, value) tuples where the user clicks on a graph • ResizeWndow(width, height) - resize window to be width x height pixels • SetUpdateInterval(interval) - set update interval in ms or 0 to disable • MoveToPage(page) - display page given (starting from 1) • IsClosed() - has the page been closed • Zoom(factor) - set zoom level (float) or 'page', 'width', 'height' • Close() - close window • SetAntiAliasing(enable) - enable or disable antialiasing • EnableToolbar(enable=True) - enable plot toolbar • StartSecondView(name='Veusz') - start a second view onto the document of the current Embedded object. Returns a new Embedded object. New-style object interface In versions of Veusz >1.8 is a new style of object interface, which makes it easier to construct the widget tree. Each widget, group of settings or setting is stored as a Node object, or its subclass, in a tree. The root document widget can be accessed with the Root object. The dot operator "." finds children inside other nodes. In Veusz some widgets can contain other widgets (Root, pages, graphs, grids). Widgets contain setting nodes, accessed as attributes. Widgets can also contain groups of settings, again accessed as attributes. An example tree for a document (not complete) might look like this Root \-- page1 (page widget) \-- graph1 (graph widget) \-- x (axis widget) \-- y (axis widget) \-- function (function widget) \-- grid1 (grid widget) \-- graph2 (graph widget) \-- xy1 (xy widget) \-- xData (setting) \-- yData (setting) \-- PlotLine (setting group) \-- width (setting) ... ... \-- x (axis widget) \-- y (axis widget) \-- graph3 (graph widget) \-- contour1 (contour widget) \-- x (axis widget) \-- y (axis widget) Here the user could access the xData setting node of the xy1 widget using Root.page1.graph2.xy1.xData. To actually read or modify the value of a setting, you should get or set the val property of the setting node. The line width could be changed like this graph = embed.Root.page1.graph2 graph.xy1.PlotLine.width.val = '2pt' For instance, this constructs a simple x-squared plot which changes to x-cubed: import veusz.embed as veusz import time # open a new window and return a new Embedded object embed = veusz.Embedded('window title') # make a new page, but adding a page widget to the root widget page = embed.Root.Add('page') # add a new graph widget to the page graph = page.Add('graph') # add a function widget to the graph. The Add() method can take a list of settings # to set after widget creation. Here, "function='x**2'" is equivalent to # function.function.val = 'x**2' function = graph.Add('function', function='x**2') time.sleep(2) function.function.val = 'x**3' # this is the same if the widgets have the default names Root.page1.graph1.function1.function.val = 'x**3' If the document contains a page called "page1" then Root.page1 is the object representing the page. Similarly, Root.page1.graph1 is a graph called graph1 in the page. You can also use dictionary-style indexing to get child widgets, e.g. Root['page1']['graph1']. This style is easier to use if the names of widgets contain spaces or if widget names shadow methods or properties of the Node object, i.e. if you do not control the widget names. Widget nodes can contain as children other widgets, groups of settings, or settings. Groups of settings can contain child settings. Settings cannot contain other nodes. Here are the useful operations of Nodes: class Node(object): """properties: path - return path to object in document, e.g. /page1/graph1/function1 type - type of node: "widget", "settinggroup" or "setting" name - name of this node, e.g. "graph1" children - a generator to return all the child Nodes of this Node, e.g. for c in Root.children: print c.path children_widgets - generator to return child widget Nodes of this Node children_settinggroups - generator for child setting groups of this Node children_settings - a generator to get the child settings childnames - return a list of the names of the children of this Node childnames_widgets - return a list of the names of the child widgets childnames_settinggroups - return a list of the names of the setting groups childnames_settings - return a list of the names of the settings parent - return the Node corresponding to the parent widget of this Node __getattr__ - get a child Node with name given, e.g. Root.page1 __getitem__ - get a child Node with name given, e.g. Root['page1'] """ def fromPath(self, path): """Returns a new Node corresponding to the path given, e.g. '/page1/graph1'""" class SettingNode(Node): """A node which corresponds to a setting. Extra properties: val - get or set the setting value corresponding to this value, e.g. Root.page1.graph1.leftMargin.val = '2cm' """ class SettingGroupNode(Node): """A node corresponding to a setting group. No extra properties.""" class WidgetNode(Node): """A node corresponding to a widget. property: widgettype - get Veusz type of widget Methods are below.""" def WalkWidgets(self, widgettype=None): """Generator to walk widget tree and get widgets below this WidgetNode of type given. widgettype is a Veusz widget type name or None to get all widgets.""" def Add(self, widgettype, *args, **args_opt): """Add a widget of the type given, returning the Node instance. """ def Rename(self, newname): """Renames widget to name given. Existing Nodes corresponding to children are no longer valid.""" def Action(self, action): """Applies action on widget.""" def Remove(self): """Removes a widget and its children. Existing Nodes corresponding to children are no longer valid.""" Note that Nodes are temporary objects which are created on the fly. A real widget in Veusz can have several different WidgetNode objects. The operators == and != can test whether a Node points to the same widget, setting or setting group. Here is an example to set all functions in the document to be x**2: for n in Root.WalkWidgets(widgettype='function'): n.function.val = 'x**2' Translating old to new style Here is an example how you might translate the old to new style interface (this is taken from the sin.vsz example). # old (from saved document file) Add('page', name='page1') To('page1') Add('graph', name='graph1', autoadd=False) To('graph1') Add('axis', name='x') To('x') Set('label', '\\italic{x}') To('..') Add('axis', name='y') To('y') Set('label', 'sin \\italic{x}') Set('direction', 'vertical') To('..') Add('xy', name='xy1') To('xy1') Set('MarkerFill/color', 'cyan') To('..') Add('function', name='function1') To('function1') Set('function', 'sin(x)') Set('Line/color', 'red') To('..') To('..') To('..') # new (in python) import veusz.embed embed = veusz.embed.Embedded('window title') page = embed.Root.Add('page') # note: autoAdd=False stops graph automatically adding own axes (used in saved files) graph = page.Add('graph', autoadd=False) x = graph.Add('axis', name='x') x.label.val = '\\italic{x}' y = graph.Add('axis', name='y') y.direction.val = 'vertical' xy = graph.Add('xy') xy.MarkerFill.color.val = 'cyan' func = graph.Add('function') func.function.val = 'sin(x)' func.Line.color.val = 'red' PyQt4 programs There is no direct PyQt4 interface. The standard embedding interface should work, however. Non Python programs Support for non Python programs is available in a limited form. External programs may execute the veusz_listen executable or veusz_listen.py Python module. Veusz will read its input from the standard input, and write output to standard output. This is a full Python execution environment, and supports all the scripting commands mentioned in Commands, a Quit() command, the EnableToolbar() and the Zoom(factor) command listed above. Only one window is supported at once, but many veusz_listen programs may be started. veusz_listen may be used from the shell command line by doing something like: veusz_listen < in.vsz where in.vsz contains: To(Add('page') ) To(Add('graph') ) SetData('x', arange(20)) SetData('y', arange(20)**2) Add('xy') Zoom(0.5) Export("foo.eps") Quit() A program may interface with Veusz in this way by using the popen C Unix function, which allows a program to be started having control of its standard input and output. Veusz can then be controlled by writing commands to an input pipe. C, C++ and Fortran A callable library interface to Veusz is on my todo list for C, C++ and Fortran. Please tell me if you would be interested in this option. veusz-1.21.1/Documents/config.xsl0000664000175000017500000000113212260623255015070 0ustar jssjss veusz-1.21.1/Documents/veusz.pod0000664000175000017500000000512612263564313014764 0ustar jssjss=head1 NAME Veusz - a scientific plotting and graphing application. =head1 SYNOPSIS veusz [I] [F]... =head1 DESCRIPTION B is a scientific plotting and graphing package. It is designed to create publication-ready output in a variety of different output formats. Graphs are built-up combining plotting widgets. Veusz has a GUI user interface (started with the C command), a Python module interface and a scripting interface. If started without command line arguments, B will open up with a new empty document. The program will otherwise open the listed documents. =head1 OPTIONS =over 8 =item B<--unsafe-mode> Do not check opened scripts for the presence of unsafe Python commands. This allows you to create or open complete Python scripts with Veusz commands if they come from a trusted source. =item B<--listen> Read Veusz commands from stdin, executing them, then writing the results to stdout. This option is intended to replace the veusz_listen standalone program. In this mode Veusz does not read any input documents, but will use the first argument to the program as the window title, if given. =item B<--quiet> If in listening mode, do not open a window before running commands, but execute them quietly. =item B<--export>=I Export the next Veusz document file on the command line to the graphics file I. Supported file types include EPS, PDF, SVG, PNG, BMP, JPG and XPM. The extension of the output file is used to determine the output file format. There should be as many export options specified as input Veusz documents on the command line. =item B<--plugin>=I Loads the Veusz plugin I when starting Veusz. This option provides a per-session alternative to adding the plugin in the preferences dialog box. =item B<--help> Displays the options to the program and exits. =item B<--version> Displays information about the currently installed version and exits. =back =head1 BUGS Please report bugs at https://gna.org/bugs/?group=veusz =head1 AUTHORS B was written by Jeremy Sanders . This manual page was written by Jeremy Sanders . =head1 COPYRIGHT Copyright (C) 2003-2014 Jeremy Sanders . 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, or (at your option) any later version. On Debian GNU/Linux systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL'. =cut veusz-1.21.1/Documents/Interface.txt0000644000175000017500000000116411662000553015532 0ustar jssjssJust some notes on the interface design... ------------------------------------------ AddGraph('function', 'x**2', name='fn') AddGraph('xy', 'x', 'y', name='data') (alias AG) AddContainer('grid', columns=3) (alias AC) List() - list graphs in current container (alias L) Save('filename.rap') Load('filename.rap') WriteFile('eps', 'filename.eps') (alias WF) Change('fn') (alias C) GraphUp() (alias U) ReadFile('descriptor', 'filename.dat') (alias RF) SetData('x', numarray, symerr=var1, negerr=var2, poserr=var2) Attribute('AxisLabel.text', 'axis') (alias A) AttributeAxis('x', 'AxisLabel.text', 'axis') (alias AA) veusz-1.21.1/Documents/document_api.py0000644000175000017500000001074312327177747016140 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """ Document veusz widget types and settings Creates an xml file designed to be processed into a web page using xsl """ from __future__ import division, print_function import re import veusz.widgets as widgets import veusz.document as document import veusz.setting as setting #import cElementTree as ET #import elementtree.ElementTree as ET import xml.etree.ElementTree as ET def processSetting(parent, setn): """Convert setting to xml element.""" setnxml = ET.SubElement(parent, "setting") ET.SubElement(setnxml, "apiname").text = setn.name ET.SubElement(setnxml, "displayname").text = setn.usertext ET.SubElement(setnxml, "description").text = setn.descr ET.SubElement(setnxml, "formatting").text = str(setn.formatting) typename = str(type(setn)) typename = re.match(r"^$", typename).group(1) typename = typename.split('.')[-1] ET.SubElement(setnxml, "type").text = typename # show list of possible choices if there is one if isinstance(setn, setting.Choice): for choice in setn.vallist: ET.SubElement(setnxml, "choice").text = choice if not isinstance(setn.default, setting.ReferenceBase): ET.SubElement(setnxml, "default").text = setn.toText() else: ET.SubElement(setnxml, "default").text = "to reference" def processSettings(parent, setns): """Convert setting to xml element.""" setnsxml = ET.SubElement(parent, "settings") ET.SubElement(setnsxml, "apiname").text = setns.name ET.SubElement(setnsxml, "displayname").text = setns.usertext ET.SubElement(setnsxml, "description").text = setns.descr for s in setns.getSettingList(): processSetting(setnsxml, s) def processWidgetType(root, name): """Produce documentation for a widget type.""" widgetxml = ET.SubElement(root, "widget") klass = document.thefactory.getWidgetClass(name) print(klass) ET.SubElement(widgetxml, "apiname").text = name try: ET.SubElement(widgetxml, "description").text = klass.description except AttributeError: pass for parent in [k for k in klass.allowedParentTypes() if k is not None]: ET.SubElement(widgetxml, "allowedparent").text = parent.typename ET.SubElement(widgetxml, "usercreation").text = str(klass.allowusercreation) thesettings = setting.Settings('') klass.addSettings(thesettings) #for s in thesettings.getSettingList(): processSettings(widgetxml, thesettings) for s in thesettings.getSettingsList(): processSettings(widgetxml, s) def indent(elem, level=0): """Indent output, from elementtree manual.""" i = "\n" + level*" " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: indent(elem, level+1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def addXSL(filename): f = open(filename) l = f.readlines() f.close() l.insert(1, '\n') f = open(filename, 'w') f.writelines(l) f.close() def main(): widgettypes = document.thefactory.listWidgets() root = ET.Element("widgets") for wt in widgettypes: processWidgetType(root, wt) tree = ET.ElementTree(root) indent(root) tree.write('widget_doc.xml', encoding="utf8") addXSL('widget_doc.xml') if __name__ == '__main__': main() veusz-1.21.1/Documents/manual.pdf0000644000175000017500000144303512275421434015057 0ustar jssjss%PDF-1.4 % 4 0 obj << /Creator (Apache FOP Version 1.1) /Producer (Apache FOP Version 1.1) /CreationDate (D:20140208132908+01'00') >> endobj 5 0 obj << /N 3 /Length 11 0 R /Filter /FlateDecode >> stream xwXSsN`$!l{@ ٢ $ @TR)XZԉ( RZD|yL0V@(#q `=nnWXX0+Зȕ;ѫR1{Ol(Lγx\䜙/V'LKP0RX~@9k(8u?̰yBOΑr y <)_Έ"<?_l) F+s9H MI#~__ Q$. R$ sŅg%f,a6GTLΟEQԖ!/Bſ)EogEA?lkJ^-ؒ\?l{P&d\EAt{6~/ǑfJq2bFn6g0<8aO"yD|TyEne~J0#" ՄJB+,>H4$É*b%Oπ/7ʷ__ߤRS耠ú-D ZT(2X r|P-AYXN"P$.8UwdY((iTOrtBtsYmKqZqxl|t|SB߅;$8&'\d`хZ3OTN$IB'$Hz 4pk'lNS'o;o/珦nKOHHՂu3B3e|ʌlI£"(CԛU5m]-qّ3)7Br$4T\N:77&M~tQAe%~K^Z]ڳ`eC˽ׯV$Yih*UWVgaݚ5/Ƭ.-ZU4]KRzuP6Xc b]iEMMOS69ނ"rsKʇl^*fW$UWvUWmz_V}ƻVvc]]Ww{nӭ+{Gv}GiC^޼f}ܤTahlf:ZiWZFo+=I=6ۛaivj{IQ1ٙ)<:hO[w6;fpE'>\rrTө{{=s7wlΝ;y ^d]t߱:.;_r{pWO_vzK7u[ۼcw2> stream en application/pdf 2014-02-08T13:29:09+01:00 1.4 Apache FOP Version 1.1 2014-02-08T13:29:08+01:00 Apache FOP Version 1.1 2014-02-08T13:29:09+01:00 endstream endobj 10 0 obj << /Length 13 0 R /Filter /FlateDecode >> stream x}?O0w7Ps>9gE*C 1DOA2 ~ݽ;*WpFPVm3hZWgY_W.Щ,剸kϨ5%"Fs*7O} OǷ /^;bI4|_yZzc^f1> endobj 11 0 obj 2592 endobj 12 0 obj 858 endobj 13 0 obj 210 endobj 15 0 obj << /URI (http://www.gnu.org/licenses/gpl-2.0.html) /S /URI >> endobj 16 0 obj << /Type /Annot /Subtype /Link /Rect [ 504.155 712.209 523.275 719.409 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 15 0 R /H /I >> endobj 18 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 702.609 185.328 709.809 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 15 0 R /H /I >> endobj 19 0 obj << /Length 20 0 R /Filter /FlateDecode >> stream xTMo0 WBIb V ]fQlcHmgAGG)J=A#(~$ ~|)hI=q !WQ2P2P1l^w dgë
RR8J$@736d|Ӭ"JN~)#G|:,!MPomD. gDz.A"b]'NA'jAfwele`7dgWA|bbOwBaKj=)wܼrֶ:%gXj2w%ia4Luw^F_AHt~O;OL YWRjUDzM8u endstream endobj 17 0 obj [ 16 0 R 18 0 R ] endobj 14 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 17 0 R /Contents 19 0 R >> endobj 20 0 obj 533 endobj 22 0 obj << /Length 23 0 R /Filter /FlateDecode >> stream x]sݿ㧻Njգ&;$7ډҞ>lo~}@Ї3  }X7gDvtbN $ބEIo؉IV5~Fs?WUȾWS=_0׬>]}d M0~}*+9ᄳ, B<&^q{8`40jhEGT_6).qFr;)d{w8Ȣ(ϳ<`_Ow_߯_rF3W\V57J! fa)*͑{Ѳ(`wu6P Sdl6Y&6-͢!m}#o{a΂B]!E˽,,\5}/#D` wZNjQZBXp',(xwY2tbO&^zQwrWySvywgm,p"[noYfsmɴ^} hy[/%}Q⅟p^!a?:[bAtu5sҢ֟b5acw/[0UMEKҀ8;dт{nA@A SD(uS5#oQjb7qҬ'Ak'^T7i0aWSpI>Q-9vFν>V9tf#+1daknBQA?鸈b14”- lQg5;WX= A_C!{H6)>ש=P'`J,Hh`Yh2^T!~AOOܴ#Y qITRp1o)uxM׹(WIoCϞet<9UA_fS}]8 -DXB L 1ɛP ( @f`*-(fkYB̜j';CaKq-hk ﱛ_CⷓMylqU ɛn-F1dDeok{fcUsޏX0[MF"mD$ `9iiȏC=W-74ſ_ Ɩ%I6CqE$“İQHƍEymjn@>#A;D9KgՁ%if;Wm!ΊpiS;`YHxI/_2N~y9Vٴ:27Lނd==4BwjW.GPwo*O˵!2q* qۺ\PoɲxEY WCzEUpÁkTATbTƇۤ< ^)(9nʊUQujR?r.[})k@=WV xcKAw#jW\H[, x5s[I{}0ڍ'P4jGV5hB5DGo3V84y(# Ck H$@dqՍ7Ă1ͫ~(X]TC/[->1!Q]vb֊]8& 5S:^p uFQ'*p=_TxyΩm lZ~E|^ٔ ^k>o,Ԋs X%B"P(C-v6Vc8=W&;7b"%~ʥ[`D(wf8@<mMQRN.tZ(6z$DOP<|۫'^Zn8J((~oTi.MM+u/ n F)1no+87],بfhAƺtc!Anjt#vͨSq Gr1˨,.F7.=<9&FwTj[",ԔY-u@טGe`{#L]a 6 mhg7mIrO@+ R(:zY&'ƤzwBqbxÔ]S^Ll.AB< Ƚ]PDOZaw.1˙Nqx繬 s~Mt:1ǗssO=4Gn[kTJnT' ~hzPL-hOy\q&H)`-3ESptl߆b}L\VIM`d;1Njcb?RծUj1B#΃l KBI 7EmX:Dނ#ctWtéa>A Ї0o7 a`8`GdXj_2-~E&"9"[2L#bA[os}ǝ P- LMJ[â`UU=PTHٷ--E> a?|jn endstream endobj 21 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 22 0 R >> endobj 23 0 obj 2935 endobj 25 0 obj << /Length 26 0 R /Filter /FlateDecode >> stream xZK# ϯ`tZm]MMVJMICݚ ZkruH  Z_*Fj6NV_?Vˉ.5OdS5%*5kr7FOC˯'S){HGQ~"t@Ojçs5N._O4*ND=ǧmmU~U=oWiU9=Ye]5'dwۅG )㚮ЅO6Yl*zyߍrn9Iz\)Y]y[,ˍL^לyB+y|({f}Dlu.ப#8U^ t,#tsԺlrݚμ۪3[^vwzyESuw117$JOҎ njܖFWΫm{(v؃~wlM+[Ͱ+IVn-b~S 1[]Wm~KUpom6$S8x5ˣU:o*uЍj L+9ޘQ̔# t~e F eK7u(t0_tTƉ }QkCU߫@JPn?uyy?ڨTmQצlվǃm֣*`4@a2n Tl0S6]9an -̕yQԫWq:h^ԙI:C umՔwX`1vah1g 9,i3j/G+)ڃ۩86LA C\Y[=޵};f="S)Ң>K̓d@p+"6zolԱ $D7QBI <OO&vԢ8 psUTeV7Ͱg?f+!Q sC6P& z9 ڈaثdfw;!roV1ռP]~Dz}ZBn&FN:{ԑY&9ˆͥI I PVB¶=ZVNOK1{!h?"HXɆ1p . ىE6t3(%CJ*T2P/"B_&=.bZ8.JUƋOdrsYϭr|7bg6Kf|ԑEpbb{;+9T@< ?P!FOY*.|y$IAnVB۔cHb$!5p6r \"!\z7gI/t>筫L]ʊ )vX/"WMW:o FJs*NP2l}axBȽHW.齿2n_EW6-G1O=FHBDt0ypx!Wk+H\s Lt7Qw6wmUVk ƖӇ[u0'he_42J/ҞQoNb8O)N1xGa\exG,]B_N.U1 fngLT{]WF#UKsDhNECZer $NM[X+rR-gywvRԻmnr{M9hly.Ԡ :fkwN5 0xӳe ߼ol{&S<Z>:rc2G z ?{@t[¢)->Yܟ J,З:" ܟYu]<[ Smn1PbږRWRHY\ bY®w $gj>Ij~]z曧;t :}J"_|19:SZV%gO2B^!z=!'+@R쵧գFt~ye=JLҠLjc*隃{%\s,Nhr"5MB! ];B0%dݣ7E TWa۩K6ҵ8Ǣvu=J%Z ''̹B>9=) CO dEuu4 f/xAڝ'a|3;*GgL҉.hS7,1UٙNѥǐ= wn_+cN֟GƔ0(+~{2Lĺio 3!B1EĮW]*F |_t֋Ψq :EUǖ~ dmlMsj+4ёa:6gB(.RQsj-*`6q5!ܴ x5"f[BEt!N4m$8Zgmdܣ02ѮiZ[oSxF[_ L"k=e.kg\mtw}Ŭ,C21cρK.h䟑 =)JOޭC@ VF ʪ.tnMBB m0@ܨzjy Ĺ@Ȧ櫩p+tPFe|ʺg+˜Q$u$7CAm8*α!uM&zş؈B+B?Oyk81h_!'^~dFKUTȗ:kM\o)JnlfF\_A7rHVo_ozޚfvDƈŏcᣠ,K‡_b;]I~+\{CR;2]< Ɠ֢{X+|M` endstream endobj 24 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 25 0 R >> endobj 26 0 obj 3333 endobj 28 0 obj << /Length 29 0 R /Filter /FlateDecode >> stream xZK۸ϯ@dWfh>E)dr.)*a$)Bȿ> 4@4lh4|}X:zÊW,?~!;5ҪnhϨK_ę+p_Ց/5VbZO",ӻBAU? d3>5ͳ''wՃ˱(@XPwG6q8$Zۂeiǒ8O74BpG9 Xڱ(XA@CgD$RuG,$ A1͈ɛU]_eq^zA!W a45Lj=sO&ROLvc`#MܛKU QZ^Ё}o03=BD-N>M|-'Npk^B 8T"!C%BUpoLLwN! Y4 +Y_<='U 2\ $CO,y YIdǐN`j1J/uD+0t}b0FvJ֛h5]w9w*4{/cRl=-IZE\R8:4's^js!7Z߂Kܓ{Oק+_4YwI[ÁUE_sK¶.Bǝ/IMNp+ x"Fe}? fgY_fyS,^Y['t<{҅WOڝ+ ;lRpJS1f*o} 62(pvP1h!FfϦ+*E=Ifu,] lFLt;d\eߐgY}Z1 (ñB,V!sBj~jwU[vj<{eKٳNf%vO"cPfOQnP^sW9?;'MWA|W$N$]Z[$qpxH| kQid,Uww`Yý4$$$Z6v^~ R\g<43{o91mɭ,n񛣼ҧ2AzYHE$"LlqC3XfmJd%IOGaHT҅@*Di2t:!7n:\r`Xm Nԙ-G8Aa,/?y8vG lG~|Z^#ST5@r/ cـ/GuLU2GzI8%O7^ճ =1JFF}z)JF endstream endobj 27 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 28 0 R >> endobj 29 0 obj 2930 endobj 31 0 obj << /Length 32 0 R /Filter /FlateDecode >> stream xZYܶ~_*W.c.Mĉ/EljNaH JcgW[@h3%=,8FWqg6 `ZS(\&(Jx& |j )$(կA"*й"OI(8\}y{UGީXϒ(V`I&%m5Ř_7467JD59)'f #4"*Id~'fF8^I" #?ysBcpld#:׹}2]6Mn86Y­guˏ'373SI\ȃM|aאuC[AVD=4o5ej vTֶu guڲL_MgȮiy7~n\=2$tdC @="`uS (`!REkkgcԳ1ş$$LBOXFʥĠma,;v);NH?v; ;FG`ꛊBpsMMj!-x+iG'4 O[΍ĖЌڒ ~hזFwIэg+]? %BKc3NVNZԐq XGF;ԽK\|}dۦYK'X]wn[i1`8J#X<-6x^7x 8N\oQ e8̴Z?V-]aW)*#ė|WGA׼n{Q6G5ª#m |#cfz~Ʋ#E?xNJOC BDZ8T^iPxgҀuD ϶-A^%%,nN#zX$g^9*B;x3#ذTM]Խ(/S-c^*.Sͺ9Q3Q{^; ,=tH>>1xzg\%R3G83SGޛ,y҃ևkqM4nׄG_-L_ *גxByF[4VCmީٯ[#fu撝kбbY58d۱k큎&DkQac m/ (S - 23u@A`1+)slq9?\*jmdbʠGƌ+:L33Au`EV(G-$D*tx;NPIiC8v]S,pVgi NvqI% THܛ4-,u#mI^˸VG*nt{7^ 8,dv%]ߐ}bR\Ip0˚"cɩE܃X @ {X'MXp//r QA^ά*J"8+<$&r@F/*SZeWU^ͦ׉r>Pf~'ŤW2}vԉa\lt8ڪvC,$X{$H_ ڏ%!0hHݘ}oQH=as@7:Ә:#(A-M*\uHJ܄K TZGq,ObBT'ce >eaoXмxփ$ |ON9^Ͼݸosl҉^@d0L0avr.6g\6SGɶJ_NN4#GɄ\RkH.ii3՟_x=vK!BJ?k&,!Kssswh ]&PWQ^lV^zCP)WMzA8 *}J^šʹB8;(|2U|:-WF侸4E="X\esbݐ&dH2GWiϽ.-!*:ѹYQw77H@!9ڶ`Lncݻc3$(QݐO1YI I hמt lo W+ ۍw_E!l(Vd?C;?rJVa.WF? yٞc<Ҩ;Ae{PGBlҼͻaX0cܳDtF o1kv}SB>x(0Rrw,ҹ*^%˖69P(+- qTQ#7+]/|x ~t/t:HZw|z- 9@ɺ<9p.{+'&OxuL@fp$50؃SP𤕺A42_d=+GM^Y"ǢAie~>,_ᐓŎIʑSDPg\-Ճb]{ 4Y\Jzu>("dw &G%H:+Ԥ2#ȐEٶs1Fi0T*ŵ'\Ӧ0VԈe 1Í4=i\ Vgզ7LmwYAe:zAzuuZy/) }VpriMfvDu!)yzIgVGѯn/M`ԥ k[ZdwOlDz;-<jKG*̕x$uoHg6F$R/MC\qق4*2`-&|B<dOƼ{QaAʹ9Sn;}%3d:AQ?4șs6sFΈw dH_$7Q2v5h 䛫9 endstream endobj 30 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 31 0 R >> endobj 32 0 obj 3345 endobj 34 0 obj << /Name /Im1 /Type /XObject /Length 36 0 R /Filter /FlateDecode /Subtype /Image /Width 961 /Height 700 /BitsPerComponent 8 /ColorSpace /DeviceGray >> stream x|JȞP Q@Yp3d)l A! ^eYh3KJQ@&<߯ɯ%͵I.Jmޡ#ڰAlo:ݛ/`Ϻ-sSNzCe>:~d :y1l;]#2VlO:;& AqtΟ]9op77sJPPps`]d ֑On1pi.AEl}uɍ<51/{Ϡx^Z֯^; 0m< z8x2)LL'̙;‹Evlu[TyC˫'\gmEG+cuˑg m3'-+.LԢ~Isw>a{F_ّtiH ͠k|LXKrQ[qPag,[S6T]060°?Kg*4~mGͭz_Q{K+Ͻح9?a() ? .2q:MI^Z_g9,ϠbͩaDK;r>x"Q;>?Zޢϝ\>8;{oQi,ƭ|?QKu]?zS<^ٷ[*aw9B맲Cdl7\N8QUyyGN_֭o-T5>jtY 1d KZ9~ͺ$É?$@A;/{ ]&ƕN˝忺nn87[W8n߇GU"ݼАķZqӣoMHX<~1'kn|;%1,; iŐ_NLTwXb}Gy/.34L)s#XoU&Y.WĨ /NT\bnמ?֣| ;uzw7?NgjK{oR֙Ǎ jzUZRgdNŨEin`ܘK[PބDlrZ-IͩVҭ=sd X槰dmUqzc!fG5m5|XE'ZO Te`1:fw '~>u{i 0[*`5T7S?}:VѬW۪%ѫrE=m;֛5y*fBokL*DEy2(bn<8^J'e(7U\Nx!̕Tra~ƚK9} s/&0p~.v̦pfN,=f։:]X37-Jֱ^'^o?O0q[4_2eQ*\;E~.Qp4~/:uowZ !{A|B_u̘%}gׯhDYtЫcsOzq}Xِͥ|8:pe}|I47GW^[O^;lI??x[T/p.٫yEmۮMzӺv4er46^eXtQa<__Y25Uz]j^yl< ק:`y_3`^gU+/V2S/OWJ鎀WO8e1T=Il<՘Wjp0N7~1%Tnz/tѧ?'k'|/gR~Reƫy)nIW0F|a䙼wE~jt̀tTO;賂O5Zozk*Eyxip厀g>>߻;ۥ"Fx *;^1U),{|ͭ)G V]oIMpH+pG_[)4.4ΪP3-o\R\%1iNX1֠>"=HB6oo^~YGْŝkE/Gy).˿wjjMrJ-'!Bn̛צoUIW;_nePKpDc5 #mM*Kӫ ilk{-JҙݲgJʟGl?:NE\qac5'L=1EնW<e`FpG+%_k󗶫+$c7nj?枌2`:}Io4W[=C_:\9dyvOd^5xe'zR<XoC{\?en,5S\(,w8ybwRfyΜ_-7 zU`Mm:W[0Ŀ kh?` DEN;.|6RgEL7FH37ʳxdT|'D%yyW^eNWFen[xʀ5'zâ|- *V߉zѳ/~ZҎ;Q؀w"j~y)$n2`yOHM b>~vС=|@?3/QK ;}1˱9zO׋U聜@զcZTU*`Nƛ3dMրK勻x^\Y=bsB'yC͒3yʀ)?#GEb"|HJu8-s=q߱Kl{_anoѼVgy8i.$be Xz )$GIדZ!>:h)rf2n@MZwTֿ=lW.F淩|}/ Y 3;rNM;u7*#xr7G>E {K?!?b|`^L] Կ 7>`o|^I{qƪq/RmJTRbijj'(oEU_Cl;yds}` 6h`LZMsݲ/^ul:_hh;}q~ʤUyoJy ;s'?_X$N^9^' N|[zofI<6'3EK l`;)ƤԁaIS;K춈u!'uY5wS#Y!U8CzRuu:oyX "`gHgk;#fq c 8C:n|(5.ۦq!/kxeU: 7_wcPCQ *澁!}Qܞ%nC6OD)N^s)A`D]9|Ç9^L#f 7o0С9 Y/_^G,Vew #z  H',ݱ~Q^ 25 J%_wjP_zL ؔj^9 :vdN87sd76, U*o%g +F Ku%cW8@VpOemq\%^kvc)2C}8}$gp g wJ:_h\PI"oic͹uWօى~ ?j+r~p~:-^6?o6"2\5UNT+*qyY=^?Sq꾘w"`'{]ц}篹35W dA\ @TE5'uz4P_Z d*wF'L!:hdV7p 6O| ր? m/^WEԈ:I% x|<"Vsp[>L\$[vpEJ^"`^i`j7uҠmrh?\h|̻UD~n6LfNΧn}xZ$_K^mB~49db/MÊ30~"Xko%cQy<rS%Uհӗ|}̀py4_W'g!`gՊs#8ܣ҂3 _VIm $;&>7-nӞ^M~i,6:/ܡp.>b~Y(~ik%sП[ ~[s5)mB_ u_WKm?@Nb6|i}^lUI:wGOQfS2GUÚ|w KsNV\;$+- ?t5iBx<瑤o:P(4z qu+Ñ/Ůq"lm*u*n4dM_Kg +}*t~$3_)?`g +ٛHb;}$iWE]OY.n->{]Nf-yF+j!v_17,'sB/g>ˇŁNf]-rjͣ;\T'wCo%ANfEy6ИZqk#Iq:K>j xyc#`gD Ҍ\F5̷j T=W^8Ά>ooҥ 10&:%c ,:}^ӫrg3[_QJ\M9S8ίNN*d\ח7T60πW8&ǜ\L4/l#G).+NE܄}x2D!Kb7&.+ps^.7x'C!!',vLj4C!pވyHl{t˾ 4.)H#}q c/Qm{#4p\xvח0@XDu;+znqə!`L6;$p6 3d<^&)qQؾ<0e].D03#-T8T~Db[ޛ{F0d e:^c𶈕="`Ǡ)F_ ӓgjS&#z=c%E(w50ڢj4"K' (dxm]%+F;I IyFK})V̺ -F}Io9FSWwcf 렐 z=sN+j=e)wUׯGԔT|b#*?|_ܼpRkdy~«y{k9]=YpkGyQ̟a}(_lʮ%O ZHƑm`pȃޤPno @!@py҇k/ɉ:nq.;~%l\ ?j7^ ) ΋[{oo-_,V?iky si'@#`GPQY%g +@!`z4*%EC!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@ 7eAyˍof6p sϻ2瞑ԈۮRט03|ϖ_Y9T_{ CΐFb:0!|祿^ۙ_UVjT5V7qo1DO|}㳳#`̗Vc\Q'-Jt"dn=aiA_sSWv s⪀Ũ6:|i56%:<Z~`*QBB8MZvTG''70+Tn7: yxFΒVc-3?GԌ?%QfyR* w[̵^:n%OЂ/ܮ]pG+n٭邀%XX+[n`~ *OC[+qo0YSS.g Ϣ]*G -?}KQ iFwK|YD07*_܋gzU#ϤҮl32>קlsۣvv"ͼ{j3|iW19H=^>? 讆^]g/虏ת{a?' uf '*D%|el?‹C#6D/*1#`'ܒF¾D‚sOhGaDUaLi x80S/uTxPj7b=# Uc˙ `0nk;]6_5&a~\L=NDՏANaL?S4{ɲOW7ǯ#1;*դCI/pz-8{ ׏OeSJW݈/M]^wm=< (6F@uZb^|?DN`7RA^}~1Fα?iy086`|eq/pL}^?{388{ 0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`0`@0  C!`3ɗ endstream endobj 35 0 obj << /Name /Im2 /Type /XObject /Length 37 0 R /Filter /FlateDecode /Subtype /Image /Width 961 /Height 700 /BitsPerComponent 8 /ColorSpace [/ICCBased 5 0 R] /SMask 34 0 R >> stream xxHqbǡ;p8-nE;px(RJ)vMl#I6keI'MwgҦ3<صNin${S(PFsӈaɼC?&uܫ-MD D.ҬԴmiAnFLCyZWFz&MeeGlҺ4er"c^,K\wі 5dKN?x"L4 y@{𢻈Iˀn6Iar@C\,^ OG_6g/ko;Hk oIBz ۋ^*[W\x{gxϚ Bw#^!bm-@tռ'(7Wݒ6 -o\Ǣ/%\N*ӈ9\;KOۅ7י:)v ʀ=֞!fcC\VBxKy7裘1O QW#GpCw5W͉h ^tySWtSPD7L0ڷ"rreMafF(΅C8v7*}8Ӄlb\ޒq 4&Xbnsiދ̀8}~sdC_>n- ]>kEmmo WU0 At g4`'(oyE9z/ץD+:Xo$gXk$sc&ΛB*2 41׵`($YvS NhDԂyRy jS$g"5f/ kØeo!Y1 bNa8W <̌+4/`_e` X 0S=Q?5jpFa6ܠ&?zoPJaoL@륀gJ9gS9p&Jڵ=y-Z EsEˋIN gƍFi~Nlp93Ah `>c Ah\8xtj{sd7?Kz6 FzT}PD%ߪ: -St EϗSpB h+!&FR4N2q>0`Ν;0͍,,՘-E!5Y_vY?\J'NuIa.7Y&3OYwn]tȉcp}ޗEfM&띿_"E#LD; EKҩ%iEƗ/_ŋx3֣w$D- Z8X`oq-嫕5kkWojԆ }|s;On~ooݸZr% wW.\l1S:uͪE~`rX]Zʉw'hpڎ؜`|/R3g'GhQ{wӳͧВC4!ж4C Dž[wLuZҨ5j9ժZ2aSJY\`~O6{$KoݺW& ysu3}>`"(ZD4'MdBQر)t0faiKpcI+2jAAl]kҸd,f9V<(@h{X7wΜ3)hI¡D3\ x𯁢>@Uժmdc;vlwNx2~ܳ{Ӭn]TD2ynӶo[z;ޒ&'vpTݨakW/BVƝqqE|h%N6+.Y2uxsg ԧw=LAK'6 - nW 3J|(UTTdd >u{c׬v4Z}S{=Υ3MvLU;jykh8)}֧hV3gHWZmh ;)-#j0[zQLS.]="EN; gN>I~-BKC8(ծ]ݛ?th!I!:#g^fP)8!mKC Rix߉Je4ꨕ+޽ި<{\XѐOV\sydZgOYrV}s6r4gh|zUt*Vժ4ZjjJQZTjBVT*uR4+U \a˫-WQəRJm]T\{)E)Ʌ`(*YR+ZR%2tiDJ#-Sz … t.z \ҲIg^)4.idf\EiihJmPOޕ4iRqP&rKi_ :0JAK~dfBQȹs'm'<{Y7٦̘>5}~qCږ@})QUhkS8>W(+[V狺`x^=VXhТ?|4~̌5 @Dfp}cN0{jQ)&*6k^.ZN].#Bl9UN#*BBy^]\\$mjlKX_۹sAYg9YkW̍sLd$6\3fp kfcBMT2^^,1F7u$9~kR@+U92 "Yda6Y&4h t6dT31M)RkhhЦ mJƠaC_VĂv\_tB=F} . DC:uC r!8!q4^߰*?˄^^`3g%N>}ϽhA+t ժU Z|"Q@37n.ж4BQ˖%mؐ}HÇD&ۅLoƍX>?K>+[LPZmkoOZ+f{q$t"b/,tmE'H3<I}GnfDn5:0)j*EvhIA,-N jIbL"5kIތf glY|\vn߯Ϗ:C؃h1{tM/lOwl/\F f!'F@3Zl.BZnRS(M!YԨކ+B Eg6!HH/\pVpАs6mX1T0F.5BӎԱ7j!YSGq0u(gUXh1*c,JP,ٳgEI sƉ;w,LBkd0%a,43\%@Zp4[,tO;lȫ, gB m,Pc.Q,b.GQXQ˔4$:zi e ƈhH:B铇K6GY3Ҩ9sC z!ܦOٓ8|b)I-[4|X"mKC#/Oo{~~.FGQQQ-[f}Cߡw5o^2AhD\ձ;KZÉ4F#Y6Ь!f3- H%*%,2 X{թS/v,4#rE*q;̂]s)lGPy&ե ~O9D:g>.YBF>?&cwr>㟍RlXhZ/Z0F ae hR{4,1l̔\͝ 3g 1miFL_?ʫӪ_"ο=S=[׿ϟ;1 Z=8v`=VZ 4rn&Z@Z~*0Kc+fx^76OL-ST2M۴TcH=8?MU+32_w8+YXJ0aq\G{{ dI(Q \("8چ}v~~J*0$(vEώW)~ x'K+PbQժU3n31ōy#a?'Y Wwouպkц*2*Z⹳Ξ=]N%] O7mXa{QTE,y s/oZxb I(hJm 攊]#:"BcI%R,!jNiQ25zj(^tL5w/4lJU;Q: Kf1N4 ZYY/0$Ǟ0BŦM#?CNc,ypo\HB$UcBIșPR)48&eY:k䉱1ϟ9z墕 mYvϜ>iFN8-dFZLh^tǹZh&!]42CtϺyx >q ܡ "CKըrȡV),“<4B5;[lXa sݼiݰ$|v -G I q3"mKC#VYԣHwEBQaOd˖.矈M }55A_  cDxPNA_ }}_ zO)Ԡw!?>ނB>ނ>>~ z gP5h/((I^ ЇwN;3`g&yjה7]_z~z/C^2z~Ҡ{/} zag=Ԩgw}(=z'<yߦ'&=:z|Q?d=zxJWA_{xI]yh]I"KFE; :~lwhDDVUEdfغH=1t> ~J5v|$v3yj9%}PpW/Fi
ɪ ,ݰa/`.g'3cR/YpʔI YrTFLل%MZf1# <#{Ν:ڠ""XI%3Wȟ*-4aKiF3gP"F 8MCY7&$G"!9E\8lRc#Bn1`>S> cF\y,P1RYڸc5WpZjҠ4KRr8.".'m߾ eɘ^lh LI={6*3 B!NiܸPJ..+ MJR4w{jڄ8SUVyp6"B-j\-%Z[E3fL7MJ^ʕ%LdAh;Xjƒ8MG Α={dg2/YyqG7va2o"C-7I!6˿f#@no6R~M_r ˑ#J-xa{D-JX+dEҶ7ov-_WX+Zq@w 0sea,Xu\j:vhQD?BҬLkP3-V jᜑvDخHЦOl ,͵A gb?n޸ZVM19pۘ,Eqz8w\a_>IܳqNkfv:lMZtѤ < -MBs|n\gN8Vt]\dhG֦fi&W1lAc8ZRKs37B4#tjp9Vp_:z""W4mE궫NՋg%K270۔ B& :xㆵ*ж4BUT.Zˆ3L}yTryp#4NCB#b yٳۮ]XT?|`9Q[m(̶i;vOxll}<%fI9mb39望4it9уQAh[m9()ʕ+8ºCht~k#&Y -cs`fёsaHG&Eږf9(^|~mkմ ,笵3{K"M55M6iQjvFdi^Ξ>ޢySP QjqU:cURFȑ=:*A6 $Dږ&-ӇŊYAAчwtc%g\g͘/(i#ƃj\m?&ZߩVmغ,c3`_Op"mK1B  oՄR4 Q]6Ǐ3jm \-y֌,4.`quJ1:W[QPn왣ؿ˒g}YKP۟%eNf8hٳ=; "Ah[ ݽTDX*Ι1i8;S6 v%zj$iGE~P)#mMwgА'ݿw8J{n:y8sǎh׮ acӲ3S5jTy r@AokT2JT;ƨ}o޸TJ%;lVVj#J!pf7W<VdoݼBh)3`*fķeLLS\ "Ǒ BΘm1zeBBlbBp8U2: >tZy|*1[$oZA }K FVW(!>VJ"Rz¼Ȉٳ+1zN9<9.V "?-\0F?+UXrڵk/_E4qZ¿'J]oT\,\ubAu( )cqqTU)hܾ5,fp5cbԖ.7krQ*E-1ܦ(#uXْpZfc_-`f9B\O#ct/_k߮Q"uJfZ/p`ָ%g޻gۢn̟3ex*y :gzRƢ xHK0CBwhߦIzzjQdFui*WtӥK7|pafڷoԸ1NzJnШMeʖY6~"oMB.]uVLL ]Uӓ;tDBFpv]rTUVSN͚5kժհa?R /{!`4 pQV^ݷ_f͛4kѴ߿]p >zd|FІ~r-˖m߶ڕajHW*)éĄSnׯ.+mP)r+>z q1r|۶mHg1cƜ>}Y8pI+k4gf9BҸѫSZnYvc:PECRZl~JJ+W.sXDD )Fѣ߷Ҟ[sFV 2u}x8q'!!ͭTR9qJh0n2z_M4o 8Ro wy3{n'f%+VgTP˞}/H ;Ǩ5*O<`آEǏ5s_y]s8S9N 4.-( akЌի-[t*T]vVڷo_|_ Opgyyy5ipۖhԻ|媷)Qd>HlѲӧOc!3>,)&OW~xdOqvE4F})LJ݋6٣3gL=k{@0:C68 S(ZcC;mVh1cF\;OK0g߾}-))?+U*y'@бqZT !C{Y D޾-{9r?qlJz5j2s0b 2 5@&MT# ޶ˡ % >?<Ԫ=uTsѽi\~YfVj6hѓ:7аAVB VVD (X ԪUM6zӧO!c|kLkv,=mc7tük=߱hW 2,X Rh>cm '> 珛rb vA'AfAZܘW-5HP ƍKbp-勷sxz-={tC!t;y>w7L\a~6Rϑ5k.QKwUT9sRЁFo8;[2'TNZpݛA"mK!4{B?U4tãa2'OyTo/{ѪhܨICUAڰ|& ICSpyA= BS O=c-իu-`4o׮В=h0-[4woFz9T A-nּ9z1:""ɱQQQ߾}{a,Y\~~sGgW-S*ݴiӷoK,#ѣG.]Tx UZ5RJYϟlٲ$;v0`ĉ^ Es)˰ -)e̜"ßrdشM qf2t(y7pխ[?T#Lh*PY[3gg7Z%{lSzi%WLjUĸc?{ܐa:^COcG/R`+lϐ!Bj'?',R^ֽoh9x, $[EjeDvo~{KSΈЦ嗰@&hE:ཟJn^E :ý^q+W 842ѣ޹k q!o-DҥKq]={]KA!4ڱfJ 8AChLIӬm{M"4\PBko߾ <.}ޜwဌ2*ۗ/_ 6mjBh93OLȱo>BGCF+=lϊ7@֮͛]z;vׯ͛p6mZ)NI5;fn>ߥT͂c:l[Mp8/?N̟g&BϝygA%10GhVFW k=}4L"""ӥ6Cpʔ&tEiTB;W={0?>\TtBliq:1n z{zѣG3f d "k֬׮AИ!c#JZQ~7Y{YuïG.tq@BT*õZˣsnl;g,Y@'ǻ&n׶#U` t܇g#c -Z QNۼyN:A;vΜ9VjӦSG4qR HvyS3 vۘm CPP JѡB?2?h}.H鵨h*ͤhS:* o܉ uօ0i@;7) 4d-k׬2x /רa}J ˟? @d`^pq)s5u{R#vubߨa2TYa32dتy'`mzt62gΒ7O~0{x7לlj2yl/_z2>O$.?լY7aE:"Cc(WG>eiWUÏz¹@C3*rqKTp1cF#|!|d@0.SJJ UصkW}KE6ɄGWQkjҴh"W]o&nݫ36=i$۷7oH~wwN94₩3.(5!tcz2F zA_'"B߹_ŊU׬8Vbʕ*wС[n?={+ ֹ_|>YzuRi<`E2{/ڮBe@Caby6nhgȈC(\: 7Y4~A!e  Էo߆ɖ-CE~c!=SL-:6.Vˋ}ڼɃ9 ]~1cF†Uͫ⿵ϓNP55=xg6mܹ֩vluərRXiؠsqE t˗#ssUΒMR_s{"IcC "S29B3gAљ͏`{\^)>|ר;4|Y 9F/g{"Fv78+B30!2~ߋSsqtmaq`*.Q9B?F& <~cwVRDA(YWxQVș=O^*3>(znS@pL 6xR"ˮ#/?k@΢˗/ӦM<6(((o<3e<|țs7} Z!4:00ݻw*ٗ7- W*le}ʕmX{fwZZ@>|I/^ DݢEpVo[Gni9ktYz.YeΔ%#Q f}~Nl31,Ξ5\=mw|< 4BS|SgtWf:K.tCo}(dɒW\1k̏-[Gwa#8M]Gny JC0[NV?yv7n-[wl? _ނ[םTO9 ;W-NNYK(o^-#ǎK @섔];PN0AIF/_lf9ٴl\l)ÍL:H`ҀfR S}nl#5T< A1(B^5^]xref<ئ͚i^e"TI喭ZB!1f$rR,"|Qo@Gϙ#4hUϺm:|ܽDZ.4QhNpŧ3ҬY8Fc>EfBED{_gjƺ@&|~>o͚5mӦMVaj+oi!͚7iԻ 'ks,3$\9B]^ajR(ۺM+c0HWQlB#{Y ,rb(1$cMU 4GUx=1ctJtunݽGk8U\9WWZE |m5ULgH:E#4!㖯5[~n1KX߾}[jsV駟|||㙁ʂۋ-p!5т~g*vQ@խxw?_?k֭֬KTdGe|Z9\욿/27zTiY:e˕HT4*͋6!㔴( rx n߶^P ܞU޹ B덋gʔ ^.\j~>v>5jBizYk>^5i14/a,66p}XDk)0O u]];Prb}¥EG~v)HZ"+JQLDqfa$ל  S|=x Mq\98:[իн{7Is-S)+& V,SY«B#>7rB1cŏ2hH"ʕ+ 9rd`` 87E&uݝ 9i Eh6m<|[n׮]3"3m%޽޻q߫Kǒ06F[TsYM!!S twKҷIlٲ 1QW.8a9ٲR4 o^<(1iU ڜU =* y˖wM&MLHHҦ%\tA1~ ~a>r,}: ?Of_L=楷kula3jU03cL3I߾i1xB:gW/J.ef9C}HpkKՇ Ub13eAsrxaųlge /M, N4q'&B˴1IÇ'4Bi Bdu놄\xqĈb_|~Vլ|>|x9l߿TW?x` Bϛڢy=;o?y+ߗ!nnN#~r6v͚=9EwSwVs.te=j[8>AN ws9&Es-j'A($idU"4U^ ²ew _<<: lgj̟={wIR'>B}sĢjM6t0Ϲ{ֈpʼn'q7u?7.[^=6qm{שàAgdʔY3d8eʔ*j1S'm rd NC6pk џ G ~jL>U…q>jLK =#hF(O)Y=[("euT^,s[ м~~| OOBT'mbY8 <92i$`i8wVd3q.\hӦ UeSW5_'u}]yyS=B\y&&M˗7|{el8͖ZhWaZdLfU%:tlxB@bUhDn3 OBҠ yZϼUZc!qu5,VV %˶ڝ1S&^v5g5m1ǎ0߉L~}Jɒ%/W쨑F{b^ ٳL2^ӌbKA[8e$^@ٽA4޽s3V HA3ok0Ϙ1VYKo>a OWeujj>ԗކ  HܿifPi\.Jx%KAKbƌF |Qy%..'A5+ "GS 4v126k I9xp z SFh v/5г'kQXk-zԮжdn߫1&BKLl_kagjOT57t"k֬7kLWX} Ӥ\2dHdP.I%mޜpaRÆ2gV,t}.]| -G;w*Uj۶m;vx)~=clrn[:χ͚5ol߾a-SgͥԴ./B}\ZД:% 2xȑUVyxx\x pO}nݺ̛̙(}ɱ9rM6->>zl͛^8w rd9B"j&B33+r0;J&h^eڐE#tre3go:&Bwm^VwzͳgM]b1 =D t2y>LgRyQzmts*`C|vҷo_|yL&\իUR|ܓ'Onq!>TwH>==4uj@@zM2`Co%ޝbϞ=n1N?ؤIK0۳bV3:lP5M|Y~3syQ ?Ъ#;o嚋>lir%)=Y9XqAh/tpطo_\s_rhb]@o߾Q=w?W6-ORI:y^u%a9sZ0Pi3d:#Jŋ}ݛAJc̏c%" E 3&B77|!}zL=zwa:$\:+JR0Gi0@kiVe腧Sl8\S5MLLk7p} )3B3qCDVUׄaaa/SݻwVBhOeΜi\nV.͐!CjU?}G hѢL2VoSr@}j=-fqϜ>2xpsgv=G_fM ݨA#GO>gϖy=z 4jEmZUGϑmE⟥;t)C>6fk0'Ho1dm"4|ؠޞ1:Brܾ}|r͞#["1-{|UXȷo7kQB@l&L9KL]r zK,)2dCQ6mڔ7ܺM6՛"֭_(ɺ+bE|ڷgGzu CV-BE :ު?ަmk(by~Ҽx.پCW^A6gz k8|rA7hvy ؀!X^?3k' L @ZՂ[R [59k[cSGDH)zҤI2dlҸS5W<޷H'^*{3\iȡ)WMESNo޼<>+iBM9!Vjv%)xG5 D TQ{,ҝ<ъ/mIg&3Ob\̷ФoJ vޝ!CJ{գ:Ν;7e7'y%K͛:pgM[xm۴ʞ=;4X".m꼴 xw ڸq!ᅷm6Cݼyspp0B|P0LHPƒ_;sF* 6n^ AjU!gV{fUChcJE>}֯_o\4>-1|$3Ǐ1<cD#u rp ZCc\ !>6!Ac-+I0"4iȁW X1hP}BRfVY 8*JK1*lϞRR|ysAAQL*V,?rЋOJ4߀ V WG>VodlLΊW`E,,B4BkM'FCo޸lS?nXy| r|1c ?SÂܿy`b}>m}7o7)8&"^ f`ICGJU)ZJ U*lY'"BOxרeOPNW%$i#%RZWcABU Kubu C9ׂEzB>$,Gh&Sh{_KGˀȃ ߪƅ\ti|]vAտurO8ʕ*ZRC{z߷Ë7!V.yޠVgAqpRxXEZNDǕKgڴn :DCovAU'_^ մYBUoSar (T2$6oӧqzq*7mDUQ*ZlbΦ%&+Ru挜 B kBrHD R qO"DH@2i9Eg@_+џRڻw5F[nƍlٲ}]vٳg:z'ʔ)7o3ge~~ÆBI1sݓUa`J8P+ r_X5 kjg$z#?=E>EF("ME,#yH1VEvm5 CCV\rڴi>|0%޻w,_Ъ/Y'̊,kki39*0 }OY:ӻRNwUM5ì]S?~v적}O<176F0kLkv,=m׃hW 2̟??!^0 b*uoZbBB\z玙{=5J,zyum0ֳ Dn[ l,$$Kcw.owt7fg]|}fycϼ;idEaI>E82Ѫ:7Eb'e{;i d9B+SbsdR@Er`bqW^y{{sdddlllBBBjjjfffnn.ѣAvu)Zy\d֍?"Bo^iypCwUc2Ĵz\Xh9"!dKc&rĥ81G".2W.s2Z7/5+tL")d*sDrqNAX$ę7Z6 .))|왹D!.-/+2dHzpO:UTjJ+OM:DhR.I7) P8B?MSSRRp~p8ﰡ&)I1-Z4矢"4U۷m2aO bdZ[ݧ͆X7ޫW-6njuۖykY[+bI6쾱s::p9%|* Ԃ%1twƻ[ysFωsK}ɍe tՂa7|f'7ºo?䫖h@%_pljѣϟ{yy=zYi[9_͇E4Y7?_3n$m<%t~f "纆"ڈnOMl2jΝۉ]У0<Q) T`߮kWe;j {`W/d%i]R\}~m(c7` ĵ-)jUkE4V݊E|7/]gBѳeQo)_7GKcʄ"_OFy@4R@[Judee1 .Wšu:+u9VYV O"'f1ҕcC`6o wL8޾}AϞ=۷o{þ;"ФrӇud}$vgEn.; 7V&:=5{T zf@صcsȻw\,& ~8v^Oi7^{89:9!c{|~hVwW/<|Es|-l7Ht =s gi!7^0ؑ !Ab <+])wilO㳘 Y?+`+bHSu9 |֬Ysy)(Gw* ԯ2BS)BŹqB|un6jBМXYke<ܒrmyW%ߜ D}%'0h8q[q`Ԝ/wfڱl!"0N, Mb׍dOOW} n%wP]EVVw} ɐW!y:uf`fZ⋖n>ձsWXD\E%eeQBhΝ#Gz䨓'O_#+CcGX.^~IP_>7󣇷N8\I 4x`1>ܮ>SA]M[BP`AEhtEx SyZV.25 Y_p-&KΙ;*ÛgjY.HRBhnx4&KKKyk'1B e,*4ΪD"iq~;9''0 ᩩw?I~ 4\{]o]Llvl6O O[`~Z/Qիx=L*{N{ۖ Sئ%Yϖ7Vy:)&r`yYl_yJՂύcfd*{pYxdFp0L+tX F-r?+y4YxǑKaw2)m7S2Dpzr,Yoaa_Ih#4pS;g 8cITx>,1fCZk*ɀpeomt_iڵS^]]IBsr$m,8sIodx>0iʩ{Py}ƝNV[l:~54w"`îq~~yЬuЪ-GVLtrbK*pۂDOE[iiiZAΙR)ᲮE_|Du[}ݚٳE^@P8IBBMwxRǘ n]!t3)&^*hʕԓA|ݹsgDDDLL  XЩS aRB${GwuٳBy*}ՙX :1G9M| wED=Wܾ!}{ |;ϸl v"aME$E~^#SNz\ejKa0s3H3>Ңכi}- s ͼ|_?R8u +4㏲+ifzx< hݳܵs+HF~}&:,4~-ٶS-w Yv?}D8/)s<==kfgqߗ*O4W.Yl*׮YKÇ7 Im#9JGim6_?J˯?o7o{zg#58t;f9k%μ1i' ~Iz$EF򽼀Y&79j~n<ށGߜ☝( I;r-XH' /(~q{eECadwƫxW EBInN-… N7XHvyo nP"'NPSim_V@#K'<}(dgcW9KɨD˱kgYUw4^_m/vcf-g~;W?rժ8B'B!.]GC Kr A:B_znޝޯqlYe’".L_<|m.)Ξ3Y+S4… %E̙3iᖝ1Zu" B_,rٙ(͵\.-_31'<<[ e}lvرx n} 4Iy4U&u%D]mr)B?!޽{)3!ajr0BڹUO xy*yxYw 9Ow6[c2WFl~eixܼ ^*}13=>Bg/90N8aA!"0>y5zC*p.CÒ%KBdw<7`Òm6r,.Ahͫ]ۅz:-f D޽AD̟5kO?ܹsO8@ngy}L&?fͥ?nvOZ+,BwzJs8>/,@lEkЮ:9{H~f m.-)22hvNm#\Bv4ʊ:̶b-(tig5)Be/(Wmʥwxd $f͚7ɓRSS3LbZu5BF䰳Sg>/3ĉnp?>.>0o~n }R}*1/?@:TozX߯s5o+AsLǏ=d(Ш )eGG罫̎7p[ H"t]04bArb$}SS4>S6iZB*"tX#t-0믿bkxͲR8;v숅tQGxwMA:(>==]30#-|߿'$q"Ǐ#!ÌHݵMO1К9Y.{jW ۷mTs߽ފuv1>wA=w@yd]jѾ_IڽyGR۽+f3>:r-x|_~G2nH>97n؈mfaMhcgZ'aL)1aA"yFL)JJbZwrܸ5\ø}Y^.͆n?~:!!Z~[ܿT\uzɶbb XjҶ¶z:_hD8"!u<W8u ޸a-[n8=:n{f2nc:8Y[zJ[kхk5YWG;#"*N`xq#c !9O"RyN@hXõkVńޯ!Oc]R++J54X:wܮlWFMշЖ-ؑq~b" }ETu(6SݑWE { _m۶իW/,GGmpWs0 40+9:eaرc03]C"@Dsu$NdJ%!3 ^*QPƎ0YlCoLK,ݥ]@Kf jr2"#ϽMXz3vﵥQ䜘̻˓}Ev#+ẗQ<}Ŭ'Tf_pdD 8x[퀟O/=\Nc-S@ qpd[Ѩ9<9!O+aEDz~PqhQw߿(W|P_+NYl`f@H]_ Xr|eL[Kxm qsxkDf^nLң:? 4Lt7gykٟ#&Zn=hu{E/:h9LH Bg'm?ja3xr6B=z*:#=kNM D xJ)$vp?jiЃk4N("!P4o,j.BuBy-Pg|;eV]ZR4m4m' eJ=BWΥTF8rH~~~AAE~a/ dg*6*j3)!!ps64Ys31 #4hoiřO9Wl;q>fJl+?k] }v|i)> 4tn&ηRk82Ce oi9~v e܎8.ty.ūkY,d Ej4#55&fZseT 9NMݧQ lZ_.]{k|l{h\7_ϧ̱6oVq){}-zb?pr}'?B{rݵ Ch 0 rc2,Eb!J[XXɲ`s#("/=#k{8Vf)Zgi,BaPB犃uk?A} ("4!1=![HDY;?7U;*G>/-xD `?ݹݻwE̚\v OmR,RjYs+Ԥ;y enl6}Tpk⣁tyrBdˉr?3hC]  "XwqgZI*y@WkW ,#Nz2Svp2^q8[#1>7"@TA-0uNf4k2fS'lVi_^Ѽy> g5^;cѸ3Nkտ1o33RgjlcT֡ԩSO7nmm2fa2tr Z=Rc~ZE8y}|:r\,iy}FODGhRabYniӦM'sآf͚~OZ' :#J) 4ɗJ'Nxt!HL1O*Qa鷶=lQD 4?8U0&:;],(Ėkq~_מ]<#gZ./p2<_F3FG ?BCwcbYY^Ie VIB tuDֵ_yT4i"q>-:a'w|f;5Pc+&Ehؿ'OVup/)biA@B6Gmp#J)#4nf~r NNN":?8PG_e kRSp}6߹L*@h'g%۴*{D e? eex{tgDY81Lff g>cy$fJ Sۖgj}QӋP47tز 3&L{W޽[Ii3xž! c?+bȩZL mjl-:VY{!@ |"Тd)#~OgaZ89}Qrbd^NrY ?*2Pqc=e`..~7+.5]qƁ"vک5.׸L8Brf;^-bA(Jݛ浒a6||)[@ KFA !g4mY ((xGh 0D\ɑ)!tUJyq/CӧORg=$$U̬_i׶^ŠvѺ:T߿Eo>ho' ^{BvQ}{:8Bv/?މP4G6?[fuBEh(m?ˆ K#Lw U$=:v\1I,vs-KnͲY9ߵf05Q@oaج9rM~c7۞fq<謡BN}d-5o2D8#=Nϫ(;Hcn`B@ݻwׯ_ieL.[C;;;eXmz )E||02-(bIRi@$#LYwnqP1K:0!ef) ׮zN9]8ƏzJ~qF2ۻF_t#/KX4x1dٗ;z@cU㴔XP{w\6#"`jם;֭ݶlܮCvv3r{wz**E|㇩63:n޲>nץ/P/B9E{wwXѤ Ν sStu_1_$?ri2v|nB!ɸ'o 4lR ڵk Eg$4¢^b[c"!C,bjl  kEDD/NC[ 6>5zdtt46C^Dxx8hE@QfQQQZQRRdfjڿ{Mhocnj)))Ց)q !0uU2#J) 4A} ItI IkV6 і]ļykA-P-Hx}`v8sm;ߴ|dq]E.iwΡJ|EE֭`Ho@Jd͞: `Q$eX;~ T_ZRvI#L4]hSFF,ptt" -]D̍?n!Chx1GF]Rn?3އ ; Tf򴅲"eeb}t%] |t!6f qbe2\.\.sYy9n*'&6v͇=%|vb1[8Q!;HF~TT(&Z#M>Om#_x9|.#59:<N:8.ў^_~/%~w  b[gy VZl--%&.64))Fv ylGxC} JP +'d|P$`EgE"b_$$$$$$;k!tUXŌ!P7Xksb߾  |.>]񼠑P"7=A cW/9lذN]BBh$$$$$BmXss1k m4)WH,028䄞=51 Hܿ?0pHpÇyyٗ/_lذNX) B#!!!!8!JQDhMJ) Ӫ$[d#XJII޷o_Pۘ쌈 8:HLwppfV⃝N֫WC))0.!4]!6T @4< lIUOSbRW41sV:"eCj>]|l~}cРaao/\&3%HO$ 6<,K&]Bh$$$$$2/0x@]Mbez*# ~Fst̙Ԕؘ}4h`q1zӓf$T=ztǒ={(ܹ3B#!!!!BhJ'W[V$̕ D6fK%C=,1!>*2n_LtdR?yKrH!I+aIgΜ:r0IBHHHHHڰ*{J K{VDY*>H֙qZ/SSS9`?..gΜpdBH5K?Uvz1k֬+W6nO=gggׯW~ h+++9v>๠+W!6 ШYgKJ ~j0޸q#1mZ?yC{3g B߽{ѣW^Bh"ІzW,H\i'DT1n988xGhww+!BPѣGº=~؃ݻHH!aـFTD&&&h˖-СC_|)STE}mذ!Hruּys`btttCf̘o]~ȑ#hѢEXx^z}Ν;W믿'`5jKa%O8ЮƍՃ @V[dii {}c!xÇ!ښFB.Іs0#FFb#FYRE'OPB;w?{+Vf[n2eN dذa \((6{ƍ> B,,,8{˖-=B0O@M49suy,?_N6 ֭K._56m !^r%:?={`ѣGaѤI5СmKDBҕBWB##jyަM]]]`k׮ᩦN ͛͒w8x ,5k6i&5sA* &๙C06|ĉ0pB</~P-Z[֭^z06KDM +5qw ##1WBbc }mFBR! Ue˖gϞŢgϞM_u&MYrvsskԨ96;bĈ_䫯z//^8qC B_t B<_oߞX#+!ξ}`lmm1>]vAȕ+WĎCBoR hH5KBCΚ5kƍDZ}L֭  ӿ Pʕ+;wb*#tz^  .\i``bYP> C{A]*Ϻu a͚50=a„or8q",_?~&FB,І B&G߽sS˗N_~w8 F2*NH*aZ >P.] ͛z۶ma֭XM6]x&Ν7nS4h@ڵk8B4ƺD5lCh(D 6?x/8p LhѢI@"?IBmXC7/x!##+[WbB#h $M40Ԉ'-[ b@o֡C۷C>&V}]X#{qz++իQFׯ_+V` rr 1c7=!6!g Α!F2@4Hٳ@x1Xwܷo_l 5pu[V jܸ?<ž={}8`⻐QAAWG27=!6!Y&$j-8GL!-~B:u#׾}uAH>}u*5l!СCh"4H>bW6,m֬>$XOlX3/C`Æ ^EDBҕBV:ͫf  Z Α@/~NxެilH5]-[#J6m `3*^Gݺu  m4vXڶmW_uo!~ZeҤIP,?3Dܹ3iLP/>|8 vmbbBh !a :/'i?#fj7{SNG;vDǸw*^zYΝ;nԩ&Lr劺l,Y-!n@aVZ`1wB"uʕ+)-]~9]Q iffF/ʒ ,Yiژ 7.>p~mwȑgϜv~!4BhBST#trrrjj  iTYRRE 1aBB#!іBW/;=s޽;))pHKKKIIILLLHHwqq>s8 4gM.;ݽ{ v #@|>^Z "zi%q\z UY YUVVUe={ObTJ%:\.!99Ν;饥^Yegi6mVVhK/]8yܜ,Ԥ$wCvWBwC_&k*|gT Msm<==FB݊o¨4k֬UV 6Z# ZK /?agn@@XCtrr2P4 ]253BhBMQ46ԋ/MRVVKmSUo !4E!6r#z)B}BL~r6Vh* ;f/5@kH䢱h 46@ x捶jz-j6l`g%Ur¬U0\Ri^^^NNP4֗#%%( jiST@SR$4:22ڦ kmS!P;wZ:oF[X2}VJr"UFcUBWs |g|11p~# B3z=>s>bi[4m!d{)'Z [n%3@Y #P42Hk!taa! B|K,Ϡm۶6lipkE⧽b7m*1#t%F2Ksӷj7B=W^erPE#R>\ChXLЀ@)!4. Y,E~wݼy3Nh T?MQM[P ۈwFl9BWhb2rmk\W!BKaI"kLUZկVvqI?|Z!#}=KKKH+GHU߽{GJ"B3<_<{.O("UR?\iI"4&Vb|ukR%a1@B/n>Y~J< -CV[\+# ԽN}~Bkp-Fh[[[?wyF!!!*]yٶm[%31E耀HD1K,XDcʨPȑf9g*Bh/$&Lq T祖sWGNNyerЊuRkBxgs3'ݻrP7\Μ>| C@hk !#@ ܤI]*صkڵka2V"4lXzTb?/9dcfvPhpQD@hX5*河0>f0tNQ$F#^_sNߺqޞh;#Νut~s5!4'Oϝ;W]G.IVX7ǡ|}}a5<==k.E@h*0~>9^rufo߾sv?8?ϛ0g`!^gr*8U[A  ЙkBs9/@I"IRZ3j_s?tyk.n\u{߼to=uT˒%K|VibŋOݱBDdb+ry;/zޏ1< 0KhFx&0޸:"r@NKݺuŦᾌM9٪+K*ruW_ xA%B:7BhܵɓD|U@;wQPYO>B"h999XG kDS{) 4xŊ(ZS4EiwA 3+(,y4㫋r7@-Gh#l:,,()1~8B۵ p&"4'ȓǧ%O" -,,rss%>\ۗp) tu2B`#æV٣:2GO::L29?St'*H#4RFhdd!]SQ| VqF+> bH WMA 9E/_<11Q! BEGYuܼͪ@ 3NK/UW]kHBh#BhddFo2SU0~>uꔶUe>}ZZZJ#a5D'OFEEqT)??޼y =:4"/?^NEzW&6&ǎoegdggU<=iիB7eld!]BNѫW^nӧeH/m^^^xx8Iz̙3rnLg&Fh9E 9d:yri/<={#V\Ɔ:4Bh}6!ڰaC  t [2Ҷ1-Ғ譀dccnBBۜ:U'3S%B޶M3+ ' v٥kꬡЉ!UB Q+mq 5ڍr"̫a%%%+EtBBBJJ  \)n۷oCHBB/:":nw dmysvw{ME޿Z@IePG1 4ϯ2s\zii4D3Pz+ ХɩNA KK놅u]:0DUpѸ1Yc$BM.Z~V8 Dhڢ2} ޽Qݫ tA!2*Srk@\}aol=omjРA=׭}%#=wR]}YFoBv ˥۷ C$Z}izҥ `|Ϟ=@[k"Bk 2ZT+UR`@/4;n\ھmaCVZN>ȑBAomcC yVu/Qׯi|tZ%jh@7*7bnggw=SSڃӄשVnaݻU56!&d*YAlmNAf"B{w䐈aN,X0] ~ݿ@Zuh"tM8;;^Z+ ҖIf͛7&TRV]PPNyĹ@LͥN"\ο# H=1 4rU­FBKџ 2=maÆ|רQ˖-P5q! N;!BWi$^.mSkD}[YY!t``@ ;w!5br//gϞNH*R\簰01 "Wl-/3_S V͞g9}VVV222~N$W~ +>LblR E@''mk^v[ճB#pA\-Z*s@иBWpr\n$C}Kb!4 wDDӧ5C)I&~>Ŧ;$>FѰ,'O/}$~K| hUMZ9@hrUYQd 1@he<|xgMCP5r3Bh}hFw6KӾwءV$KW^U9Sl' (2Blll OŸ 4lE?c61sXɫi&hƂ?xY+oN "۷oϛ7M6_>oF:wZmcF ? MDhu\! # 322(ր /^߹sgƵAgN::ses45 [zp8n$COj:#9#?Y¼\l8dFmٲŴiSYS##0#o_g<{jYNǍ#4`DEE-..xrE"7ut脄={v=N<8\ԻPhB*x$mUm:݀.G grqj+B-qBBv{쎌NH"ի!m]~g_bcc_mkkK;===#4>@4$oݺ2\zBjG1gmߴ93gϋU X, D!tAݚIAhc2jD9+O*kevMDh;;M~޽{o"(//O cbb= {{ÇS0xڵ nٲjqQ]ȓQ O뚢H xh&HBhB=)7\Ɔ[Ǎ%bFmHZ߮ 0rժU#G455MKKLV/_T7EvsskԨ&5:zB#F888P_=mȪ[+rH`Qf $0z5J~H\.7 *ˆRǷʁ`BE}9.iG|adD6ymu& US9v!]sbHLL Pe~~YFF܊!k׮U7njP6mCq׭[u!׊a;$$$PY"`hlgOxyyIR*i Y4y*fιcWX,VӓI۞=Qi] !nEҤl"!tUQ Z%*kV!]Ck@Gu $n VO넰&k֬N7oܺu+Ŏg<&0,z =pvpq.HBB;ıCF wHu^{+ŊZi|ΎF?/[*1@4VLǥps3gSJJJ ņ({?I~9{؅{RaK\cN|b=c]SBhǴ$ hЖ-"tGG5c5E}i L6BU\>c qԈ)ɓ;w>"NǑP(t(` ~s>}NRt΃$?$E}%7O].YKB+gZq$)xoT79Bh[Dh///ެ7vXӆzVT΀'T@>.tVV{GSNm߾'b-w'$ 1xvKΐ.]䚚&OnzrG2 G^8:7B; c!mLCAAA&svN2T JMM?33ѣsB[n%M@h z…\rq~ *qL96VύLwc ZBք "qBhFc#mu0 Y[[kZicǎ bg~c H$3s8aÒt̙QFdgRGG,rGNYdH"9B@Ҏ<!my妦& f\>|8_ ]]]ƍ㘙HܧMbݻ P(8͆>jl1w.VWw<=OXYB_9BB7]/S]c-p,4}kAYьE\Z%BZ[[/]αsnnn鉼֚vI_`zJnn9DZ) 5L?rq&&6C(wDM(G\QoV-SFj$ 즦!iS|.8ZHAueF"tyFI t=U@ Ţ}m RW\9LI: Es}}3NNdXWXGV_?u VdA t BhQh Y\R&oRu/$r`6NB[vL_d2Sإw}q#\^^^}NNNvA&//3f9v ޚ7rs!vp9B|pZqMKAUK^e/gil/>fdd@v(ɹ4Ж 4Į8g8D\z) 4rzZz I{xW01O馦q >0rdjz%lΟ93u䘽;v]NBr4fo>R"m*m;4^݄I_0tƅ. 9t|CSuGytJ&m4\'p AyyybBUWWgeeS'xoҤI}37|'~Ɖ'؁a8>НJE.t=x^q)PD~c ά݈'Fl7w&.8ڵҎ(*nJ+RSy܀>bجnA=o~Ū;S%u&rX&+ Bh9-5=Bо\}L@tpӘ%KO릤_`k7,&{,`٩VPx}E)efdNTv299rEX\555doX BS ׁZ@9B _ѐ_ Np\/E3ec4d$ tD*N }uoJٖ~F٪v BhФ,چGyǎ’%KV^ >VI&sv^7~G!+DMKhnnf3{&ua Chxڵuj!q?S޳xϊ\fE ZY}U]/S)Kc PSZ-|;NI ?!ߎH7IԲ΂~W }`0~NlU`UD@R*BҞ9 ,VJyԨQ<~O>_:#zŮhϗ"g}Ay]+9=L9.39&DGkB$GC>[1~q aC@7p2{"1s,)m=`m!vƉ}}}Ůp!+**\\\Hi`@G]UUť}β t_p, '*Y|?LbB; [z(_yDJ#FO;?ՑT~/' 3Ȓֳ fNB#]jdv A宮)! /e\RZy, x][nݖa^Vhg+tJnMHB 1c(?Ok}_D1p2{"1s?e++4\'!4Nľ5] DGh1<w޹Âh@&3Ӻ)))\դλ1y!4i B{Ms2}aaϽ?1:J7,׿u{eBhŤ PU+!}/Ay0B\>y*^jYgD f`ap xO;k!tTZZ:s̖z##t{{;ח>"gEEEWcqnt ի_d&BckLQ?yR7ðf O=#9 6&rS$r4:.JT&NwC6:IϬc@/@OIdZK T6:p`!vLw+WPuzݺu@[luVhn>^+{ ia#?iBU0Eqq{auy975ŀ%1@6@QLjŰl!raz& Y,ii;  b;x.!t1eДq;,-??HGhgū GWbX!Cf*?11ĄQ tCC#FDh)ӹDqE!4{+̳A餏_πA0,h=Pl ȞUmŋ`,aB$gSB[ڎ81Xt]]صY@$ESOot`͚ݤCC˗/sEHI*+ \l.塏pCmôN:^|rB#^N(P@9$.|-5#=N2B[ڶО ~gϞG"4g~jT#0ŋBnR~ [L.%(+W2s BhlP)ddF L뤹!mj*FcZm߾}Μ9vM'[ȶ)S,^*gb 2nxes1d~~~| tt;xيB;-/&̣B[ڶ>>>.]2G{L*>&1#3q+W]W|bDoor988بW쬬,cHNiGMh6(JwYttw^|sÆ|mE_V= ZH9BB O cVWƉ|}}kkk]=<<~3g9#)˗]SScov+((ȜaM^]GT4ȡC^yǗM vpFhEIL47s{gɵѫl {D=wF-;zdG^Z@ tS=U!4e@hh׍Xt)Q4A}Q׮]t{+x"Ǡed2kQ*.. 3yuA6aF#G&BggO~ VYn-Z[6 tFy0W>~lw!4m 5)B̝;KQ$?a+&Crd%(8tlFkL*``cע2_ܰs6lp^܍}EEr46_谠_˞~K/e+O !4ܙ}"tX@3'6!fm!4 !#grB xKKKu&Cr4 6mܹ.c8O?)෇f)`;11B\lng2?pvjjБbQE tiG@h2Y1zOv>K3/O9iL%TXsWb`Aּ} 4$;}03##f!4ޛ)&\ co ԙiNg$QIi3gޚC 75}gΰkF+++M;Tzz:_K$@sg(ښ:!Rܽxo.CI z?JӠ~" ,KFJvj 5pCUW_ dc"R3m(Dh:?y٥HH8Gm͚8@'33?tu-+!MSrrrZZɫSBWWׇ]]9 BʻB'$<= }4sJ`AMD}4vj E5jiWWGgg{[kckFߐBȩ 7 !:]Gʊ+}:h_}~&vJ4h({u֬>YNJJ2v"Vnp3a<}էnz ?~j"4Mɓ'B!E-=܀O Ĵ!+OЖ>BiІmϳtIOOHLzKK}&'ƒed2c1O|dfJTL !4_r46jD+cߘ?}M*խ?B[-m %y4Bh$*((xxǎp'֮jj֮e>wss[tGF܆X ~?vi܄\2=<<}`14)YA Bh1Ѧnq6ʒk՗ "k8Ս $f B[.XB[ڶ3gtww_r%;Egeeűd8<I_s֭ۊ;c\JkίÔ}"n.6!S ]ZZ4~bDpkP3fܻ裣SSS/]"4r!B߷[A~Oe]=ViuX]ׄL>\V VkЖ"4𳫫Z^D/ TD׭Kh~kiwFAP`rq= ʣ>^?թ) gsf0{by$i00B|̱(.](#B;~~#i M@DJ;% gC/XC tsS==!4e[DhayQOsԲStQQQxxCw~}j '͟AfЂ9'Wׁgg;!+# EP\tsTzz =}D8e%D- m֔~m~+;nNo }]W,]DkO=$!4𳋋 KStYYYppe ;o{ &~&ǞγrԕW_u?6lOi}rjhh#ó&}ٹE8Bh! 7`u\_'G,4룁!҃}TQ(B/^oQe˖) իx'$`|^]tˬpHH4N70͝eE"twwwbbbxxcErBEG]Vʻmie?67UV-疿N۶m}_E7#1?ݚX/QޠR gCMz֬Y766ޅH4;>hvZ|Y,еMڠ B^ԃȫ*;Dh&xqf:KkʎعsN2SD${$&ʟ71BҵZk[Gh?9L,\fk*N\.8xS(o]ˊAz a;pB7M񑑑#s!GƶB.1gJE5omHKMRSq)'4#!mF<9 Vq1[` $] f]۳o%7T+^;v2@R 7Zuu5UeBh!4Gc^!eFU||4{!t_IS`'涆XE誊:&oŠ֌Рw_yEKkH֣n|;*EK0Lay̙oE! v|bo Į٪ySL_`7..r>u8W2uH\M!RQƵv^j^7x~?,?'#!G 5N3!t$lee"*%ҢtZ?&C(nii ~vuuU*U/5PP?FH￯;$ںg\ k[pg :̧ć;?^ƶB+U۷l8h#._>Gq'-Xe)mz̄pnߞ? e׎9s9@RܵիV<]"4;;zh\n JO70.]Q5s '~}wĮh?kqח͑K/C PXXҥK&/"vdV$9Z'cέ.ɓ!z=7BhY94 ʉ'?ˊŏ=(,Ou8{_J\ #U#X~_z{{rEpD] ëhR6s襪CJĺȳWFBhdEed GN#ZZV$9BGZE)d̎`?_X/C/ 4΍qn}}}弼<!) xe_{6mBG 6p/<).%yooSoꑑaaa b$?^] E[BS ZH1!trbheY"2S ~Ԩw*Dvd=ƂZV65* }ͱ} 4΍…  v8'W6//6& G#@Ljy9X)-6p32Į(JHM}~<%/o9ٙ7]SSh+V o)!BK²YMvj)Hyi@hz: t#4G B(߿6B 88q9[ll D2V=>DZ8VM a3vĴfzxޱٳML???Y7#2x/=dd)Mt&ZH1!4|(,,H*-NCFF7Yq8Bh{ChR)Hĥ 9!!(:B7= 0dŸ|E#8s?±~3px/P]]>o@pC5Y ,"vpQʌ-,H--+.Έ[v:99cÆ i;vRРzqA40!!R:vgp'g261Q[X.:BשjB$2iR^$:c #۩ "IzJbh}]K+BhEhOq\\\ddN[ى'vɽZ}EXKt>AChSp\-,ƈ~olF =9Bg=='vO)))]]]Ehe^Uy-;+.'+e'%%II$JR$)Ņ)EE60? J `܄qk9YXpvflVl4&+#:3=*3-*#LOHK%C Sa$ZCCB_ p-Ɵ뎉򋎼uo&) N ɂS-@{$%4)'NéOKM KK^ #.4QK3c".kp%y¥$qY&'Jz.BIz&.ipIQ*qmIR ;YR\\O87'!nx_22 T2Yuu`#i&?ܾG8ug:::֯_wS?GoxtlG`D;i֌|=~N!:>^^bۚ, +BhG-tZ!+WU6jM*µAcj{Fz{׮-|X&`C=[T57OEpݭp[kmvIt;Z ul'www$I]?!NM=wZg8Z7kOw[SiJ  yl.W+_+~mWڮѹ{.ruP:pM'.턄'Nl߾]꘥z ~}Gh+Jq#~c3$4>(ZZZ,!^l*fZ@Q#322Ο?KQ9sfEE/P/` B7l@ TsXı~{ |@U|N {?@$G 8| RBX|x@6ʕrlBϞ&y߾}aaa)e@dko|D=L) <ʘ9n=zb%5oAunZiȡ333r`&rBh[R!#?먮׷VQMz1ԩ#4G$hWGpOfx&C&nXXMsaa_yyUB 5uɉ D-0Q+59<=5BK+=*+=:'36'#V!++6/Z~.+7QPXX튕XtVrQA6HK~[ёP\Wf N #jeF^NN'+Kx;7 ٺFם0֝0NNGu' N&z^NJt' v'$ ;aDF*ٝ0w'^]pW5z+.5 +qLt'LZRRHt'I87>΀h/ZӢkU1)A5)>()>4/;A۝~B[,rL)999$$DIҁ<Ƿ9.$'ٖ͛TPn"ZZÒ(;Л>VEjU JYIcI$MujHv٪ !zZ֖@"˿9U7z4QfGXۍdwBGaKG![w.ԝP<1u'u' } KFKz/!ѝ֗.-/a+o\޽ju]5ƅ}^yOFZYrs2I^RrRH !gRPKo_I}R3 a*-^GKm8Ǫ8s6rq{OR//o|sԩn~B; (B;.t1ddd^+qY1@oB /5k1h&)lPıLBM0 ǎPGZOԳUL7ʡMX!sBh j+ef2[&Qi*e&.ƿIEOG-ڨi'8 cM5U5&0} _<&vcci+g瑫 VoQmh ۟4'_Լ[O1%BhBhddkŕ۸qʕ+ׯ_gϞҒZ&V=/ooOf2B>}Z"Pu:77Wg. *))腛) svfhwww)kn whGOmEsy55_8k$?88/cϤG/4' 4 o_$l^HhY\-K3 ~_l[ЕVX⧟~ڽ{g߷brx2Mʦhʹ`md-t08,G@UUUUO_Q`fϞ-qC{_>!&38:3=&݇NBh>]W[e];JJ$7o(/+I+O:xb`0}ŬT4pm-f"4ޛ)eV;Ewuu%''ѫ5t566‹ $Csssyٜ *7b91BEmK^^=>E4 8v8Qݳb-MYAM"4ygm"Bh$cdB :TXrR_dRR7#[JyQMuÇ׬Y-(,{?{=().۷*a|/m[qEMs`Eso舉 hĢs.dИ1cNˆL&rP%6EHm5?睞2MgnGYRT[vmaqt\4Ǣ(&)zތy?#F2VB6 ]g0!'*L5dAhX2?ۇU_x ?;h'\(ɍݴiSAA^iŪ҃wΝVQҼt'$)z˖-pwwwl:JMMMaaaTqqE7ѧ6A x8Tu ~~;63~{'mJtPm}:#xG62_}|!4Kȶ‚5k]YnͽޓR^Vp]wm{^nR2۷q-ڿ$9iE@h+o&000..>_EEE3(B?J! Q4)0uJ'L 8hzp'F&BOBh!% Bs,}.5diBe f@lOV$Jx(޽Pu]}40;+ RN:THYVLy޵(zL+ B7; BSJAAAj>L_mܸA A!M}aa-70б^juu>>=>߇cgңu;w$J4h?")!4Q2 YvF'`eJ?`NDp1jl LKM\vUFzr~^VttKKKYV|颻Lr6mbAMM*z5#>0Bjnn ScN9-b*..ɹrJEEɛ~eA9n.E;ptu5T8?@{sg_]]]p%&&R gR@=-"4!"tM>isZNsɠȒ>b.I͔S٢V 7o p-*2?޽Ե:wy+$BczqФG?%%Ŵ&&l@p Yյ&}ȍoWrCD `vP,p(y86gq;{8{j䘘㊊tN g|s1ҊB ).mX"ۜUʒ&JuÇA @R^6(BSdFhL =66600ޅj_t$6cfTk+>wnϰϏi^ݼ6Nݴ IRBCC+B; #J4Zdd!4w47766(R +4 nTӵmҖ`MpPRRqozxx{„ rzT~c9(;R4,00#oݳ/q,\cW_ wb x˙tɼ%I,&.:@hM)+,VdQFF]+ejUUC}MOx6uECh{m ONN9A袢ʊ WW&7Np@pKvdf5fSn^*++2_+уFGˢVdr&ڞb%˃h2cC}#D"qss#;.EϚ5˾)z97Tn|\ݷ"^_׳Xe&@oE?t̼xuDMM Q'^D# 4LLOnF,mL&qqqT3ߺue L (R`J4p s) kd{şqȒ\i`>swP$BV)gH/0(mZɼWrB,m5 4|8P7PgR@/B-.Eߦ0 pl5&aE?{y.|[bñg ζm<5BhK?hF~S!֑0pq-~30e9Fh"krk=[/ɿQIoPbk6ЖhϜ9T*=<{vϰgW fN$?Ab_0EF!3j:OHfd._ɖ% VbBvo;< 7!Ez޼yΝ t}}}GGI111X:22R& Dk0 (xKzN=+~c3@-ƃV$L*jIHOD=B fi1!'`Y]WdG|O:B9-5Qc_M˶7J s8~\.wuuEm.rCmBE{?"ff[^ ";7s¤V111"ޭm3ZXGʤenCp!& m!4BS $BIѼ]۽487Y*0fx Ǧ73㉊?}v-e.nxxb3#"## mi!lI!I jLlᔥ$_CMmQ~={v^^Bh3f 6VS!BF"TC±CbӲA0@bZe/X^<:<<,b%ue,BL7@[r2#9ͥ>'(ĉڴ47~Up.=@K.??4nSk^ٷd7tGyyyhh(3ܛ}v".kChJFlBʲ̔İ￯clm?w,"ryI-";!)F2Ajb9(݆cpRlNfr*ݬОbW {]=3q}SN];6!9bB )[zp K~Zi*hq-=m0Xo+J }6o^Oע>so^z՚5tn }[#/#Ӏлw-//7jQ7aMXbC2∊ZXܶOgx!I0,dfS' E>B`n\N:h<>kWQ+YF&ed wV$Gس{$'':tpߏۭ"f7 G/444000$$$77KO? B?6&<@i c+UCCLܤCsa+&6'AfZwB&~6}W7.nv昨MݝN=)!?Ι3fBܿmuΕ@%%%EEE@~KMݸ~ GGG؀ ꤤ$LT}!CмǟЏua[tM3ja\s٩!t3ZTC39aHLí!:ڼ@GpاrB7&L+Po鰫 (Wt犖B:/B.ٻCAODmN zz\^UUUQQMrH$h2XzjEh9K_{Fu'lmmDNV}1H7~Eld6?wv}:+y/6 &ncuEgY]WW;sU_z]+/$ZPTWWWVVE х@wdC4,]+/EfZѕEW|/?tGh4I4%$$>|x۶m9r-[x}"1JOr帩FljFh''ϥGB&?qs "4ydK\ffB33#  [9]E֖?<&Vu5ZT tQQMB,tᙀrYӧÕɀC7QW}"mDǏsLPkK `iMAϠ3`p<,+2meNH+:ZVkhx_L@hJUP^UEFD6E@Զ-r2.tHHʕ+yy"q^a{#O\Q]K`gwB9os=;N^9Y;Tљ(*k2BhZ-jjjȈh`撒rH-udoB#~W;!b5]~]QWWsױcBwSww˹sԓ]V,jB3k_uMF>7XB ]W[|EtAF,ҝPPB[m]Xv2 ;w]11bY?B달A/>hQ> 2YS*`+ŋ5 "k#7ac9wb6Ai6[J'Ch!Zd]7j9!FF6L̂еL(JЧO޺HX=dL@MՐ;?Xn[?_˴-ל2Y(#]Jf(ZYBM6 BLRl ٛ-Ǐ:rW3Yx Fѷ+Z(>aJT[nə7oh\8xӦ2KrqOI I6!&(h l9ff3ɡ!!!bccHJҶ6;ϢȇB(6 |mG2:Z_}խ{Á/{yaee@~O؁!Bhdd̈́---,]__o7-@+@xǏ QXXXZZp. ~Kobb-_>|î]{0`>vХEBhdd͂d4Q\\ LFqj[DhjGhYBDS||<1ce LQrVH2űjاJ NoXÇ'&8:GyEWs?p jZ(!FF6LJFAMEEEudtvvFELf޽!CwBnV|Z[UAa;7**ʄ~ھe˰::<;BˊVh$aZ`e]8ȑ}G?vkpdIqƂЍ9Μ99R+??Ԋ,hv%=jIxULpi4>>Onڄ57]]]* III7mzqۓ@$aZH=gLthVf2MNֹh&(Bg2#++ ! :{WiHՍǺ~jiV_dgҏ<_$iJݿ97UWS˄tJ,&''SCtttܹ `H! KOKP+56a8YpBC,҂YWܹs"qzH~Bl.} 49sp Sa] vO;]i1 hZZZxxxH)ZUUA#+VsAh!B @Lt>A6pdGB2C"?Q9S\pQ՛9V,WS}#47ss>y|||n;/^ܻwoaaammmggɛKr/B:9)AK2kF!4<(h lhkk'9M-p͞wĆXqǕI}7w2Е mZwB:s^^̙c9 ?:uR14 xdv]]],am!Yx6+݌ChcuubB(_"s,81={y,Ӫr/Z'lFm6(h2#??HOO]UlNJ) ~Y<&Fi1] {{RR,Ж!45;!0 V,[h\( "NSGX &"M~77Q)a,2 l~& -jJ| DzWd"?SGZlX{K˖a Bh.B-LfS' խmt) lRi9y0}֮WL*:ֹ,z!]'g>/6f!4ixZLuuu-{xxԦeZwB&.5 }W7,̎Vʱ!=9sPL&(\g2#55v3$˝#6M;8ı b㫕=h.E[ رrOǹ0vE.2­\9Z Y'Bh4qz|֮zGayImTF`Ah;N_#]eg㯿ރ|Cp,Olp*'؇<4GJcScc"LYyү+ +"vp:/Bn fN!4njT;;M8.3L@hd#!őLFq٠v5Bhծ]ڡشwmW8n1=%+Kmy;[}ΣFyy. OBq_iYލc g@ }}Y@h.  ۚbR:EhKgrd}n;?p>?:OrI8AW|#Qh  ۚE# Ghooo._{LN_jmG>Qu5 &D |X B(mیm5mmGy e FѨgG CqBz+++ˉ₂D@tuu"t/FEhT*/Qܚ&unc0V2q4+` B!BWUUY4B3b-µb52k6+5|6Q4+}Y9.@h0XokB薖DDtIIIaa!h"ѵ"*Az t,#BoFo86%:LfcE7ٌq @h ׷ M `0X|>2񊊊 sssQ ޓr%POupugee$$$Dݭh03Tt43rc8&`4]c>>..NXm }Kol#mllD2* 4m2j++P/d!B ]1t 6%"aboYk5 FANFѨ'S\M4<<%Ikk+"gNh…\ \/Z:A%tɄft#4 y< mFh|PGtX6[ѨWUUiQh 4夢mOOHM ˇ."}Ceee%%%+h۲&Bhの!tg'p(2cf?0cOJѨozX Bs\ЦqPp'5m$&tz{{S@h@P]]MDDxdm@hHߖVF-gcDZ hB=/8ZIvhZ5Q86u9jASmsFBhEEHrI$E رc"E'>s#.^*6Rmm# +Hlhh R%|vغ5`֭doA޽k۱.;z1@h@hh`4(1Q/,"lq~MLU,B7B T*upp .SPʱcLjΊl"t A7/\z/|1_ ҩ[C/߸v4}w>DZBA̢ۆR4ꋡ'VbqBm-f#!]fPn\䀺gϞ=~O?=aƍ&<εQ#ïD݈{+1>pRBa"twg@huz,qҨ_zg۷5pAR@hQhLaZ U1*6BZ3J*.^8-Y,J:+Bb3G谰˗/Z)JnܸV$|Qy "&7Yg{P~Y͂jNzevEKn] 4eTN4F"#!4Q9Q-BS'D"<=\̟˽]RJ3Ny} s@ᶶʌ(RSSKKK[ZZ b\/,dfB7ꯙ6^g}kOgNԩ&GBsYL!4ETiR]#4u4BwY;T[ &T*mllp\SS?wBb*kFd-dggvӦlmO7!9 ln1&F o}mڻmGJLx @G0 qꄦO'oKqvֆ~4nݺuurbbbaaa}}}OO#RMMXۄGˆR4굡9/W-WI׮ӣL@h D-@hYNҦM<==6oތR\HElm'<3ǜqm0[pG:PPՇ(y(6=E2ErrѣX@h -YRFX}}Od`Kayaqaal$TTZZzzF0Ko##ryĿ qB 3x===l#!H7qVm}Lixxo+fa`v:k(Eޜgg>YYS `LڕMMMCև 9.@hXZ[\tUݭfuy[$ݺ.\Fh9[B @>p-00q4١hjѭ(fcNV.x233xNMMx---k֬M憺Ĩ˗ ltЅCOrMl@h I$xŢR8GئMJ)~Y}v;;;/LQ7lԩ|'''@h. dno.IIvB#`0X蒡 . 4 4}fOsF1M죲& x `.xqpp\̕w{JMq}SCeyin~E],tЅS{A58n6eGeM1SP x74@h no[53 4 48.|e]/c?9L_8^f[ْqBzڂ6Ϝ9~cT1;qlclc9`0q1f  4 m%nomYͤvQXXW^^cQ6M̪~3ø{ڒqBz[ Q3i"$f]Z!!!43766GFF"F-Wz"&‹u27?>0+ٚ8.3{[[9.@h0Xo+!ti}RcZ+BUTT!f.,,cя7d6X?-p|<URR{e˶ܸ& , BJ]9bn"rmښ9***))z``f "4?caQJߦR[[7߬?a{R^yeO?5L@h  J=0+ :O(4W%4xlzMMM%DXWWwv _`f1I6091_PoqwB䬴ӻX@h  J!C(520!D6Miii7n+--6At45v={j%`.DZq칻<ՅfJ1E6 r 0 ;a(gW, 4mbn޸tgN;{߳'n޸\/в' CCC}Ut.ʃr ,=( */Qo@øҨ&kl2^}W~uz0l%ɷ [`~Bs\ЦtMuɵkRc at_(VOѪջ65%MBOV{z>e%x*ڀah_ZS===Yr-Y6c2Dº6E],tbcnBkBhc@u5SOȂ%((*7X>=3f}@*#! ӕ:\3fMȖfif蒡 MZV1 K}Ab[$ 28Sz}i(K>Y uz5@ɓ] bMTD:u2d9w6*$niibvNxddiɒ-c䗩BNk1,>Saa)M,Йϥc0 %@FŋlboX B&C~C&زeKxxxgggqqH$Zd + Tl582BOi3.ٸq15RUUpAՔ9.=FY A¼?"ڡ :*9 "83b9N8s,D2mGї1u{o/>x2}GFF322x0<< q?+3[&+ Lxέ=}ۖ])RF蚪܉9~'}:QϳtVܨĆ,:w=D?(HgduFzhz2??ii߿s@XXT*կNe JԀ۪ЦZ77ݷ-7(ׯ!-D~T_h7>sg_沬gMk-_(fff޼y3** !4i\XNeHy!SF课bL.~\0 Y@󹳁(%Q⥳"~}Ю,\`)IE/Mf%%%111555ׯ_'OO/8qsy_ _ZzȈNb34 2C Z&}ݳg?񧟞e Bs(~.aUKaa!Db]M"t̵@Z7񳭭mGGGCCùspӅZQ{u ļ]=iiXw7V_͛\]kjZ) 4x,NvV(Kbxď> ldqwA5DFK%͊NIKt,Q\fmJϟ?O3gooo^^^ddd||A0Ayo*Lb9plnkSGFb8~_hkk3^ZB6KK<=\̟˽CR v4"$6#4fXMlhѮ++++"""!!Ekd1ab9΢jۺU}}O<_׬MN,E*g9.c gop6f0J.\8+QzV![5ͥaO8|&񳃃C R4==srrrkk+WVV2ZVCG: yXSaڿ 0?{weUJ^a&6m5UBs\:! 6#F<[d28G!(:>>񳓓Ԩ.D/`R!86A?mM^Z6<8æM|v@ܼf9.@hS:+.Yv z -dD@ jߤG j8vfp[W! ˎ矟ܾ?tZzfM̌$@h3dYɀЄF-hF:bݣēyM~f%yC臥AsΜUv̪UbUBs\ЦtAqkLmMiЄ-7zppXDߏgx&a"n,>#WsK6q_ [Y)KikvT*Ȉ@fn*@h ڔnjuJfktBRW޼׵-, ?===%Q@AXj8mcX23<(|Y;~.(Oj'L^!;;;;222!!Bs\&vKSlFK\jjBʲL0׭YH\N$+-Uڲbۇҟ drc+I Bg1]v-XGGy-Ҵ==i̙="""..AF ۀ 4)VQ,~nrv⥳E/bЄ6+S }رbkUEe~~-υu II{>NO]TTX:&&F P ~ڵO֬KkL\vɓk֬`MD4tS֮T,)Yz ȹ(wuV'T 4MTWW+%xh~ =9~Q3q#º@&o|2m ۴"!DHqwKP_`M8aCt[:B HSc S:'Styy-i='a= 4.)ӆ'&劊 QQQhpߵr|(&kkbQ@h.  3r[[[K Qf[:B[\ B@lzLPz=92?},Gömg5{pp"BB7-B+\UUXڵk#/37oފت*,$IZ[*/\8XR:Rmml67%lX$5Ckְ},D?/=CCC999˼ W1~΄׌% ҄Yѵ# lF FnVuuZj=,1BwttgddΔ+ t^ۧu:djD &i]Z?VWW744c__qiB蚪Ғ; 28?SBN7gT G{{{\\\dddSS.13yYu)B?aBAL޼Ӽ555))) O}07c@p\ŭ5↬芲 QO`04UVUegƠ5s(4GD׹Hu:D<ڌ%]Џ(Kpp]jxxE})t;H.DEsE6=~*yGbb^K@h.Ҏ^qzvVlfzLVF\vV|nvB^NR~nRA^ra~Jqajqᝒ;er^FEifYergUȪmTe(/.祗 dbSyE% QIyyo~nb^NB~N|rv\q9Yq9HdΈʈ̈LLHJOL~'2NxZjĝpo&&NM KI KN 9!$ lBỌK(芠Ks'YvB,=5"N$w]֌(CZ~uɊ͕9 Cv$& .**HTBfC\q*qmnRdٍnJ٭̯WdWUT\!ˑKJ2x%C+BsZQᝢ‚Ԃ伜DiJB N(+ho7(9#VhFZRRb dXlnnly~[_g7''(良L$]~rNHDwV(JD":o>>ox{{-XRBhaSM ;[:EX*DtJ7#K w4HXBQe垑?I(&W;tonz{[fOɥs&{@hvZ,DFFI%C3]_WőazU*ɶl=<`. ~~W*>U d;_~c&=:wׁ]Uq 4\/(дP3B24]UUu[cԊ@Ecr|Y:waDm"֖<}'Q*66j <B :^QZvFtzjdFZTfV|ҢTك ^zEYFeY9rxlU ~E(҉.0HvnZwmYqΈJGJ;,qBGqK^п?V'ć>`c2EJ>1#oc\Tg,ADw <][G!jwJ8!Ra4S6][ELj6|ۈCBBx<lXZpYcrؽmgњ -#`|μg-LAW0 'Q4ET*DvMk$'$oW8B *et&`Q,j.G،, V@esP"x wлDqDD}k6"T;y}bgtG$,VX P(DEqG4e!4[KFhr: 4@.-n `ashԪ1c` |[(B_[;\PPH9 c1b9>WWYZZaL6|ʱˆbqeq).L}WZZ8~b6M&zkOd= O^0dTQ6,Y\.l*/)̸{NrqQNS=eRI 1-))....//7@S#?l,G)ݧ }MkֶFd Ĭ^"Ckv$&$R~YYZE ӤhŤv)r 4Bd +k?cm۶=z4::_Ҫ9IЊj 5Xj8 }-otpD铁ID7R͕} B2?qv·k4)ItZ|| f˧Hst֭[z#G߸qĉ֭E/oݺU[SvHYջ(wub ?Bf)C =~BϏ>}f&EK-W_ Zh vˇ2#lmy0P9)))_MKo_oa3f S_BsY4d c$r8@hm2>|AP/ߵk{ϗ|||D-| sE¨sghbZ* I@irTl88Ȏ+ 3)N!^ΣOFBs\ 4LJE):P5BNfnk:mfz[6XsM667VV{<ĉ!F) T(}@;=oO:(Mӎн3/4#H$b䏐 NN35bJ_ңߴiCj8]cO&O ppU~MDqdeeIQXh@hKoQ%Pl|ښ_橞gKw_+޶m[ЭeI"$ܹ\idžƍ{}D 2M;ZB,"'Ֆ6DGGw&a1!o!:m.uttUM:YJ~*P'|9KOL~:JDYL_2 [_?7t 4ǥk,49Qu(*Dh5R)(lTVoڴΝ䀀M(葃v6OE44Ljgax,te~op3DIA]xd@Зawp1*\J`q@hQhMΚl kJ[;-uu;v숌]+ܼyCA~vYiarr‰'jjj: ;rp'vA>|hΝ5m^M/~U Go>*ns /HҰ*rbggrZgڐ2Ec3ycFThzf!tL'C5C5'vX?0Snm]v8q63NRb9ҥ6 # D"JKK4vq4d&ң[!Y5dxۇd6[oM, ͢ 'O>vvf֒뮄s╜b;kc"S4Bs^ִ:!,pE}b8))ɓO> HV f6pG@h7!!!11m"i@RгTGVѕ@L"4RWWWXXվ[݆bk6=O40l7b؈:_\u*79*-))9jhV 4̞9RiTTTFFEu[niZ$Į!#X ǎ> Sbb7߬yŕ?_yg֬%%eը:88H@EX 7Ιr#fnEDw;{#`]&f1*B%%%1BwIN9/AP=pie#2̿iZ;sCFm渨XWTSkT*PEl lzs4 MO'3`u=.Tv9. Fd['t#ˌ螞Z0fꢢ4A!o˔y\ `,\4Co|񒒣"#BqiBb^q`ϙ8% T8UJ7A0yBMMM+B?dDJlllcc##w&9s)S-d %я2Bۭ Ԅ c ҄e%iBaVxhAVhc^+$A_4nV JIpE^pᅨ 2y:ʧjkkҥncX/g- 4ǥ :%c4QE F&jo MRj'W6΁Mo"Bg dvJ=~rDټ~Eb6l鞜]]$@-gWWvռDxիjf+@h;%-j)z t:)1klNmEhf%zpp0>>>33Ӑ-[_]dv/ϑ-tn% /zhRRR}P"ܢ"F>eH$TbijjAVrr)SDSߟJ~׿Sm}agP{v,طxaۊ+Or~+SQbb qq \ kGVa syi&pU6M9 (/?ݻ7<Էf[㫀oGȚJЩ^`xxV> K1 ک233) )uݫuzؘ==udyJe699;wN j&;;۶'G;(*mkZm,B6(v1IPN+scG ?/ @hFh&<e&.Ai hB"?!!H$~Q#Ȋu.ӗ.ex%YfmiҺn׬w$O++VcQSSN*2ye%~' 4\8wm)- ~6|cFiJ^H@_~i݋btU~aܝs rVs+i5w:L$=:446+ ,kn~o0 j+5+ ޕ7`>8a!-嶙 ~F億 &׻7o:4~7ӔY;@\뮡zSŦ;,J҆q!EJ:8 ^^^9 \ \77gθ932s۪m^S@ oZ[h j422risOq2ci>wrxo5 =2򾣣Zn$B3+QƫB?yyy:9.!b3gsfL.JqX-|Y 6CB!Boٲe%I)4T *:77du:³}-=F*K]۩Ü0ն6hL}}}DEVVێ229]я3+B3.BM6r'fµkWq[j7 kV*E)]׭Qx b.Js m$9+;;ӓh_wuuTNee'f)tuգ=RTBJBTݬ<{u!!Qq@h 4NhTHFFɓu"lBR=r5ju]]}ѱOs^zi/^ziΫ?jff,\TW sa GoۻrՃ'&Uxx8P 74gZ c֭;gYр 4̸q@h.!4>Jѓ)(s,t^Z'%/Y?.^VsgͲYeuVxr<nj`>)SV,ߚ;D@hb{}=٢~X x9,9dﱽ,B`:Sj<߯XQWW^VQ?g7/4(渘Bh Ô(B5`EBWWWc?cǺ*Rv8m {O~Nվ S"جYd{vNig#&PUU]Т_w7/''Oó|`dF_z"7Co"`FWh[~!U}}}AAAZ'`~V'pY 4 BX%)Ж۶mӞIxa$_Ku>HU]]ݏO =0-.:,ص@,4{  4eVE,sFh=QJT $oa5MZV} hІ(wu#7 F]UUkQZ2n qGFF_=)5x)n_{)*--g>_tj ˛1cO?}ů9KgwQwwkBhv+29foM3_vp9#\?~<,ڂ$ /͖UYBAh$W:6Ry)fd8RLD̲ˎ>HN\z';˫څۿedȊBxo ܸv]iyv+=zE)d  i6 :|w~~8cļ۷tqNɢ-K[!kŊfl*eB+|@w`6͐?,=6]W1~acħ_C ]kr[s4,͎nvQz X2Ba~>mzjŤvwoF/GgR;3Dh{EOaWstڝd* 0[ՊЖ%'s8 4'<%J_{ 9ӥK/ݼ=&~Fڅ~-m3m^gO<>lpvQdYݟ}o˦Fe+JtM*fб35DW?'P'Bc*"Z)][RO>'vS ~7cSxc^_dYk6 68 4 9swwz.]FFfg;suuu@˫wD'gFd>5k&\O0yd =6tpAv t]#iNm L X)Щ1`uB0qЀ椀ÇBE5kZ[[i .LBfJGg{D\Hރ1@qiT-O>|OcJk!$<<\{> y"4/ ZVkr_jʠHQzFjS*@h0qЀf3AA/^(+|655䘠j```2'⯧.S#MJiiij:p_cR+D "__姤d>/tњSWںkL?%ҒxX!4jYTS:)t  i 3n U||^D]Bxxa:+Gsl+b=wҩ~aO=u뚌YGG~E|YB+5qVMTe`fO`0BzA 4 4Y`..c|K#8jHE"QFF)̢r8(**RNlk]F||\>}\jd/RW~?5KmeEBk"d@hm@*]srb ز|NsHZN>{ ov/tV_YY9@=<跜&^u ]%n[!ݛw MM `N9 - MЀF'XyÇdFpJ'SodXȏS;4^%DS>"x<ŏ/^-N6UWW)44ԴnǎST*)Mq/1;;uTI4Ye!,J) :9AzΎfAttt?n]@39{~y'.tN_UUZIL4́h/s݊" w 九º4Onܸ|+ًg.]YBkJ1BSԫLH*@h0؆@@hsPss~J'T*?!"S7v9s۶Nݼv$bow't71!t''OtnJ***ƏIgb zWUl: M7Ds=ЭbÊǷ./st{׶_u_,R--u!jS4U qbmMmZQ mY^K޾-ql>w6Y;'b4NδN:9[ZZhNN^^u7tPB yuދ³Hlkkk5MG!-n3UzS2ٕ!46UR~*(PԣUxm uwwkEquDGFFLHcK$]cYU߻'wEEKH{.ݾq3NArrګgKT 2[h2 mAJHHCP|ّlWff ZH_B嗧J|3jwDOSlN:@DVHHτtG[] @-?/;-ƛ `!o *򠜬l@@hKQoool,TKZd7Ƭ@G5ZqY+BWEƝ=f-[YO>jcc#b]׼@fk<"4(tAAA^]UUURRB|:::uj~[9.F0M)oiJdڼT顉('B{}Y'.ZWfxx"7ikjjnߎy;oܸ4Khooȸz)6Uo!~`J? ?~}iBO?/wIӊǨI!..r ^~6b=vmҙjzL+裏,c#4̺U(%^DD7 4B_yeѳnyޙ@IQ쮻/٬yݍ/ Kt_B@C܇*fxn31"bTP9`hf`{`ٿl{z}SU]]]MQ]%I=\3uuIJJR?}Z*j&jL,MWw7iLظX/j9A- t';;CBG8VygpƐ&Cg}(qKʳcǮ8nzm!+Uqwao߬.=(=(n'~w]>`ʛLzv.߿0I3^vzGTVV60̮]ԗXABG8%;XCh$4 j?ڟ.vKʎ fU%}:]$B ]TTt)>b -pky6zfLԘ_}yᦅZ6~ltAe\222~_O̼\M={($tF w. !% MB>Ə~4?3nIyov׍7>t oе.mCC4GBOx^%rΊ|,N_ Qs=zkN#G233 > #B{$4$4 ҆.;G===ڿ>'A؜W_=+R{Ԥr7/͗|tliQzT|=>-, yyyCX끜.))II:$tc{BQ,JBwvviC IBI$th2|x_uWLj~ַboqp…1?#kؼ}v;::̙3*ޖ?o_aa1Ҙ_IR~OsĥO-]RuNW *** }=//k$tB $t_xXO_zbTԚ[c {W;l\qE>sɒV˖=*k`ʀI9\1e9|mG=֋^\eE/Z߸>&;fƣ3TD{uIc .ȁ#n 9Hh Ih: wtt\~l~r62Ƒ>RJqC< ~q4uTT$ x,-X02:Ze`%`)>z&_z%^ZzQ/~X>ۄ>O?ոﺯO;BAIIIqq|[dr; #:⣡UbHM&Cެ}D,sݘJ*ōQ׎wx*igBJKJŞ<6vՠjBT׌viGBϸbΏwz ,Iw;gq/LQ˗K 󅅅x⤌+\wq׎Ӟd  MBْ$tҲFۈG+{eŝw}.1p&ѣ_K,n%vl۶p5+V}ɧ޻e f_K-W'xO=Y~Ą}k=b s?Ԟ" ;n۶m° Kȷ' kBg'VTuwmцTEBc%I谡J}.`ʽWH+^7V*Zde˜-y< 0zO㖘ؾ}}}IIϭ|G"jЙnoo709q|Pe˖_)ch! eB>VR矝>tډa>߹U%Ƶ}1$Ia޽ϭqĉL S})OiiiQ˄+zi,O茴_|>ذ-~n†놴\brB*w;wN<.)R1؂ǖJKnv MH',Z訯<|(1^ecIBၨ5tǤ)F8Z78x@e$ts4w-%{zz=}PhsθUs'cV⭷d?{V~}"^5%jg݆W8g7 kB6+M9[[aĐbx1s' MB999MMMF77gΜ5߭U˄չ/zr4u"3cLˁ0}{D? ^Tc>2c*H'\ZrQ]YWF )}1,%I:BCBG8$4ْ$PurrrO!s…<>uԩk3ng3s#+}[o)ZGNV'xg]e{9\/pLhZ| I IW]т]vx^kWK'<عB:!͖&:Nn'mm|Qtq}AQ6e\vu?]GlM"] :v'ϛ``C&.u6žѫN\XSUwЇpHhD%K7 1 m/ S ٳGȜ%|@t %ڲ{{{Ϝ9>nZظb+~y~kVׄBɻe >tGxlX Wğ0+]$#lIhBQnvszZh'חk;YF1#eDHs϶9]MpSߎ}{ΝAev'~يBG<$4ْ$ŵgZIII~/Arrrooϟ?&w0b]в\󔖳Չx7mG}Iij-4 ЈfKBC{o%+?lLQJ/X걥O/s=GhQ|ɵ?n߮CZvAij ЈfKBC0p tnnrv;$'' p+\%In>l9#Z.h^0u:,CBG8$4ْ$_ fIb?*yO,%q>}://OtN@ Ge_*}}; #lIhzKuѡwZzK8Ihobb^馩vE@BG8$4ْ$EJnN 1`h/&{.:!͖&. :˶ =.- }U$ͥ?p2P #lIhoKaF]]]uuޱ***%$%7p+;Q:!͖gI0 !!OXz/-pBBG8$4ْsmໄ~MB h$4 !tuu@B8qսIh0:!͖6$ɓz{ioAuu߫{`*$tCB#- MBGƎp8eeezWD6;V*9r!#lIh$}bނ{ ЈfKBk|d}iI0''}]»_OСC?~nF߸qec&>>#6mz}GBG2$4ْ*~;77 |5SN^usԩ 8l؏ȸƍ[;|/Fظ?~?ZO6W8q'{睷s?V\d+q\?4 sֽLBG,$4ْ ʐ=`B>cSyeůظ~Tcz ?YzTc1Ov۶쏷_+KgDE oHh{!͖PV܃Pٱo`èC9]G3 ұLi"]7 $ЈfKBc+}{|~|է2o}W9T$y>u>E4C7 $ЈfKB#"oHh{!͖FD c-\+BB#- ZV 텄F4[1xs@B h$4"bkZlIhD0͵ m/$4 m/$4ْtHh{!͖CB h$4^HhD% BB#- AlIh:$ЈfKB@!텄F4[ m/$4ْtHh{!͖CB h$4^HhD% BB#- AlIh:$ЈfKB@!텄F4[ m/$4ْtHh{!͖CB h$4^HhD% BB#- AlIh:$ЈfKB@!텄F4[ m/:;ckq)8Hh"$hO#5yi2 o9ZCB@=3jrQ-S}DB@+1pIh^HhD% pHh{,SvWVz|#b7ZEEx`^|&t,/m[]ͭG1(Znmql߶I` ^|&tScEmu;>nQ~Z{[˓˿ؿ' rS>moS $4QZYpp1(7x[7x`^TqaJUE^KS䉲#5yY1(7x[7x57VttddgٽmF|R .GvUMU 1L8P]Su@[U32r k?>Hh+qOhaGۉfGYIFNٙrrs ddVeVeVgU:ͮpY]#[]]Q]!ͪC^,T^^*iYxԲ"1 DIEI߂Ă܄|aΡ<>uPٙ2geJߗ/#}oFڞi{R]ƥM={8i]h\%HIr/Mak$^,eL+^4׾\f`n<\1kHbA^bеkQqas*<,.9W6ʕk`yizEYX-YY.tbvgamuNmUNMUnM*]),Ϯ(ͪ(*/ob>Ml19[ލ]S.6fd{|vVЧ*gzWǩ3]M8%8.(mkpyB]U'. ^~qR~|yZt5 :m9{F*>]x[Ჳ眰iOWog{{}Nω)ɯ%s|KzM/x]uP~uZqε\\g\kEjkv}e%++k w[VSךメw\\]=Gś]∈VЈ8$FDDD Ih+!@JHhDDD01 $gBK3T}U$dhWSrFDDD% m%*{;wc{]Zt< ozh$MZG` wj@DDT\\\# m%BkLYhOhUDDDpu$]:`%P}f~>EB[ 2$ ~XhrR9cUf1j?s ]hI'_#n=o}N6T. cWgSUU4[J|&w3X?Ա޿9wFDDD 3} @B[Ip/ͮ`DDDĠ;X?0$7\UYV#$ ATcNJHh+!CS, -9p3vweq|""""zcEBğ>kwWFl_CC"*Oq]A>PmJ|Q孭MvwetX5*bqpRӧ2<*\V.Q~' endstream endobj 36 0 obj 13094 endobj 37 0 obj 104063 endobj 38 0 obj << /URI (http://opengroup.org/onlinepubs/007908799/xsh/fprintf.html) /S /URI >> endobj 39 0 obj << /Type /Annot /Subtype /Link /Rect [ 326.875 759.389 523.275 768.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 38 0 R /H /I >> endobj 41 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 747.389 118.39 756.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 38 0 R /H /I >> endobj 42 0 obj << /URI (http://opengroup.org/onlinepubs/007908799/xsh/strftime.html) /S /URI >> endobj 43 0 obj << /Type /Annot /Subtype /Link /Rect [ 124.786 669.693 373.136 678.693 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 42 0 R /H /I >> endobj 44 0 obj << /Length 45 0 R /Filter /FlateDecode >> stream xY[ܶ~_!(x}[AS4mڠ[3/[ɯ?(K=nߒ< ?J(HOBJ-*郿]t;1Y  9Mf~~fV' *NPT' :+s bOr4oZ)Q@oy.w1x/F/v .2hC8_t7u@޵в^M#wDtYqj"]j'0lsT0Hq*fz%`͚Ak6R𸈮m\΋MfLW2ٵYAQR֎G^1>j`Я7,yq~9E dYt:9G./Ιf>N(H=?ٷ $nL6fԪ BԐ< 7v\GaΘaod-[cY-ڊmz2.}7s+F^Q̇ 6]^m=<_օd>tTYɔ}23PT˶`+L.J&u1.Q;^aCǖq&QQ{lEšr{M]Ern9K,!$D]F>t!ԧ QiY8o: z4Xt|Fhrwk]pC67F!(nZkh-#bUo)؅7guavemiv4ZP>x&#DZ?սH4yn A$zS@6+HUysT8!ЭB"]k\k%N|c5˧g-}g>2mNOQ7EQPD*Xà%UZxW%> endobj 45 0 obj 2541 endobj 47 0 obj << /Length 48 0 R /Filter /FlateDecode >> stream xXMo8W:%ӒXlSd lEӃ,ӖIt):/!EJ7[ڃE|N>8YFEJC)q& a46%s$#_Ku.|e!_;/+U6 {f>A6{b}O2(\!4Cݝ ^m6}>*jsR(4ME4Ji2wv 94Ô6ϫgݽ+݈^ti XZ lTuڻTBJ-BtsZÖ5XSΫ.繆5Υu`Ě|f܀Er ɮN.!Bw_~_ϳ8Z>HO?x(&əē!XLi,j5 Τr:isi6 V UQr#ŅJ DG F;q&2^ر֧3xZ: :l̊;igU1cuxcm^ / g/$c48ƕMV6<MN?Kbb%WW!ӳ!28[FI͑29r6\Yh_դRWUyx-}U{N4:u@XB`=cڞ8Wm |AYQpR%6=3yyP[W㰯UY"EiʈJx(f_~,5 ^Ȏt7w(eB%O{‰,w\ 4H#sUɉnhii*ʼ=|['ChiW-9teSVe {KQ $K_q4\VPWWq"{%j9}L_I6hdKI\E޳\ҲQ䫁5<-&E]ώږWe7-.sGlB<}3AdefDB|z 7`&#(w%=ջBzSs7@el7G̹}t42wuﵷ ϒ.4 7β HEocBLvWRH`]_@1sxkQ}?EM1QxU q`&umvdjD"HZ:(7ZԋTwdHD#LZ;=y@4?3,JJ;;gs~lrLwn[sHC_m%H]vdۏw"x!~!.dwVp[N5(P7]@yE.OWV;9Gwl ƤN]"㊛8wސ*whp?TyVrw/ ygKH|tC#FvOK.TrU=C?4<.涡 endstream endobj 46 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 47 0 R >> endobj 48 0 obj 1469 endobj 50 0 obj << /Name /Im3 /Type /XObject /Length 51 0 R /Filter /FlateDecode /Subtype /Image /Width 471 /Height 541 /BitsPerComponent 8 /ColorSpace [/ICCBased 5 0 R] >> stream xy$ŝ[fvc366#7IhgfW=3{F !! 9n@̺שׂyȌ2"+nKptgdh0* Wl Wd[+B"|l&++ЀM [1sLt ӖJl>W77*kR(2bڈ-J2Y9eyu>*֙D(QaVu6xle[໪} ؉5#K v5,񫯎ЋX}Cz`_ecaTAZمgZB*Zɦ -ZʐokeiL!z(a=l y xxcBuC7 l aQ{"aNnnK/%\KTS΁E lõp(Dʄݑ#an3R0f?nm7W Xd%MnTJ5OE˹LS.Ixd$ 1 Kf@&31v)WZLoGTl:::wo|#gO7<؝+ ;$aH?fxXp ;aWU,gG$-鷩^3hs!PO*d"gGT72ؓL6*j34ZsXU&L2YQq3lF:_z=)V:g+!<7aaJd:f80paey/W7 mR=|v-}oW(Kt1_/RT?[[r'_}~ϝhff<,.twy7O:xW#=R&~Zpm]% ue]m*j ΝL&?+ꪂD"]2Qx`; |&we2CTe2ݹ+&|%clUbkt~˿&Tu&\H ;{>:OeZ/;{<v|2Xtac!!slC/OTGNW3lJ}0MILOc`_c!LGB;I-CT LCLvdH!ں[`%maz^cKF'>_w?&6hJN&o}t^B>s̱; TLqoSQEZp{QNQTi5K9T,E6qJ\S ٪Ԭx7"^smxG¥b>:8qo_s]281?;! ab1'gI3IcZ  Yu&lL2qm6L*ddhfJ~;O{ lսt~wY,<^xT"睷^{f.Mv ·u$\p]V*onolTʕJ\*KB1+DFDV R+I u| "lR&(Qk)U)ԩv JJ*DIJU*.  * 9rHPOQZK@iPKCL:bu?T:xWO~7#K%4UammI*#g^ K: DΤ='dة`نhaܹ&Gh{lpI@§NڙL&#-GH sYrr"ùXm|l}891CKwW_z7#< 76r oK֕Uw(Һڠ^wtCZ=TɬxwTPЪ>޼R!OB\~V&;dݵy֖DZo:^;O-7,j઄7+!<59T<[TcphYU+z&ItWڐcv2Y/*RT1)jʆ7׷Ie?S&e .KӘmb#mymBD80M/$TO^N&w̃n|J 3J}?=(Y\R<3ѿ=DhmQ4+u>K[F&ru'ukֺ'?fdR/ekИRdu|{L?;i:=|Wz'~cog*/O?;f+e%.0$,yx$4tn|t T8A&Sz.J\#Jqqq~49~imyO`~♧[#i7$3+6o똾FUf+(v{x24|\گ󋽮QLύ9̺ #L CgiO7#.K2Iionv>KTSE~[3]3{&ܐp)KUd6d9ؼAl}.?燴}nmzlm\*LgH K:ݫÙyIJG;;Oݹ*7 *K&c/xw__L:7-׿CTUh8vgCh\j7̼x^n^0s^ɞ3ӼbuuW͘'S7/|(z kla]ta^|=6_.Qb":ե'T!ׇҴПL(#!\[="=q샣9Ju\ڵue&i>;2ٙ kl]NvTK}kfH* T-ؚxwIH#GIu?C_e4 t70LO˃k0ׄwS]f]?\'[-1/w(V))QkTe55aZjWJcq%eZdFcW:®*J}RCxt{a~LԦdN&ijk Lƴ sl[5Reh4XX= ?6^l"7SW1i˦:>=cr11ԻT*θ9*fK1R6rmڶUZXykǯh)1ؼn9:3d$]&hCwfFgL (pk(1Sf2^dDdbW&j0cV6寑 dPˤ}uf[Vu4k Bj%>K gsxZ yIxGśկh+_;#?}'/|WiPm4fW1J,ۘ96 dS0ZY0%vu ^] ,!L.*JFznx,$'˖0YQf}BIB⏯zzSgL*Ng(kմޙW_ǭ Tr"szL9gPq*YƺB6r#ZYLnv.͵\u׮{V)kpƺV%ˤJެjPS2T٣_U뙍 70W6&auv5CRq6L667okg{icljYXw3cQWjEXWWܻ~ ͭuIܛFxLjF9iY[LdYf [U6=ޏV2U\ϺUU̍.Scl욐yNrsīz{5 ץ,̺3 b>^%j<.Ljk0pxb$H,P|pm.[Y[][!fVbijuWSKuKqbq~XӫcWvRۘX^r|bnft)bpfrpzũKS&'.NN LLcc}/]Lt]#CDaybҹ/%.ęSDS}'zG5t}XXwݝǺ.t'ԑjPת.Q:^~='POW{4{z'<ڷP'_n?X/Q34x:::`B#ht21=63F=cVtc}Du4'ks:Zkȥ;E La^ﵱ?ZedReu6Xe.DUqI#U-V- ĢKrr-[׾x=voY ߙG=ǶEjFCoAB 3F_$lv[/Er"{;y6%'QHXZrzazU2鴇ff R~2mm7O'C Tʹp8xH$T)&I?@_U¹\.-TVL4^, ~U%I5eT nE3-Ԫ/j 5 FܐpHؔ[w8jXE`5$,IL6B>{RfCu"- -:!ayn#Ag^Pn,r^{g`3,kRJXQ, -4Xd24yU omQ  w%\ k3Zxy3 3O"Z)ꟶ2Kl6lENysdڐp\L+zOtgb܈T>U%S.RE2) TɌY])3/mDek?X;;0`cDw{ g<r˔ XZx.fVJt"e`f4U6Ɍ9`]ŌGnR!M#Ȱ_b(Oy?ؓp\C'( O4~ t\fCy"ϻ=Ӆ\E)U4$ "0߆0i Ngn'ag WS1i,9$߽r1{.'Y.e% e:ޞSﯭxު )ŗŮyM o2%[= q7;3;1^vTuB/Hh _ꫯB4辄 HWz$WIxVAHY5 3\N~k?w{C/#m~?eRkzziMg([qB#{__i`RSoiE wttȏxUp-x:* 'vsUOГp$4/)JŔcޗ"р&D%6JvѬE"'Ϝ9Eir){ԉgO)Wt-KXNtU#9`ipY4~Ӿwhg̣ENI޿DmJ9ۆf+?s~1 34[~CAaKXC`Z?(`~F$ aܠ$1aޤQ{0Tߙ`efh$.I؎QB+G/us'/~ꗏ^xwyt47$ e#!ȩƸjch)^L߀LKXۀ'2Jn^~7x쁟J~F<d/L Vi@R+Te"/s>k旗gff'''<쯟|г怡T}>¯} y8,,,Β&&&}t/Q  D" |3$\.$ h[h4 9"Ak6 [9ݐ]}te[!e%&K8B! 4IvO†ģK=ݧH…lLNM{=oUEG,4$I8s$F% Sz5 !)[qB#aM[QŽ_eĿ^^M0bXkIoK \k"W2:^SIP(p$[Hn\o/sWgh-8Vy?>>H$ZHM <5 ]z. 'IH0gH`GWr"ZX* snÑ,/ ,M/_ Sy'Rđp& s9k p`~d`i-btt 6h hq0{= ei5X_XdY'$\z$ >Ěs6  tI8Cxު ),l:(IRH"Zp>oH8t`>I$e+.UHжIx}}#b }8oC"J$ ѓƆ,/ ,M2$ ѓ&G•Jy 'W#a΅[A$;srۋk? K"ZĥKpú[K@,GIxkkKZ A/^pQF#wfH[G ,f/[9-K,Kx{{#aRt]©ҁlM]'ɓR$NJ8.a HQs:@3ѓ+W`~d`i-b``m_²u^h ӈ`xK5aZ= j|24 'E;!uUN+$֐p>U" 1g*c!aXM;K&%L5$L4t Ev$Ȭv*gS+ې0sjg) &/Gp$,/GPzshC‹RVMXppٔl!aI onno )^U $bND$ >DO4\&X;I ?[ s~,;Ix}}s~0AO•J#B }8]{V#5of5iz.˜79 S?0M[w@ГpTHX{{[m +qCSz3R$0/36 Y:ѐ]'cN}#avHUp]bԪ$bȑp:nHxU\6R T\O'&f afffHRHa]H,$ FO…Bayy&0MH&.J؈Xx&LD–p l0m),LRH‰D.ҁLM'ȓRmo`'h$WVVṹ9}$x IؑI$6Hxuu&0MpC©IJKCI8ٗl]HжI8˭H+ HDQ$lH4]0NdS;P1 G"I”^@IdHT^j f>p ׭ CMa +#ᲄ ;?I8u"U@RVoIYb$l6ˑ0jHxE?"OK >e_g.WEH`PZ^ZZRG$,vg+‘p$!J+4VG"[HJܐ0?%܁ZX0M'3a| Gh4K++++Ho-`p,ɰ2La}$`H`k-'aK_` TI8p$Z1.C.$켄Ku0s렵 wdA{MS4= is퀄mH8Pǝ!zN&Kh ׭p|ώaծÐ0e ev%&(aHX]6r ;$RSyު ),trYp<l^V:0 $|*$ h[$87GHÖ(G4~p0D+/G"rn><<үp2pCܸUp*5V/s>ko444W=kG²u7]0M1MZ+ͷ@sJW~rS=C'x{^;6$jHXH|[ˍۄ"0ubgZ{~oïp׏7^{w|oI+I Ijs$,23tI"Kh3:Г?}3>w ޓ<j- s~Mc ܡ Zܘ `' /6Mߍ/MS ׭YPn/xx$ gvuwg~5ˎ<̿cwPy!et}$K̒ẅ́j$ C|$bH6x)u!a@G  v]I8T޶" ZRVU &%XV:0IqT aEb/!q)Ujv?5` ; U$.JXKKSSH㩄mݓnoMœ"/%l@$4uÆwIxFUG~9=o!a|)&R%^R:!ɓRmH;^- $ ρRӱ;X>H%)?D›؂D%sz{xuC u[eo5-P†C35 gSAծXBhgjR”^LMdHT!a@ @!0x$< Jnݺu\Eb©ˏh pú Mw^8VjWW燑д?l^$l),d"`R3`Mȓ.Օtw^4'7){oA<.K,7)x@L&ȓRVxDL . 7Ah AnKR$HǛNj!aZvpVڧfa0-AHo$\ID@vS ׭ _J؀pp5 vu]8 Nyު )CYbI /*I$yR\9 H>x(aٺ0m %IvÐ0u euu~(K̂TJX\0mC aI82W ^Ja HЦQFצ:$\U:zު )‡Ē1-*I$|V\ 6킧*wyq-J ֛6 6ِjWy: .%-ҁ45mHxRШ+˚0$ h<0k9 J60#ri[ثc!6#:,rxHvfyWS3.'6|&܄7+~rc[;^y 6:\")V㹄 ?K9NIX0+L&r<44*$ $<_f~8n۟ jum-Zz MHy,T$&Jn͍%VnhG40LoJzz4Հ`2~DG±ȌD܈D޵& UHRr\{8+m"q[f.| K,7)y~5tlV6Ҭ0v$lJud}Ny aXq,C ZPp:yנHçJؑF0Zu6`PM²&%LL$LlťZpx.7>sƦjv@;~,DD%l:JȫSzH,p>}%Q zYJ eTsoϞ~$_0XF%ፍ}/a8N;HX d9%a%[#fK H?v~G$k f^z"y2k$^;6ێEf")$ %s|=z.KV*Bʺ/dl'bz\K ӓ YXd %LF CsHVKZC aH<C aHطLN >wBomΙsg&N44EYZ Tm =ݗ/@@B6s#tʕmN'F ]l2͡@\cլMLM\$I&Pa677&x2Cjf2y~(< >Á3376> HZڽ҆j{{MEjEjx޶bJpl_1e Hh#z=]+[pV Pz1]Hlz0%lhf{ %l vo_œx^|L[nv蕃ss[oUJ㞄APڽ{U٫d& a20}+X6q s].|N\O}_s'i+_iRx2昹}$7 W " &?A Q,ds7׬|F ыL TV0gbɱgٙ`} dۿ};ndR#VH{IO*PW!,ڜ:+Qp 8f2x=0R΅ÇE"J9eV¹\.-hhpb+mɷLL]' ͂yBx$l9Wp'(ay a-{Oay% .>c w_86==Y*S> RqjZz$ y+ǚB<;T<0^X_ ,ж׷0?b8pΜzCn,DX Cz FӖ- }hs. LDSμ?K@"Qx2CD440<>uOPf=x_g OC aH<C aH8dK |( ;,NO$L F2ĕ+|܈ :!?H8615qqkkkscl(y=:! ?Hxf? olF4u=:! %^=]*? 3gL?4UɁ?`g#V]]P(addd߾}cccbQBզԞ!RHN8M{ ;^o1~KoX$ XK_g?YM96F 鄪 _2~$SMB7wWN΂0[%Nzad̨EB˸!aUоvz/YgG$C 5KUBm$<33OLOO˽駟b-{&!J9Xw=Y/sf8΄U/hS†tc amڶ'G|><37p7<55EOyjᥗ^4p&,xF^~4[~1YTP$vBSN'Qmۑ!3R0HX5Ixnn[n馛_Wi'LӖ%l805^7$lpJ‚}p*`zə_;S ~;;w]z|/wzv1nL-f"!e ⶄ_GfJfh'ix8h۞ rt#u]`zwϤ _Y NJ3<366fw( $ <plzzT*B\)s+x  LK869uJC)2q 'Dԫ#A󁫠&+ @ @!Mpfg/ Iϫ TĽ!]pb=N d: T/!MǬc>rc#LFx#qtȫϑǂk;G~s@>$+ǂc;w^; |h?$  aH<C a;BK ΐ -ղ餋sÙtʕmhgܰ&ŭb>vfc95Onlg&b07Z@>FU7/@-?Ƕ6ȍnfM}٘?-GC a-?e{Hxct# aZ!Ki3'%?gTq́WpDJfSeHxg&ihN5L`!s HxDŽ6xJUSݔ0? 0 -4 #A 1eڒi (^m$ E6^>s9"#C|l9I8Nsj`o a@  @!0x$  @!0x$  @!0x$  @!0x$JĢ,JC)=o= co/_8<:+/uŅ1fIKLJ=l=RJI= opwיA,ԪG<3[?ǖf/ommmRFP:JOGy{:ultd [-YUmO>lyHx 2p&qΟ;,KԶ̖R$(aJIf:֋4/G2y S{K=lsYUm{- ǢSJP(addd߾}cccb\.Q̕a &[ڱ!aHXD@kg>sZjz.M^rGc-s&lm&'e guB,җ~<<11A)HX#D4N[doѓ-UXR $l{(r m?x/I8N۷_E16%lرr^fJ! Gf9[F$"~"cѵO<1==-K駟b-{RTgEd2< 7p7OMMwyZx饗2@)ItʍD*j#f"^C #R<所O{uT/2CH2ro>kh>|5|JI%afQJxnn\n&z<~WU~'鴷| װ0nKB$Pmp^bHB߸k?'o}X)%yw>H$2`(a Ujisn3-js;l2~ebV†﶐kdۿ};ndRӢrF뮻ww@)9` "ΛrC$lJ9>*joJ3<366H0ZB}gffJRJ,˥I:J+aZ>kEHEmm>p<;O$Ţ5@ioAGѱw`9po:10 *;- +S?r!* RJJOGy>X $aeWf$/-.u^8:¯PJI({/{ՙ$o}=UGk̖dbz`JC))]=C(8X[]B %H4>00x$  @!0x$  @@db9Y\[NA퉟H@XZdRWa=sCo/_8<:+/u1f!h`v&΅ZU^\Ό gV=8!a`xR䵱`V޾ϝF=^- >EL:::ʧڔmUDǜ?斵(eW$3#`BwIpP*j0eOR;u $N %>pS$hyO$0zێ<)"s aUKԄ;8$vUbΐUrJ|T 395t"Ҏ#ÜRҿ\A9x9I k;*^*^$^p>1vP,oƫS\IWn*JrjUH1O aT;/4͇zRji [TAdHH㥶0\.o|ݵA3de S}3Q,_b!L sb {x/D YE5<%)eLF@vfTr׾|өl99@{l:Bg2)&zl:S-=#(#OgWe)a! WD8<Ipi >ȺԪ$ajajg~gH>z/4$lJ9W_H$T)gU{% 3[>J9r# NKpV up+8V a2-=jwIv#AŽxH]-NHbR)M&2J}HxւB³vK!a9f˧Sb &N lj5[-A84}rzzT*rijjZ٥WPyD/@HU _ltr5OɐMХ3\2X|2 E.tDX,xr^ Ԟx^vL&žjL-jtr%K#ᑡt::i<:Z3~TDJmK-̦se%=38ɍ͑}䉥ˉhh6?;tܱ7^ړZږZWX[XZ\B\kTbHnl#Ѯ\6ZptJ`lbYԞԪԶCRl&Bn$C6a&<1֓̈́S@<:ZXd6Ak <'ɍYͤND$19;5@K<?-SAr# ^R#Ppr / += $Vȍ͑p`~(XJT~7Dnlhz9S^"Hn$C:КW.W"3 rcs$ZELx@,2KV$76A±ڥtک=·g^$*#00(̘ǬJ#@!0x$  @!0{OƗu\-Q5R F2ĕ+'N,̍PRLUJ$ @KZIssssjbJ.ZuKm Lb͍|p4\j^W(E*u M.J^KDJιBUMRL aZsW*B","}sQVM.,fH;޶{ ,aiC9iTM Rj[Ueo*f@.JSe)EGf%R *) g^#W $ @bGLWH$ 9Hye*f@.|F OTz6#J+.aUа̔ARL aZW%,(1HXRv"W $ @"*˥D-mH*V堊Tm3V\‚U+ҋY)&05SqXI І@ 0m Ǧ'Kb(ȟ SʥT| [64:R(+u M{D"^P,7_o#WP{]jP,E*Y:sÇ  z\( ^:|*3|TMRZ aZHprvjܙw_?r~TZ*3|TMRZ aZhhjiax|b)C #[TTT@ ` `TA)0x$  @!0x$  @K Ð0x߿˦\ya Y^!JyK̈́XEkȒv@r<ArJ`g lzxdt-\\pi=|X&F endstream endobj 51 0 obj 24185 endobj 52 0 obj << /Length 53 0 R /Filter /FlateDecode >> stream xYKoϯ`&`27gSZ%5b3#"ٳ$DzSU|Ll~ճ?Vqwgf 2 ҂(̒< $`<gSeծ_5 }/ϫOlI>_4'8] .2]ՊYH&x ޕڹokЇzrHcCcpЂ{4)ڞj[cf&L@⫝̸g80q)5,51tL']4 BTWU5G[GL_d=r[#(,p7uaH#6(N7d[SJ%QEKEf1sY17 R[M[AH$G " s:Y .nCEYU.}mo&1)No7pӡ =)HƫcLpP ifL|TX trL:yyS $-wcUzS rv y rwu.3|5u"VjM쟽#R` FDy{nMaZjAJL];u>EKv{@e'd.IpQf4 oM\+x(,`yѶ,Q/2l*Ji{ m:FZԭ"Ć TuӍg7F +OI6qn\j qRoFZJKgtAx,%XDn;V1V({U֓'Eٽ2 = 0zIe5F }W oC7J Xn>v'8c:0hb?t~R}ڧ}G[4+cUϴɿW؀C(i)fV1OtѮ*Bd3+jnrE(ai0/΃&||욪r6|[L|%,o?w@M1BhCh3+sMx dkrg#!O{)kX%^\?Dty0L᮫ǖ8rstkT/crfԿ6;U3=rҧ,vNVa0aӍ@Mi<=et-|j{sspm!"kjގy;jtrĖ@/7ñ|!Cm6SB\whGK} snt:ޜ[hAE]iDبe |$Ƒ~أ / &^_y#5L43 O9!AyO"SCY4V@_Zky%G蕃܀gpFQL VDxanjڀIŴx= n֐6.9ӏ ]bQ"t!8iK+v4!> endobj 53 0 obj 2283 endobj 55 0 obj << /URI (http://www.w3.org/TR/NOTE-datetime) /S /URI >> endobj 56 0 obj << /Type /Annot /Subtype /Link /Rect [ 187.993 539.33 346.873 548.33 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 55 0 R /H /I >> endobj 58 0 obj << /Length 59 0 R /Filter /FlateDecode >> stream xk6 !S[dfۻt&K㹛83m֬(GCMoIgj$AA/7 /M3+o = IH1"[/IˆOH!8@!ӛ+Mu㓿|,/]nW+Vo>8Ox۟#d}&^^YIe!YoHw=eߛJ9)p$I<7K/LfΈ+ @Ft `#e}Wױ}7½tI%AeI!/y]5Wn0rPet>xa~S]9OzzfiKK\7-b~>Yf^k%߬;XJoyw4x Aeg@ hYt}be؎:o%tgimCwOՐZ8Ud`NLjQ00,R3ks[7xɋ7Wj=v+465ԅrpLյp3*pnƶC[(FjCݔ96?vykgnnw [H9Xnr˚͝e\hg L&Y+j9V؏Dj I V3 Ev nZOE|p&Z LKhF*) _gXЙ[-+6J8ԑed4N^ΔHfEQ_0(jB,Yg $4u@-"lM2UIyEɲl`2mkI˸NfވM^oo*r>ATxhؾ+&6*P_m^5]U@TXpZWf鎙շ64qA@ybI'>#]\ A&9T%c۵QY~jg`-h^MwӢV1IEMxW\ bڀr-R8lK`M(=| \oRI',@HvONp 27, %'gQ0|s~=ĵF1Q"uIrU V)*AUK2zz(=LfM!ȿ/c}FK7]-r8 Jn,L-8r̜X+?C!F'NG*+v&Θu>՗Weo!8]So( XԤѢop`&WuPsYsgЎQ; '2e Mcl;X[Q9yr 'l#*y }ܦ ⊮z)Ϫfmzu{@f?*/Fd `^ΥN{AI#LU(@sVk0cqKhNMŎ E񱨘nwӥwDm"rln+,_a +@u R\Au7]Tp'l [jNy(;y@\1N83͠wZu۹kg߶Qʡg)Ֆ #[mgZ%sVrN姻-:"pZ7mGjI uUlb Pfo爏!E+'qѷ] y| Z+m?aA `m4j"p4 &*#hfMIsŝN 'rgN#ӄrMhq2̏dJ VMI ܖ H3s}bʙIūɹ̇8PNH;Kݗ 08-3ĉ)nm̗UFb||#%/k?ӏ(oMl#%MX #|[,hZ 0!u a QU Z}ǝ(I%,53ĺI0)1`n0qi@"ds0;3Dz0GsYp-0A8< FB[xN^_l\GLVﯖ"%JL5@%:\^d+K@u KBVOXJ*Ylz4+M[8󑊬tz5+$hˊD$ϓ޼$ˆR@X<7cr endstream endobj 57 0 obj [ 56 0 R ] endobj 54 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 57 0 R /Contents 58 0 R >> endobj 59 0 obj 2972 endobj 61 0 obj << /Length 62 0 R /Filter /FlateDecode >> stream xZ[sۺ~)( vڤÙӉ=J,N(RŖίxQ3c|K`Xԏ^ɢfYN60Qا}?$ADYLJqL\{'|}9[údpّMM$O^nx$Κ8h$y1hr#ݿy--<ㅲWQ*'gIӔDbR玛9O,]J" ((xtT.jO]]t) Qwa8\Fò>3(L( dXO8YˆCҙ$ 'KcX~gh1A}2Dm L!$ x -poOL 8TQ t` ! ̣v)[wBvGa2T3MR 'W3tk=4"_?/fʲ`WZ#2aͮ-f37cD0;cvS +85y`xgo'ܙ:\0GD׏d<]rX~Lbiċ͞%LirIVJ%$Qz  !+"m`2'-4<4/DX&b{dfE.:m3e?u#87 F"BgYWS?bWb8MjoFbd9e qp]/˄7ĻSo@IO)ߔ<HC %7Ȁ|O8Ąc] ZD9@[/݀| X%RCè%KSbM S;5W̥>Ni5b/+ 9}yڛ>mn=g`g^:3GBD2TYz,9?U)k[;O͖'#՜oUYC׏Kub UMAwg3|=pUVR`QSǑw[۱p%TzjzK; $ֱȆ-+S*Elbdv/pG,E?9mٗCQ@`lv׼8͑di4t-G9[z0Q {?at"[ E[`L Bgd3ujZ8h> %v?&MP#J> ";tGrD%ovm[/o?^f8v2є3L!bΧ l >.⋘)ZGъC/C׋s/k :X׃}<ź e^:uey ϝȼci@_#j Zn4o3򖎁qH)H2?f"M&#{ᵒCJɧH]ٯdp"K|Y -uZS:g&*;] Rypl8L2HQu9,TH@3ggq Hijg:g+IbH(:e}-I)8eZY ֞0 F&:D 0)E[I 83Dz`m5zxUҸwv@JEzf/~oէz&tꠜ-R 5+~53B|9Po-$(efL\j ԾˎZ9nQ/v&p~W@C S󎺾q =< ;`.t7Cw`)ɄKPIoכ endstream endobj 60 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 61 0 R >> endobj 62 0 obj 2975 endobj 64 0 obj << /Length 65 0 R /Filter /FlateDecode >> stream x]ܸ},:G{Hn+iw͌f؞=ٝ~|)Qe[-PD(߯B/HB(JoS_街G^)RIA셉^Iw<i~jW}^|Wp)K7I{zuwݎL~eeYWqTȻ_mtՆm?T̄Y81!)C&WErYxq8DFSH7m%ݷ+t?El镸GK=aqK=ƞwf<ˉeh9cu1A6o5.1mJĦDwj7T{Ldīn;\7==nK""ͬT+nJSp`5(wb#B\ k1}Kؚ>po}ض2K"[2~$_N#~ Q-.z.}m÷΍2,8,󚶹Ļ' OT˼pE_91~ ^/baKA$S,/jŁO2XQN #2s{2pHc "oON-{U͛4jwT 9Nm0-'B4s';97(FD{5b*FHexPK7i(n6E{LG,ծB U[3'DrMBvqOGb}CSFu߷֠{f EU?LiNwS5+8gS9ht~dtȰmTNKLm!XjeVQ=,jBC:pN90ڜ#Z>v$ؓ8 kDW5fn2F0B֡ [< Rux.X,F+ aYAs"jFKl_ $/\,ke>UAPVf˟V_sL\v u lr nz*qN=S"٬ϱ8죙3:?z}K*,-(2jWAF1ص+ApYybzf~^ciC$XrE<$?H889ʀdFp{k.fUQZgTik/} TBDi} Տ $ L#;!lRS] f1<[8>En!94R #rOLOl)g(QT͖7\glB=38| ?z2q ;9AacQ ԗy`_lϏy#@gĚ?bJ /X[ak mr[f%t⅃=|"ܵtFYF1ܴE01 W7_A"ʋ!Tn E,YYY+Ы[Vb$*s#<|y"^8!ڊi9xOy)dx" B?T/1f(FX}ꩳ 1XE 2]'ntAK2(l].]#vY ZL5`G-'}MQ[ t {9i4$AJ18KhEgX> b@BTgQ7atEwQ"(^3G6=6]볌̪1uQOuUBE;˩oFcYp5$Tf_E9CfqE憌!/˥: f[2)K2`_|{K~Op]Ӡj%},ZK?&Hʉ _v]98 ;0Hͺ+mbyg@ׁNY[>Rxd@oa!A:-t@+V!ChշjZ%VNTDCJq65MaeHyr${go3m% !%YY$LkJm FbXg4ٲnqu6-!fA%X.Phb"CUO4d}:GO:/\Q>1^nCոji Vyigp,(sBa6"f&u6+jҞzԁ shbo|"=ټJt(9)p-ʟX})BD"P obUަp4XWg)?r3V#Zzy4p]P3ADeވhy+~Cl%%4Caz R_`g?b#gBRE}1V!,Mxs3yn"Y 磰-t0GO!c)?d )Cu+ ϏMG']3奁^-1iSBfhfh@!?A 8y ii gc-=mIƦBTfV?܈kF[;o`!`O. IfyPh )>I>D^~fkWߒGz6&s+`MaXN?ʬz[pߟj5~ƕU^8(z[дO-??+^dXzVRM8 ķ>Eg10GY`6{)?+K a'YiZnRDzT4HKrT?Qq&4*QK Ǘk_zHiˠze|_\;w<8)58cLɚjޏ^b& A?|Mr?z5*s!4mbf}TC7զLk)sJ4]CzԎ,|RpvSu,yuEv#ADK.,GdtgHY`kZ?J8C(F1padܷ˛| ޵ endstream endobj 63 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 64 0 R >> endobj 65 0 obj 3658 endobj 67 0 obj << /Name /Im4 /Type /XObject /Length 68 0 R /Filter /FlateDecode /Subtype /Image /Width 484 /Height 543 /BitsPerComponent 8 /ColorSpace [/ICCBased 5 0 R] >> stream xy}k?XC3k JXXDxv!"W2IYIexH@w]}} WUY/_uVUʤ܇裏hv Kn6yG"|v7Y'6@N -6aM|6'uiHB>Y%H"%#͠!iIQ!kܖM3" bvخ)6[UJ".,ޜSX+QP#Z30*/-_}an6[׌2vWeTZ bB` \8F;չ˥ELOFOhFMEQK{cWt-jk]h]0ΧS6D)&quѕyڽ}Kr)}XQ ]Wz 6|dXH v֞ZM] N=е;c{dp[ {'jR'&w{1$L*FǛe2t<_!i_gԙb1IǂT2\) ꆸzGKDXڷheIDxùDhtw{~.9S  ] 꺩1WˤtCDCWoIc#7^֎wFŢ 2(Z&#]erfd2o"NGs6iھy Y/kڵY?_??wR.[~7ztdO|_{?Á=3oXԺպn,iSحə/*Z7n\MVx#}~X:2d]|VMuZ@E?h\6^Vw6?7LN o._`vv" .-v_|?#x/Fn^+2Iޮ]c[K۬y<ֲbW]QHSõgf\oϥ~ sQk2A%ɝL4(uhE=5YʄCt2ax86`!@ElRr _Rx399؏|>qP+G_{ߧ1#C׿GO}K3ukOm!m!osL2ȲfioXju_}&'ҩDr@ F,s1 w2IbFw2$l 90g^MYLh+W3uo~_}7 ?U9 v"~8󪦲䥋<7&a3&￙luMյR4Gt+]SVJnT2&6y۩̽z\ց*ڝԇ~:TGzw*I&"^St~wt_NV&N˥LLq粩LxΙTӰI^#xH`)jKWO|;*+:H@?zfɦMH3ǣA>oqGgj]Sعֵ9溺z֓u5+ vyK"fJ[[fPl.ShML!U3Z\lLTBJ~|l" B| [QQ; * 6v ,?!jW#'bx_wꉿo>x-7^QFnjM/]x,Gͤ2Tۧ͜L ޯ~ ~h@k06ڨߏul2T@>?MM ,RM}F_f#9Uk!v>v3ڗu \kUZW%Ucn:38bik3Q]=.+*4Zia ןFt2MzxOYщ\?KMŷ[u2509QS7#ÇK̸2_QF:qubz2 +*jN&9v2sd:g9^=Q/uUt|O|O9ب &Q]UN'>ܟY!YΤ?ֽ~ګ\V*۵KKખi]SPγ32\W'՗?+:ifNJ!+ɶzYR΁xg =k6{hLzPQn Ѩb!{O{A?O./_͏/_:LFitjڸRjmk D'k~y۵ZVƮ٘qpVढ़qm"B dˡқ ՑXO٧?qm T*ZLbueoF{:>pA:~GճLuΜ9$|Q:ɐ?NO|.«IB6qu\o;&ʋqF*&g"3:Y,Ϸ ^'4_[Բ4? !~gojzjNrusnI[e& #6[l3җgT??*#}c}e ēxӺ5B݇Y6f8gg^4S"s\W9ohw"PuYll-k'G?"?}gLjzK$ ]?#7ѻ3S44ϫ{ffz`vۺlαwR{Aĭuul bLVeOcCXjۅm[WϪCMQG8Oql>:rݷ42=uSrf2 qm.4tsOe8e̴꜆&|M$tSBTrEkDvuOO~>g>'?`bx&]y2L7oX٫W.'?]329/R ,]M1c yl㽤öC:a{X1״98Bf!_,ffZ^]LyJBO1lC;ʔk#͠|!W%W&[%'3P^)W"a)BaavEvT ٬&ʪuee:WSdu]\7251P/팩f2dr'#"6q#uQ&ժpnlѴ¢Zml͑3O>8h-X6s77r.t*Z uo~8U*,uAJ1(:-L):+^xR1lrSxӷ'UD"YJlF}xόD:nobL: DL&M͕*FgrLW2Ql:DHh5\ Vʔ?8_z壝 bX_'bՍUbvX!VWg֖gV}ʲ<2A,-jJr]ھ.a c ܈Mbnf&2SSe&+ܘ'&&ƮO]F7{Fow ]I ^!/\4yp`?qqq27#gyzz~Ov]\%BK`v=ޣr+Vkg+߮V{6n>5:{|KU#V" ޸X+Ј(C1rex \C,Cjtgf1rm|2G8N(óP, iŕL.3<;34KF}y8at1"qYy|f e)#e.5r$ j4#"jZJ,SIUV\5\mٷd]r/ɦ#$dUꜹTOj 4[mZ}=tbI.*̿/!mH`֜d#7VO1G;M(k ;pDQi͚g9 2?+R>XE:jU[n]WC\b%UxrGJ]*i66rqZ.q͕ f-\`Yp*fZtٺvhl9-B}k(/;KZZH.##+Fa|`R׳v0r7~m?lJX;T"xSeIo|ّ xcE"6z c֍lFk*Ě%ϦZ&c3I[,޶aoaɹdfϢV]6tK]-'mm7Dи̛v;#3w8WnAŖָ>nm~{mcBvhosËZ {dS3C?7ZKXYZ^k]fv.Fx:={& }bn|}é(R_Nsf1ql:ն8WΊhw[tlKTximV|kەsK\Ἒe9n@@J$b~/~#%qݗkyjH2E\@YN*cF]]0y\(טIfI3stB:8~X0/߂ږrs!] 7@~.9腫m khZ\r&`;@ WV|LO4W;tt7 W|(i_)T!q"kμ׎ cMӾY#a{7h,5WkJEؕ;Xﰟ[{VQ3\ l6x^<_P 9fsZ~7{X9p5\ W ?p5ȏWgRDv@[@$m6Tb:6lذ lTrcm4W'[KB%$L)9^]F[l H$H mS TDmDf%R(ѮN}W67 | 6OyRUk+Wo2\ îU9ՠ2yM8NLgEVv+Wnܸ~tJ4ds\85y3\ Q\M %/\ mƗ^]]LpJW5WaYc-¯,!"5!퀫ct$; ec4|lUVeD¯xԐ!cܸs2"j~BfL~38g-cZi(96VUnV<  j(9TsC,Xe򓀶q,1grjKuL~e/p5pqb,`NC,+O3Y˶02@W8 Ip5ҥZ^1 ?p5\ p54 \ ;W[߸~A T՗H W@Ӏ@~j@~jN' p54V W@""}ʮ.RL \դP-su2$K 9]XWSD< Cc iy@sꪜT2 Lj{Gў)lw&Yj?.[n\mc )tⴼh\fKMpu2-,zss3(TZQLf]]]!pi]NWVr,M3:ISL5\ uu$0[*8VG+a1O%ѕP_IeLh̐_%@'!3oi&Fq"0ŧTƬ4*ږI1- T29Sթdh|lpkk+ZlmmR|3!mWeNRT(Y=Y=w11Y>̘jÐݙLFS*fn:W\1_$dd"84t}iq1&60?G͞:Z M5Ya]N%CS7\(˗/P|gYs iY%@'!ՅsgߝO -RLl03i]Fnq5)|nnW)fCHj"]vc===P(J%xGB^xitZw6O鎙3|v!Mݻo=={>Nsw>RՂ՜h9[-Ȥ_u_?Nh#t&v]`9Ů\V H'x/|D"ZujWUΒ"1ь LD*)<_ @+H~Rw@w]{3W$!  iOWЦT\} ܀h[h7`jϙ[~@wblV WKB8N7߂? [J3]-B$|<:ga\0h#hF#2fU1SmE`-k`YCV7h [E3@BHeW#DlM[W"}WK8eQ-OYMs.2?8\c6$N.jTZsի L&#jHmږEL.jK8?.ЍBKqY\ :YFf5N%bэŅl6(ŧTƬh$2PÕI1x]dNYqX1~n.{Aw٭e4d&[s5Tķ殶&\tu<1>6xAW_xS*cV:lP#p6cLbVI|`1dŬ`egڪ?3'l&}3&t:ae2rLlD*-еS^75ͽII[ޙ B6V4u5s{zzBP*J&x,M`^8Cn[Dܿ<{䉣/tw;9rWNb'qj"l/?믟xztL!Ng)N˻VE7sёc a>G[Z؆]Ev-p5ȏ;Wo&b+5jh]]3\ M;W[.^9[qu4 $R[-TavuMp54 \-@8xT]}DjKp54Nu׏>/đÿ4Dgʶotg*\:]Q>ԫ'\͡W^Z5dP3љrvnîU9=ecm6] PKΞAg3"MqgVSZDgF:sJ"%ޚkHᕖߥ̈́{:)݆}WWPWW~fy#`mk۪Wҩ=)Ig2ovڥAeēք T\I%*Ecۻnu#^-LLfg2o6wK xg#*c7l&Zq$R[m5`وh\駞 u5y#B׫l֙m׍vvp)vfګδ?YolU̇xlVCf&fsƚ~|zmeFiG _W7fG&ؙ؍ LfǺ̶pu#:yrtgvxe-r󴅫IN'S#~M!/Nsw>R\/B[tf[Lq7KW[߲9"7O[˰Ȥ_u_?Nh#\/m.3+.^C̒xj[N;vF#i.n*F,iy7@CqR:YYX(JYF[[[ ,.֒~nN.z֫j'y ]]s].-Bg7u:fM󮆫Ps=W+Nu>ITZN&o>[oR)5|{{{nv3)DnrNgu  e)f!fip٘j҈ T]}}-6Չh4F~>U۾PrD!OY;UYYf7pfꈓx+‰G,š"@L 4)DjK\=8pO~Ţ++7{~k=r+1P88-Fh(?KBP`~xJ.L'q0Ҋorfjhn1P|MI&LJ"mGf3T|#[K, /E>ӱMI&6)9U\,1RSW/rL: h#qru*ꪜ-THONeґT|=] H"NI$R!Wlp5t*0$$OR(Tķ 䟛H'C(\ 6)n:$HmׁÁ١t2)}@T"@ %6վF"bw `/z$W/Rq3'ˉG Rhs\4[&ɓWG+J,d+!v Hm%t?KBϜ,Bt -%H$:ru1!*|_VADYvٙ+58`ΤCqu$4PQ W ?p5ȏWsKHMWWNH.-%?(ӆkr.m9qu`cffzh{{{k+DXB,J3SPcnz nm lmB R]\=W:Ipcv­[t 6f[Prvtw77 \ 6Ȥ#bJ"wҨ֧jpS%Qqh:98p5üD{UnRu gHǣ6h(XLӆ8cN4NZ*q*̞ѵNl)t`41P\8 -Sn;~̄@m`` 0K,?l񛂳c:{/AK$k R46͖98K*5֬nsUCNY `b̤Dz\7[X[gg5t|&pgkq.giA?[1v|j__;7e>hnxۨ6r=cvcbpmڪ+%~axuj\"W5-]M F%4F3+,guXx$rׂc"UdWY9ۢ Ai e& ^;`*cxrK&3O2ZlHK[^9WvEh]qڠjj942o CGVi?V՟yu-TK*9puCWQg6fTխjxs.M itevr^03ǁgCrzIM7/Y=6l˴ ]jVy~in.vN+*!u 4-̘m~=-#~,Av(O`mU^Ď/ɲs]D][f>+cPVv efJIi:8ix{[4up0 wp*%t|)]O[19sB MX +wRugHj"C<ĜIoԐՂ1=VՓSDv+?=j_\OZnw\yKlB܄ mjgx/MT"S Zց_k(~S9 M 8ܞ\~ؽmj,/17e>wPOeN%@4aNows#RkǿZvY87֐ 4Ν r9[lH 9Jvӽ'.1bVO9k{@݀ @~jNu71~in@{YnFyې͞1ny'w*Z}ySb@sNf8rddhȬ2fol̚éۥƘ[͙XH*;Ssqn,'YLL8*`fD0.Dp<6; Ϗ~ T/%Oݫgn a)2ZWwi6$ u20jftれ{]d6H*ù,4j;ٍU󰶜؍^A7WjM)qv| F[7\ qU:-܆,&>VMs.pܙ9XmՁen 8l;AwlҘ6qJ0HL~7x}iEm7_D2 ,6NRjeHjМ~@oXx \-p5h#W8 ?p5\ W ?p5\ WՒ0'_~G_~Cc}G}'vķ4y='Ƈ ӧN|puq{#Z6t64i_𞴮6NH#a\> I@)@YZNNf|@3Nn;StضpQڗF]woV@}x躴NչYZη6!g4+E0P|k@8Hȳ>Es 5y{Z iGWmK-sDú̍o 6`pU3j=CJH;yvj@fnZ4îE*ۧrwud:n6&ikj㌔VȌfjc*։4\ dXݻo=4޳t|=wojvt5l9 j0?+\ :\6]w*8N:4i7@%&f `-j@~Zt*$ 9p5\ W ?p,̏ɗ_ї_0Y~=g}n?="sNsAȻH3g` ?F.,_%4`'c*кf:6)q5'21I2ڷo"cGSC+/߯F:e5Ֆo \w@f4vP>p5b!wo;zϞ=ܽY̫qn'Y7uaVI##^-lKTxim]7Yڂt!I̍yl@׫9g1xUKj D"v>P2m4Cfmw79'͒Ƭ=%)ҁǂAҝ>0ݎ͢&Fn^E-B&x;zi]e\-g}mvqavyM[^+8Ņэ9i] 87f}KkΆFz5\ m\ W ?p5\ W ?p5\ WK+^t6\W#cW_;84@tuu)΃F:1]~ƥ{7 jrأJ`8^hW3E\|_H۷Oʱ)畗Wtt5GRj3޽{o;C={>Nsf1Dsם |\vqρN6[\$+O]WA؁_8CdBԳmjW(ҁǂAҞۗ}##:]D\ڗT*V(dS.F:wm.r6oFȢoqɷH_: 4{WfՀX^9w Z]5S\ W ?p5\ W ?p5\ WՒ0?'_~G_~Cc}G}'vdiq{{.N:O-Mjﳸ?4Οwt?^fbΆF: ޓ[ȕgC;:9xr\b^d Ծ̟S1CNVrNEZ~L6%w5?Pgf~Lf7~*Lp5ij>Es 5y{Z iW,me(-#NJ?xB |Gph] i#W3], ijc*fp5)})9v0>rjvw][j~A~:޽{o;C={>Nsf1i/WmBڒ7'M}>sם |\6v6zϸ@c2qY[Yq@/\ @J$b~/~#%q FN34]72nNXHǏ BJw@w'dfo,h5S4iwX[˽fFxo0@1N#}qadc}c\oV -j@~j@~j\.k3|(ly@sdd2aue8ⷼ phinvt{{DY[mT*Q|J>F#F#Pik+#jy@Ցoh֭[|^&ᔊe;Ί k=:I\ .+l6FFF80>> bZ{OWl^|wuujEQs" -R<G$g,%t.v@C)-__''')W4v3YUuY+f.e<"v2 ]\H$8OlOOtYfiU/Y~Lv(dAWW^2F#ggg̨/v%Σl%Zfb!ͬrZY<)Y(,@UAL&L&_}[oK/%klļV|Ds*@|KWo*:;SR]TWO^yPȤhħ%@u}nڿO?OՔr#hqgƴ3ad<+^1u %kUhɄ?OBIMpPqF, h\_Wկ?a4Mj6ՔrƸvSճژ@mn fb&eh<,hDzW@C8@[^gFj> %#WHq@߹|>"KZ9t:Qo8pF~5WrCŇ-'_&:>5!Mk𒖀մWO6Քru@L&N %6Ã3X2{ˠs3S|.. IJeWw-cL:J mGPqTh$%~WH.18rH$rNEHqt*DË:._|#7oW2fFbR|f>aģ>')9k 3Sדy3"9-8È?A mt? /M ڈhx1 HŹD0<AʮBDl5Z+? J m} raI&:^?/v9e%R(VBg^[+c18DY!6ͫgR~PyW+XˏO6hoʳ9W>YSWh&p5\ W ?p5\ W ?p5\ W ?p5\ W ?p5\ W ?p5\ W ?p5\ WBivjKum4?LD?VHDG@[W''K4gږJ~h6[]kj((#p5ԍp8i Ee O_?w֭|>jKި4YS+:"bW&^~X,fٴUēpr6 ?gz^MQLj:**˽$tYj@2@95_z5(޺5 \ @UYJ`#pNggٖ7qm*qTg\ pL\ݘTfI\ji6^UeL&)pB"0 nTgqW{@6QLj:*X{@δֈ t(&p5t:?u"q5\Q]m9 t(&p53I5-jSvt0$ޫL󹸋M|lm|nz\e59V(- Oh$͈d8XZ9|ur+ `eiLZ^XYZ~ ?sS endstream endobj 68 0 obj 21828 endobj 69 0 obj << /URI (http://barmag.net/veusz-wiki/DatasetPlugins) /S /URI >> endobj 70 0 obj << /Type /Annot /Subtype /Link /Rect [ 197.55 612.577 375.6 621.577 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 69 0 R /H /I >> endobj 72 0 obj << /Length 73 0 R /Filter /FlateDecode >> stream x˒{͖w*LSNfk{khKzxDy|TN3s0  O8M37Ϛ ֤!QeWWzft p~%Yp::?kߌwB]"NOkeh[O5Lq6py($8 CP@B9v t( 9mGQ^՗ݱlWJ {GCQ ]oh sYҚwkc`Yс91YZv)k,yi QlKV-ȳ Ft5'ic YQ(u,Ձ0w.RvN 뫜֋,}>6g[ L1I!U*r;B1T 9۱gW]˱= tN%'` R;U{4gὖl="RCQk0Y1QF]8瑱š5cUdvh*ڼ>̵/MEa0RCEs.;cs͔ m?ZF?:ηO'e7Ҭuq`?iTp6tK'h乯 AGݹ5ل{EjIHz0e.p = _Ć=Q'ݏn/1bEהI *B8ݝ2q֯S*@L4qGq RTwM5XARnh &#e|\֪l-hdUJȜ:Rh\V2$_cwE?EL4/C 9 U ՉDDNU4))ӱM9,šJjqBRBN ƷHd0"~`'8<6Ffe"<#k =shd /d+nfov+ Ib& H!+oc~+Pkvi}q Ifd˯HHX1iL%(mj(ka4sO9ld R s]w7P]_9N p:UKl{/ކVS4fd1~i<,bA&!OpM8v9U)er-obҹL[¯r6ZRUԁ+O}l}Y^ļpY./x)BbDߘTf8B3N]YW=?V|,X.39j$z^vѝ ~쪦:u 5a1]H1a""BV^0.o^G6'c$](Z$ <%zLSȓE昧0L2ؒ7H6²hTtpfk`"r)˥ }SbeфuY4>1F뢨k;xuZ;C?^o=j)9W աyb `jCU 7~K4DHs%goƩΫ% q( Šn[y0S;-3B#p:pZh i"aF!T(j(]2GQ\> endobj 73 0 obj 2329 endobj 75 0 obj << /Name /Im5 /Type /XObject /Length 76 0 R /Filter /FlateDecode /Subtype /Image /Width 471 /Height 450 /BitsPerComponent 8 /ColorSpace [/ICCBased 5 0 R] >> stream x %G}`0:Žu@6f ^6azlcxmc@IH KAZ5f鞞k_3fUW]*3+EGe~/;_B!/j /\|ϋ/( ( BDE^dcaRoYB蚦*Ľ{;N^+ԫy7 m(Qs(1PݳK&^] \h+ޅ <.6grъ໪} ؉6#Nnk%Wu,jH&}rN QO*Nɧ\-Z+ߒCQ%s`#_yXKX*$ዊ=̛ M}N4E DŒNaK!%V+^́E zKfrP&qDU !󰔊\Xf`eOmٽ̕rn{sTHk{1 rJ. F@ A† ͍B~kQK{ ~$LjXo:ԑ'Ѓ?xO<`o&cQq@ɊDarU"~dMғ~#5𮘁ޓjxGϜ'GN2$!唋#B<$v*΁h-RLh2=Pq`Rk+ >׾g~mǎ o|eёW/MY/.>s'7\we?/\{n'Anɰ•<,>,bǯ*>'~ي~]Vf`smu7&\=L2 fteB"aԋ|? :w^?J%_j%G*@?k^[|o|_N<;33Io-.v{߼Oo Jm5ӢDh!-dړ-A GA7U <91_::;3l/-si l[ Lf{(sLʙo)dd>cmsqa6*ѵjg%G˥6+><~g.Slnsw=]*d Wy7ppSb ղ|<_:6WCŜ8Yor [ɃBJŬsBv*To&W셴d"}]"i0l+f[֮KslMŸ_ʥɋw7GgWX)N?r<{T!b17;~:̣6Q"aI%Y{ب ÉQɆ1RQMu.P؟Z:G@eŻo {@µj14|JΞB>M00vjdRȧX)L&L6 mdl!medhmUT&菺KEͽ|_?t{X[]7}o2[|{Gpӽ|؃S%ܪZ%\ݽZZ+Ur%W,g LEJrVBt@^QP(!,bNJՠnPvRkPicWj;AB;-nŽcܫѲ5ЏZ {,yN  Ỿn|{wrmy¶0)m}QH.,d16Ư6 n,'%d8`نȀe6 %#dlpM@K/=3BX0/ߜ(p qJ6l&o3ݻwF(eQv`l}ۜS o:^ˆ/uknd Y67nI[hLb/7~UpP&ВcZ~ƴy6V,.Ln2i+RmM;g).WY,6̈́m; !Y1ǽv̍mZJcB`BzMCWQpWj́{Aff[I#U2prc)mG?77~~ӺµZE6WϞ%}s,6OyYej*aҊf!2O>kIRi$[Ͷb]Ey`'4F1SµQ#[_ַ^HskBX礽uٯ|Vriz׉cBݍl5S$hzfcmbSb-s$l[6'wl,'d}9암jWʺ7$ ›+6ַ]Xwu7.BFPYȲ|E~/l_06 )dZw6\~~ _ reW3,f[}7*O-HX'GjOW%rbniXVTqiϪQ?fҍ׬,N',M[ߡk^3W ;#H5YmLpUpBJŽ 篥;{NZb"&|@ mMX_.nR[(hT 5Ncq֠Ҡ@_+6Sit6(i޺&-כ˿+Ǎ5 5<;鍝mHB Cxi&d26:׽䉣E+iWl^,dyWnBqZ!HzA!kOj'^ympg$s}KLrl}I>{'?ՊZkT W(6<<Zߞ#ەJnj<=T}O06n =b;=ޭs~aTs\ I_m8ܠFEt~ˤ``C6n iɡ7WfG;!yyK~‘1 &DKk-diρLfbc3[ϩc 鈫M]ugȴ|ngʐEˡf>ۈf{}_'`2W.&o3_ڑOOת86 *rK[UV ۅui`jckfc1YN[18PCRߝ,6Cxtoiqdjrrzjun&mF!lԬw76,L2ef%;as4[Z۵prT[ݯrlrB1ϙjDS'7 .̝w21>p)-ұt/ WN2q>N +6Iء?ukvvweKA|95Ěp&l!=_֩kG5HU(4TTTPք5u kŶCM hO7^4yU!hr:kkӯчINs.̏9?};BL~M_[27E&ioZj4X[|rX⋉7~ޑ-4T_ Z(='n<ؙ=~7<0З˥)Qau*.mrݘjW؄ׄ] 6؟ ox_ >pi~:1C6Lm0K%sxYɱ-qBj0ejC 1(.MݑKmX fhE•jz׀xLY\fI@G;ޯz㭷pwt7oy76A \UT8 ep4TZoOɮ3dWL HXCx}֡VqMׅdu&khݭpkpZ7%撯\L`wTƳjN ̊xaT)L&U.ȯnړ3K=XvVEd6f Ɂh!g1EwKy4׳r&^BcEWwm}/U5 ͽE۠&2ў#U Mv kvYf#mT ';́Y*doO]mWb6@~u-d,Z(72ͽ\DTx w rx}|krA/_;NfL.2קٲ!ȗ~m*֧fѫT' gYu&aSYؿdЄss[ +~Joe@^ iӞ{[{ j9S-+F9^KoWR7ono.6m&6 \66JY[!L֖եeぺEqܘvv~j)ƄDsڧ#ai)鉳S&MN$8?~o||o|Qpψ{d0Iй]gOϞy0L3}?M8KxV=O~H)S' OґtHzHyN S럤zqpgqϝz!5x$5C'1M˰Aý #c#c@X?aB2'9V2r" L Na%p@d61$5q#6#h~[,^Nm Lj5ZEgKNM.k\6IuJJJʩ4ՠ,Ghn',o ]ĘY#gJ`|7s7\mF ?kܑBJo8\y{ ~cvT 2`.yXO7dE/Ah߹.GTYup_ӈn)mACz)C̓qYI>j]e..5|+ Q-#xAbZ7 8UU eI9+ѵǺ4wl{o8*%2*=$ߋ#LGʳ5NsOy5KJ ʘTEJw84$Ud߈]hiS qTk10=qIXEdwT$bV±Q1_6!ErNղ9m_E[2ʥX׳xCsoͧ{3M$g{c ZȤ-S\7F Է]kTob,mcc9! 9ً˙j_vש@1tԻ˺; Я5ƾg '|B@qmk%V h0z&aq֒+-Ewx3X);CgX:?gԸ}L0L8I8@dc7 ؘ!dazrN.MsqY4ͺreץIy ,i*.{Vqm,)dNH:||7-`c]omWx~-a YIrB^?f"YXʍXѮ98~A {Wq6+dy'{rXfvu|-A |PgbC;KX*Gjݮ&aD72׽qׯ WJBnSf'u@F IlDGƕM*lu8-N!OBR"&$>XR)GHHHH JCb$\o--\xAOOJa<$-a=^~SqP~x%nS_'|@_Z&nFk+L\X#}7b(SI uY|<}ëpt]cx](>/?C C}F Ra{l,+ox6M:Y@,j65TGOC؞r?xcGčĐaK8sJowCw}ٗU\Cͭ&Z޻wjč+Ð8 9:'{s(n+<*v=[&ZV 9|'Ѳ?EjA::pú>caf!]'ZD2G|)/좖S:|=p#0CR+8wVcgHPt - &:$J8;%j ;Rݲ ^xD^ B >eTG.I؝|69RpZ@)ĐRF?0(@H B aPH$H$8;Grgv-*VIu84gDaYmGF41 X<):dzK8VHXd@}-_Y"[N%v,Sk%<s`\ έ*}NXN wP~Q%r-ϷC(aj 9' f/_ֶk8x/Z= *Ruv-ǵV45>/uPy幷ȶT!10C:KmHû3 n2}L6Y7.Qa(x"{l9"HՊs`T,l?K'2 -{h~%ar3}]N ˎ v ]qȆ窆+:M"\68.m5\* 7CJU0:8qfsyn’/ʿT)ҵzK8{rnYs/r_|M'g~~9}Lq0'qh~<ۨmTU@@H Bb"Dkb6V=ȮjU{HP}?*F},U s^9aKX6D)<.B!v>em:YpeZQk`TX!ԥÈu1(VXUdNCEP퐺lEiKmF-vM!.HؚXsr lZ⊈Uį"ιb;˂׈ A 7F` u.jLض3$lM֜=,%,xVNpַ"q&Kϯ"HՄusYaMS%ܰ {Nכ%Ap@ 9F"WjxARkE]'!JKNXlq0]ĖY;' b][Ǫ0F2nVR]XKPokJ;H]jC~=]ˑ:{X`"M0bQvׂמ叻@nH:6Em+)1¹ltyɮv')C=UZP#Bɲ\OEjH=KֈKe$Qv9+HxI&og]YUVX.R`X8!wFɲ#۹bſA粺 2n#^)z`x6/?T 7EZʏl~zw걪`P~eGh{Ned;SM $̖;Ju *mΆ`6~~םlpNgUR(ֵE]Ԗ|ں^T~*3/G@nmٸ9U󺄟'2j&Q~Gflb]s:ǪSRDN%r]lsm6J^S̿ΫRR[<Ŷ-[d݆XVY#7~3|0݁!a$ @!0(%g1 PkNΣ8 ? U%I8~ _\%Ev8čC%aG"_S۠$n,x0O, 7#a xiY%6 k4U+geum8pι5֗[*D):jOs-U8ᢞ+jNN_C85"F$b ;b'+.;uy"k"G]O XVCDኮE0>[,D/I]CddK?=5hvBzz=d`E18z.s=tT5PO V0AM?A(%lg<>%\ȭ5#CzC9gٜ?;]ag x[t ^!+-;l5nԚs86 ;[ +'z%ixKqv@h 9Z1&nu=̄@܈[r#Gmqk |K8<^@lpӺc-ayC魥=M T+{CJՐp>f d%lZ)UH< /um["Ք0h 01Hv'   aPH|$<;7|׉S}:}x׉Jxn~Rjo~<߈')ᮞ,R&bSpw!!!!切c+)BBBB 1$H Ia A»J %%yrb%J$yԹd$ܰn6z3%I"7#QcyLD䑨!K؝鞣W [mt9olltuuvZbcn ]G+[/m]RpḪؔpu %2&|f d*uG¬mYXP0bHi݈%uF98bǵk:/YOFLHX%lMTUr󻰔; VCHrDQbH)VE2&+:/#8G!a$}9$ut]aIjdI(t]uvrW 2+J$mu'ϟg <ȊsyԺ:j6,$|Ru-fib#brK"lo.0gVđ0q#1Q^n:$u!a$$$$ޜ~VpTܴKxRuBLr[הĐRF C/"!!!unO !!!unt*0(K8HDpJrxk~M6=] pcs#5~jNCNI흽5'%6*o=j Z[4Q#I̊$?3ύ/Vyb36Ut 7CJVz-XIv ._ڿ`BlnXl9g9Oa^T|'+wn V )g'Je9" 4 ?hyU<֗X%(Gg-26= ԝl%XOqiGXqImcJЎ_ LºrWIy2`Yda%ڎ/0c^Vkv 9K.6@²<ܨ7~8 mG )?1uv'Gn_C§!t)10e9'Ο[JHh901pP„ a# 7 !K؝Lϩ!a+ᧉ! 0($p fR 0 D¦u!a"T 醄! 0(@p$<Ia݆@% 7됰;SGt u+0iJ~M !)SlnLwO!2*$ R@H B aP$ @!!Hx'=Ia]HYlnLu|Jv!fE@xn_%1Q aMB EpϭY9? :)]6,2*$ R@H@% 7 JXkV4 OCNXNpJE]]OCJ)&0H B Evgs}Z$<)eTH@!0(@H B aPH8i $ܰ^qpjk -DwדU+0iJ~.'!EDjZ)BSKŭ\v $ x gHRʨ o4K0nZ)•5HpxqڄQ!a@H@# 7 !K؝' a+'! 0(@H B aP$ @!0(@H B aP$ @!0(@H B aP$ @!0(@H B aP$ @!0($f^KxLy Mf5_yTmVϺ3ʪok %jHHy{O&C#^%)qz\'3%szÜ ۜu 5]jKPS!b54Ȫꬭ7JelsՅ|V+7CF 3}ϒ˕r͹J@"]alz '|)DB:0ii]4YXmN 0D+Zjk]pgp&NY""qc4>V+k"Y>}ftvlrBFU 0qZϭUy$>98|/2ESÞ%LEZ׎M Qڨ&Nٵr9GH /̪cBgzLOj\z655AŚ;u^eS!JRU5 8gW˥ qc4:I.G.ޞa~2tU8jNy~.Һd$2qⷚTL7F#᱑Sb*Yɤ@H,/:3<<й4Y6Qڨ&~sbE$15>S*nkޞ!ڜ[&n@ɩեQzsR'@H,7CJMkOR[3 h$\"Lx@J{m1 GSڳ3O hYهS^%~  [3W H B aP$ @!~$kc|ya- U|zM / :x%L.87Rg^xb[gFH;]ݴB0t9ϛSg/\OoD?51@*IwtdH(@4t]"DyTjsooVioo{+UU&2eɱ;}^;mNMOOdys}.^XT[. eRy۷]nlD.>v{ի}{?|W?dx}&yprmTד;;;J$$xJZ_HK7 - izk[ ? .GnĶ~_Ź[nټ&w~%̟8E}&/ FBCž]c|Ţl|ͷv@H8v|.wǯyY+޸V|M7p 7<&]ssT¶QIH X;9E(%o 'Xכn馛̝]w3Lr* .ww'>UXkr;ɿ.͍|/{٣}ٖ)uDu-O"4HiP+i[vi&ҥ?O箻2^~SW  R].#/{G}^|r$/z;W?ӛB;sĉ$/ [*d{;C 1o-Q7#["??~ d>7ZƯ<$/yɳ?Ϝ߯j"z1'S<;^K:=ȽR "gx\ԀirvJUIY-%kkzj`6W T.owNߔeCq@vw>&f2q#_$?8k??W"ajiq0D@}ioI? [;EZ)J*4ת:wz -_T_vϮ3|qxy9̳xedUlN~c5UR7M6Aq@r;S?K%#_JSb?~cNޛPO83Co"`ףߟɤ+GbJ:z'?$gvlIJS&\CFCu}C/X"A&_~wJ6 ;T&B)=v߽w ~MCڤԮ|G7M6D}rSǏŅHV{@- U%&&vuvCo$?vrjyax|lsm*jvuvB C[&}k=mԮnZas_BCH B aP$ @!0(@aH@ CT̽ +bZ&p 3aZ-ReÀ^c-bee8_-Ώ,o@]]!3K R1O٥ũSW:bZۥibݘZ endstream endobj 76 0 obj 17856 endobj 77 0 obj << /Type /Annot /Subtype /Link /Rect [ 211.13 322.417 389.18 331.417 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 69 0 R /H /I >> endobj 79 0 obj << /Type /Action /S /GoTo /D [54 0 R /XYZ 72.0 482.653 null] >> endobj 80 0 obj << /Type /Annot /Subtype /Link /Rect [ 489.395 207.534 523.275 216.534 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 79 0 R /H /I >> endobj 81 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 195.534 141.748 204.534 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 79 0 R /H /I >> endobj 82 0 obj << /Length 83 0 R /Filter /FlateDecode >> stream xYIw6WׂyxLVM2\OKk@6T^" IQAuS 8Ӭ$J)0!Q%+ #"CZ}Wp%O7I?<\S4=:'),KRCP ccJIEzByN(&S$w/ͼ/4TF~ZذߋҍԷuy?r.UaXyY'R,.Lʵƺ/\^hHʜD4\خ'&_FLxqIhB$IsZKf,nͩ{^[,ܖ$F |:uc7Xs8(Uwb8ΝbEen)$-#s8"9/||YyRas cEݠAN'S_$őNS͸[.XLԘ ^!!k;/lݵr;ؿ6'QeyU7hGB Xǡ;;dTkEVigG#@Fo( x瞺\bޗw .bHb -޿._$,R(IpkIvHǀ?5C~d4xј CSQm-*pi 続(}aFpPG.k5L"H"d,3~ Pg>Ymꠟ<`Mֽ80oB>Db&(Pdm&s в&(znHl/^W$I`}XQ2).K.xt"XfLuw+V]?GqS7?]A j ь}֛BCZBSNtc4Ǭ҈E 4Tly0-;` SezQ;a.[ϝDlPHp fہ ~N9s7T펔Q|뫸X4$&J#?Y45*ՙƁX?w됆7gcgJ*ukX3;ԫiy?d8mINiFW^RѾ Qqj Ɣ0HL֎皡ṣQCsx}` YI1IҔ9(.޿H](߱;; uC7g")fҬM{F?42pN]0&IYcmu[X p,OKuU6g%e4pG q?0sgM8vDB 풲ɮ1JCٱbt rb8pdTjr;8!MW8ZGrd@WͶ> endobj 83 0 obj 2434 endobj 85 0 obj << /Name /Im6 /Type /XObject /Length 86 0 R /Filter /FlateDecode /Subtype /Image /Width 853 /Height 601 /BitsPerComponent 8 /ColorSpace [/ICCBased 5 0 R] >> stream xXZUpuu[Tp CŽ[8qz{OVPAeȆ2/ Ҧi4yާOd$N l``0Yf!4YMɦ̯:Q= E'_A!1C>f̙&+RŁrI!I%BC*T\𰯆#A2=6[|9Zhj%;soaMiXD0@J`~/ďqBPg\ S_A?Z @i =kW;Y 8q6D sE8찱nu( a?wNJvl x5N|ɑC/}!d.4 @H5@l $%I ~Q~RT>BGFT)Ȉ~zg cL)PCƠy_ǎ9vaM7Zu{6G8l $8yb yGiR) YDUR~:Ӂf>*gJ_ {oWG(qcaժfͼ'SzF,r;v[׿H xrْAoi|<\NЊ'C_1_s!>D\xd(<#Gq df D+I{ }6h"6ʾG(V?VGmGj-UC| t ׿jԸ=dstn۶~[Q"M4cۆcGrSum#|2$Ϩѫd7+Dė _,O_| >)ü@!_9|a:SdBCp堻DcG $NΑ("ĆG!" E 6! R-dڈLAj16r, qV Л4)6vh^"Ak '$ Cd(@OtSa~E0~Z_#I6<BofŤIW."\_ Z811:55N%6/?6Q2l,BC`na(|XZk,0 -X0bl)ؔL7ɨ8ߩr#/2skok𥹿_m-h;UkcɒE 1L?BP={hTc# Hݼ9{줤#¨씔L=DTp떵)ɱD}oܸQlL}X_>,4!{$#fD^,O#ހJ`>x *H1'U Udf·|Hi=Wzi ˠ0O_xK D??jHDch#nݺfLʩ GB ׏t$ "NW4~,8+kw7HJ&{`mAo_۩5iO4!#jbO^P}x}tOW!#y^/ {h5۝_;'`wÁh7?5=$zItfP|1Pg?1 O;H]D6+Bڭ+Kkx3@^ #Xh^sEsqONeTB"R>] =p8\11׭ بwF|noÉK}I?S?ݾR+O?(}rg̏sg|$"M $ 3NhO?Ւ'Ո54ϔkA1JE 0@бC-Z8ej_|1'?Cxw'6pGbҾE\pX'Os|^>x8$`+B_ӾPKƌ7mH-]d0)9yL`0W@!Pn2)0aXiPmTf^;fܟf\<)_ ԅ?t ^6e '1N3.[2پ~c#wAWcݣCXR˗-*Z7Ol ZݕOF^vҧٞ_S剄Fd_7n yn_߻kD6.WFKJt\ s7kǂ2H]y ^Afimᄤ]d̷MTa}dOƈ}WݏFWcݣׯt6]Bdcm0ϪnpOnzG(@&Y"z][B?IA'O}@33\:fHH)~"@(?">>}קOA?)}y!9:'2@޾B$}zOl>(Cb^_H߹,XcGWtON:Ak"zK5еs/ODS 1s'{+g+]n'=n~DtAfYPD zIab Jch"5|kte~;d_sS.& #jۏ4ʍ+?G>pT_>LTGS }j 2J+* TE$ӏ\CE*i11tmQgCy V˒/]cÞ>7o\NJJI%ч 6h 69ج-um4l2m&n>Ԛ\ k?tB{V] ZͻfmxK~k^|c-R41qz]5+Tk@:x#<&22rT,i q,H(!{S/#H<М*g.同"tHS/{&)gcw? H7ƨ \Se fd(a% Q X$联TΊABBH ;$,3ES0{fQm}ϬZ.y5(\Ndnz2'=AHrHGOOsSiBI~iHR՟1I@4FI~rIM '{r4BSs$!|CH"x qW@d> #!?b4F5D}# !{(J7 A ̄|A;_`绯"#_ԟL\|6<7ބ!F>_yIЗ'B^|(|x)!;{=yx oAf> z7}x+/(^^_>Gxv_ {=4@s u@d,DrNnFdJ *'zAOAԙ:T+4DsN$ }B Yu on>HF/sۇ!) >!ER@K/  (]NFSs^B=^o WјV!CbM Onآ5^{o(It,"y`&fb&f";$ĞE@p$BetKkS9ZA4>hceRkzxk T_'qP,^`6~&`SC+y LE|o:LϤLYb 1f|PA۠9T34,#Mkӏ$ gYPBd}LuyA>%࿊4 ufhބ-۠/t {Dj1LAp`=P%/+h I;!=ZH5F:#xF4^ӣ,|k͇>F~sr|Q,DB3mǕogȇS+Qĩ $$ 1j10PW*;rQǥ~ԦDFs}&х:ɓlAф&D%ZU`h;E6H B#I0 GQR6`tD@%r!2fLȩ nqYtv,QSOOC ;y5ʧ33Oq 4bT0R7CStS}4h cqœ&!:Nb7jz7CYG@=P? +zU\6h ?41Z3m6=òW>k0E$f? Q/1Ʀ%9֧a2G8!icF#xh.K ׭i GXM[/e on#I& DɒGl]IUI90.Fu>JPi g\{|1\D"%R)gątU,1(xE\0l 6-1f4@_LR>Gtc`@)#u&06=RC_1('v#LLY !i'2h930=*W}Q>  !C9f?6BH )j3o?,,&i'mwTQ4=Wx|CcO5֔N qstv.?F G38R1X04'$͐"\Q,?bRɄtK1+1X9!ݵAd'$o4!iKݼє#C!*LNdSEe#Ӱ(` mĖA&-$o+"X0ѣ|ZHTMlT1xc(-'&;!fh"+~hMg0=zd1P?v.$,,bBd"ӂ[>IA匙Q*L a[|,KLIJks`wB~$xcHB[7/9} 8{) 2 BA=emcP(jMO4 緇']=OA]v!)1⇥0{]#{ =lt! `A~4@87- _,:xAh[VZHȦ  x%hL=~q40[2AF"3CI#@.RIl|fFvvvVVL³c>/w$++S#H>I XfJ)sD D?(e2)r.'^վ}-lټcG[-ɔTTHlJ_P H8^R@3U&%l,G.)= |i9r)JB%p8)B+? 8ݐg'"V$1cz 1ڤ-}tsJcXXL*QB98NF _.͚5K(mԩ ?u-RHVa(G 0`A!,~}];7N&vvdԩP.Ebxdgߠa.ݺШqcێNjBIϜ9sT*!K8Q)_~)MG$%˄ȾBC; oݺuNlmm;tеkvڵhch_F E 'eǎcǍur8~]v}~A;Qm,^XJRڼi-p[i)\M$L(`"Qd`"y[dI^RSS>6 @_/irぐ9)Cs8Ç?}OYnܸWB.%ˎ~BhJ _ <\&^rҳWՏ-l$(2*@yE ֯jA%'MvǤxq+EutR 5eddxyy5hРA, ɓ'm5KSUn(&?i܄IujW9jJaGN8gSbӀ~9o 㟱TՏf:]_Jㅂxƌ+V~zPʹsBJg];ϝ;RD~.]QQh/ &i?lF۶m7nܰaf͚uرgϞ}mڴIjr,bQ*EUmtӮuW~eX*M6kѲJ.!]lU+H:턻Zj]10yzzZYYͻ1Ǝ3e_߮E}:c>;tpd Xa+z)R NЋwl܈N|oܼ~ݺ8tVnKbpդݻeN(%DԬ~ +o K,ASrΝW\i4 P F 27y]vmӦMzVZZ5:5 >-[D*ʶ9^߈izLc~kG'zVXn]}ऐ _&MvmvKj.hox &^.G;9;?`\.7++%J_'ѺQ tYYLG޼ys}g( יS۶iנA؋*U4nI'L0vm2}܄q6ULŪx14 *WcۺB7_?*w?(L! (4O&/j*DBVO-)~ ]v.>3y^`!B,IZ7ksEOڷv^ܼHuj7Zx_J.] Bx10fN{@&߬  "1yxv v0 dĻ̙@_&QL^TTLS*+W>wB.&{o665F[V-йoAīg/ `eÇ0G_լ;=!mл,d"oO P>l]˖-k֬Y\knӦˈ#OѾ}; p<;ZnӮ}wtN-*T֭[׾}{CUw)[X,& :7*W.?z1&h5W3$8i@/ޟwJG&jժ3ϟ8qtLPdN:rjO0ooo/>_/_$|>njR>].]hZju֭'MhѢ 6tMY&Y[[i!+:T/ǧJ9m%44T\5F w;A\ʍ6Pʔt*/q?ߍ+0@k V :n7W5k 皵._Q#Tnj Qʌ6X.͒o9+r5bp]OpG|aѣ TRgnhS?+͚v,\G~_l9,P‘#GCB6|M:DD[('F?ISGI!:t?k:k y;6ؾۢ-7j&Ԯ'"kլ?6ؤG1Ph07c;~?>4B2%JR<==_4'Maih׿ϫVVVm،N:xΞ={ժU۷owuuqN R1/22:C Wj/\v}PRր׃a g"%R '%) Ka߹a9;-V&B_ P+ } ;;;8 ,>Rt3@an];tb_J!oՍ9&p?kR_NS)ƍֵ[~abŬz::0SxJѠS+T9s6hq۷mDf lٰcfi\G*PD?DirJƾqyUͽ=%DUf͚XQo;}~()SEPhu;~xj2222ѿ(F.i־}m;|qu׆Gm^6h?u&5ڃN^xYYYO<bSٱ!W6g#e'.)QO1܏hzg=GХOk޼έ[4mٲh^~ 6qDH'[nuww4p4&ޱcGUtooY|iOW ?kWܷo) Bc9߆@pF NX{$ #SLQkϾUrkkkH-RDRwٳKr rSm>#h\J4 Tg$zWB[?: pʋڹ [lٲE!KNFٸaf5Xjɞ{Gp|Yځiq1Z\μD{Y|9Y'y٠n9#jD;7MFg {65&gZjQ~]n:JRr:0)WlJ6˗+5wfA{GK@0}m[G9m; 룬K.E T966Rŭ.mqmu.ϝ;~B;Qӿo߾ EǜoFُ蒠ĻX-yij3i pܸqSN]`׆ ]\XiF$\7mzڂ7`%п,Sܺ.ڥd7{%4STJd4+5rҿ^ˏweԠRFeQ)SPs7U9s ncƍG ~~~~H9173|R_%_C42m|9]'8FѣW*WvhAxp%78u\^kb&J={6*{O%ǏDΛ7Bg``(0?>/MGO>C G"\ kڥ3\yo-7l'|N#޹{!Sf?C G[]7ѳ#kyBb NЋ7OKks3?ٷVHpC};99)JX]5i,a>/ ?%?B'HõZ=.߅/}rrrtuuҥsV-! Rsnjo龥˗(Z~ퟗݑ'U]yD^=Mɥ }2Z.1CqÚm[7@ 8yTo& "\thAo8)RAHHH_йj.]޸In߾]{ \?̸Ha46|iȹ͜EDM%j;muF 7ھӁ]WtrX fjV{#}GLoߖ#Jcƌqjɤ"iYq:K4x9l#LJV- 7nxέt+Vk.Bo_2|l9Ph{ &,P?4/\0h؅1XC? ='ɾ}CS-:w!QpwPNm6-uZf_r쒇E~;AtɆgآDwvm;9ߙ¿%:x)Io? |3@98vչ%QL1b~>nsl tިw_WCЦ8&ItCzIƌ 8!C_8'NA֭[Zt?=GCyAwyf6e4 ݼylr&d5m& /^b擰6U%|>_=7p}[.v(ӦN[ҴjYe<ȻyD6$%=*SԎEߡUK/Ex{Kԩ'/\4/]hV?4\{0gkݺ4tM;M׿۷C{KLRBBBT*~9[yc~gO>sا/nntA*栯}tnΉ%6aP8ܳhѢqqqR8i@*NOK*z TƌSt? |2eѻx*\/_sKD*Q=<} [h٬D#a'ؿ{n+WRQ#!f.4@}U#iR_823}1@}[0>m:g4 |#rOO!':} o| +kkEVVCP|po)ګ} 0͝#&H 6cuOl>t@a.;ksv~͚؞ʥʖ-?bL*p_#ͭv5N"_R)5տM'ܤMoZsݑ9Ai٫[>.S.RpQI?~; ;Á_BB@r͛˷ ^$ޱEի9?m*g=zƦ_ĦPO: &߱cdž zO==]'Sǟ?1,~~[hҤӦM[vЛgჵN'n9W𗰰0vL"i@/GL'8vU&1A' X~F0R1YJЭN9}/Cf_HLP͝owo׮$'C *˗/ ?iPi:J,]DIzڔ`[6\Pi!JH+0/呿j&TTIP?eK{MCP.lB^~^M^2&r Y};ִJ[3[@ N *ț8J N+W߿br2${v<%xXďW[.\0dȐxCg=}AT{QԿ)SWܽ YG]BYgPȍWx3g PV>m2Љ捋:Q(7;v@a߾}ݑa˔r*ը Miտ>]1" W/n߾}׮]݃lݻw/^bͪ`>//ҥKU*200.(W4qf% oQֿ&M : un=꠭+5hժQ>FiѢFJAEÉ( Ey[<#""ln*w L)`- JѢK%KūѿX &L2J.3_ppÇߡfLwz\6_ɣ͛7% h)'ty?!e͜5Cjl>8+S%8vd虡:4r fZR@ 3 ޏHDyT x "33sرHZ w֪jժYYYzGmM6w ‰~1G.("@&yKN:^/XzX1+f``%1e/?HGPTJ3xQhQt%5h*ݳ-kn{ZʤlߣMmA*T-׶S=] ܏ }rτӚ[(S/13fӮW~2eO-^|ʝ}f&0ԿwA//`hxH}ۦbŊm:G$~h8]֯_omṃֵ3֣K=X'Øx٠g7o\tsvsgeprXr?CQx\~gN%hpe(TU(׮^v&_ݡn}e).!_QNDT*^V.=حKg}V*/鑿*E/מ-lgzo 4oװ_~E2(ȑ#'BIIA+QjkR\ׯ?3͚TRl2kV#He*WܬyϟfVV&Z%I4oȶ%4ײe# )Py޾ЫKJR4~PFw{A֮ڥn7j\j-Kۯϟa5~E ߹smܰ7!aF1LS :J$Hy{{F|ێnq">51a,XPUw͚9rXi_#8<<|NΎ8h`DDL7a"O7'P@(vwE @P2  ,JVfTrѮkg "Qmٷn^HHR%V?=qDbŊP'OT  Li}>4fޝk4Nڀp0_V|sxrJ)P #kFAYjD'~8IO?0ȈPs:Îdggqo~IPi()͊7L0їt0000X 1[JAc+ud``` 􏁁ᗇ#!.259 tVw810000000V0[o 10000000V`;Pag?veO6m0k=I6t1 !?$1Q0Q0Qg"b lBJJL,bSA1nqr ]T*TN!8rYeWXHo }q9#/.i? 3Y{7.M xj*׮Y׏-[4?v)ąs'YƏ"El߶ϟb(a/(9 "-%ů V`7]{gɓ=qT*u;w1…  {Eڸaͺtݱ9!LJI%<0ȝ;w?__߿OQrرcرcC'ߵk(>}ԯo?)t|!"d%:&݆ᬃnÖMw!K-~Y= x+| ai !%La YTceJ}znk/A,|₶0q_7lή>ֹurv6`(O<)Qѻ}ڵk3TeT֑"#CžY c22U\XiH&21=+R/` RgS)묬L;;;]vʔOտ\)l2 |۷oǏᑑ111,ֱ{mZ)][ Xf@ b=9?M9{߰cf }8ق@.vvfM"xL(g&R_Kq/-nZCVSV{6?%:f/(/]/Xlag5aߣw"K, ^ Jp KhBIWN>҉b:uznTEZ:մkWáUMmXW5\mڝZָfz{4#HDu&Z[W;:-]s|q!t4#nc-;u~+1 S*W.9}ӧMǻy_xޔ88 g'P=V]NT*6yZψʡQ#)nsO,&w_Tg_h2)=K":Ov*"S>tss R1W׬ ggg+++/^ 9PXF qhq:pqVNʕ+} }ktYFV:OċV4<6?#[U]zP"?kNzODlサםzDp;fOΜ>nkCBa3N$|п MkϜ>._<0= & < ,1 jUJM?Wm6fCWN=[nJ)#3Q`FM( epEARJXW{b̸8(q5 nj3n8??bR.JS% !QG1iမ?>y`׭oްQ_߰ 5+wA"b_w~2W+uxR( X,432݁]8ﺵO7o\f_'8Ŷ ^AI;BBB(Qm%44hS&K݇]J-֢d xydB\!R f)ٹ x j7ީYMhOm|Z?vZAw9^~RO'oMGs5cGf<пKGϜ6#c[]ځ Fq׋uW|\_)ŕg::t{3oCz^A6p(@Hoؙgn>3۱g}" liOiB9܋ʌdsv'w8QnMMLdUmaZE>4Pًz%K̪}۩vx~(~U3?@2vrv8wF.ۢUk;~rcuf8زu[HCPeʕD"b͚5}ƥO}E._&/X0ؑ}/_+%ܾui&LwG 1ܯhnqUۺw݋5AN0J(hO96K,b B~`5M {VikVӗ|.R5[-𒸌 zww^ Op%OKwwwAё›I#؍Jg?ݲBdtc p֍ eYCltxfzh ۿ׮{4j"KDi|Ł^Oz©pB9m8cz捺Ϙ(4dKh Q`꟣m˗Q2.^tlG^p)_k:& CyŢ$UgQDܩ[q4{aČN9j^R}9wvη>wzOn SR bMڋ'y{@.]m/ܼ[8kSÏW"^z&6}HNYߩf% { M%dbİ..\zq=ȹ}vi^C riPǡG85<OpL{54t٭n(}p?40lݰ~f5B>36޿jԤvaVkY2dsM<jO Umz6t?x?~g|5~UC2oܸ!k1տS'Ou;i})d;vhPň0*߿m2Ν;7ܸse!Kƣx)de7VۇIC2ERϊ>uWTmSBgSP $ -Z@ǥKyH rY{F ߦ ?ooop_t?8ծU3+;y4#z/䔹|4~O{]oeiSvu-W")"|oJC| wS;IB NjwEQ}U*7͎o ޟs;5=E X/<6߇\&u{?FuE=Arӗ-M?"K$_رc?~D[.^#uߥ7*p?gGsS5_Gt7l2etOs+^R["N"m? M,L={6Z:yDс~7oöYׯnl֪\yJK7_An+Zߒ%!) Jg77eDzfVFdh] $gNݸaMIٙ"'8w8eS)ecǍmۻq2V.u'OTJ7n\[z=2.: D6 ?<%sSݧUț6,K !R 7$$ݶҥKC>u:Ԍ0kP۰a_TTA e1QakTL )dziAcsapMOtrt ѿ4v0,q=6&rj?alnC9n:P>=>d7xCn_:7y'ԌȀ-qScm/s6juS7'y͢Ԣ@o5v1MkYR ={On|i]9+9>>|岑cauOϟG1m}.?{gFyﴰ]J.n[h9ZZ(W܄@YJRp(*B9H!pA Bȝ8N|Jlٖ|-W7[Kc[},f|s:.~R,2Cd)#|Ϗ_`chs2i%WW| ͍o:ӟ:27LʶV)q]M&1``F}맫V?~􈏮GR~dΛ8_oO&M|಴ڏQ5y=Fj78KÃɿF^|ԯ{Ꞿ]nglگ?gYvˉ+y/qC~mwWR_Ych|bF(DwWkW}qSo~۟#{9̑P囏]+ˬ]+Lgau~,OY'sOʿ;>'OvZH3*ʏ;j∇^ssߞPYW.u-ʠ!Q)Vozϩۏvě'? 8Rʖ5񣿪Q;{{ˢXbcw+i~L*~H s.¿ ? .Ybo=]m渭 G WRR8ktbĞ_uU}70R/K{iwI*NՑ7?އ[<\~w}3ƒ]{s/kGOyᣵڙWk jƮ I=뢙/[Vrt=f<—^A;wh?2xO`wI_Oxo32tG_z+rqW|3Ҽd<`ڏ|r]޿LK|͸Dg+w؃={(G92}#"d?LnK,iiiaG>&> 65p&l۷~b MI:%&<̑c?3GO?5co/k/_n矤L>i;s9ֆKol:K祙}GUL}fxs޳>/l4m 0zxڏ[-*g0<_Ūs.9a0:1;ʫnKW~xq2-j޼M0&oO6UtNxzeKyHԹ+{{ѝmuWu'K'.4_/\={A7l1,S\^'Gn\3K|rO+| O_3Yr+Y䊘}3\ķG|bŏGV޺wo[-pOcgmT'z2C Vl6%xӋw/4n|WZ5Џ  昑l蒁W?# ۅ/bZ`GFu}~NjuG{3qcכ(l2vl?ˢ-M_, :cަG|ϱ毭x[34?/t{n9ZN;IԇTK<mH:-N4|i7q~-(vvaʦXK}s,+Uۚ: t:.;&g{~vmS.b87wt^yA`?mMN>st3Zsy)hI}߸.n{1?5s8 'Џ͆QE:&Rw>f3vܭ:{f?ާO>S!:3=Tk;g|s"V}1R-*׸߱}șX-/%WS]qY}f2|+rs0r r]/N|0~,Ud!.j=k:ݵ[ònaO39^/7A"_y'?#wT׸jZű ';OSl+eE\]3%0LKG`_dy7J: ptFB=ݝB_| GꜾЏWۏC-o7g³dt:%]O꟢[]]}g<&EJe}Œث 4)v=]E}}8폾mXaϩ?־bG4M}[::1HE?2{=n -]\\X'_L1~"VjooO/A}~)CZG"[{%=ն-Vz[ygO=Sz\ Թ9@\;:`pQmpsʩ;H&nUxvi293=O1TWg׏ Y^Y'}}@z|/{9_rL'SL}a8< bz;Bov-Ɇ5F5~3~| }kZ{:i|$}ӓi#sss`49sJ?o*b y2}aWe`86ă8*?|ET"zL+zn6:֯_O&p~_d2'M=84{r8短SJvο5ktvv~Ep"=vodvu;iRx"t;#&_?NQ9OCO>ʕ+Ƀ9q Z`wd6{8^ &@߭o΄ɓ'5fvOY+w^Ȼ=捓&rM)Gb~9e͓n@n,xwP3$&;ۻdЮboʓI߿p/8ohjnZd*ˎ_?t?7cbbǶ-3 ]:扟_ksD? <>Q3.汷yZ=$+]]/ʸ 701'ްhtӐ=!+IRάb"X,}ѣ=:e-ӦO1cƪUV+V:ɚ*V\IVAR،8#STwy7uF{B1ٿ5?_씼_Zf=pq59g0' *LW ::H hWg;htxzza =I.kvЮtxɂh5aA!BB!+B!y%B!0DA!?!¼2>"a(k5UG+ʢc5DELY[VhPJT RJSW jڨS &c٠zjnhjTYMDH&6S٨4ZQ_C4jȪwXPjrR$S7HRU<0eՔ֔*Jj+G^-?ZUY$<"K +e2a䰴␴⠴ly}%{JI2iG^} yi(+drY!yEUUGԖY]\K^tEImMITYSJmd V+Tds[,!+ru,MNAodS4SpqJmѭڌQVl1ɖLmFCAKm:mN-'+"UQ<jJUd,)]m(P<;tj"A-茄 DNgt#S3GV`9NSv賛9e'1J=̏6=1){iOP?q"&I!c0Py?/yi_׎zc/+3(f*$D!uum6{>}B^v>v_v fc}yӑi6ޱ响k>)_3-fu#$Md#7_Q!q&Z5)$3bv|f15U$whQoi5 !P}ۧViʿΰ@0j ! Lv$L&qIn d6f UlgN?|jy~`!1SڥJ̣ }a4o/ P(?ucdRV@.BQKgA2xG/8ʿ=-M:vB?r@! v[Sqqu,Y2o޼ŋ]HaaK>oN!΂d623Yy O&9q5؟3G]3\!Wc>G(cKk;851G`~76π7{,ٺo߾9s̞={ڂoٹaz]v62>/):ݜ΂^iE5YFFͿ&vs#b'RNG8sQ2yagwy=/ 6Y-z7w2m_9K'\6mZSSSK_ӟ$H-K_/+#|m'f:KiNLc<=li.^xF׏5kVh_Pg2|_KKK~}gyYo67˾'u:]]]_$F#eёxz/cΜ'NLP_yh82ϗI%-دD/k ή---ݰaM6IR2% ~/Z[YFff-˰ aK-μ\3ptu{z;;#HG0l0FI;iDɂȩ@f#3G!7?%p# {_LujMN5m6{[Kk=~ R^OK -H !\mEAe?(=H\aߩy gGͿsTRvZyWy?ɥʤGGCDI. ,:eᱢB89V!#3K*$奲 JVAT ^=VVZ&xys++Ae^[EkB!z7%+JTu|$'U&"2ޝn۶}<$rm"H:I ysIe?n@0| q#1b?OtÈ[ qM[[[B⡟+ nĔ۬>0 qjH贶;v,ᷘ/=kb7s&.B>_nX ?=u&A?W?@~-i_ƒuْ·c}_v_ CO6?6t.z?qo ʿP4\d/39O!2_OیXa^{[TuFB!QRqhq_{%C?B!w.TRd[nݹs={YOݜl4$~qQɐ8|Ϲ8Dq 9]>6;7tb#I}g B#۷oʔ[R1mH" pDe2T0=M@(d/Ə/3u䇌t/ 81=<M@({W>Ϳv'k?ݻMВ}=␼nH+vF!QR~0I& r)L=]>6go׮])ڏz䇌TP#qlb|L{! t?{O7q{h?7|smӓkxdGl[#ږȪ _]7z"a.H6Kq~}?=Ƭ(;@!B&wP$H+K =NT#@9| !'D_P=柱-\k k >m%lzoϛ@%'/z' ?OyjGD,g֗yX3?q2MVF5Ƭ(;yWdp@(r@feĚFT n^8r$67Irn$=̩RfydD!k0˕mĒc&ѷIz#6sb™\uPfydD b@(_߈ӉMckYh&UZFM/cT= Ayބ(8_o]s5ǢH$S>NzV/˒cBc;Ri!.XnosF!R_3Dٸq#{}8R{`_Or@HIM?s¥~) Qq"у~IM{K]G?YI)T= AYxބ(8?Џ_yM㮣*õݸrpH I{ 8Osxޘ_pͱJîx{ŭ{J?۷71v{. ?^3$[*E)haǡ<ϿHOu5 FZq\i}LyD;Z/h>-Ac;JonC8͒lU&7{gG>rxqXC5d_P=vuDdl@s1ueTِ DB();=$˿@4>wc4!Ϳ;ېXQva3I& rh ); 򏛸  0Dq ?h)_{K8ڐ DB h3g`.& )y~0EqC?B!.&?m>_y%7c"̿K; EğO3ʿ<M\A?vͿ:CYEڂ5+cUW+oku;wnK~$ggf)f4t<+x~\?]4L=Toet7ق"M@(_Vy3)~ݿ?Pr;M"f_?nb@(X!?n"BAA h?L:Ns(a,+=`G%B!wP-?PhU$*R檐U 2 3#3e2ZkmU&Z ?ng2Z[[޵@")ߟ^G2 B8V)ƆW]uUA]vl6z7%Fpȭ~o3s6p4[LMr3#{}0-~A_㴛=. GE尘Jhg#o=wB._a(xmZu\VT%?*.)U֔)AVtjV-k*@0jƨ&򯂺a *Ljʨ5ȲJR4ZLjRB Q5T+deuR򯲶$jqFqLQ}(9VWUˏTɋHV JJJ!TrPZqPZ~PRq@R}e%{)-3SC? 2ŨUrZS w?Bd e P0~T3~ȓ0~'P|31C!c 3~T1~PGЏf7CY@oѡjj#$j.%G^s߀&FDmϊ8&9S×T6VFZY޷֖CP*\Ej ؑi {^9*ctcvsGu5tՊ"iF!bB@G}쳲\CaBA(J=N3yeCw?ЏHo>oσ<>?eHA]Z~#cNG!B'0G_gF;Eiv;uRzXDRjqӇvAS(蒖D)~BN^=,fCqu-Yd޼y/^vmaaբ,C '?_8tcu[~(,.ͼo߾9s̞={m;w_nΜȗvj9;5dU㾂K&.B.埦t?(CaM)rp@[նFeKGm[g͚UZZv6^l%O/CI_ 旬Ӄ!ŜofH~R&mY>) [lY7m4;v[ɨ}s Iő)+W~qFka.V {x啗-CJ.  :N1i_?O8̙ld0)VaX,/ozFx$nqDQ+I.]k=.K[[7nXw[#f}]cN'gC!B&wPJ32Էj​P_m2ϗJ.|sT*EYgG˵چ?P-H'\gq e; />&̿dɚz+0'J:jrܸ*dna"I[,-f}CC-YgkȔd "2uԨQ(`];$M!RLLs+Ͽ4(PN!QS__3v옳:k *UѠپm#G.{R#^`*Rk V'[t…7o{ hnn^Eg7Gq;EqՊ!WKs%Kg6Q|>x8IN9J_'.l:Zٿd7}a>T(Ni֬Yӣ\z92TB^D & ɝ]_zlo[3aʊ:m` ېHxWla@(߾#BE!!dIq||.4.B5aD!&~$?--CMN__! E71 roFe'=m %!E@{w 3:G__P@AY"qD!&~ ?AAY"M5?y?~; xf_7q r3Vs-Aȋ?@l/|CMn_y7z`];$W__y+ba͛7477ӻ)\Dq#B!7!Q7(79%79!"qDqC?B!,?AAY"D!!dC ~@Y"qDqC?B!,?AAY"D!!dC ~@Y"qDqC?B!,5j7J0{ wS?!K7… 7o>ܼ0 B7!dC ~@Y"qDq#B!,?AAY"D!!dC ~@Y"qDqC?B!,?AAY"D!!dC ~@Y"qDqC?B!,?AAY"D!!d0ZJ_:k^];$beҤI!dJ?Lx6_ 1 pBq_n/@( ,q7?!K\E ~@Y/ nBM \_@ BM\%7?!K n0B% !dπ!B!dnj߇  ?!K߀GOG B DοD@ ,)/w/1QNZz}<H$N駟wS !dK,b_P@AY"R_9# ?!K nByDz  ,0B5aDqC?B!,?AAY"D!!dC ~@Y"qDqC?B!,?AAY"D!!dCč@~T ,u%!U%;CĊ@TUKKVe5"2C?BAɗMZbR@Pjn?8PeAWuYx?Cfc<p0#IN_{իWkX,ע(FJVh)mBs،&Cmyӑȿ<H.;RUyXMuqTY[V_WPK JUU4zMA+SVtQU&ZC@0ȿr٧^#өRFEBP'k,k#+-%*kKkKȿu5%:űZbUTUȋ򈼲P.+KWJˤd2!䀴⠤@e*W+/WVxO)̢'zKP^B!/zȋE^2YJ!"VJzY+:׽HQUTCyl ԆAm!u5Jbtˡe)Q)E63jc#jN#%%8Z"Hm}3l6MIOinzJ4rZND=K% u eMYUUWԕ>OSʊC t4>)AGAg8&\}P ډAZ-jm7ڱj~-^pA}@!:]]3vwK&9~'NA69A}z~zK}M׺H!1ovҷD%[umᱍ<3m&mSF|Fȓndc%o: 2P`Xn@K&~.BaIJd9C.5%*/  ʿn4H+ e*/Q,T,W)_/6TP84RZf JN*C?~fC=ژu2z'G{TMW6ZK=dˬ,DLccu9.$ BAY}FDo^O3Nog ɂk}K_ ]e_S]ժyر"LՠӇvA_@ nIW\|lݺuK,7oŋ׮][XXhVF q:zS`J"jڷoߜ9sfϞ]Pfׯ3-]u Oq]q_֎T})~4++~[j1-{jr[or֬Y.95.g+-͟}|A N7#?Hf,:]aDθ Lv')gÎgIx;c=` g3xm6m?hЌ~9|„Ȕ+?ڸqz0T5{x-CĊX/ED x;=wSW3zxO Oz\&eFS]?j7N"97~8"i9bҥz=k~lmlgo=Md "2C?BA<9ٳ#oS8TnF% suMVB!;묳>ZV*u-H'hgqje?XG+$r/YV%;c)l-ܯVՍ7(Wy 7Ljꫫ*ׯ_oX' Z̺ikW)De*./pϙ(΀.swJAAѠW*ƎsYgM8APk4oz˞bA뮥)XڂUD!b7Cr/ىdNM6t47/YZM}mwVUeeׯoll{[S,]>4qꏖ.}/?.k^];$beҤIpl &Vw\)"?%ZPv:Aټlٲ+WJJK}eKAA x~(:8'"+Q_pz]HnذۦMR)B/ /31 Dp7ř78HD::::#{?@ n!m m~ouA_f/@( ID!!L"'!L"'?a$"q"siRa1  ?2Uh?yİnR,Cĉ@EQɏ?տ Ͽ "JFV|x{q3E!DC?tji#a`ooJGk6[=ê6X$'B?{Rrގ/tdH֢&>iy=ƨJVAV<ǯȿP_[H8 mY:;;JSaGڬʂy1kg?@:ڃ5 ܱs6}> sY^;s|̾ȿ_Px9f# ~ԒHYcef-_kgA_(HO6/(,&ElZ;s|̾?@q3|&O?tK6a5L,hV%w=aοLۏʿ#>L2@M&Kv?)ΪN}y嗿e| M ⯟5d'O%X$ ϠҪ$ %Oi4h_;id[鬚  οu7&4Y%?=>tJuCy2ɳF4鯝4^ItVwL\_ @^#ר*RHX nN/V:;E`E!Dk,ɌϿ3tf˯,]U!X5)/':'N i YQkcA_qS̿s6oKpMWUKSH7IVکKr?)ΪN} 苿_iJ!}>zԒL6vWJZ4̨KXf@BȔB8?~.jT_xu_Gnb\C&tjٖIBo-z Kcƌ:ukQz-pR";~zL_|d߷g+G4w`V^!B$?yN;؃E;Ͽo!BA4C?C`2IJJ\JYL%0u5j&L{C{ Vs-[%C?BA4ᆼ?[瞜0~'n)~3_d}36@p;x?!y2[z#GN|I\ߐF#.Bz2k;x`Bm'67*?@'7b֡3~J߅0<^# {lb((,&::oxYt/!'Ӿ&R!ȿؗ)EL;2Y o9I_ӄ]sȿB`\EAyc"B7B (Z 7-p~xŇKb~buzsM:?Џtn <9PIoڸ/\wޞ?;E \nyC!` ȓ)0|ɪ+shZfhtZVVWWWƂ˷l. #)~B\P|(6hɒZZZHZ,ɤIj4JUSSdɂO7F!oܞ۸;οz1O4iF!3خbtQS'i&qlڎƲeKIj˒/lYn$A .$@\xI+#0.Ņ<;,ܳKb5;7_& <>WoZ P|nD}&Y1@mrj\?b5 5[\\Nn1@,k'ѿѣG;zh'##X-9(x\GпJ/!VBH$BP ЮzB#X-9(p8 jWNOOkrI9Bn0<}bѹŰ/w#u2oeeEGпJZ Hݬo2_SԔʷٲҿUD"GZ?(Ddarڔ)RG*J}YK[ӹ#_BHBx=# !񺤱lnnAYK[B'r_?bXA >UjDgnZ]]5RskkK֒uo)MbB+[Űu}}}yy9a,+++bW*I#|4B_Su[hA!DjӮNy^q?X2D*~Bj%L.C{IōEj )7o?i5#X-ѿܤZر"5G!Юf~=6;::U & !_/-WsҿM?ԔvoddD C!V [G`ֿd23)mmm͛7?bpGq??mvi3NsssC!V YȝiпݺuKGDѿJqαcT'^W0 %ֿD"d,RSH!}vG{~ayK?iп /!VA^DzMLFEjZt&u_bpG[\\XYH?xO;sv_{~ #X-VJ?;v4 8'5|˭ӮKϯvp!jAKih{s5:>RGjJ}YK[o?sBH_! ݓH)oe)Uf~_b#"uKY}?ҿ5K$_`R+AA![]]nf~GGGX,0oyyY/=~کh4[?A!K&:0/kWWOpA0 ` _,ӮKϯv꯷7!jAA!F:ѿJ/!VB]_q?m淧gffC!V xοfcÉƓ<*{lo~,PHG<Bth|qѳ]NF@9-/ F*~Bj%L׮^ }?U!44JQ7??_@!tw$ ?U!4G_gQun3D*6=zرcvB{t;Vп:SEƯ*>4/_?BH[ʭLQ;o(Afggut#JpG!:Wɭke뙹_ u_1_$]BᥗVW%_~tIiob_@!_]R/B;LK/ZV `pFU?twVi%%̌힜ԮN2W)R+o fտ%GÚ55S'k?9r$]hx"Z/3?D iVLBj%V?xO_rKпLV?>xC+ˉ E_[榱1ۭ](NS~o:ksu|?BHQJК?ė"?O}~d, EΞ{g>z/?77_ZEn(4՛c!C3kH 5;*;GRS@ ίų-V> kl>}LɳgNS[}p/=m"Z +M`W/O>y/\m@>֐пJZ&ӏՏ.9 GCo I)B& !t.Bb`h SKW?BHm~LÙDJ&λ?Z~_ބ=>ފ˫To~tOKxnCPMr!B-{3w?s?w7V!/gWtпֿW^y!]պԩSdr~}}@ >vue\r\տ.`>пֿr,ZJj[Yr?33[YrYںOu?{.7ͺ蟪!( +_w-!cU{VS}VloV)7o޼`>п=6@#__J(U%@`D|U8*<4@p8Uyi?Up|Ҡ`ϔwll8xGe'.?ʇP3p<UɴwܹVp(Gsg|@ (m_Ur⠣g!W>f@ 1Zdվ:pW>9!쯙vj;ܦHj?f=y0N:yO<殒m_Wϥ,'Ci^zyaue)]"x_H׿dW>&,vL;_Vo Wg46VS鿸ֺ";ʊ!~a65S'k?9r$]͸ml~i}=5_?p4{m5읆ܶWgi/W>?+eEr}>ݲ\_M1>ɮO|Yk~:hX_?[+RXmuNCxn߽?gS~\ee9C'?Ȭ`\RwDnmmnm\_KsryVH͌[ѿH|Kv{3|kw{ kLk/㹭jOyq_Gy_<E$BQWP8T^ ͠J֌єwп,)__g=?p51%R:丐iݩ{? w7ꟚLޓ¸}bwbw|W1.pscxch]~5lpߵўL|ɟ{/|~gAy$a!%A! l3X/kp!ze9"KQ&ys]XNqsy6Zj7e7͛[[†pkk-[\ {Z$[lJi-jFnTJoMAkt/zEb}3g#EGiKNOwѷ3\nN3;yN'/ϥd)ⓃN`R(uuu;w]ѿ,c vY+ڣnN}L?O/0]?k0M fK5,oq+}7hW+“ 7_XD!_c^1$-ɝͪlU':ϻ$_` w0`" kq]G7=5:{o'۷o+f( #q&o66S򸲒if}̯7K,zNmʢYߌ!)g$[ <с7*sF;G'e#7 endstream endobj 86 0 obj 52849 endobj 88 0 obj << /Type /Annot /Subtype /Link /Rect [ 86.958 678.031 132.518 687.031 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 87 0 R /H /I >> endobj 90 0 obj << /Length 91 0 R /Filter /FlateDecode >> stream xY_6i7QǒC.Mtwnoh[Yt$jm~ d9t?AA,b!{(ď2fIF4 q A=ds<"(s! x3kf!no Sg FOm$d_ȶ?^KYǍs/0 ӜA=ٯwʲ_J6>^l6 јh6`Y.AAt1I/fވ C{C4ǿ7wN6{FWc[2bY0Q})Ծ}JprvvfǛml-CD',zu/: Pr%>RUl\)Z(Y˲ڜ.[ެKnJvk%JĞopcUn 涂q?D[@⫎lw\1@=SdEp<_:ϼd߱cՔر5oJyVsi}yՐ^e%Ws:=Yn LM stA&H"YVR)XbkdUwCp-N"pǝ/%ET@ҳK}ޛF] x|G v:9D]wc|!HAp`%%tDGg_t+Sn,O}0Ij񽔌/H( t$[")l`beViMiԑPrN(0r8x>x"go>^uoj"GEc oZ<cL]΍BB@<] R*SR=>lQ*3THڑ{Iga{W nE~|1ڶzgs PWZkջD$=$( i6U)ka'H ex{6Údߠ 7+е('*c*C.8;W -\)Ĭ`t'o=nsj z ^yn%0N4޲ʧ;GUg-?6vj(PLOG^G m.S8*u3"10 ټނ$07<Os om}a೹zd0W7&+yp ZQz7B#C/tJR-j1jlS-zCg7'j}ܡvtթx(KFHEJuISy,4][FNU&q2N%+W> endobj 91 0 obj 2634 endobj 93 0 obj << /Length 94 0 R /Filter /FlateDecode >> stream x]o6ݿȋUoi@l]uEeC]E'RwߑG[;>yw_˒U)򊓼jyc[~x{;rۧI^7D妴&cb:Bw8<]Z1te41ri)(<~N|1=~1r4dnt.^w7\]O;M'S+ʗ.hUueh1`Eꝵo.CυU'q eUk(1-:4RWx9`ϣ'SVՊ0몭rBS '٦b%I֮M3k `<*V euw&Fnض=tIA.\EB@mSiqVRR_/W"㬯.Yib*I'@@| Ic[ǎkmkOX_nH5|ɫ;U܌,"6@E.,ŻrSb`íc{0ȆI$ty!!A7WӀnP/Ju/qe 7 K*cŁre8 bzxfI Rإyx#/|/H!p8dN$+;ű<{~PxqZ\BeC^e!jFuUzn#geyk059BڻpvڪVq2`AkH"9 =8ӮȄMҦyBMoNJ10fmعn:~tbo0Cz\eb7M݈w` BGo0B=0vX;$ՠb`31Ǎʼn.=ʹ#;FOܪP,&Әãr}-ե<>R@yhc՞qRBծ$FAM?`U{!˪+e4+7`rQwrܴ$މC #';+af~ӛvF5b" X$GI6ͶhkzYd 4\[y48 %7|nrϷjRҥ%+.XsǗ T`[0[5c ÔZ&ֈgtsԇnZ Ǣ+aoADGnwg8pM˯L弐Q'$]_x7Q(Lk/C|M5ijvN R_釉i6ݲdfw?bvϫ8UoD$8ŬYv^匜s'J|H$X͇Dwms3?" eL:V'ۺ. ]Z(+/51%z}t篻(iD#A(WMfbD[]ĉ9e:V(nYj803%j榁$5߉$"wމo_eGB C4fZ endstream endobj 92 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 93 0 R >> endobj 94 0 obj 1966 endobj 96 0 obj << /Length 97 0 R /Filter /FlateDecode >> stream xYo8_>8FH~=#w^d%Uxr(,7{Omz~C933F<{-$Ij?Ĝf xy>a!ٓ>+$;)7:+f0w&=ͼR_g&<܅yG(MS擄<8ߓ.ޖ}V./ɋVJ\N1{3UXW-MEHXE,':$Nrϧ,P xxˤOihcv;a䪬kr[Q4y9ᡍrro`p)d}h3&=C-|{AbwPSk27‚9U1 V >jr_͝-uob3wLX=6OiĂ4?vJ3 E\9:&c,z{&UrGHs5dEZUyg%{?CtsU Da㍇ u:E8 sF&ċ6/6tay=HHƊg64jy"i0&}μ)\W*Ƥ˙)icIX_aH֊TΜXmOakuyf'xc~=果4f~<:kC%E3}7Lj .AqpZ ~ҶCgcaN.IfK\e5\z.DZW/ '3-t[9mvt^JΠTFJ,Ԗ|Û7_c;XE-WB^z]Ͱ ctLOE6g t'Kp-4䡱SD%*W?] l!ҽxCTKDixb ddž ?\d"uSYAV7sLy4 bYbW:kŤ &zl$8`2EY8SiwH?ź|C!vA>5;lkjQuοZZQD& rj7oGqZ (LMܩ-=}-Gin*/wVCP{wngZm43/VuG QH=AgBlt<;q1oD+ Ei3?i:GP߉|Hb pa%V]Sna KSch]7xA-od/n6uVm>/<,1}hżr;5$+!b6[Я휞w- x +\+k!\D Wzn3zfŔA gZwU B?(yy_p Y01tsCWrc _љ%U8#5:{bl6wwԉTkk|ƞ^÷vQI:pSYk Sxd q22Oh@.^[RV"Vefy-浇j'C c[FFeܓ@dɤ9qZs6zj,b( 8v$GRxI~XkKaRWj)R= 9Efػde( d>a8ߊP!~G`{ub b׿f^0xbl^Vumq!6?yTiD#6.e,u@(mFƒBePn&&W.k^{ZMޔ&,oZ׌JKucB;@h#ƃXUxV JӲ`SZ,^5u~{L.$aʹ9NIuHZlFvQ/=%ԵI6m\u/in]]\ ⁳k|_]3KwX{koi>Ny2:ȾIcSdhsӛ}‘нaYץJ]OL.G*z5QP)tΡ[bi~r) endstream endobj 95 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 96 0 R >> endobj 97 0 obj 2376 endobj 99 0 obj << /Type /Annot /Subtype /Link /Rect [ 87.27 412.066 133.37 421.066 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 79 0 R /H /I >> endobj 101 0 obj << /Length 102 0 R /Filter /FlateDecode >> stream xnίU 0##3JI/t/4A=ꅴr}{UuEo4$8M3tV_9AXEDBGB| [Ooz_M<{Zy V>9Hyq}vGb?Y83G }*Z,jgفGPQI{2FW`?N97\h38 ST*8:˿fza~ "$˒,+r퇱,W|1:Jq,-hOzr?{_~0٢F~Ho,OzG&|g1|w/g61a@ɌliŬY$V*A!NѦn&uSџ4̥ij?~hNk5|lZ R |"mk(eʞ=Wi VB'Nt\E׭EֈHHqOAlMwݕ) 9"2'Iu'6Fs]pƽy7m>)xyyv,QMWPtUb R ^$wNy$ld8.vΐ=GgZ)I;~sKo,f>:{Fk :-J7Ht2P2-") 8P (؟^?DT=`WTEI+D&Ab%,'$u+h,A@"S oHT,=zH:7 Ce ްąM}4Bj'"vB;gG X"K |S| |[~xzš9l4֕%?<_-8- 'N3` 0k/VǾ endstream endobj 100 0 obj [ 99 0 R ] endobj 98 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 100 0 R /Contents 101 0 R >> endobj 102 0 obj 2030 endobj 104 0 obj << /Type /Action /S /GoTo /D [21 0 R /XYZ 72.0 769.889 null] >> endobj 105 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 730.013 132.548 739.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 104 0 R /H /I >> endobj 107 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.488 730.013 525.488 739.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 104 0 R /H /I >> endobj 108 0 obj << /Type /Action /S /GoTo /D [21 0 R /XYZ 72.0 740.03 null] >> endobj 109 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 718.013 120.99 727.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 108 0 R /H /I >> endobj 110 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.703 718.013 525.703 727.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 108 0 R /H /I >> endobj 111 0 obj << /Type /Action /S /GoTo /D [21 0 R /XYZ 72.0 529.571 null] >> endobj 112 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 706.013 148.22 715.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 111 0 R /H /I >> endobj 113 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.529 706.013 525.529 715.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 111 0 R /H /I >> endobj 114 0 obj << /Type /Action /S /GoTo /D [21 0 R /XYZ 72.0 471.9 null] >> endobj 115 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 694.013 149.44 703.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 114 0 R /H /I >> endobj 116 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.667 694.013 525.667 703.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 114 0 R /H /I >> endobj 117 0 obj << /Type /Action /S /GoTo /D [27 0 R /XYZ 72.0 205.889 null] >> endobj 118 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 682.013 261.607 691.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 117 0 R /H /I >> endobj 119 0 obj << /Type /Annot /Subtype /Link /Rect [ 519.901 682.013 524.901 691.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 117 0 R /H /I >> endobj 120 0 obj << /Type /Action /S /GoTo /D [30 0 R /XYZ 72.0 688.483 null] >> endobj 121 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 670.013 138.33 679.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 120 0 R /H /I >> endobj 122 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.743 670.013 525.743 679.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 120 0 R /H /I >> endobj 123 0 obj << /Type /Action /S /GoTo /D [30 0 R /XYZ 72.0 292.499 null] >> endobj 124 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 658.013 178.32 667.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 123 0 R /H /I >> endobj 125 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.471 658.013 525.471 667.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 123 0 R /H /I >> endobj 126 0 obj << /Type /Action /S /GoTo /D [30 0 R /XYZ 72.0 216.951 null] >> endobj 127 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 646.013 202.572 655.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 126 0 R /H /I >> endobj 128 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.305 646.013 525.305 655.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 126 0 R /H /I >> endobj 129 0 obj << /Type /Action /S /GoTo /D [33 0 R /XYZ 72.0 596.193 null] >> endobj 130 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 634.013 141.0 643.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 129 0 R /H /I >> endobj 131 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.574 634.013 525.574 643.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 129 0 R /H /I >> endobj 132 0 obj << /Type /Action /S /GoTo /D [33 0 R /XYZ 72.0 541.614 null] >> endobj 133 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 622.013 170.88 631.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 132 0 R /H /I >> endobj 134 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.381 622.013 525.381 631.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 132 0 R /H /I >> endobj 135 0 obj << /Type /Action /S /GoTo /D [84 0 R /XYZ 72.0 581.852 null] >> endobj 136 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 610.013 148.811 619.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 135 0 R /H /I >> endobj 137 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.523 610.013 525.523 619.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 135 0 R /H /I >> endobj 138 0 obj << /Type /Action /S /GoTo /D [49 0 R /XYZ 72.0 769.889 null] >> endobj 139 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 598.013 136.684 607.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 138 0 R /H /I >> endobj 140 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.465 598.013 525.465 607.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 138 0 R /H /I >> endobj 141 0 obj << /Type /Action /S /GoTo /D [49 0 R /XYZ 72.0 314.959 null] >> endobj 142 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 586.013 180.27 595.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 141 0 R /H /I >> endobj 143 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.321 586.013 525.321 595.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 141 0 R /H /I >> endobj 144 0 obj << /Type /Action /S /GoTo /D [54 0 R /XYZ 72.0 769.889 null] >> endobj 145 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 574.013 223.225 583.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 144 0 R /H /I >> endobj 146 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.163 574.013 525.163 583.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 144 0 R /H /I >> endobj 147 0 obj << /Type /Action /S /GoTo /D [54 0 R /XYZ 72.0 513.83 null] >> endobj 148 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 562.013 166.1 571.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 147 0 R /H /I >> endobj 149 0 obj << /Type /Annot /Subtype /Link /Rect [ 520.553 562.013 525.553 571.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 147 0 R /H /I >> endobj 150 0 obj << /Type /Action /S /GoTo /D [60 0 R /XYZ 72.0 489.849 null] >> endobj 151 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 550.013 136.319 559.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 150 0 R /H /I >> endobj 152 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.571 550.013 525.571 559.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 150 0 R /H /I >> endobj 153 0 obj << /Type /Action /S /GoTo /D [60 0 R /XYZ 72.0 245.241 null] >> endobj 154 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 538.013 141.852 547.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 153 0 R /H /I >> endobj 155 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.537 538.013 525.537 547.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 153 0 R /H /I >> endobj 156 0 obj << /Type /Action /S /GoTo /D [63 0 R /XYZ 72.0 769.889 null] >> endobj 157 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 526.013 161.399 535.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 156 0 R /H /I >> endobj 158 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.551 526.013 525.551 535.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 156 0 R /H /I >> endobj 159 0 obj << /Type /Action /S /GoTo /D [63 0 R /XYZ 72.0 668.434 null] >> endobj 160 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 514.013 143.89 523.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 159 0 R /H /I >> endobj 161 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.67 514.013 525.67 523.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 159 0 R /H /I >> endobj 162 0 obj << /Type /Action /S /GoTo /D [63 0 R /XYZ 72.0 558.26 null] >> endobj 163 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 502.013 182.117 511.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 162 0 R /H /I >> endobj 164 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.409 502.013 525.409 511.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 162 0 R /H /I >> endobj 165 0 obj << /Type /Action /S /GoTo /D [63 0 R /XYZ 72.0 472.086 null] >> endobj 166 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 490.013 142.77 499.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 165 0 R /H /I >> endobj 167 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.678 490.013 525.678 499.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 165 0 R /H /I >> endobj 168 0 obj << /Type /Action /S /GoTo /D [63 0 R /XYZ 72.0 305.193 null] >> endobj 169 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 478.013 191.553 487.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 168 0 R /H /I >> endobj 170 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.217 478.013 525.217 487.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 168 0 R /H /I >> endobj 172 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 466.013 137.426 475.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 171 0 R /H /I >> endobj 173 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.565 466.013 525.565 475.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 171 0 R /H /I >> endobj 175 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 454.013 207.358 463.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 174 0 R /H /I >> endobj 176 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.117 454.013 525.117 463.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 174 0 R /H /I >> endobj 177 0 obj << /Type /Action /S /GoTo /D [66 0 R /XYZ 72.0 769.889 null] >> endobj 178 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 442.013 185.044 451.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 177 0 R /H /I >> endobj 179 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.261 442.013 525.261 451.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 177 0 R /H /I >> endobj 180 0 obj << /Type /Action /S /GoTo /D [66 0 R /XYZ 72.0 687.275 null] >> endobj 181 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 430.013 208.071 439.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 180 0 R /H /I >> endobj 182 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.233 430.013 525.233 439.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 180 0 R /H /I >> endobj 183 0 obj << /Type /Action /S /GoTo /D [66 0 R /XYZ 72.0 611.077 null] >> endobj 184 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 418.013 286.587 427.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 183 0 R /H /I >> endobj 185 0 obj << /Type /Annot /Subtype /Link /Rect [ 514.697 418.013 524.697 427.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 183 0 R /H /I >> endobj 186 0 obj << /Type /Action /S /GoTo /D [66 0 R /XYZ 72.0 136.195 null] >> endobj 187 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 406.013 247.806 415.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 186 0 R /H /I >> endobj 188 0 obj << /Type /Annot /Subtype /Link /Rect [ 514.963 406.013 524.963 415.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 186 0 R /H /I >> endobj 189 0 obj << /Type /Action /S /GoTo /D [74 0 R /XYZ 72.0 769.889 null] >> endobj 190 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 394.013 174.713 403.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 189 0 R /H /I >> endobj 191 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.459 394.013 525.459 403.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 189 0 R /H /I >> endobj 192 0 obj << /Type /Action /S /GoTo /D [74 0 R /XYZ 72.0 691.153 null] >> endobj 193 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 382.013 268.085 391.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 192 0 R /H /I >> endobj 194 0 obj << /Type /Annot /Subtype /Link /Rect [ 514.823 382.013 524.823 391.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 192 0 R /H /I >> endobj 195 0 obj << /Type /Action /S /GoTo /D [74 0 R /XYZ 72.0 397.653 null] >> endobj 196 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 370.013 183.005 379.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 195 0 R /H /I >> endobj 197 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.403 370.013 525.403 379.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 195 0 R /H /I >> endobj 198 0 obj << /Type /Action /S /GoTo /D [74 0 R /XYZ 72.0 320.917 null] >> endobj 199 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 358.013 156.246 367.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 198 0 R /H /I >> endobj 200 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.445 358.013 525.445 367.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 198 0 R /H /I >> endobj 202 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 346.013 181.492 355.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 201 0 R /H /I >> endobj 203 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.165 346.013 525.165 355.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 201 0 R /H /I >> endobj 205 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 334.013 145.44 343.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 204 0 R /H /I >> endobj 206 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.514 334.013 525.514 343.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 204 0 R /H /I >> endobj 208 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 322.013 141.56 331.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 207 0 R /H /I >> endobj 209 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.538 322.013 525.538 331.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 207 0 R /H /I >> endobj 211 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 310.013 147.22 319.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 210 0 R /H /I >> endobj 212 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.648 310.013 525.648 319.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 210 0 R /H /I >> endobj 214 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 298.013 137.22 307.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 213 0 R /H /I >> endobj 215 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.717 298.013 525.717 307.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 213 0 R /H /I >> endobj 217 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 286.013 168.34 295.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 216 0 R /H /I >> endobj 218 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.504 286.013 525.504 295.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 216 0 R /H /I >> endobj 219 0 obj << /Type /Action /S /GoTo /D [92 0 R /XYZ 72.0 624.193 null] >> endobj 220 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 274.013 182.22 283.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 219 0 R /H /I >> endobj 221 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.411 274.013 525.411 283.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 219 0 R /H /I >> endobj 222 0 obj << /Type /Action /S /GoTo /D [92 0 R /XYZ 72.0 551.185 null] >> endobj 223 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 262.013 173.33 271.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 222 0 R /H /I >> endobj 224 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.471 262.013 525.471 271.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 222 0 R /H /I >> endobj 225 0 obj << /Type /Action /S /GoTo /D [92 0 R /XYZ 72.0 466.177 null] >> endobj 226 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 250.013 142.78 259.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 225 0 R /H /I >> endobj 227 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.678 250.013 525.678 259.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 225 0 R /H /I >> endobj 228 0 obj << /Type /Action /S /GoTo /D [92 0 R /XYZ 72.0 393.169 null] >> endobj 229 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 238.013 188.32 247.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 228 0 R /H /I >> endobj 230 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.368 238.013 525.368 247.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 228 0 R /H /I >> endobj 231 0 obj << /Type /Action /S /GoTo /D [92 0 R /XYZ 72.0 272.161 null] >> endobj 232 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 226.013 176.11 235.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 231 0 R /H /I >> endobj 233 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.45 226.013 525.45 235.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 231 0 R /H /I >> endobj 234 0 obj << /Type /Action /S /GoTo /D [92 0 R /XYZ 72.0 187.153 null] >> endobj 235 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 214.013 179.43 223.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 234 0 R /H /I >> endobj 236 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.428 214.013 525.428 223.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 234 0 R /H /I >> endobj 237 0 obj << /Type /Action /S /GoTo /D [92 0 R /XYZ 72.0 102.145 null] >> endobj 238 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 202.013 147.22 211.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 237 0 R /H /I >> endobj 239 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.648 202.013 525.648 211.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 237 0 R /H /I >> endobj 240 0 obj << /Type /Action /S /GoTo /D [95 0 R /XYZ 72.0 651.457 null] >> endobj 241 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 190.013 171.65 199.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 240 0 R /H /I >> endobj 242 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.482 190.013 525.482 199.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 240 0 R /H /I >> endobj 243 0 obj << /Type /Action /S /GoTo /D [95 0 R /XYZ 72.0 563.425 null] >> endobj 244 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 178.013 134.44 187.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 243 0 R /H /I >> endobj 245 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.734 178.013 525.734 187.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 243 0 R /H /I >> endobj 246 0 obj << /Type /Action /S /GoTo /D [95 0 R /XYZ 72.0 440.961 null] >> endobj 247 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 166.013 169.44 175.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 246 0 R /H /I >> endobj 248 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.497 166.013 525.497 175.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 246 0 R /H /I >> endobj 249 0 obj << /Type /Action /S /GoTo /D [95 0 R /XYZ 72.0 364.929 null] >> endobj 250 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 154.013 156.11 163.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 249 0 R /H /I >> endobj 251 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.587 154.013 525.587 163.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 249 0 R /H /I >> endobj 252 0 obj << /Type /Action /S /GoTo /D [95 0 R /XYZ 72.0 242.465 null] >> endobj 253 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 142.013 153.32 151.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 252 0 R /H /I >> endobj 254 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.607 142.013 525.607 151.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 252 0 R /H /I >> endobj 255 0 obj << /Type /Action /S /GoTo /D [98 0 R /XYZ 72.0 757.889 null] >> endobj 256 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 130.013 173.87 139.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 255 0 R /H /I >> endobj 257 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.466 130.013 525.466 139.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 255 0 R /H /I >> endobj 258 0 obj << /Type /Action /S /GoTo /D [98 0 R /XYZ 72.0 670.604 null] >> endobj 259 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 118.013 168.32 127.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 258 0 R /H /I >> endobj 260 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.505 118.013 525.505 127.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 258 0 R /H /I >> endobj 261 0 obj << /Type /Action /S /GoTo /D [98 0 R /XYZ 72.0 595.319 null] >> endobj 262 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 106.013 138.89 115.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 261 0 R /H /I >> endobj 263 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.704 106.013 525.704 115.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 261 0 R /H /I >> endobj 264 0 obj << /Type /Action /S /GoTo /D [98 0 R /XYZ 72.0 520.034 null] >> endobj 265 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 94.013 162.78 103.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 264 0 R /H /I >> endobj 266 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.542 94.013 525.542 103.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 264 0 R /H /I >> endobj 267 0 obj << /Type /Action /S /GoTo /D [98 0 R /XYZ 72.0 285.834 null] >> endobj 268 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 82.013 175.0 91.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 267 0 R /H /I >> endobj 269 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.458 82.013 525.458 91.013 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 267 0 R /H /I >> endobj 270 0 obj << /Length 271 0 R /Filter /FlateDecode >> stream xOy;?ֆooZRpmd& {nM7&i>ƛ&Йznj؅EUB"M8(OkW˫Y_G{UW?/W1_KY{~/W~u_q6߽:W/|ۧ98?xZ`>z>Sxtyoͧy|pyz~w>O~z'"NOէ?,JI{'I<$*qxA"EBX1, CKRı,U$KRɲY5^R7[TwԽPTOYd9r$I,Y.drze=IJbYX"ebYX*e#F,,,,;Yvdr A,'YN\dr"M,XC,!VIJlE,[VŲUl,Y6ldddddɲe'A,YNd9r"E,7YnbX!qebE,e*Q2*Y'h4 ~8;zy"}?W ^?o͟>ϺQX {bma / g&xC].|C@j17|C@jq7"oHMx~V_R7[TwԽPTOYd9r$I,Y.dr|C@j7.boH-boHM,Y6 dɲ?9__[QT/g:RF5Y.dre! XP7|C*dȲe# N,;Yd9r$I,Y.\dr82C,(bE,eT*Qɲe#F,{OC/ysu.=~|o߽q|eZ{~|xHъy; 4]\qHqqoN2kx:篛z|"ϧ8Tf?L~}˯~ݗo]q>K}C;8^aWC9:RFzK]2Re#F,,,,,;YviywQ(TSyPݨToWSM,7Yn̡bC2RebCɲe#F,,,,;Yvd9r A,'YN\dr&M,XP@j̡E,s( XP@j̡dȲe#4yFN#N:}17o~Dxy6=[r=FnBx~_/]xBXK-yԢ/Ǽ*ydQ=RGSzJ=r A,'YN\dr&M,X5Re^c u˼Zjk˼ZjldȲeeee'N,;Yd9r$I,Y.\dr˼Zjk.bXK-y5U,kɲe#FAAAA,;Yvd9r$I,'Y.\dr&-qebX!Q2XF˨bU,%}>AsO$Qt뗧η]>oB_G?}xÛw>^L-ϿGxa+}28jIy|QKO{ɛrS S S SM0aTNS:Mu2eTnSݤMI5'R'多SNI5眔j3fTTTTvSMu0aTNS]Lu2mT7rRTsJ9TJ9T6SmMj7aTNS:Mu2eTnSݤRI5gjl)l6[ -͖fKaRl)l <_4́i|񸫁iߺ/Pw͇~+}Yg=Q'X̌8qH7)7%%o7ƤEy7)S:Mu4eTnSݦIZRփTk!ZHRTk%ZMj3fTTTTvS5k|Ik֡nTԹfN:׬CT&M,XC,!VIJlE,[VŲUl,Y6lddddɲe'N,Yd9r"E,Ynd2C,(bE,e*Q2*Y6ld2|kE qTwW\Jyw>~z?}ՋB4z^Q X% XzJ}Q=Rk('ͼy<5hr2[P~w8Twka)ZXF,Yd9r$E,Y.drenW+XvPjܮ*]dȲe# N,;Yd9r$I,Y.\drܮVjjܮVjjbJM,Y6lddddɲe'A,Yd9r"E,7Ynd2Z,sZXvRenW+XvR%}>As6C)jKsgG/WxyWG_LZ;sR.R(?QO?n7ˇj^\Z)/ʣXOay|XnTS:Mu2eTnSݦI5'j`%/SXI5簒WRI,Lj30000nTWKG^Q,'Y>,ߔjyTnSݤ#ZI5ji)'J^I5Gj3fTvSMu0aTNSLu2mTnRT 8H5 F!(TjTRjT6S Sػ/:rŁWC>q}~>~C5Gl_{wmߒ(zK}T/m yX>)/Y BidZ*dG?ocbyX>)MA o7)rS:Lu4iT.S]Lu6M zj=HZ BjZ+jT6SmMj7aTS:Mu2eTnSݦIRmBj+*JjLj3000nTvS:Lu4iT.S]Lu6MqjF!(TjTRJaRl)l6[ :ʑka|[|F.?n,yS~w}կ|ϒ>|?n=\ 8|:RϻR7[9WN5Y4ri多| 4rRi>多|Mj3faaaTvS:LuM͔ M͕ M͖fKaRl)l6[ -͖fKaRl)l6[Mkqveiū]O}w_BOoWW6!*qY:ay; ygX>)?_Q,;c|Dy|Q|R˛rS:Lu4iT.S]Lu6Ma多H^H57 Ts+!j3fTTTTTvS:Lu0iTNS]Lu6mTsI57 TsI57 jnB6SmLjjjj7nTS:Lu4iT.SݦMu&0rR C$/TjTRJQI5l6[ -͖fK;#;LL6 9GK\M4yɾ)?/p`~|ˇ;BOG5Wu~~+}9x9]e^8$o7yɻ߀䦺Lu6mTA zj-Z BjZ+jT6S S S SM5o@9h/7y Jɻ a_|W^sEy.c<,2vɛ妺Lu6M9饜Ts+y!՜RN9镼jNz)7fTvSMu0aTNSLu2mT7AqjF!(TjTRjT6Smzr\4Tmq]x?|7/7#9|p_||czqo\+uzP>gT/'Yj=夙{3h%d{Mj3fTTTTvS:Lu0aTNS]Lu6mT7rRTs|O9rR=Lj3000nT{~^F|S>E,uX,7eTnR=多{ rRTs|O6SmL5L5L5L5Lj7nTS:Mu4eTnSݦI5RT 8H5 F!(TjTSmL5E<_/#8|r\ _u :q +7(⸋)[꼿<ԝ%u\zJSpI3gN)'ќK^4jZ>oRz{Q,owykyI0aTNS:Mu2eTnSݤSpI5R)8多SpI5j3fTTTTvS_-_oG[(|R˛妺Lu6M9Ts .y!՜SN9jN)7fTvSMu0aTNSLu2mT7AqjF!(TjTRjT6S͡&mhy+yn@?|_?Dr;_y*{ ;\0Toswaʻ|R~SNy5Su>oU˻(~X,0aTNSLu2eTnRۭQNy5 ('ռݚTvkj3fTTTTTvS:Lu0iTNS]Lu6mTvkjnrRۭQNy5+(7fT6S S S SM0aTNS:Mu2eTnSݤAqjRBQH5*F%ը6[ -͖nR0Q |q~5/,bo}X=Zt=IݨTosM9i[B9DsM9ܛrSmLjjjj7nTS:Lu4iT.S]Mu&՜{SN9jν)'՜{K^I5ޔj3fTTTTTvSRKQQ=RJu,7Ynbn2ReN˜qC]2'Re#F,YYYYvdr A,'YNdr"M,7YnC,82XF(bU,eTJ,Yzg>x9;m޾Z߽?~LJv9O3K{9/>W?x>>.|?7ygɻaJ8έ^^NbP7[E Nbf93'hν)'Ҝ{SN9j3faaaaTvS:Lu4iT.S]Lu6M9TsM9ܛrR͹TsM6SmLjjjj7n?+,ߔw?anSݦMujν)'՜{K^H5ޔjν)'՜{SnT6S S S SMj7aTNS:Mu2eTnSݤAqjRBQH5 F%ըT6SGij0c58.|{}FkvSzo|?O|9aמ&=c6un+uzIBTOsZՃjd9r$I,Y.\dr&-iδ/XZ,oҾbyۓjldȲeeeee'N,Yd9r$I,Y.drmY,byvjm:mYɲe#F,,,,;Yvd9r A,'YN\dr&M,X6Z,oʾ.byTjm) X6,i4ތ~b9۫e]֨?wx~?pͷ88/Y^߂|{y%^SxBw/Cbݡ j1ݟu٠۽٠۔괬1j{Ivw9<,[fTS:Mu4eT.SݦMum-9ߐR %'J!Lj30000nTIǞ/G<,f|SrSݦMum:.9㒓mB.9fWRM%7fT6S S S SM0aTNS:Mu2eTnSݤAqjRBQH5*F%ըzCLl|l\}3QjN^4o_߼?&>ӿß}y~7:2ŗrTmν⸫a7ˇQ-4iT.S]Lu6M9UTs&y!՜QN9UjN(7fTvSMu0aTNSLu2mT7TrRͩ多S5I5jWRͩLj3faaaTvS:Lu0iT.S]Lu6mTsF9TMB9UTsF9TrSmL5oʰqVe&CRqˡZߛ^{㟾~Q7ӳ\q~Ǎq=%3?5yaIynXf|STS:Mu4eTnSݦI5wTsLI5wTsL+.j3fTvSMu0aTNSLu2mTnR]0)'SB &多`RN &Lj3000nTvS:Lu4iT.S]Lu6M &多`J^H5wTjTRJaRlv{ewP~{.+?L*}i<}߾{\ɗ(wM2ϣb͋gVnruK'RYkH}8r|Qy|v~~Ƿ}o?y߽͗/9Gx_8Y{NV)o7NTnWw=k__˧x8n?^W7+/C[(?z(˧y a9e(j^"y%ռ rSmLj3000nTS:Lu4iT.S]Mu6My 多H^H5/CT2I5/CT6SmMj7nTS:Mu2eTnSݦI5/CT2 e(j^"y%ռ rSmL5w)(%LrG;J:r1߿N8)?O)oj3000nTS:Lu4iT.S]Mu6My 多 I^H5wQNrRU]j3fTTTTvSMu0aTNS]Lu2mT7.I5WuI^H5WuQNKJrSmLjjjjj7nTS:Mu4iT.SݦMujꢜTfKaRl)l6[ -͖fKaRO8 M&*(Ł%.Ƨ~߾w?]4sFuE}=_z%ayՏcrE|sq.%/]|R7ˇLj3000nT W ߔ繻EyKO]f.SݦMu]rRC^Hv.9!z;wT6SmMj7aTNS:Mu2eTnSݤz;wTo]rRC^Iv.6SmL5uQ_<ͅO<-:}o3/["'#{~i:xc}qoF.!.%oOy|_sKɻR|R;\J,0aTNSLu2eTnRփTAj-Z BjZ6SmL5L5L5L5Lj7nTS:Mu4eTnSݦIRmT[!VHRmT[%VMj3fTTTTvS:Lu0aTNS]Lu6mT7AqjTjRJQI5*͖fKaj/YmюK\0 Ϫ߼ӟ>Z4.<Qq{Ճ-u^Jy|Q~<,'(SN9PT6SmMj7nTS:Mu2eTnSݦI5ʔj%/eI5ʒWŔ2Lj30000nTS:Lu4iT.S]Mu&(SN9PTsL9@YJ9PT6SmL5L5L5Lj7nTS:Mu4eTmllll6[ -͖fKaRl)l6[ -͖fKrDZqmypxDx3ϢtC_~x/wo|ݧ77= _>p>KYf|Q~ a[UT/w:R㰜4s?3qSN[JrSmLj3000nTS:Lu4iT.S]Mu6MrReܒReܔj.㦜Ts7Lj3000nT{Ʊ^oG[(|R˛妺Lu6MrReܒReܔj.㖼j.T6SmMj7aTNS:Mu2eTnSݤAqjTjRJQI56SmL58qeSۤ]}IyU9.|;pG|w7RBuP=ΫСnTԹ3r͘)'܌YJ3儚1KLj3000nTvS:Lu4iT.S]Lu6M3多1K^H57cTs3f+f̔j3fTTTTTvSKЏyf[$a"!y)_<#vq㬴Z-_'IyxY>,7fTvSM0aTNSLu2eTnRmTok!/z[,96C^IYrSmLjjjjj7n\F|cN|RoI,o9ɻ妺Mu&fIYrRmTok!z[,6SmLjjjj7nTS:Lu4iT.SݦMu&fIB͒mmz[,6SmYy_j{޶8Οy5d]~Ï?|ןazx|;_ϬEZq[_8oRʻ*/i)a<-Y>,ߔjyTS:Mu4iT.SݦMujTs(多E)'\/*y%\/J6SmLjjjj7nTS:Lu4iT.SݦMu&\/J9zQ zQI5׋RN^rSmLjjjj7nTS:Lu4iT.S]Mu&\/J9zQ F!(TjTR -͖fK)eԽ1hS((njxS~|ݛz|?>[;<Ր2u=:׹FzKKxT>g乀G|R x$o7幀GnS:Lu4iT.S]Mu&ەzIvezJ+%7fT6S S S SM~WcWRʛMw幀GTnSݦIvezB+%'ەzMj3faaaTvS:Lu0iT.S]Lu6mToWKNqjRBQH5 F%ըzfTsR:^}]~y"yrAi7߿.a+ϗ;b!`\wmPPOPR7[IN:ߔfλ%/oI4gޒW2ͩ7Z^byX>)f|SޫrS:Lu4iT.S]Lu6M9TsM9؛rRͱTsM6SmLjjjj7nTS:Lu4iT.SݦMu&{SN9j)'{SN9T6SmMj7nTS:Mu2eTnSݦI5RT (TjRJQI5l6[ -6(ؘvx˹wx7޼??X6N|\>.Gk\p(n:yoK|RaW˻:Lu0iTNS]Lu6mTsH9(rRQ"多D+(rSmLj3000nTS:Lu4iT.S]Mu6M9JTs(y!%RN9JTsH6SmL5L5L5Lj7nTS:Mu4eT.SݦMuj)'%J^H5GjRJQI5l6[ -Vr$|9Jy$1MO?oݟH_gCٺl`>=^G89Y>,ߔ?w)avSMu0aTNSLu2mT7 rR(多3PI5gWR(Lj3faaaTvS:Lu0iT.S]Lu6mTsJ9 TB9TsJ9 rSmLjjjj7nTS:Lu4iT.S]Mu&՜RNaRl)l6[ -͖fKam:rbQm~iՁC~+_{߿Yf䎫G+>?y<,ߔjyTnSݤT^jrR{K^I5uO6SmLjjjj7nTS:Lu4iT.SݦMu&ռ=多jRBQI5*F%ըLjN5{|8oS?pTsow+'?#; ?97CTOss~Ճ-uɝ%g)'eK^4Gٔj%oLjjjj7nTS:Lu4iT.S]Mu&eSN9ʖj)'eK^I5Gٔj3fTTTTTvSͻ>|ͼ%yQ|Rw}Y>,ߔ]%6mTsM9(rRQ6多l+(rSmLj3000nTS:Lu4iT.S]Mu6MqjF!(TjTRJQMj3`{~i_x9ʞ߾o?~ݾJ?آ=~W_q^:RT/CRSdȲeeee'N,Yd9r$I,Y.drze=IJbYX"ebYX*e#F,,,,;Yvdr A,'YN\dr"M,XC,!VIJlE,[VŲUl,Y6ldddddɲe'A,YNd9r"E,7YnbX}>AsO'h4 }>Asm{`1~v[˼w[ݼ ?n=<1ȣʼ8{[YT/W:RF5YnƔf6g6Ds1d)7fTvSM0aTNSLu2eTnRƔj6j6TsTs1Lj30000nT:z8.yX,oW[nTnRƔj6Ts1多%)7fT6S S S SM0aTNS:Mu2eTnSݤ)'\m,y!\mL9jcI5WSnT޶<;Gq/~}7>yw?RO:qq;ORtrA:Q=RPwY{ǚ/sIIy1Y>,ߔwMu0aTNSLu2mT7-('ռ多XTkWR[QnT6SmMj7aTS:Mu2eTnSݦI5oF9-$/XTkjbrSmLjjjj7nTS:Lu4iT.S]Mu&8H5RTjRBQH5*F%հRl)lt[Ѽz n4h8v^U9VwMJ\mS|7Ǚ ?Ճ-u.˥[(?8(I4RN,Tq̗Z-_-%'Y>,7aTNS:Mu2eTnSݤr)'\+y!\K9\+\Mj3faaaaTϯJz8-yX,oW[nTnRej.˥TsY.多r%r)7fT6S S S SM0aTNS:Mu2eTnSݤAqjRBQH5*F%ըT6SMϿ4r_8^Vo_z}? .Oh}qCWxkF9FzK}Eyn .9n:y%M$'MY/scpɻaq4r7ŁWSϟjISMy|X)?)7faaaTvS:Lu0iT.S]Lu6mTsbJ9TB91Tsb*y%՜RnT6S S S S SM0aTNS:Mu2eTnR͉)多SI5'jNL%SMj3fTTTTvS:Lu0aTNS]Lu6mT7ĔrR -͖fKaRl)l6[ -͖n<&zy2.&q7>/ti={>.{V˻|A|R1H,j3fTTTTvSMu0aTNS]Lu2mT7֯JNR_ToW!z[*6SmL5L5L5L5Lj7nTS:Mu4eTnSݦI~UrR_ToW%'U+֯JnT6SmMj7aTS:Mu2eTnSݦjRl)l6[ -͖fKaRl)l6[Mcq8 {K8qĺ^NcÝeZ9m9_|4߆׳Q=R/T/RSrf9Hy{/BH$'B9j-U'>ۋf|SnI0aTNS:Mu2eTnSݤZRփTk!ZHRTk%ZIVSmLjjjj7n2c)n|I:,o2mT7TA VHRmT[!VIURmT6SmMj7aTNS:Mu2eTnSݤAqjTjRJQI56SmLC>C3#ίs/G~;BFsaσ)J!8b uNz)o7yEy^}&9WfT6S S S SM0aTNS:Mu2eTnSݤ^I5'RI/多^I5'j3fTTTTvSMu0aTNS]Lu2mT7椗rRITsK9WJ9T6SmMj7aTNS:Mu2eThl)l6[ -͖fKaRl)l6[ -͖fKI1ڝFqֲ/ zoFw<]yqՠTn<'xQ,oʣZ-_byX>):Lu0iT.S]Lu6mTsPJ9TB9(TsPJ9栔rSmLjjjj7nR9-TwԳPTOAu,Ynd2'ReE.bSQ2gPW̉dȲe# N,;Yd9r$I,Y.\dr|J-qebE,e*Q2*Y6ldmYVhx۶ͯ^8 endstream endobj 106 0 obj [ 105 0 R 107 0 R 109 0 R 110 0 R 112 0 R 113 0 R 115 0 R 116 0 R 118 0 R 119 0 R 121 0 R 122 0 R 124 0 R 125 0 R 127 0 R 128 0 R 130 0 R 131 0 R 133 0 R 134 0 R 136 0 R 137 0 R 139 0 R 140 0 R 142 0 R 143 0 R 145 0 R 146 0 R 148 0 R 149 0 R 151 0 R 152 0 R 154 0 R 155 0 R 157 0 R 158 0 R 160 0 R 161 0 R 163 0 R 164 0 R 166 0 R 167 0 R 169 0 R 170 0 R 172 0 R 173 0 R 175 0 R 176 0 R 178 0 R 179 0 R 181 0 R 182 0 R 184 0 R 185 0 R 187 0 R 188 0 R 190 0 R 191 0 R 193 0 R 194 0 R 196 0 R 197 0 R 199 0 R 200 0 R 202 0 R 203 0 R 205 0 R 206 0 R 208 0 R 209 0 R 211 0 R 212 0 R 214 0 R 215 0 R 217 0 R 218 0 R 220 0 R 221 0 R 223 0 R 224 0 R 226 0 R 227 0 R 229 0 R 230 0 R 232 0 R 233 0 R 235 0 R 236 0 R 238 0 R 239 0 R 241 0 R 242 0 R 244 0 R 245 0 R 247 0 R 248 0 R 250 0 R 251 0 R 253 0 R 254 0 R 256 0 R 257 0 R 259 0 R 260 0 R 262 0 R 263 0 R 265 0 R 266 0 R 268 0 R 269 0 R ] endobj 103 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 106 0 R /Contents 270 0 R >> endobj 271 0 obj 19294 endobj 273 0 obj << /Length 274 0 R /Filter /FlateDecode >> stream xko6V~ȇ^RCA]zٕtzg8/in~p n<{Zg QW,$ߓ~>yXxzu';0EQɽ(,!?OWo㱬+r`5%h+3o#BEa&&2p,%aY2_Z1tA42|ibE'4wm[n;~pAQdEK^>x)־>8r;/\۸brvtǞ /UwZt1ذ3pJKVdRա10;M7ثU9p!m JvT±l{xc?sAR@Ì\ {9ˊ#rC C"V -;+ 5xߍƖR/j}+pDط!;v|Ix Gk̨[N%{U^p4$0-zvj;@^[Y$Zn+bN ;|Ekv v¹/Ԩq><p؞3)^*C ]dv es`M4̋x[6MW Z_EK1GYv0c `?ޞ@K) oD5I뫱8.|qca0BX;qRک{-dVb!}h(vjX8vdhZسle`ˢ<b8$DBT\5r=kkfnog1A.BZeɡ7o }KlǨjGq6_I# Τ6=x = K(bgWB܉ݿv5)S'g_i.p=we@_ɏN㢀rH"ߒwn/Bh.i74犌2/)rq_H EBY_$9Yp `x6^e.ھ0Ͻ"O#DyJ@6<,:s}u#:"SEUYqJcrJrFFĕBm8䳳 ў~] }"R}f'86Df]LC'} *޴dB0)o4x[daJ !JC0>հ͒lp5{Ϛ-{tQ I J1(ʁTRTitdEU.7DY8='偗ɚ -{+C_F0<%cG8$ %_e.DL(V%!֭le7GWMy</Mr 9SHwl|` 9W%7= GXp0؆?#*Ca;K]9"v#lݡvW3Cj K+?%/<;u}48a&p!"D*/,-|)/M ܰowbO|G`YuM^+ۋ5(xWF'tzhC4<24XzsH>tZ O{&rb#,<]~Q0ڽ-#]RY_7K|ءRdft S(zWݎLB1~<ұv~>#W6jFI?&ԊO<#,}XKQ0&ԣvN!IzyY"]ӐRGJhA2kW/JIvi\d:Œر <=L^) EE&X4W5J `0,{'JL6)X$=@ )O{I8D{v8RD9W,wă؊8LÈuW.ՆM~#y߸l/FXp !쁚Ӆs$/k[E-[X_C nMςZz(qm*|1{N8,WmJa! n١qQE85<)y ὖr> endobj 274 0 obj 2431 endobj 276 0 obj << /Type /Action /S /GoTo /D [98 0 R /XYZ 72.0 489.115 null] >> endobj 277 0 obj << /Type /Annot /Subtype /Link /Rect [ 95.688 85.493 138.468 94.493 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 276 0 R /H /I >> endobj 279 0 obj << /Length 280 0 R /Filter /FlateDecode >> stream xn]_1ȋ@~1nM]A^cq$9 4qE->s̜m.7O^dS/~5d!RJ|/ߏH{aBj2F% EhWgt=y,|ŗp$>{^+Rl%|[^'3q}Ӣ(^'$|z6%9T #UӱvK7+YuH[Q)儰ɲu80,9Gv^Ii`0rk9aEm>uߌp/SAPYQ/_oFOX \'ƷlÛR Dl"oo@ݲc/JQWӞ򶦈4`tx7]U3ĕy;$sO7Lf_KJ/+! Z*oB(}cMe T=I\UnIUYmX5Pzaٰgw_I[lXjm䬏@o@AuGҭ\Z`dՁ+Pwr: ?oDІνUC BOmmiD Dh%') H\.ސ 1Gm[#bǕy_OyE^-mvIVLՄ-=I崧O&}"ƜtK]5/+O𛾼ߧSbk3/x +Ae`lN \u{᭘rI/{#x{E֕GF.Pd>CfxeC!&'1SC?P=?ӧmO2/'W7%B 9P`.)H5=MGlG0^8T ;p_^zrj0*,O"s}FBNT6D΁1]p4OGꏊ}j,lE(kWˣZZbʺ:~>N۝x]>Ѓ`Ϛ /w˾>#Zǖm)F~mѦʟ0ɏ]z8Vvd R1֙+.h@;&8v.> rn1M -̟V Y jj¤61|l3y^0df(a\IJ~% h5Ck u9n_7-.1ZjjƑ nѰx\MGc `ʺS\ďa҈S~_$iN j pJ} .S aib+Q։~~UaV z)GTt-ox]m)7UwrVL(Ee> lɑ?vwȲ"'ޫ1YOj׈ ;-:vb5=ΦűiZwyM`v9"b t1a~xm kהS˥ @_Х1֎ o2`7k5YؽH*amkH?†Y%ԮY|fW_LLˋ܇g^X1ÚZG+P  M4m٩#D/+F=g8j2kv [C_4mTCnh{ĴVREQz x%&O)H|k]t0I$^W<Ȩ0-_S%0[ DV3+OJEuήTQ8s a@ }9hAs2[j05h%gtD1**] \O?{E|fGVʁĴd:İGdV a/z-`]uCwק> endobj 280 0 obj 2297 endobj 282 0 obj << /Type /Action /S /GoTo /D [98 0 R /XYZ 72.0 254.915 null] >> endobj 283 0 obj << /Type /Annot /Subtype /Link /Rect [ 388.384 579.033 443.384 588.033 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 282 0 R /H /I >> endobj 285 0 obj << /Type /Annot /Subtype /Link /Rect [ 358.9 555.033 413.9 564.033 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 282 0 R /H /I >> endobj 286 0 obj << /Length 287 0 R /Filter /FlateDecode >> stream xXK6W]`ԓzHhٮAh[,:z(Ve9$əoFuA/O/IJwv"?[!b#x4D;4 QxV3 (dq/עm}m?/߅`Z!7b%OGj>]/3T@yوjSq~]c%[mvS iDZM:B{4OϝsN}mo`F>юq^OŮ|6]NnXy((˵g$WxRiFzݯv_JeMUMO b;w{Y5"72Z4 rATQQչ,)mn{jѹ%yt'Z>Їh-/7 //U^zLU:pD`A |qU Q[/߱EqO%hZ~C\.'>(q\K5 5= 0A8j) = mq+, /dӨc^f򈶼%JgZ @Lb82xrf,!vo}R#˔~*~Bl}y ,0=W? <@z8J!`Xa,H[?S:FXP;w4v]}уe'/ѡF䛋ǽK5 9=\’`fN\>parTBuc81ր% w'@Ce" 1] qm} s r%ͳ*!>>6[\Le&.Sg;o"mͰkw63SE`8)%5+@Ct@Α-(. _=s`ZW`ߎ浵8k;{DC^0/ϩiu[֗u#x Q?`I[ b%?)0l#CcFCEŅ?0sc;LV.O]Fd2ma=p7>`PO((I @0[770E+"ȧ_.+Raf(/;D\\7-?>. endstream endobj 284 0 obj [ 283 0 R 285 0 R ] endobj 281 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 284 0 R /Contents 286 0 R >> endobj 287 0 obj 1554 endobj 171 0 obj << /Type /Action /S /GoTo /D [288 0 R /XYZ 72.0 591.341 null] >> endobj 174 0 obj << /Type /Action /S /GoTo /D [288 0 R /XYZ 72.0 419.426 null] >> endobj 289 0 obj << /URI (http://barmag.net/veusz-wiki/ImportPlugins) /S /URI >> endobj 290 0 obj << /Type /Annot /Subtype /Link /Rect [ 227.25 352.527 402.53 361.527 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 289 0 R /H /I >> endobj 292 0 obj << /Type /Annot /Subtype /Link /Rect [ 237.629 227.253 280.409 236.253 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 276 0 R /H /I >> endobj 293 0 obj << /Type /Action /S /GoTo /D [281 0 R /XYZ 72.0 336.733 null] >> endobj 294 0 obj << /Type /Annot /Subtype /Link /Rect [ 365.104 215.253 385.654 224.253 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 293 0 R /H /I >> endobj 295 0 obj << /Length 296 0 R /Filter /FlateDecode >> stream x˒6@咙 &]oy7GIv#A+HگO Aʻ{}n 9' =Yf}$> b1cBGdG."R k+xK~U3F6pVq&KVOl ߓ|lf泇wÔfqeHJ?|2ߑ/7y* Y]~>{;P6`ߛR9!'K$a]|I4nK ܃F|a;K#b V(Խr;h iet$ȷb9I@4'*~eJ/#̇QQWѮYBcdYxdrUKy50J)% 4DRJnT+&E;jy,*'/#)}4Rz`tU{N~AvnK|^*}t}C<@C½2Nmjlx_QBPpnAFcUq i;:knW۸hyBu՚-,#2| Ӕ1IKC:/V-tƫBdSɪzC zz-Ąvt zg1e!8.T6{~HEɧ(0{#eovElj`\‰Kq5nPjлd cs ' dQTb ǡ=K;YE礵"gGд//>\k4B~Q폠'l9&D-gKےN;^I39 t`u1A 2un2rj~~5SJXɌ(32#TA7i_|+]xjyɗ}֡ZٌE׏!N,9P}!< n%&;wT3Bh:\k![hpwG+ 3CV,G -[C$f;-L uyFᰡ`zR/ӖfQȘ#Ǿd{ӇI4Bo$BA~>XoqCQB1Ur>x>Fƌ">Jra-Lrn=N;LDk{7o?̯Y܏>>͘7BG~|Z-՞(8)a  2[ c5/|e6nvyw !Ҙ6%f8wP!D&S Wjz}lkwxue2fe ND+HliZIp^On(aS G ly˭x5\7hʌ&C;>PrD$ޑ]vۮۿxxx,?(u6 biA.e2EmXnCZ،*{ &:O[B_B j@eSx .lCQQHuUل̱˲>/& pL0cQ7B<B/)/a ,:!yD5q =vC~ WH"؍kQ"dH8fІ7&O,IGu()!8.Ps}ϼgytShDKՂzr]qe"0Q-w^K hgn}Rӛ\f](:_+O c[z#_9jQ/T#F5ZXP4vA6&Pl@xZ9/!$5VG?DIh_KԅݎwMt V‡(sbD1J`~ixOWc"+AtjKzOd70ÁkfAh2B0i %|X܎D=HZH_ŭK #HRo4wo%O?ޑt'p}64?4 K! endstream endobj 291 0 obj [ 290 0 R 292 0 R 294 0 R ] endobj 288 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 291 0 R /Contents 295 0 R >> endobj 296 0 obj 2582 endobj 298 0 obj << /Length 299 0 R /Filter /FlateDecode >> stream xYK8WrIw8䰓lVmjM6[۬CIm{~%R2 *|7'|e9)՟F4 abDG  bR(&q`g2=ߪ^y䟫`Ζxw\yWKf&3/1= 7IF WdS7bD;Vodկۡg6QؤiJ=d(B|3bڂe Acć9N$<^[8빾.1w~)ƻO ʮN{Luܒ7i@Sr,Yec-حeYh 7`ҙ۹ۻcאּ0؉M͂ODH-]D(ϳ&CPM8#FsqVi2 {~$AJ,}/Ve[WjA1iĪKV?k=W\wZG2wX۫FT%Q:(NR`WAZR?E7a5M0>^dS@T}5O!H.$=DLTkVdHRg b8R=􄑟zx?\IC3f!܍J^wvI5EG\х IPЛ btsb'=w!<.+VePɓg`*s .L n42xma KXd>)#q b<'pRnH:K 9|= Z]aC>G|Ik_8Wu'Θujh/,AweseGZ7N긜Z}/%~5˩OLZ~UukY.@sw!C#;u3}E'.|j1 ~y %ESC9V}ɺ%({銳\=YctY Y:#Wr19YYB:[Y9s疵iYȩi1߂o5rz+C;זn!})d[QI<$-B>2QP]_rd4gG(…vjP/|^g4=/i) HvвnOҕ G$iM<h9ЎdqtQwPΧ ]F endstream endobj 297 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 298 0 R >> endobj 299 0 obj 2166 endobj 301 0 obj << /Length 302 0 R /Filter /FlateDecode >> stream xr6b{Fݙ&N(!t`ؒBB JTz}.}?з Eo' )N ͫɷn "C b .E4|BN AL>™M է@oI?fۻQf b,hRL14׫eU:GeQ TJ4 >׏h5L'M$t:؊XYn׍]: Xdd,:7KYdl Xx.ތu\aZFq5+"rPH.P_[Qce },Z^d bZl}w{5`B>z$UPAL<:+SLYda4"{N=`yHRn je|9⤂4cjY4LNHu&f8dD 8I~ Bs^륹Q"mR‰.(:F. TƐP!{վOR_ZW秣NqFHb!Q8Y %҇+*5b_9Et!Iq@Ɗ I}-_߷IlBPw.J$ 0)HUCe1T[h&J(ktNMծs8qɶh/߯TE ' -WBBfzt7QhޱjÅZMԪl.6\P^l$=Ԝ%ֵVB)h=Th$v+( 6Y `/b~{ҁ.΀EtuK~*M%jVܫ1_Zχ䨌:ja("0֡miHp`#"݂UF r /Vbޤ.~ŧP'7lV1aGMAM`iP'%5ɋrǜ8ӰcTdҼ=j7n#OTO"ϵEnFA[t.W\Q/y! cYg;Xe8΀aeqqԷ0LjcI10՝Ӯ=)E~~jNJ cZ(kcҩ^Jn8Dn!tm֍ef}Bb$dB2SPa캓*T).7L=q>5euYاqy: 0"q(whק,"‹N ljӂڶHߏPޣZm 1;|fUz躑",w-\:,2caG@OupQ6^@IS^,pyrq,=4-pjceHkkٍq``K$_:+Jam"|>N3 0֧ zŗf[Gedze(K?0ۙ'7Ӳ7aK05f> endobj 302 0 obj 1780 endobj 201 0 obj << /Type /Action /S /GoTo /D [303 0 R /XYZ 72.0 769.889 null] >> endobj 87 0 obj << /Type /Action /S /GoTo /D [303 0 R /XYZ 72.0 740.03 null] >> endobj 204 0 obj << /Type /Action /S /GoTo /D [303 0 R /XYZ 72.0 740.03 null] >> endobj 207 0 obj << /Type /Action /S /GoTo /D [303 0 R /XYZ 72.0 485.801 null] >> endobj 210 0 obj << /Type /Action /S /GoTo /D [303 0 R /XYZ 72.0 427.962 null] >> endobj 213 0 obj << /Type /Action /S /GoTo /D [303 0 R /XYZ 72.0 339.792 null] >> endobj 216 0 obj << /Type /Action /S /GoTo /D [303 0 R /XYZ 72.0 125.71 null] >> endobj 304 0 obj << /Type /Annot /Subtype /Link /Rect [ 318.9 659.691 339.45 668.691 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 293 0 R /H /I >> endobj 306 0 obj << /URI (http://barmag.net/veusz-wiki/EmbeddingPython) /S /URI >> endobj 307 0 obj << /Type /Annot /Subtype /Link /Rect [ 451.885 499.301 523.275 508.301 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 306 0 R /H /I >> endobj 308 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 487.301 193.11 496.301 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 306 0 R /H /I >> endobj 309 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 140.077 null] >> endobj 310 0 obj << /Type /Annot /Subtype /Link /Rect [ 333.06 265.122 344.17 274.122 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 309 0 R /H /I >> endobj 311 0 obj << /Length 312 0 R /Filter /FlateDecode >> stream xrܸ}I*=x]v*$UΖ CbfZ4iA>Ee`xzՔzMSE/PE:E^]fS9(PUH2 ^5()`fЁpaֺ P'RT(#*cűeGQ'.|Dvf:Hγm[hВ֦̅65w_]ƶe(A1,K"9K!^R,srt<</7^KB9AH=HGہx_R.q!-?;qeߧ\`pUttH1XjG;WQnVU)CqKayѯ3D}.ށ}]B%K9F>be:eb ڸUǫqiU8?뺂=0(J4M)ʦ |.%o ˷/ἤ~@ CnU-Qy>bvZ $礏MU.zЎ\ȓVe#Z4Cry^HK򲕼 +G,>Nt٠OrG@AUCÂt a zD`5Ta;=I)!"Ԙj}(ߍ3]jw`x))(*tT,} d, #?pFJ7TR@@[uJZ+kHg~II² /lk3b.=5xkYw6U,òoxZ CcuCmO;4)DHw` H|`]uTl 6x1{Ϭ2|QuӼfϚO$HڌI)\2isUtSi-AD=[5fG*0*05($kS, c]W*IsWJb;/\z @[oi[#_O"9L 9Y cxQNMStg2gGZ~iȚu~{Sشf65l =; vky@㤕*\7WRX_BqbKzf0^Ұ˷QjrvNfrHb2!2e& b-T TeXszexxٱ?D#X0ÀkS] 4^Ezn1wu* (oP|.&Fj6e.I f֐e "^SMVo.A4WDbVmFp'LEQwGR5~Px0>ء=3D`噣 Ӝ+>g_ Vãlf2 xߍ߂i;`lj{\ t#f Fh\7T`S5š1+gz\1fDEA+w|j>>&^Lmx3Qbp]51z֍I2BXq=H4)! őqՙjk`* '& |e^H c҃A׋\ U34,KZӴKo츶96Bzz [37NOtek;¢1ɲ܎3úl}|GU# ZhR= D;♉u P\hB&2p^UL7M`VJL;K!'-{%&vhSw0O]sCsh>b^XXi?h IH 'CDe ݱaZpmæq.+RekYaf{#GS[}L岥+ W޴&~5jkj32 O eLڶHaOJh hE[7.UCwM^0JEFO[OM+'UvT|Zc z'c 7<0QꉣЖti[ϙ/i!H endstream endobj 305 0 obj [ 304 0 R 307 0 R 308 0 R 310 0 R ] endobj 303 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 305 0 R /Contents 311 0 R >> endobj 312 0 obj 2344 endobj 314 0 obj << /Type /Annot /Subtype /Link /Rect [ 446.355 685.37 457.465 694.37 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 309 0 R /H /I >> endobj 316 0 obj << /Type /Annot /Subtype /Link /Rect [ 265.03 599.351 276.14 608.351 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 309 0 R /H /I >> endobj 317 0 obj << /Length 318 0 R /Filter /FlateDecode >> stream xXMo8Wp(@v=@M%&*Q(磿~DIlsHpqq c$(+?ybME,"$@4,B%NE:G3 }]A/}=9" g,Fu`#hc!Jj3@8˅4@ ZUY:G)zf;Z]b&cL\:M"`G룄 ]p,:'3.x{6Zr,zƹ.J%LKj;f:49esyn'~RV&UZ魟JPBLWu#s?Mjdc{:ԇr-9ԃ`q6*Fgnz.#sYBGGa D{tZ^1>N0E:zW*f6%ZAHlsC͍y/.{Aa[5(9hN/dr^aQW/#`,F Uu=pC!xn8:&վ<;2KlAzZ[`3FW eFk5Ynu-u\/B8Q㤺4xnCËG9䌍M1t梂2EFMN"ĒQ'3OrW f#{w)++Gʨ_yxb挃INmv7%I5f!$퍂-ڲn1ϝIWO0$EJU#6P}°32}JI <1H?ȃ%~_*VD]H_ҋ PChF  ]7@-<_6L9ȨLLJ.Cicze n&zOes)2a>~?/P0wCZ\ . VV ;ipU+@!I Ë˧LοNgGZ.8C QWݎVcѶQr UcZ󻮵1=&uN]<:)6{Y*K'qYD2X\$"tOntO oSeDF/Tj AwtV1וM"c :]+S0b싪޸%P%:b(~ĻtB7`u:m۲ZRϕ-L/\"ˉhLTOk7>IܵN)<'b'> endobj 318 0 obj 1634 endobj 320 0 obj << /Length 321 0 R /Filter /FlateDecode >> stream xWKo8W9%R/KEv =h[tE);|Hػ<ߐ?>_i{ira>J*^ȏ F;tQ#8B0Huplo`O0;,މl-Ňvaxw#Ɩ8XA}WY!J0_;#rV2(HKr _|Zbo-{JS IÚN'KqzAO9+V"4:tO4:y}6[;;n܊{Qpla˜9f-gWZuUM5{ϷF{q&IMmZ"RD-{RJ59jb|jδlޗ&D/Y2[/'k2 pv5T'cy Sg$voFQ)݈DҽbƲ졒r欷GY;7ky`¤n#Fe,f1tZ&^J}č37=zp"2j*7 ?jlU+V4o₿n Sr=A6jdРqDЮ%i/m.s?IM€Pſ]ai{ #Jca14F-qkiwDX~2ZOƬB:^זsrˋwLq6֐qℯ U7%yeZ5lC.'&( ];cJYYxxE7W+#cQX0XϚ Qi{ ̬. a561v V5ECsi`3h3Kj \Cޚ5ۜ2-cN N\Y|!9s?zI8OggEA?qUmϘѸ,2@ 9sPl+@ #%蜪p6y'+*C<|1k:x{3oȭ^_HA~a/~k endstream endobj 319 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 320 0 R >> endobj 321 0 obj 1303 endobj 323 0 obj << /Length 324 0 R /Filter /FlateDecode >> stream xWK6W 6kFGvS@lvA@[\Y$*";$[>Cr߼>f6X]ȳIŰ)fߺyBܠ-9e`{)r38Y9٧ϸ' v3qm~ۀoе,=? N‰ÐXmϖ|%N1X9kn;3-tXlʹη gdlY~^^EL,O.ێ0qDN>, 4wϰcvgݽQ+utCQnc' - _?"+S5;<ּ.n20|45QX+i|3I$:X(B4j60Si\ҧ,]˒b䁐 >@Ȫ&Cy(tBU"FnC[ԡkN1x>.VڝaRٺ9S DcX79Ič`@76&{Bc!g_Y~dŨ0((NhJ7ހ+t#ޚC_ۢ3ɉxmlcbƸ~ /0`#q Ex]՛؞+,IIG(%c|Ş4#EǍFg/ ս6v&l*RV7\*}lBW24P$Yq-3ef3#`>dEk 2+v2l`nwt۟TUES^r]D]V&|gZd`7c12d iñ)p-4!sv=c7%%w%]lyj>Y? T0_F;\)($GV_3丞󝫇mc6ܜy5Ƿq2W2U$z<龀=_3ЪbL=͐SAku Ǯ~HDtXw8hr5O)j+NbƦ4{б)z)_fR] 2-xS*""g_S+(҆~4  ͡f qʫ[ *#Ux Ĕ C^jgRl endstream endobj 322 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 323 0 R >> endobj 324 0 obj 1477 endobj 326 0 obj << /Length 327 0 R /Filter /FlateDecode >> stream xY[6~3f4u/mZd6ZhItEj<ίCR7y$v>;Q^xȅ?WxNhS,n=cGR:]G^|)D p;_ggaGrᢟg2qNkޚ0sny.Zn{GNbu1JeMr~/{ZCw)ߋ3}uME-'c5^ubqx?N9f֙ Sيֵuڮ0714qM÷Bm{^PgWW'x0^⹩O\CNѣvHE^)'(*R^EJĕJ [qC }oy3M:A_nZmuF]' ![aNJ<&E 61GGGu)YrbnTA_ɯElix\=?${c|ߒg&k:9$kQ`t);Nix&-TQmu{dߡ=eȃmCi* $GX ua~0Yb7cjo ]~lwBcS*5 |)=(qi##WMtɫ^݄3ۉJ׺es&ukOGy U*->?jehӯh %d{:%@K`pK+D]wp4I&~{!O*4꺚-:U;!ؘVRsm<=5N3fa2pg5-wq6vROuTԎLBg\MqYFEcw[bƮøvHi'Z v+k_`1ӌ9~ ]n:&z!gMkݠO.L*e?kcV).T"F~lԷ rdrLw8Fmn>&smVC2>hE1Yy'[x@9{g-V nZS#:m х\{GӋ~1X3瀰BJ_Pux2z+K\ ,h @լL$!RVl]K  Z.tVn"; !=/R'0nZ7%ϤPyTT-xAݓ 8نjT"B9n&&cEB'q1<.Nwuk|'ꈣ @M@X+3P'؈*t1/.7!`6 _ZfHY鰚L,sneI,ӡ>34\M"ID:0د=G}L a>upi)_(U2\cs9^(Sˀ/Ƹ)^ endstream endobj 325 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 326 0 R >> endobj 327 0 obj 2057 endobj 329 0 obj << /Length 330 0 R /Filter /FlateDecode >> stream xXY6~،DТ"hm"FmȮ;$Y-.8o->8N =,>y" ^4p\p>@!ΌԌs=Y ~\}{2p¹\zx5¦7kno$N8bDQ??km]jc]jvh/g{CMWV艢8*ΖQ܍VySs83x6Wߋ:XW;p0]$Qb+TNJ;"h*MAm65`6rl͝2ˈub2;{ΰgVe[ukj2K~#MݲǼ?ᔟ2dq#fN6kwą[,ok"")ifO=~ZS o[QB ]Vٰs8X<4ff!"?IjɑKd(Q}l_fH,d}ׁ0\۱8i,êփC[AE>3ci8NWHkislFfOv8o0JԨ2Լj3BGo6 Uk%monhjx`z |Z`Y&B)]E6ԕ%SIg!m&YI`cD85+t0"krjNޣveU92 佹oU)+UF.,-4\oC(ATJm -P7 'O0.D@iBY H6`<ϵI &HM/24-;LvMVwf!Km؁Cc߲hMS}wjiv{VE`vWjn85Ϊ;x1=eX ֏dfYO.r˱'._NLg0V* r!ͬd͏5oڷRSC^8\I~\TKE\ΜG4*_UԸw .s `FgQ'v,@Q/<*f'+JUGY.էuӞ ޳rb]V~D'}U7$0Ɏ [y7ުG{M/qi \vi?Zg 0zҳXm؟XN_K DgQ_Jt9.Vo1o+ j&Y%6b]z w8t)|cmԗAQMOh6&BÚPPqi~54EYFT#?q, [dg,/S]`3. ֲ Nm5A ~ߕ҂;^һ{6X.CH|`O%ik>*l.4*m -=9 03XYb/f)R,mI<#@d SXN~pBw5;^ "(y*UHgb_bmiv"T]~FIgϣ7?91MA\ϪTE6Hiq6t]\t́*ayRVz^|!Uyw5r4W endstream endobj 328 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 329 0 R >> endobj 330 0 obj 1919 endobj 332 0 obj << /Length 333 0 R /Filter /FlateDecode >> stream xXKo6ﯘvX).(#NjEEq}Kj xX| 9 ?m။GxN'-Q;N@B C!rͨA>&ˢZ~q{2npg6W[}^\lksafqK 7ő'u]Dw^T[>aӰ0mtOŻ zuV&*N̞j:&S! Gy7ƕm`8j'#~΢u /Ƹ,\%*Ipʉ'Hcb]B]=+\(FF [EU{N3mc 4T<6P\HP]]zvHq?F1?:mh%W,fMCyͪL.iE+yC[YTU=RS"W``[< uc©- %Q<tJnJz!湁!э鏩ȿpZnЍ>}klN>pDR!1twC{K!uru"Ie_O_ɡ-P%TyGrii%ヒ&WMQPnNxJ6MN>ZɪsLU+tnć# %Y~-?m1 ޡ'1x/yx itJfЃh8V[CT/U~dcāe+Iig$/VxiVRszB68@E0ّ/ၖlo Hi4 /XeCiETqAqEC)U`1V'3Z Pw"3x~Bf|Й*w# E1|Y:w&A:֘ĆN2XkgE&fE~yګYv.*.jMMghWh?Q0:kϥtmŏn^v,fz4wMԁKsYw:YpvY%gKcV[cOm6[/Cj3^e 0XX^C(;xJK_1A xp"T =mcߕ& tW&m!']]ׯ VWc3FzCw(Gtp~1Oߠ;HO& FHgSefCU dp2nuZ_$2ɺOð6]}N~}ߚTB̸9KrwVΆI].BnH\yN(nE**ե`X)&܊3~ Sx%X"D_&Ŏ};%C۠;*i'ɨ&Yv=Q׋+ endstream endobj 331 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Contents 332 0 R >> endobj 333 0 obj 1393 endobj 335 0 obj << /Length 336 0 R /Filter /FlateDecode >> stream xW[o6~ 'EPnh6 PX$|CJ)N@m}<,@}?0iѪ}P~+"8! !*x+Bֱ|daFϪA?>~ ԇٽ[/_S|=~\n"x8SSBE)f?%xgW/mZHkYMkQ6ן=# ѠqB$IIU2L=:9X;]0h]vv:EdXj H pQ"2\t\:h&" 4;Ua[\Ux' t7o#ҤʖAډA'p8<$kAXgZh`畁h\~+{o GPFV̗𗷢WY&W,x@̥øˆ.(Hsm`,iNm@fR?3DiF&'YEqR5QT^ Co<tP-:WW?Sk^3I̼K8/Ȕx  C=Cso(9kU,CyreYfS%(Yϫ 6o ?6:,TWc!"?nh0ML6'0_}3+$޶ `wM?gsd> endobj 336 0 obj 1199 endobj 338 0 obj << /Type /Annot /Subtype /Link /Rect [ 82.196 688.506 127.756 697.506 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 87 0 R /H /I >> endobj 340 0 obj << /Length 341 0 R /Filter /FlateDecode >> stream xr6b9qX|wzI]3=Xige:YCRHdzrr~`/ 0+"8Qm&񨏱PeOQ*y,~zw?_>ÝwZ፨ŽeKϫ}D0ZNS1EXhOˏU??ȐDE)KUןݪ罋4oP=a:XUUȧCCxn՝u \E2F|E9~g#g_qU-B8c8r^!RGB]. apَ1t]ù>O&↑K hE!]܋Ҁ9HԠ1I 'lebH,R9{ytNS|{[E0k h{P$qr\ F5#UIu04L$hsF;!E%2^$&x`@1tl0 %T}`X<'|#bW: oHaE^8 k%)v.zn[/ ȯ4v ʤ8ax =_ Vx<`NI%(WS {@Lg嫽/WS\Q8ǰCy-D`GP FG#afiwpVfXRoteou|σb&ؽBF#Nm~̓n,֊ $d9F%֥oH-$BxQIGPᲑKv`pvvgQ~1 m^w1_.zPYLnQ2 lP>If Rcݾya@\jv EV~6MIMsG"+]up/k8;;a+S_`Et#t)gm/oocu;,rұo;6+ X_( endstream endobj 339 0 obj [ 338 0 R ] endobj 337 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 339 0 R /Contents 340 0 R >> endobj 341 0 obj 1480 endobj 343 0 obj << /Type /Action /S /GoTo /D [272 0 R /XYZ 72.0 506.153 null] >> endobj 344 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 759.389 182.23 768.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 343 0 R /H /I >> endobj 346 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.411 759.389 525.411 768.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 343 0 R /H /I >> endobj 347 0 obj << /Type /Action /S /GoTo /D [272 0 R /XYZ 72.0 370.016 null] >> endobj 348 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 747.389 187.78 756.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 347 0 R /H /I >> endobj 349 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.373 747.389 525.373 756.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 347 0 R /H /I >> endobj 350 0 obj << /Type /Action /S /GoTo /D [275 0 R /XYZ 72.0 526.433 null] >> endobj 351 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 735.389 188.9 744.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 350 0 R /H /I >> endobj 352 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.365 735.389 525.365 744.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 350 0 R /H /I >> endobj 353 0 obj << /Type /Action /S /GoTo /D [275 0 R /XYZ 72.0 416.105 null] >> endobj 354 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 723.389 183.34 732.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 353 0 R /H /I >> endobj 355 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.402 723.389 525.402 732.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 353 0 R /H /I >> endobj 356 0 obj << /Type /Action /S /GoTo /D [275 0 R /XYZ 72.0 158.321 null] >> endobj 357 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 711.389 171.67 720.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 356 0 R /H /I >> endobj 358 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.483 711.389 525.483 720.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 356 0 R /H /I >> endobj 359 0 obj << /Type /Action /S /GoTo /D [281 0 R /XYZ 72.0 653.241 null] >> endobj 360 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 699.389 183.89 708.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 359 0 R /H /I >> endobj 361 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.398 699.389 525.398 708.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 359 0 R /H /I >> endobj 362 0 obj << /Type /Action /S /GoTo /D [281 0 R /XYZ 72.0 553.533 null] >> endobj 363 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 687.389 155.0 696.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 362 0 R /H /I >> endobj 364 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.595 687.389 525.595 696.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 362 0 R /H /I >> endobj 365 0 obj << /Type /Action /S /GoTo /D [281 0 R /XYZ 72.0 455.501 null] >> endobj 366 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 675.389 135.56 684.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 365 0 R /H /I >> endobj 367 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.727 675.389 525.727 684.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 365 0 R /H /I >> endobj 368 0 obj << /Type /Action /S /GoTo /D [281 0 R /XYZ 72.0 367.793 null] >> endobj 369 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 663.389 140.55 672.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 368 0 R /H /I >> endobj 370 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.693 663.389 525.693 672.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 368 0 R /H /I >> endobj 371 0 obj << /Type /Action /S /GoTo /D [281 0 R /XYZ 72.0 245.761 null] >> endobj 372 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 651.389 173.88 660.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 371 0 R /H /I >> endobj 373 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.466 651.389 525.466 660.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 371 0 R /H /I >> endobj 374 0 obj << /Type /Action /S /GoTo /D [281 0 R /XYZ 72.0 147.729 null] >> endobj 375 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 639.389 167.21 648.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 374 0 R /H /I >> endobj 376 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.512 639.389 525.512 648.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 374 0 R /H /I >> endobj 377 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 757.889 null] >> endobj 378 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 627.389 152.77 636.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 377 0 R /H /I >> endobj 379 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.61 627.389 525.61 636.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 377 0 R /H /I >> endobj 380 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 671.87 null] >> endobj 381 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 615.389 153.33 624.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 380 0 R /H /I >> endobj 382 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.607 615.389 525.607 624.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 380 0 R /H /I >> endobj 383 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 597.851 null] >> endobj 384 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 603.389 181.1 612.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 383 0 R /H /I >> endobj 385 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.416 603.389 525.416 612.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 383 0 R /H /I >> endobj 386 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 502.071 null] >> endobj 387 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 591.389 139.44 600.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 386 0 R /H /I >> endobj 388 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.7 591.389 525.7 600.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 386 0 R /H /I >> endobj 389 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 428.052 null] >> endobj 390 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 579.389 132.78 588.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 389 0 R /H /I >> endobj 391 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.747 579.389 525.747 588.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 389 0 R /H /I >> endobj 392 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 296.272 null] >> endobj 393 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 567.389 184.45 576.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 392 0 R /H /I >> endobj 394 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.395 567.389 525.395 576.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 392 0 R /H /I >> endobj 395 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 222.253 null] >> endobj 396 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 555.389 151.66 564.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 395 0 R /H /I >> endobj 397 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.618 555.389 525.618 564.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 395 0 R /H /I >> endobj 398 0 obj << /Type /Action /S /GoTo /D [313 0 R /XYZ 72.0 124.234 null] >> endobj 399 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 543.389 196.1 552.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 398 0 R /H /I >> endobj 400 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.314 543.389 525.314 552.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 398 0 R /H /I >> endobj 401 0 obj << /Type /Action /S /GoTo /D [297 0 R /XYZ 72.0 689.559 null] >> endobj 402 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 531.389 177.21 540.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 401 0 R /H /I >> endobj 403 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.444 531.389 525.444 540.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 401 0 R /H /I >> endobj 404 0 obj << /Type /Action /S /GoTo /D [297 0 R /XYZ 72.0 568.163 null] >> endobj 405 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 519.389 163.88 528.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 404 0 R /H /I >> endobj 406 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.534 519.389 525.534 528.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 404 0 R /H /I >> endobj 407 0 obj << /Type /Action /S /GoTo /D [297 0 R /XYZ 72.0 422.767 null] >> endobj 408 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 507.389 208.32 516.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 407 0 R /H /I >> endobj 409 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.233 507.389 525.233 516.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 407 0 R /H /I >> endobj 410 0 obj << /Type /Action /S /GoTo /D [297 0 R /XYZ 72.0 335.536 null] >> endobj 411 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 495.389 228.87 504.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 410 0 R /H /I >> endobj 412 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.093 495.389 525.093 504.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 410 0 R /H /I >> endobj 413 0 obj << /Type /Action /S /GoTo /D [297 0 R /XYZ 72.0 224.305 null] >> endobj 414 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 483.389 198.32 492.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 413 0 R /H /I >> endobj 415 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.3 483.389 525.3 492.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 413 0 R /H /I >> endobj 416 0 obj << /Type /Action /S /GoTo /D [297 0 R /XYZ 72.0 125.074 null] >> endobj 417 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 471.389 191.65 480.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 416 0 R /H /I >> endobj 418 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.347 471.389 525.347 480.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 416 0 R /H /I >> endobj 419 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 757.889 null] >> endobj 420 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 459.389 169.99 468.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 419 0 R /H /I >> endobj 421 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.493 459.389 525.493 468.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 419 0 R /H /I >> endobj 422 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 649.033 null] >> endobj 423 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 447.389 184.42 456.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 422 0 R /H /I >> endobj 424 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.394 447.389 525.394 456.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 422 0 R /H /I >> endobj 425 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 574.207 null] >> endobj 426 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 435.389 192.76 444.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 425 0 R /H /I >> endobj 427 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.338 435.389 525.338 444.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 425 0 R /H /I >> endobj 428 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 441.351 null] >> endobj 429 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 423.389 166.1 432.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 428 0 R /H /I >> endobj 430 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.518 423.389 525.518 432.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 428 0 R /H /I >> endobj 431 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 366.525 null] >> endobj 432 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 411.389 189.99 420.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 431 0 R /H /I >> endobj 433 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.356 411.389 525.356 420.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 431 0 R /H /I >> endobj 434 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 245.669 null] >> endobj 435 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 399.389 169.43 408.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 434 0 R /H /I >> endobj 436 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.496 399.389 525.496 408.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 434 0 R /H /I >> endobj 437 0 obj << /Type /Action /S /GoTo /D [300 0 R /XYZ 72.0 170.843 null] >> endobj 438 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 387.389 131.11 396.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 437 0 R /H /I >> endobj 439 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.759 387.389 525.759 396.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 437 0 R /H /I >> endobj 440 0 obj << /Type /Action /S /GoTo /D [319 0 R /XYZ 72.0 769.889 null] >> endobj 441 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 375.389 137.78 384.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 440 0 R /H /I >> endobj 442 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.713 375.389 525.713 384.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 440 0 R /H /I >> endobj 443 0 obj << /Type /Action /S /GoTo /D [319 0 R /XYZ 72.0 705.153 null] >> endobj 444 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 363.389 176.11 372.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 443 0 R /H /I >> endobj 445 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.45 363.389 525.45 372.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 443 0 R /H /I >> endobj 446 0 obj << /Type /Action /S /GoTo /D [319 0 R /XYZ 72.0 608.417 null] >> endobj 447 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 351.389 143.89 360.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 446 0 R /H /I >> endobj 448 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.67 351.389 525.67 360.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 446 0 R /H /I >> endobj 449 0 obj << /Type /Action /S /GoTo /D [319 0 R /XYZ 72.0 499.681 null] >> endobj 450 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 339.389 129.33 348.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 449 0 R /H /I >> endobj 451 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.619 339.389 525.619 348.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 449 0 R /H /I >> endobj 452 0 obj << /Type /Action /S /GoTo /D [322 0 R /XYZ 72.0 769.889 null] >> endobj 453 0 obj << /Type /Annot /Subtype /Link /Rect [ 72.0 327.389 222.756 336.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 452 0 R /H /I >> endobj 454 0 obj << /Type /Annot /Subtype /Link /Rect [ 514.917 327.389 524.917 336.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 452 0 R /H /I >> endobj 455 0 obj << /Type /Action /S /GoTo /D [322 0 R /XYZ 72.0 710.171 null] >> endobj 456 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 315.389 199.555 324.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 455 0 R /H /I >> endobj 457 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.167 315.389 525.167 324.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 455 0 R /H /I >> endobj 458 0 obj << /Type /Action /S /GoTo /D [322 0 R /XYZ 72.0 605.012 null] >> endobj 459 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 303.389 227.896 312.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 458 0 R /H /I >> endobj 460 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.099 303.389 525.099 312.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 458 0 R /H /I >> endobj 461 0 obj << /Type /Action /S /GoTo /D [325 0 R /XYZ 72.0 415.827 null] >> endobj 462 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 291.389 227.349 300.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 461 0 R /H /I >> endobj 463 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.101 291.389 525.101 300.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 461 0 R /H /I >> endobj 464 0 obj << /Type /Action /S /GoTo /D [334 0 R /XYZ 72.0 745.889 null] >> endobj 465 0 obj << /Type /Annot /Subtype /Link /Rect [ 120.0 279.389 235.213 288.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 464 0 R /H /I >> endobj 466 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.047 279.389 525.047 288.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 464 0 R /H /I >> endobj 467 0 obj << /Type /Action /S /GoTo /D [334 0 R /XYZ 72.0 126.305 null] >> endobj 468 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 267.389 162.894 276.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 467 0 R /H /I >> endobj 469 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.401 267.389 525.401 276.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 467 0 R /H /I >> endobj 470 0 obj << /Type /Action /S /GoTo /D [337 0 R /XYZ 72.0 769.889 null] >> endobj 471 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 255.389 186.309 264.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 470 0 R /H /I >> endobj 472 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.251 255.389 525.251 264.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 470 0 R /H /I >> endobj 473 0 obj << /Type /Action /S /GoTo /D [337 0 R /XYZ 72.0 433.006 null] >> endobj 474 0 obj << /Type /Annot /Subtype /Link /Rect [ 96.0 243.389 177.554 252.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 473 0 R /H /I >> endobj 475 0 obj << /Type /Annot /Subtype /Link /Rect [ 515.307 243.389 525.307 252.389 ] /C [ 0 0 0 ] /Border [ 0 0 0 ] /A 473 0 R /H /I >> endobj 476 0 obj << /Length 477 0 R /Filter /FlateDecode >> stream x]ys Q}$JQ_ Q3 $YykL"1sWx\_+Z/?fK^J|~?~sh_wϾY?Շrǟ/|?Kz}ï}Z>|?\K}}?|˯?}~o?____?~i??}^I4|)\?WKO?7?+vsO>|}?'-OlÏ3_̋'ק`{߼|6~O1ϟ>Uy]_s=ݷ}O~ׯӮ[ۅՕ>Ӳx{=z{Z7ĭI%^Wj,C qowEp!SN"Dp-[7^>`+  ` ````````.]"8Dp)K."Ep( `T QA0Fh x[CCCSSSv"Ep!SN\"Dp-[7f, `V YA0fLL&e&דʶ,22ˌ2gusO?ͮyk,}}Yo&yYw7pS}~V9;T'S,TՃ-uoTwr A,'YNdr"M,XQbyqRI-g u3,,,,,,,,;Yv|-zPNzUjdr3Z,ϸ*g`'XF3Z,+ueeeeee'N,;Yd9r$I,Y.\dr3͓Z,<*g'XflbM,ee,אl{f{珬뮮x;iJ7^V[{Ist| 1wi7V{/v{_ l^E>/¼ٹ/]_?P_?RՃ}Y#uzIzJ A,Yd9r$E,Y.drJ-gnu37Z,&gn(5YYYY&Y&Y&Y&Yvdu#UxBu8l4{\t祿m%{+%~Ȼ!"N"zݻ:ToE@ݩ^R/:Rd9r$I,Y.\dr&-gl(X!U,PjIs-}Ss/ޫn~V<ӟ |_I/ӫƸyum< z=ITOGIsO'i4I$}>IsȽg46=۾Unx}7 1K9$O/_׺yۏl7g .`-B8jA|z qB8:ToϩPw) A,YNd9r"E,7Ynb8j|zqb8j LLLdɲ>2=z4;KYNԫPT"M,X>N=Z,b8j|z2222222ɲe'N,Yd9r$E,Ynd2Xf,bE,eV*2Xf# wryC3mg^w7?{kߌv,D7 ^_NJ0.zE7랦ϝ%a@TO zP0k!ZA9$oDzP cvSM0aTNSLu2eTnR=7QN1+('sT djjjjjjjj7nTS:Mu4iT.SݦMujR=WQN2++('smz.,ߔ_)յ=yTIya7˻:Lu0iTNS]Lu6mTωĔ9rR=SN\b)70000444nTRX7Y-_jyZnTnSݤz,TϩŒWR=SNbI]LvSM0aTNSLu2eTnRBYH5 f%լTj6RFL5L5L;Wcqq{_B,ۣ7v<1fg.%KJuo?w4_@Tʻ3c/H{%'_TTTTTTvSMu0aTNS]Lu2mT7>JNWR}T{!o+vSzkg o<-M+yTnSݤT{%'_+>JNc/aaaaiiiTvS:Lu0iT.S]Lu6mT{%'_+>JNczK](wޫGY͸=w>x7%Eϻvf=xH!>f;KP 1CL3_:22222ɲe'N,Yd9r$E,Y.dr#K-g u3CZ,&g~,5YYYY&Y&Y&Y&Yvdr A,'YNdr"M,X1byRK-g@ u3,,,,,,,,;Yvd9r A,'YN\dI#O'h4 $}>IsO'i4I$}>IsRv˿|R~=cyYs;|X)fy|Qޫi|0aTNSLu2mTnRmT[!VHURmT[%H5RmT[3000444nTx!gw,/g<-byXnTnSݤTjRJQI5*F%hTjjjjjjjj7nTS:Mu4iT.SݦMujRBYH5 f%լTj6RfaaaWb_>N}^ۡ|{/>>e=_u}ޗ}͂׫KC^0L9%odzNTTTTTTTTvS:Lu0iTNS]Lu6mT Ô9arR=' SNa)70000444nTQR{{ ca|6˻U-OMu6mT Ô9aXJaI0L9)7000444nTvS:Lu4iT.S]Lu6MYH5 f!լTjVRFH5?FC3$1\[x;$_o!ŧoge?ח7Gσެ{Q),TՃ-jTwԻRM,k!ZH<%zSNw i<(e/xG7,/{<-S:Lu4iT.S]Mu6MgO;zK^IiTϞvIiTTTTTTTvS?<-VߕoG[(|Ra.SݦMuN9=WR={)'ճ]FgO;Mj7aTNS:Mu2eTnSݤTjRBYI5+f%lTjjjAeqy>,eݸMiO77_dpů_&|C,ԛecMdAy|s>Iz<-O%az[nTS:Mu4eTnSݦI&L9m”&,y#ճMrS S S S SMSMSMSMg={Q,ˇ,/W<-7mTnR=ۄ)'ճMXJg0z SNg0Mj7nTS:Mu2eTnSݦIlT6a+m”jVRFHlLu0aTNS]Lu6mT7>niTA^IqKMrR}&vSM0aTNSLu2eTnR}&9>ni6IqKTInaaaiiiiTvS:Lu4iT.S]Lu6M[$'-m6IqKTIn6[J-͖άom\K׏f/Rè>~wn{ח^dzf-/izKyX>,ߔ_oy)jjjj7nTS:Lu4iT.S]Mu&VHRmT[%VIURmT[#H5S S S SMSMSMSMSM^6=Y,ˇ,6mTi9F!(TjTRFH5vSMu0aTNSLu2mTnRBYH5 f%լTj6RFL5L5L|m}ߥixfy-oۓ??}_d\5[YVq3V!O'!ˇi*?NWsںi.yX>,ߔ%0aTNS:Mu2eTnR=_N9RT&PN%ozrS S S S SMSMSMSM8>G<,og[(_TnSݦI5 F!(TjTRFH5F3000444nTvS:Lu4iT.S]Lu6MYH5 f!լTjVRFH5R/w4!=CתƱCs|Fo_*3~vWwW7{ެ{Q5˻MBO7 yX>,7iT.S]Lu6mT[!VHRmT[%VI5RmT[#L5L5L5L5M5M5Mj7Ǿ]jm|Sy|QyZ>){<,7eTnRBQH5 F%ըTj4RFL5L5L5L5M5M5M5Mj7nTS:Mu4eTnSݦI5 f!,TjVRJH5f3003,R}qtF`팽7nG`nzٌ5~O)ܥ=n o(oʯn|UI6mT7 z7WR=I o('3TTTTTTTvSGu;-hTwԳRTOW:&E,7Yn<jby&5R@Li& $$$$N,;Yd9r$I,Y.\dr3Z,(Fj<anby0Reeeavișה}=rp};9ףko ˫Ρ{ů_[_SS։[7 u_o(/ʯ7\ aTS:Mu4eT.SݦMu9%rR=I^IG9S$ozNTTTTTTTTvSG5J'|R>a|SrSݦMuZN8IG9S$ozNTTTTTTTTvS:Lu0aTNS]Lu6mT7S('sJT)q9%rR=Qnaag{ta@lvλnOwe*7__[],|z~Q|#ߓ_|wY-O'XMu0aTNS]Lu2mT7 zFWR=AI%ozFjjjjjjjj7nTS:Mu4iT.SݦMu RNg4H9 zF7R=AM5L5L5L5L5M5M5Mj7nTS:Mu4eTnSݦMu RNg4(y%3ThrRFiRlqD_ʿ<{\c\fllz;&?_ۯ<=Vv9OI9O|X)?I-_vSMu0aTNSLu2mT7>.zT=HN$'E7R}\ vSMu0aTNSLu2mTnR}\ 9>.zIqуArS S S SMSMSMSMj7aTNS:Mu2eTnSݤ6[J-͖fKiRl)m6[J-͖fKRJd>.Qjy|շo?}_>ol/ynzyu{zW)%b,{;.ʯ}>azIy|Q~Ϥ<-S:Lu4iT.S]Mu6MFI(y%s@#zhTjjjjjjj7n|-{+ߔ_3)/g<-_3)Mu6mTy9QJFI(y#sn#Mj7aTNS:Mu2eTnSݤztTϑ9ԑrRJH5f300 YjKǦ}-kov_}~w7OC2G)zx[N͗|KۏeYx7[)|ahM5L5L5L5M5M5M5Mj7nTS:Mu4eTnSݦIq{ErR}"9>pj6Rfaaag6X{=Jmfp0orOͷ߿ݍx*vǧe$͋:7U_on=W?7UK7jɻsSivSMu0aTNSLu2mTnR=[In%zFRNgtKvSڏj}=A=|4˻Y-O'X2mT7-zFWR=[In%ozFjjjjjjjj7nTS:Mu4iT.SݦMujRBYH5 f%լTj6Rfaaagtޣlmwm>/W| ᑝO_Rou֗[,%R>)?G]R~wDʇsE9ZL|R>Lu0aTNS]Lu6mT7Q"zFWR=DI)'3JTTTTTTTvSMu0aTNS]Lu2mT7Q"zFWR=DI%ozFjjjjjjjj7nTS:Mu4iT.SݦMu%RNgH9Q"zF7RFLfKi?빟u]5{Yx;Jo?r<#ͺ g"^OP%Y8o>]>6'a|S~=AQ-_vSMu0aTNSLu2mT7('sz.T?7R=PnaaaaiiiTZ!yZ,ˇ,/W<-7mTnR=PN4+('s(z.TTTTTTTvSMu0aTNS]Lu2mT7f!,TjVRJYI5f#l#RZYw;^]o7[佔QoNj9 <կ,|z=E=K7V)HTԻQM,Ϭr)|XnTS:Mu2eTnSݦIRmT[!VIURmT[%H5RmTTTTTTTTvS:Lu0iTNS]Lu6mTjRBQH5*F%ըTj4S S S S SMSMSMSM0aTNS:Mu2eTnSݤTjRJYI5+f#lTfKi٨wJ1RzӺ֟'p~Z_o<_8}{֟WP^I eyť,|RazKy|Q~<-74444nTS:Lu4iT.S]Mu&ճ_rR={)'ճorR={%ozSnaaaaiiiTvS:Lu0iT.S]Lu6mTϮbI*UL9]ŔULvSM0aTNSLu2eTnR=)'մRl)m6[J-͖fKiRlqVǨ{nYw@ӷMw\c,x=;d߼sӺ>y}~z}n~:Rk[r1Kqit1iOOow8w]oywt}]Y#?? C qxAEpZ1EkZVqU ϖ6 LLLdɲ A,YNd9r"E,7YnbيX"eb٪X*ekbٚXFAAAIII,;YJ+z|S~n[(?7yIOW<,7eTnRBQH5 F%ըTj4RFL5L5L5L5M5M5M5Mj7nTS:Mu4eTnSݦI5 f!,TjVRJH5f30003ϱw&01\O8Q}Oݷ[~O}f?e7?Ls]?;nN ?#&a$/ϤIrS]Mu6M9A^I1tTc'I1xTTTTTTTvSMu0aTNS]Lu2mT7>FPcy%JrR} oAInaaaiiiiTvS:Lu4iT.S]Lu6MAINAAH1TTTTWc dP^l} =//>ݷ/͖W_57߮?{eO#Rg:To{Sd9r$I,'Y.\dr&-e+bيX"eb٪X&ekddddddddɲeÿQ,ˇ,/W<-7mTnRBQH5 F%ըTj4RFL5L5L5L5M5M5Mj7nTS:Mu4eT.SݦMujRBYH5+f%լTj6Rfaaϱ:~?{{Z4{;'?6w|}w7|~K}v}=IG9(700s\R{wi7nG@9~O?0᏾/?{;xZ^8Y<뙟|X))6mTϖIlT϶IlCvSMu0aTNSLu2mTnR=('ճ HJgzvQNgMj7nTS:Mu2eTnSݦIT. +]@$y#ճ rS S S}fu( \z7ifޞw7?u}_?~19z֞~?/ +,{lo;K[zJ}=rKTs\byK9*Ij<%I-$222222ɲe'N,Yd9r$E,Y.dr9 Ij,ߔg[(|R>Lu0aTNS]Lu6mT79-z&WR=ZILk)'3TTTTTTTvS?<-UߕoG[(|Ra.SݦMuRNg+y%3̥T8WFgKvSMu0aTNSLu2mT7f!,TjVRJYI5f#le]{>T7K-컁Yk?b^O|K+pW7͗ubyX>,W~^Lޓmwg[il˗<,0aTNSLu2eTnR=ǼQN7+c('s̛T1ojjjjjjjj7nTS:Mu4iT.SݦMujR=ǼQN7+c('szyﵯO޳ c޻-|zO0nqa,iqa,aTTTTvS:Lu0aTNS]Lu6mT7]ٵ)y%ճkrR=6)'ճkrS S S SMSMSMSM\i7G{({B;;KsI\euPM,7YnbyiJ-g&U,MlЄٞ)5YYYY&Y&Y&Y&Yvdr A,'YNdr"M,X]Rك)XfˬbU,e6FAAAz /O q,euW?|> endstream endobj 345 0 obj [ 344 0 R 346 0 R 348 0 R 349 0 R 351 0 R 352 0 R 354 0 R 355 0 R 357 0 R 358 0 R 360 0 R 361 0 R 363 0 R 364 0 R 366 0 R 367 0 R 369 0 R 370 0 R 372 0 R 373 0 R 375 0 R 376 0 R 378 0 R 379 0 R 381 0 R 382 0 R 384 0 R 385 0 R 387 0 R 388 0 R 390 0 R 391 0 R 393 0 R 394 0 R 396 0 R 397 0 R 399 0 R 400 0 R 402 0 R 403 0 R 405 0 R 406 0 R 408 0 R 409 0 R 411 0 R 412 0 R 414 0 R 415 0 R 417 0 R 418 0 R 420 0 R 421 0 R 423 0 R 424 0 R 426 0 R 427 0 R 429 0 R 430 0 R 432 0 R 433 0 R 435 0 R 436 0 R 438 0 R 439 0 R 441 0 R 442 0 R 444 0 R 445 0 R 447 0 R 448 0 R 450 0 R 451 0 R 453 0 R 454 0 R 456 0 R 457 0 R 459 0 R 460 0 R 462 0 R 463 0 R 465 0 R 466 0 R 468 0 R 469 0 R 471 0 R 472 0 R 474 0 R 475 0 R ] endobj 342 0 obj << /Resources 3 0 R /Type /Page /MediaBox [0 0 595.275 841.889] /CropBox [0 0 595.275 841.889] /BleedBox [0 0 595.275 841.889] /TrimBox [0 0 595.275 841.889] /Parent 1 0 R /Annots 345 0 R /Contents 476 0 R >> endobj 477 0 obj 15174 endobj 478 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >> endobj 479 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >> endobj 480 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >> endobj 481 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >> endobj 482 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Symbol /ToUnicode 483 0 R >> endobj 483 0 obj << /Length 484 0 R /Filter /FlateDecode >> stream x]Vn6+t#RE |HvN>OgX#>.fu5Y|N߷K~׮ۖky3+|=%w}뵾<. *ou;xnݧ_%_R߶R}#ԗ\^bu){\/; Dz'巌?YPC\<׻/}}{I-#_;6 [G'³^pT8). W (F7JQo~(F7JQo~[v?Cgv<2.x&NQ.qDžx 9'q̑8jX0-y$N =h6Ye-9$}BS\/U8Љ|Eܱ ~ztԓ$zztGúIpe4=A3zYZQ Ϭe-k%g_CV^y+o{m핷W^y+o{m핷W?(A7D;&;Ȃj,Ƃj,Ƃj,Ƃj,Ƃj,e#,rA `(O0DӘ raA'9b c$ưI.Xg:bm:"?b.L&[b־I~@YSf&ubd36 .C؟<'sp#(zK֣a%z<3!h?ŐkΕ<rCPO-c$7C[ȹ*u*u z9Ў7~XhhDo&ƅee8'#r cV<Xi(3 5p9ka|#^KFNǀfFdO=tBG?w^p8r$p8阑.3Rҙ 3Hg. dĞ!=bySOXI<_r'L=OhXG!laҊF+ObI ƀ61ϔ^mP#ԏzYJ endstream endobj 484 0 obj 1047 endobj 1 0 obj << /Type /Pages /Count 35 /Kids [8 0 R 14 0 R 103 0 R 342 0 R 21 0 R 24 0 R 27 0 R 30 0 R 33 0 R 84 0 R 46 0 R 49 0 R 54 0 R 60 0 R 63 0 R 288 0 R 66 0 R 74 0 R 303 0 R 92 0 R 95 0 R 98 0 R 272 0 R 275 0 R 281 0 R 313 0 R 297 0 R 300 0 R 319 0 R 322 0 R 325 0 R 328 0 R 331 0 R 334 0 R 337 0 R ] >> endobj 2 0 obj << /Type /Catalog /Pages 1 0 R /Lang (en) /Metadata 7 0 R /PageLabels 9 0 R >> endobj 3 0 obj << /Font << /F5 478 0 R /F3 479 0 R /F9 480 0 R /F7 481 0 R /F13 482 0 R >> /ProcSet [/PDF /ImageB /ImageC /Text] /XObject << /Im1 34 0 R /Im2 35 0 R /Im3 50 0 R /Im4 67 0 R /Im5 75 0 R /Im6 85 0 R >> /ColorSpace << /DefaultRGB 6 0 R >> >> endobj 9 0 obj << /Nums [0 << /P (i) >> 1 << /P (ii) >> 2 << /P (iii) >> 3 << /P (iv) >> 4 << /P (1) >> 5 << /P (2) >> 6 << /P (3) >> 7 << /P (4) >> 8 << /P (5) >> 9 << /P (6) >> 10 << /P (7) >> 11 << /P (8) >> 12 << /P (9) >> 13 << /P (10) >> 14 << /P (11) >> 15 << /P (12) >> 16 << /P (13) >> 17 << /P (14) >> 18 << /P (15) >> 19 << /P (16) >> 20 << /P (17) >> 21 << /P (18) >> 22 << /P (19) >> 23 << /P (20) >> 24 << /P (21) >> 25 << /P (22) >> 26 << /P (23) >> 27 << /P (24) >> 28 << /P (25) >> 29 << /P (26) >> 30 << /P (27) >> 31 << /P (28) >> 32 << /P (29) >> 33 << /P (30) >> 34 << /P (31) >> ] >> endobj xref 0 485 0000000000 65535 f 0000399950 00000 n 0000400264 00000 n 0000400369 00000 n 0000000015 00000 n 0000000145 00000 n 0000002823 00000 n 0000002856 00000 n 0000004090 00000 n 0000400658 00000 n 0000003804 00000 n 0000004314 00000 n 0000004335 00000 n 0000004355 00000 n 0000005373 00000 n 0000004375 00000 n 0000004453 00000 n 0000005339 00000 n 0000004593 00000 n 0000004730 00000 n 0000005615 00000 n 0000008646 00000 n 0000005635 00000 n 0000008871 00000 n 0000012301 00000 n 0000008892 00000 n 0000012526 00000 n 0000015553 00000 n 0000012547 00000 n 0000015778 00000 n 0000019220 00000 n 0000015799 00000 n 0000019445 00000 n 0000140363 00000 n 0000019466 00000 n 0000032763 00000 n 0000137051 00000 n 0000137073 00000 n 0000137096 00000 n 0000137192 00000 n 0000140322 00000 n 0000137332 00000 n 0000137468 00000 n 0000137565 00000 n 0000137705 00000 n 0000140605 00000 n 0000142171 00000 n 0000140626 00000 n 0000142396 00000 n 0000169192 00000 n 0000142417 00000 n 0000166811 00000 n 0000166833 00000 n 0000169417 00000 n 0000172723 00000 n 0000169438 00000 n 0000169510 00000 n 0000172696 00000 n 0000169648 00000 n 0000172965 00000 n 0000176037 00000 n 0000172986 00000 n 0000176262 00000 n 0000180017 00000 n 0000176283 00000 n 0000180242 00000 n 0000204972 00000 n 0000180263 00000 n 0000202300 00000 n 0000202322 00000 n 0000202403 00000 n 0000204945 00000 n 0000202540 00000 n 0000205214 00000 n 0000226368 00000 n 0000205235 00000 n 0000223300 00000 n 0000223322 00000 n 0000226327 00000 n 0000223460 00000 n 0000223540 00000 n 0000223680 00000 n 0000223817 00000 n 0000226610 00000 n 0000282587 00000 n 0000226631 00000 n 0000279689 00000 n 0000346515 00000 n 0000279711 00000 n 0000282560 00000 n 0000279850 00000 n 0000282829 00000 n 0000284892 00000 n 0000282850 00000 n 0000285117 00000 n 0000287590 00000 n 0000285138 00000 n 0000287815 00000 n 0000290109 00000 n 0000287836 00000 n 0000290081 00000 n 0000287973 00000 n 0000290353 00000 n 0000329896 00000 n 0000290375 00000 n 0000290456 00000 n 0000328995 00000 n 0000290595 00000 n 0000290737 00000 n 0000290817 00000 n 0000290955 00000 n 0000291097 00000 n 0000291178 00000 n 0000291316 00000 n 0000291458 00000 n 0000291537 00000 n 0000291676 00000 n 0000291818 00000 n 0000291899 00000 n 0000292039 00000 n 0000292181 00000 n 0000292262 00000 n 0000292401 00000 n 0000292543 00000 n 0000292624 00000 n 0000292763 00000 n 0000292905 00000 n 0000292986 00000 n 0000293126 00000 n 0000293268 00000 n 0000293349 00000 n 0000293486 00000 n 0000293628 00000 n 0000293709 00000 n 0000293847 00000 n 0000293989 00000 n 0000294070 00000 n 0000294209 00000 n 0000294351 00000 n 0000294432 00000 n 0000294571 00000 n 0000294713 00000 n 0000294794 00000 n 0000294932 00000 n 0000295074 00000 n 0000295155 00000 n 0000295295 00000 n 0000295437 00000 n 0000295517 00000 n 0000295655 00000 n 0000295797 00000 n 0000295878 00000 n 0000296017 00000 n 0000296159 00000 n 0000296240 00000 n 0000296379 00000 n 0000296521 00000 n 0000296602 00000 n 0000296742 00000 n 0000296884 00000 n 0000296965 00000 n 0000297104 00000 n 0000297244 00000 n 0000297324 00000 n 0000297464 00000 n 0000297606 00000 n 0000297687 00000 n 0000297826 00000 n 0000297968 00000 n 0000298049 00000 n 0000298188 00000 n 0000338110 00000 n 0000298330 00000 n 0000298469 00000 n 0000338192 00000 n 0000298611 00000 n 0000298750 00000 n 0000298892 00000 n 0000298973 00000 n 0000299112 00000 n 0000299254 00000 n 0000299335 00000 n 0000299475 00000 n 0000299617 00000 n 0000299698 00000 n 0000299838 00000 n 0000299980 00000 n 0000300061 00000 n 0000300201 00000 n 0000300343 00000 n 0000300424 00000 n 0000300564 00000 n 0000300706 00000 n 0000300787 00000 n 0000300927 00000 n 0000301069 00000 n 0000301150 00000 n 0000301290 00000 n 0000301432 00000 n 0000301513 00000 n 0000301652 00000 n 0000346433 00000 n 0000301794 00000 n 0000301933 00000 n 0000346595 00000 n 0000302075 00000 n 0000302213 00000 n 0000346676 00000 n 0000302355 00000 n 0000302493 00000 n 0000346758 00000 n 0000302635 00000 n 0000302774 00000 n 0000346840 00000 n 0000302916 00000 n 0000303055 00000 n 0000346922 00000 n 0000303197 00000 n 0000303336 00000 n 0000303478 00000 n 0000303559 00000 n 0000303698 00000 n 0000303840 00000 n 0000303921 00000 n 0000304060 00000 n 0000304202 00000 n 0000304283 00000 n 0000304422 00000 n 0000304564 00000 n 0000304645 00000 n 0000304784 00000 n 0000304926 00000 n 0000305007 00000 n 0000305146 00000 n 0000305286 00000 n 0000305367 00000 n 0000305506 00000 n 0000305648 00000 n 0000305729 00000 n 0000305868 00000 n 0000306010 00000 n 0000306091 00000 n 0000306230 00000 n 0000306372 00000 n 0000306453 00000 n 0000306592 00000 n 0000306734 00000 n 0000306815 00000 n 0000306954 00000 n 0000307096 00000 n 0000307177 00000 n 0000307316 00000 n 0000307458 00000 n 0000307539 00000 n 0000307678 00000 n 0000307820 00000 n 0000307901 00000 n 0000308040 00000 n 0000308182 00000 n 0000308263 00000 n 0000308402 00000 n 0000308544 00000 n 0000308625 00000 n 0000308764 00000 n 0000308906 00000 n 0000308987 00000 n 0000309125 00000 n 0000309266 00000 n 0000309347 00000 n 0000309483 00000 n 0000309623 00000 n 0000330141 00000 n 0000332673 00000 n 0000330164 00000 n 0000332900 00000 n 0000335546 00000 n 0000332922 00000 n 0000333003 00000 n 0000335517 00000 n 0000333142 00000 n 0000335791 00000 n 0000337843 00000 n 0000335813 00000 n 0000335894 00000 n 0000337806 00000 n 0000336036 00000 n 0000336174 00000 n 0000338088 00000 n 0000341566 00000 n 0000338274 00000 n 0000338355 00000 n 0000341521 00000 n 0000338495 00000 n 0000338637 00000 n 0000338719 00000 n 0000338861 00000 n 0000341811 00000 n 0000344077 00000 n 0000341833 00000 n 0000344304 00000 n 0000346184 00000 n 0000344326 00000 n 0000346411 00000 n 0000350202 00000 n 0000347003 00000 n 0000350149 00000 n 0000347142 00000 n 0000347225 00000 n 0000347367 00000 n 0000347505 00000 n 0000347587 00000 n 0000347727 00000 n 0000350447 00000 n 0000352498 00000 n 0000350469 00000 n 0000352461 00000 n 0000350609 00000 n 0000350749 00000 n 0000352743 00000 n 0000354146 00000 n 0000352765 00000 n 0000354373 00000 n 0000355950 00000 n 0000354395 00000 n 0000356177 00000 n 0000358334 00000 n 0000356199 00000 n 0000358561 00000 n 0000360580 00000 n 0000358583 00000 n 0000360807 00000 n 0000362300 00000 n 0000360829 00000 n 0000362527 00000 n 0000363826 00000 n 0000362549 00000 n 0000364053 00000 n 0000365802 00000 n 0000364075 00000 n 0000365773 00000 n 0000364215 00000 n 0000366047 00000 n 0000398000 00000 n 0000366069 00000 n 0000366151 00000 n 0000397275 00000 n 0000366290 00000 n 0000366432 00000 n 0000366514 00000 n 0000366653 00000 n 0000366795 00000 n 0000366877 00000 n 0000367015 00000 n 0000367157 00000 n 0000367239 00000 n 0000367378 00000 n 0000367520 00000 n 0000367602 00000 n 0000367741 00000 n 0000367883 00000 n 0000367965 00000 n 0000368104 00000 n 0000368246 00000 n 0000368328 00000 n 0000368466 00000 n 0000368608 00000 n 0000368690 00000 n 0000368829 00000 n 0000368971 00000 n 0000369053 00000 n 0000369192 00000 n 0000369334 00000 n 0000369416 00000 n 0000369555 00000 n 0000369697 00000 n 0000369779 00000 n 0000369918 00000 n 0000370060 00000 n 0000370142 00000 n 0000370281 00000 n 0000370421 00000 n 0000370502 00000 n 0000370641 00000 n 0000370783 00000 n 0000370865 00000 n 0000371003 00000 n 0000371145 00000 n 0000371227 00000 n 0000371366 00000 n 0000371504 00000 n 0000371586 00000 n 0000371725 00000 n 0000371867 00000 n 0000371949 00000 n 0000372088 00000 n 0000372230 00000 n 0000372312 00000 n 0000372451 00000 n 0000372593 00000 n 0000372675 00000 n 0000372813 00000 n 0000372955 00000 n 0000373037 00000 n 0000373176 00000 n 0000373318 00000 n 0000373400 00000 n 0000373539 00000 n 0000373681 00000 n 0000373763 00000 n 0000373902 00000 n 0000374044 00000 n 0000374126 00000 n 0000374265 00000 n 0000374407 00000 n 0000374489 00000 n 0000374628 00000 n 0000374766 00000 n 0000374848 00000 n 0000374987 00000 n 0000375129 00000 n 0000375211 00000 n 0000375350 00000 n 0000375492 00000 n 0000375574 00000 n 0000375713 00000 n 0000375855 00000 n 0000375937 00000 n 0000376076 00000 n 0000376218 00000 n 0000376300 00000 n 0000376438 00000 n 0000376580 00000 n 0000376662 00000 n 0000376801 00000 n 0000376943 00000 n 0000377025 00000 n 0000377164 00000 n 0000377306 00000 n 0000377388 00000 n 0000377527 00000 n 0000377669 00000 n 0000377751 00000 n 0000377890 00000 n 0000378032 00000 n 0000378114 00000 n 0000378253 00000 n 0000378393 00000 n 0000378475 00000 n 0000378614 00000 n 0000378754 00000 n 0000378836 00000 n 0000378974 00000 n 0000379116 00000 n 0000379198 00000 n 0000379337 00000 n 0000379479 00000 n 0000379561 00000 n 0000379700 00000 n 0000379842 00000 n 0000379924 00000 n 0000380064 00000 n 0000380206 00000 n 0000380288 00000 n 0000380428 00000 n 0000380570 00000 n 0000380652 00000 n 0000380792 00000 n 0000380934 00000 n 0000381016 00000 n 0000381155 00000 n 0000381297 00000 n 0000381379 00000 n 0000381518 00000 n 0000381660 00000 n 0000381742 00000 n 0000381881 00000 n 0000382023 00000 n 0000398245 00000 n 0000398268 00000 n 0000398378 00000 n 0000398491 00000 n 0000398597 00000 n 0000398706 00000 n 0000398803 00000 n 0000399928 00000 n trailer << /Root 2 0 R /Info 4 0 R /ID [ ] /Size 485 >> startxref 401299 %%EOF veusz-1.21.1/Documents/widget_doc.xsl0000644000175000017500000000275611662000553015741 0ustar jssjss

Veusz widget API

Can be placed in:

Display name: , description:

Setting API nameDisplay name DescriptionTypeDefault Choice
veusz-1.21.1/Documents/manual.xml0000664000175000017500000031304412263564313015104 0ustar jssjss Veusz - a scientific plotting package Jeremy Sanders jeremy@jeremysanders.net 2014 This document is licensed under the GNU General Public License, version 2 or greater. Please see the file COPYING for details, or see . Introduction
Veusz Veusz is a scientific plotting package. It was designed to be easy to use, easily extensible, but powerful. The program features a graphical user interface, which works under Unix/Linux, Windows or Mac OS X. It can also be easily scripted (the saved file formats are similar to Python scripts) or used as module inside Python. Veusz reads data from a number of different types of data file, it can be manually entered, or constructed from other datasets. In Veusz the document is built in an object-oriented fashion, where a document is built up by a number of widgets in a hierarchy. For example, multiple function or xy widgets can be placed inside a graph widget, and many graphs can be placed in a grid widget. The technologies behind Veusz include PyQt (a very easy to use Python interface to Qt, which is used for rendering and the graphical user interface, GUI) and numpy (a package for Python which makes the handling of large datasets easy). Veusz can be extended by the user easily by adding plugins. Support for different data file types can be added with import plugins. Dataset plugins automate the manipulation of datasets. Tools plugins automate the manipulation of the document.
Terminology Here we define some terminology for future use.
Widget A document and its graphs are built up from widgets. These widgets can often by placed within each other, depending on the type of the widget. A widget has children (those widgets placed within it) and its parent. The widgets have a number of different settings which modify their behaviour. These settings are divided into properties, which affect what is plotted and how it is plotted. These would include the dataset being plotted or whether an axis is logarithmic. There are also formatting settings, including the font to be used and the line thickness. In addition they have actions, which perform some sort of activity on the widget or its children, like "fit" for a fit widget. As an aside, using the scripting interface, widgets are specified with a "path", like a file in Unix or Windows. These can be relative to the current widget (do not start with a slash), or absolute (do not start with a slash). Examples of paths include, "/page1/graph1/x", "x" and ".". The widget types include document - representing a complete document. A document can contain pages. In addition it contains a setting giving the page size for the document. page - representing a page in a document. One or more graphs can be placed on a page, or a grid. graph - defining an actual graph. A graph can be placed on a page or within a grid. Contained within the graph are its axes and plotters. A graph can be given a background fill and a border if required. It also has a margin, which specifies how far away from the edge of its parent widget to plot the body of the graph. A graph can contain several axes, at any position on the plot. In addition a graph can use axes defined in parent widgets, shared with other graphs. More than one graph can be placed within in a page. The margins can be adjusted so that they lie within or besides each other. grid - containing one or more graphs. A grid plots graphs in a gridlike fashion. You can specify the number of rows and columns, and the plots are automatically replotted in the chosen arrangement. A grid can contain graphs or axes. If an axis is placed in a grid, it can be shared by the graphs in the grid. axis - giving the scale for plotting data. An axis translates the coordinates of the data to the screen. An axis can be linear or logarithmic, it can have fixed endpoints, or can automatically get them from the plotted data. It also has settings for the axis labels and lines, tick labels, and major and minor tick marks. An axis may be "horizontal" or "vertical" and can appear anywhere on its parent graph or grid. If an axis appears within a grid, then it can be shared by all the graphs which are contained within the grid. The axis-broken widget is an axis sub-type. It is an axis type where there are jumps in the scale of the axis. The axis-function widget allows the user to create an axis where the values are scaled by a monotonic function, allowing non-linear and non-logarithmic axis scales. The widget can also be linked to a different axis via the function. plotters - types of widgets which plot data or add other things on a graph. There is no actual plotter widget which can be added, but several types of plotters listed below. Plotters typically take an axis as a setting, which is the axis used to plot the data on the graph (default x and y). function - a plotter which plots a function on the graph. Functions can be functions of x or y (parametric functions are not done yet!), and are defined in Python expression syntax, which is very close to most other languages. For example "3*x**2 + 2*x - 4". A number of functions are available (e.g. sin, cos, tan, exp, log...). Technically, Veusz imports the numpy package when evaluating, so numpy functions are available. As well as the function setting, also settable is the line type to plot the function, and the number of steps to evaluate the function when plotting. Filling is supported above/below/left/right of the function. xy - a plotter which plots scatter, line, or stepped plots. This versatile plotter takes an x and y dataset, and plots (optional) points, in a chosen marker and colour, connecting them with (optional) lines, and plotting (optional) error bars. An xy plotter can also plot a stepped line, allowing histograms to be plotted (note that it doesn't yet do the binning of the data). The settings for the xy widget are the various attibutes for the points, line and error bars, the datasets to plot, and the axes to plot on. The xy plotter can plot a label next to each dataset, which is either the same for each point or taken from a text dataset. If you wish to leave gaps in a plot, the input value "nan" can be specified in the numeric dataset. fit - fit a function to data. This plotter is a like the function plotter, but allows fitting of the function to data. This is achived by clicking on a "fit" button, or using the "fit" action of the widget. The fitter takes a function to fit containing the unknowns, e.g. "a*x**2 + b*x + c", and initial values for the variables (here a, b and c). It then fits the data (note that at the moment, the fit plotter fits all the data, not just the data that can be seen on the graph) by minimising the chi-squared. In order to fit properly, the y data (or x, if fitting as a function of x) must have a properly defined, preferably symmetric error. If there is none, Veusz assumes the same fractional error everywhere, or symmetrises asymmetric errors. Note that more work is required in this widget, as if a parameter is not well defined by the data, the matrix inversion in the fit will fail. In addition Veusz does not supply estimates for the errors or the final chi-squared in a machine readable way. If the fitting parameters vary significantly from 1, then it is worth "normalizing" them by adding in a factor in the fit equation to bring them to of the order of 1. bar - a bar chart which plots sets of data as horizontal or vertical bars. Multiple datasets are supported. In "grouped" mode the bars are placed side-by-side for each dataset. In "stacked" mode the bars are placed on top of each other (in the appropriate direction according to the sign of the dataset). Bars are placed on coordinates given, or in integer values from 1 upward if none are given. Error bars are plotted for each of the datasets. Different fill styles can be given for each dataset given. A separate key value can be given for each dataset. key - a box which describes the data plotted. If a key is added to a plot, the key looks for "key" settings of the other data plotted within a graph. If there any it builds up a box containing the symbol and line for the plotter, and the text in the "key" setting of the widget. This allows a key to be very easily added to a plot. The key may be placed in any of the corners of the plot, in the centre, or manually placed. Depending on the ordering of the widgets, the key will be placed behind or on top of the widget. The key can be filled and surrounded by a box, or not filled or surrounded. label - a text label places on a graph. The alignment can be adjusted and the font changed. The position of the label can be specified in fractional terms of the current graph, or using axis coordinates. rect, ellipse - these draw a rectangle or ellipse, respectively, of size and rotation given. These widgets can be placed directly on the page or on a graph. The centre can be given in axis coordinates or fractional coordinates. imagefile - draw an external graphs file on the graph or page, with size and rotation given. The centre can be given in axis coordinates or fractional coordinates. line - draw a line with optional arrowheads on the graph or page. One end can be given in axis coordinates or fractional coordinates. contour - plot contours of a 2D dataset on the graph. Contours are automatically calculated between the minimum and maximum values of the graph or chosen manually. The line style of the contours can be chosen individually and the region between contours can be filled with shading or color. 2D datasets currently consist of a regular grid of values between minimum and maximum positions in x and y. They can be constructed from three 1D datasets of x, y and z if they form a regular x, y grid. image - plot a 2D dataset as a colored image. Different color schemes can be chosen. The scaling between the values and the image can be specified as linear, logarithmic, square-root or square. polygon - plot x and y points from datasets as a polygon. The polygon can be placed directly on the page or within a graph. Coordinates are either plotted using the axis or as fractions of the width and height of the containing widget. boxplot - plot distribution of points in a dataset. polar - plot polar data or functions. This is a non-orthogonal plot and is placed directly on the page rather than in a graph. ternary - plot data of three variables which add up to 100 per cent.This is a non-orthogonal plot and is placed directly on the page rather than in a graph.
Settings: properties and formatting The various settings of the widgets come in a number of types, including integers (e.g. 10), floats (e.g. 3.14), dataset names ("mydata"), expressions ("x+y"), text ("hi there!"), distances (see above), options ("horizontal" or "vertical" for axes). Veusz performs type checks on these parameters. If they are in the wrong format the control to edit the setting will turn red. In the command line, a TypeError exception is thrown. In the GUI, the current page is replotted if a setting is changed when enter is pressed or the user moves to another setting. The settings are split up into formatting settings, controlling the appearance of the plot, or properties, controlling what is plotted and how it is plotted. Default settings, including the default font and line style, and the default settings for any graph widget, can be modified in the "Default styles" dialog box under the "Edit" menu. Default settings are set on a per-document basis, but can be saved into a separate file and loaded. A default default settings file can be given to use for new documents (set in the preferences dialog).
<anchor id="TextFonts" />Text Veusz understands a limited set of LaTeX-like formatting for text. There are some differences (for example, "10^23" puts the 2 and 3 into superscript), but it is fairly similar. You should also leave out the dollar signs. Veusz supports superscripts ("^"), subscripts ("_"), brackets for grouping attributes are "{" and "}". Supported LaTeX symbols include: \AA, \Alpha, \Beta, \Chi, \Delta, \Epsilon, \Eta, \Gamma, \Iota, \Kappa, \Lambda, \Mu, \Nu, \Omega, \Omicron, \Phi, \Pi, \Psi, \Rho, \Sigma, \Tau, \Theta, \Upsilon, \Xi, \Zeta, \alpha, \approx, \ast, \asymp, \beta, \bowtie, \bullet, \cap, \chi, \circ, \cup, \dagger, \dashv, \ddagger, \deg, \delta, \diamond, \divide, \doteq, \downarrow, \epsilon, \equiv, \eta, \gamma, \ge, \gg, \in, \infty, \int, \iota, \kappa, \lambda, \le, \leftarrow, \lhd, \ll, \models, \mp, \mu, \neq, \ni, \nu, \odot, \omega, \omicron, \ominus, \oplus, \oslash, \otimes, \parallel, \perp, \phi, \pi, \pm, \prec, \preceq, \propto, \psi, \rhd, \rho, \rightarrow, \sigma, \sim, \simeq, \sqrt, \sqsubset, \sqsubseteq, \sqsupset, \sqsupseteq, \star, \stigma, \subset, \subseteq, \succ, \succeq, \supset, \supseteq, \tau, \theta, \times, \umid, \unlhd, \unrhd, \uparrow, \uplus, \upsilon, \vdash, \vee, \wedge, \xi, \zeta. Please request additional characters if they are required (and exist in the unicode character set). Special symbols can be included directly from a character map. Other LaTeX commands are supported. "\\" breaks a line. This can be used for simple tables. For example "{a\\b} {c\\d}" shows "a c" over "b d". The command "\frac{a}{b}" shows a vertical fraction a/b. Also supported are commands to change font. The command "\font{name}{text}" changes the font text is written in to name. This may be useful if a symbol is missing from the current font, e.g. "\font{symbol}{g}" should produce a gamma. You can increase, decrease, or set the size of the font with "\size{+2}{text}", "\size{-2}{text}", or "\size{20}{text}". Numbers are in points. Various font attributes can be changed: for example, "\italic{some italic text}" (or use "\textit" or "\emph"), "\bold{some bold text}" (or use "\textbf") and "\underline{some underlined text}". Example text could include "Area / \pi (10^{-23} cm^{-2})", or "\pi\bold{g}". Veusz plots these symbols with Qt's unicode support. You can also include special characters directly, by copying and pasting from a character map application. If your current font does not contain these symbols then you may get a box character.
Measurements Distances, widths and lengths in Veusz can be specified in a number of different ways. These include absolute distances specified in physical units, e.g. 1cm, 0.05m, 10mm, 5in and 10pt, and relative units, which are relative to the largest dimension of the page, including 5%, 1/20, 0.05.
Axis numeric scales The way in which numbers are formatted in axis scales is chosen automatically. For standard numerical axes, values are shown with the "%Vg" formatting (see below). For date axes, an appropriate date formatting is used so that the interval shown is correct. A format can be given for an axis in the axis number formatting panel can be given to explicitly choose a format. Some examples are given in the drop down axis menu. Hold the mouse over the example for detail. C-style number formatting is used with a few Veusz specific extensions. Text can be mixed with format specifiers, which start with a "%" sign. Examples of C-style formatting include: "%.2f" (decimal number with two decimal places, e.g. 2.01), "%.3e" (scientific formatting with three decimal places, e.g. 2.123e-02), "%g" (general formatting, switching between "%f" and "%e" as appropriate). See for details. Veusz extensions include "%Ve", which is like "%e" except it displays scientific notation as written, e.g. 1.2x10^23, rather than 1.2e+23. "%Vg" switches between standard numbers and Veusz scientific notation for large and small numbers. "%VE" using engineering SI suffixes to represent large or small numbers (e.g. 1000 is 1k). Veusz allows dates and times to be formatted using "%VDX" where "X" is one of the formatting characters for strftime (see for details). These include "a" for an abbreviated weekday name, "A" for full weekday name, "b" for abbreviated month name, "B" for full month name, "c" date and time representaiton, "d" day of month 01..31, "H" hour as 00..23, "I" hour as 01..12, "j" as day of year 001..366, "m" as month 01..12, "M" minute as 00..59, "p" AM/PM, "S" second 00..61, "U" week number of year 00..53 (Sunday as first day of week), "w" weekday as decimal number 0..6, "W" week number of year (Monday as first day of week), "x" date representation, "X" time representation, "y" year without century 00..99 and "Y" year. "%VDVS" is a special Veusz addon format which shows seconds and fractions of seconds (e.g. 12.2).
Installation Please look at the Installation notes (INSTALL) for details on installing Veusz.
The main window You should see the main window when you run Veusz (you can just type the veusz command in Unix). The Veusz window is split into several sections. At the top is the menu bar and tool bar. These work in the usual way to other applications. Sometimes options are disabled (greyed out) if they do not make sense to be used. If you hold your mouse over a button for a few seconds, you will usually get an explanation for what it does called a "tool tip". Below the main toolbar is a second toolbar for constructing the graph by adding widgets (on the left), and some editing buttons. The add widget buttons add the request widget to the currently selected widget in the selection window. The widgets are arranged in a tree-like structure. Below these toolbars and to the right is the plot window. This is where the current page of the current document is shown. You can adjust the size of the plot on the screen (the zoom factor) using the "View" menu or the zoom tool bar button (the magnifying glass). Initially you will not see a plot in the plot window, but you will see the Veusz logo. At the moment you cannot do much else with the window. In the future you will be able to click on items in the plot to modify them. To the left of the plot window is the selection window, and the properties and formatting windows. The properties window lets you edit various aspects of the selected widget (such as the minimum and maximum values on an axis). Changing these values should update the plot. The formatting lets you modify the appearance of the selected widget. There are a series of tabs for choosing what aspect to modify. The various windows can be "dragged" from the main window to "float" by themselves on the screen. To the bottom of the window is the console. This window is not shown by default, but can be enabled in the View menu. The console is a Veusz and Python command line console. To read about the commands available see Commands. As this is a Python console, you can enter mathematical expressions (e.g. "1+2.0*cos(pi/4)") here and they will be evaluated when you press Enter. The usual special functions and the operators are supported. You can also assign results to variables (e.g. "a=1+2") for use later. The console also supports command history like many Unix shells. Press the up and down cursor keys to browse through the history. Command line completion is not available yet! There also exists a dataset browsing window, by default to the right of the screen. This window allows you to view the datasets currently loaded, their dimensions and type. Hovering a mouse over the size of the dataset will give you a preview of the data.
My first plot After opening Veusz, on the left of the main window, you will see a Document, containing a Page, which contains a Graph with its axes. The Graph is selected in the selection window. The toolbar above adds a new widget to the selected widget. If a widget cannot be added to a selected widget it is disabled. On opening a new document Veusz automatically adds a new Page and Graph (with axes) to the document. You will see something like this: Select the x axis which has been added to the document (click on "x" in the selection window). In the properties window you will see a variety of different properties you can modify. For instance you can enter a label for the axis by writing "Area (cm^{2})" in the box next to label and pressing enter. Veusz supports text in LaTeX-like form (without the dollar signs). Other important parameters is the "log" switch which switches between linear and logarithmic axes, and "min" and "max" which allow the user to specify the minimum and maximum values on the axes. The formatting dialog lets you edit various aspects of the graph appearance. For instance the "Line" tab allows you to edit the line of the axis. Click on "Line", then you can then modify its colour. Enter "green" instead of "black" and press enter. Try making the axis label bold. Now you can try plotting a function on the graph. If the graph, or its children are selected, you will then be able to click the "function" button at the top (a red curve on a graph). You will see a straight line (y=x) added to the plot. If you select "function1", you will be able to edit the functional form plotted and the style of its line. Change the function to "x**2" (x-squared). We will now try plotting data on the graph. Go to your favourite text editor and save the following data as test.dat: 1 0.1 -0.12 1.1 0.1 2.05 0.12 -0.14 4.08 0.12 2.98 0.08 -0.1 2.9 0.11 4.02 0.04 -0.1 15.3 1.0 The first three columns are the x data to plot plus its asymmetric errors. The final two columns are the y data plus its symmetric errors. In Veusz, go to the "Data" menu and select "Import". Type the filename into the filename box, or use the "Browse..." button to search for the file. You will see a preview of the data pop up in the box below. Enter "x,+,- y,+-" into the descriptors edit box (note that commas and spaces in the descriptor are almost interchangeable in Veusz 1.6 or newer). This describes the format of the data which describes dataset "x" plus its asymmetric errors, and "y" with its symmetric errors. If you now click "Import", you will see it has imported datasets "x" and "y". To plot the data you should now click on "graph1" in the tree window. You are now able to click on the "xy" button (which looks like points plotted on a graph). You will see your data plotted on the graph. Veusz plots datasets "x" and "y" by default, but you can change these in the properties of the "xy" plotter. You are able to choose from a variety of markers to plot. You can remove the plot line by choosing the "Plot Line" subsetting, and clicking on the "hide" option. You can change the colour of the marker by going to the "Marker Fill" subsetting, and entering a new colour (e.g. red), into the colour property.
Reading data Currently Veusz supports reading data from files with text, CSV, HDF5, FITS, 2D text or CSV, QDP, binary and NPY/NPZ formats. Use the DataImport dialog to read data, or the importing commands in the API can be used. In addition, the user can load or write import plugins in Python which load data into Veusz in an arbitrary format. At the moment QDP, binary and NPY/NPZ files are supported with this method. The HDF5 file format is the most sophisticated, and is recommended for complex datasets. By default, data are "linked" to the file imported from. This means that the data are not stored in the Veusz saved file and are reloaded from the original data file when opening. In addition, the user can use the DataReload menu option to reload data from linked files. Unselect the linked option when importing to remove the association with the data file and to store the data in the Veusz saved document. Note that a prefix and suffix can be given when importing. These are added to the front or back of each dataset name imported. They are convenient for grouping data together. We list the various types of import below.
Standard text import The default text import operates on simple text files. The data are assumed to be in columns separated by whitespace. Each column corresponds to dataset (or its error bars). Each row is an entry in the dataset. The way the data are read is goverened by a simple "descriptor". This can simply be a list of dataset names separated by spaces. If no descriptor is given, the columns are treated as separate datasets and are given names col1, col2, etc. Veusz attempts to automatically determine the type of the data. When reading in data, Veusz treats any whitespace as separating columns. The columns do not actually need to be aligned. Furthermore a "\" symbol can be placed at the end of a line to mark a continuation. Veusz will read the next line as if it were placed at the end of the current line. In addition comments and blank lines are ignored (unless in block mode). Comments start with a "#", ";", "!" or "%", and continue until the end of the line. The special value "nan" can be used to specify a break in a dataset. If the option to read data in blocks is enabled, Veusz treats blank lines (or lines starting with the word "no") as block separators. For each dataset in the descriptor, separate datasets are created for each block, using a numeric suffix giving the block number, e.g. _1, _2.
Data types in text import Veusz supports reading in several types of data. The type of data can be added in round brackets after the name in the descriptor. Veusz will try to guess the type of data based on the first value, so you should specify it if there is any form of ambiguity (e.g. is 3 text or a number). Supported types are numbers (use numeric in brackets) and text (use text in brackets). An example descriptor would be "x(numeric) +- y(numeric) + - label(text)" for an x dataset followed by its symmetric errors, a y dataset followed by two columns of asymmetric errors, and a final column of text for the label dataset. A text column does not need quotation unless it contains space characters or escape characters. However make sure you deselect the "ignore text" option in the import dialog. This ignores lines of text to ease the import of data from other applications. Quotation marks are recommended around text if you wish to avoid ambiguity. Text is quoted according to the Python rules for text. Double or single quotation marks can be used, e.g. "A 'test'", 'A second "test"'. Quotes can be escaped by prefixing them with a backslash, e.g. "A new \"test\"". If the data are generated from a Python script, the repr function provides the text in a suitable form. Dates and times are also supported with the syntax "dataset(date)". Dates must be in ISO format YYYY-MM-DD. Times are in 24 hour format hh:mm:ss.ss. Dates with times are written YYYY-MM-DDThh:mm:ss.ss (this is a standard ISO format, see ). Dates are stored within Veusz as a number which is the number of seconds since the start of January 1st 2009. Veusz also supports dates and times in the local format, though take note that the same file and data may not work on a system in a different location.
Descriptors A list of datasets, or a "Descriptor", is given in the Import dialog to describe how the data are formatted in the import file. The descriptor at its simplest is a space or comma-separated list of the names of the datasets to import. These are columns in the file. Following a dataset name the text "+", "-", or "+-" can be given to say that the following column is a positive error bar, negative error bar or symmetric error bar for the previous (non error bar) dataset. These symbols should be separated from the dataset name or previous symbol with a space or a comma symbol. In addition, if multiple numbered columns should be imported, the dataset name can be followed by square brackets containing a range in the form "[a:b]" to number columns a to b, or [:] to number remaining columns. See below for examples of this use. Dataset names can contain virtually any character, even unicode characters. If the name contains non alpha-numeric characters (characters outside of A-Z, a-z and 0-9), then the dataset name should be contained within back-tick characters. An example descriptor is `length data (m)`,+- `speed (mps)`,+,-, for two datasets with spaces and brackets in their names. Instead of specifying the descriptor in the Import dialog, the descriptor can be placed in the data file using a descriptor statement on a separate line, consisting of "descriptor" followed by the descriptor. Multiple descriptors can be placed in a single file, for example: # here is one section descriptor x,+- y,+,- 1 0.5 2 0.1 -0.1 2 0.3 4 0.2 -0.1 # my next block descriptor alpha beta gamma 1 2 3 4 5 6 7 8 9 # etc...
Descriptor examples x y two columns are present in the file, they will be read in as datasets "x" and "y". x,+- y,+,- or x +- y + - two datasets are in the file. Dataset "x" consists of the first two columns. The first column are the values and the second are the symmetric errors. "y" consists of three columns (note the comma between + and -). The first column are the values, the second positive asymmetric errors, and the third negative asymmetric errors. Suppose the input file contains: 1.0 0.3 2 0.1 -0.2 1.5 0.2 2.3 2e-2 -0.3E0 2.19 0.02 5 0.1 -0.1 Then x will contain "1+-0.3", "1.5+-0.2", "2.19+-0.02". y will contain "2 +0.1 -0.2", "2.3 +0.02 -0.3", "5 +0.1 -0.1". x[1:2] y[:] the first column is the data "x_1", the second "x_2". Subsequent columns are read as "y[1]" to "y[n]". y[:]+- read each pair of columns as a dataset and its symmetric error, calling them "y[1]" to "y[n]". foo,,+- read the first column as the foo dataset, skip a column, and read the third column as its symmetric error.
CSV files CVS (comma separated variable) files are often written from other programs, such as spreadsheets, including Excel and Gnumeric. Veusz supports reading from these files. In the import dialog choose "CSV", then choose a filename to import from. In the CSV file the user should place the data in either rows or columns. Veusz will use a name above a column or to the left of a row to specify what the dataset name should be. The user can use new names further down in columns or right in rows to specify a different dataset name. Names do not have to be used, and Veusz will assign default "col" and "row" names if not given. You can also specify a prefix which is prepended to each dataset name read from the file. To specify symmetric errors for a column, put "+-" as the dataset name in the next column or row. Asymmetric errors can be stated with "+" and "-" in the columns. The data type in CSV files are automatically detected unless specified. The data type can be given in brackets after the column name, e.g. "name (text)", where the data type is "date", "numeric" or "text". Explicit data types are needed if the data look like a different data type (e.g. a text item of "1.23"). The date format in CSV files can be specified in the import dialog box - see the examples given. In addition CSV files support numbers in European format (e.g. 2,34 rather than 2.34), depending on the setting in the dialog box.
HDF5 files HDF5 is a flexible data format. Datasets and tables can be stored in a hierarchical arrangements of groups within a file. Veusz supports reading 1D numeric, text, date-time or 2D numeric data from HDF files. The h5py Python module must be installed to use HDF5 files (included in binary releases). In the import dialog box, choose which individual datasets to import, or selecting a group to import all the datasets within the group. If selecting a group, datasets in the group incompatible with Veusz are ignored. A name can be provided for each dataset imported by entering one under "Import as". If one is not given, the dataset or column name is used. The name can also be specified by setting the HDF5 dataset attribute vsz_name to the name. Note that for compound datasets (tables), vsz_ attributes for columns are given by appending the suffix _columnname to the attribute.
Error bars Error bars are supported in two ways. The first way is to combine 1D datasets. For the datasets which are error bars, use a name which is the same as the main dataset but with the suffix "(+-)", "(+)" or "(-)", for symmetric, postive or negative error bars, respectively. The second method is to use a 2D dataset with two or three columns, for symmetric or asymmetric error bars, respectively. Click on the dataset in the dialog and choose the option to import as a 1D dataset. This second method can also be enabled by adding an HDF5 attribute vsz_twod_as_oned set to a non-zero value for the dataset.
Slices As Veusz only supports 1D and 2D datasets, you may wish to reduce the dimensions of a dataset before importing by slicing. You can also give a slice to import a subset of a dataset. When importing, in the slice column you can give a slice expression. This should have the same number of entries as the dataset has dimensions, separated by commas. An entry can be a single number, to select a particular row or column. Alternatively it could be an expression like a:b:c or a:b, where a is the starting index, b is one beyond the stopping index and optionally c is the step size. A slice can also be specified by providing an HDF5 attribute vsz_slice for the dataset.
2D data ranges 2D data have an associated X and Y range. By default the number of pixels of the image are used to give this range. A range can be specified by clicking on the dataset and entering a minimum and maximum X and Y coordinates. Alternatively, provide the HDF5 attribute for the dataset vsz_range, which should be set to an array of four values (minimum x, minimum y, maximum x, maximum y).
Dates Date/time datasets can be made from a 1D numeric dataset or from a text dataset. For the 1D dataset, use the number of seconds relative to the start of the year 2009 (this is Veusz format) or the year 1970 (this is Unix format). In the import dialog, click on the name of the dataset and choose the date option. To specify a date format in the HDF5 file, set the attribute vsz_convert_datetime to either veusz or unix. For text datasets, dates must be given in the right format, selected in the import dialog after clicking on the dataset name. As in other file formats, by default Veusz uses ISO 8601 format, which looks like "2013-12-22T21:08:07", where the date and time parts are optional. The T is also optional. You can also provide your own format when importing by giving a date expression using YYYY, MM, DD, hh, mm and ss (e.g. "YYYY-MM-DD|T|hh:mm:ss"), where vertical bars mark optional parts of the expression. To automate this, set the attribute vsz_convert_datetime to the format expression or iso to specify ISO format.
2D text or CSV format Veusz can import 2D data from standard text or CSV files. In this case the data should consist of a matrix of data values, with the columns separated by one or more spaces or tabs and the rows on different lines. In addition to the data the file can contain lines at the top which affect the import. Such specifiers are used, for example, to change the coordinates of the pixels in the file. By default the first pixels coordinates is between 0 and 1, with the centre at 0.5. Subsequent pixels are 1 greater. When using specifiers in CSV files, put the different parts (separated by spaces) in separate columns. Below are listed the specifiers: xrange A B - make the 2D dataset span the coordinate range A to B in the x-axis (where A and B are numbers). Note that the range is inclusive, so a 1 pixel wide image with A=0 and B=1 would have the pixel centre at 0.5. The pixels are assumed to have the same spacing. Do not use this as the same time as the xedge or xcent options. yrange A B - make the 2D dataset span the coordinate range A to B in the y-axis (where A and B are numbers). xedge A B C... - rather than assume the pixels have the same spacing, give the coordinates of the edges of the pixels in the x-axis. The numbers should be space-separated and there should be one more number than pixels. Do not give xrange or xcent if this is given. yedge A B C... - rather than assume the pixels have the same spacing, give the coordinates of the edges of the pixels in the y-axis. xcent A B C... - rather than give a total range or pixel edges, give the centres of the pixels. There should be the same number of values as pixels in the image. Do not give xrange or xedge if this is given. ycent A B C... - rather than give a total range or pixel edges, give the centres of the pixels. invertrows - invert the rows after reading the data. invertcols - invert the columns after reading the data. transpose - swap rows and columns after importing data. gridatedge - the first row and leftmost column give the positions of the centres of the pixels. This is also an option in the import dialog. The values should be increasing.
FITS files 1D or 2D data can be read from FITS files. 1D data, with optional errors bars, can be read from table extensions, and 2D data from image or primary extensions. Note that pyfits or astropy must be installed to get FITS support. To read 1D data, choose a tabular HDU for to import from, enter the name to give the imported data, and choose the columns to assign to the data. Multiple sets of data can be read by repeatedly importing. For 2D data, choose an image HDU. Enter the name of the dataset. The data are imported with pixel coordinates by default (i.e. the pixels are numbered with integers). Other modes can be selected under Image WCS mode. These include fractional, where the pixels are numbered between 0 and 1. Pixel (WCS) assigns the pixel coordinate calculated relative to the CRPIX1/2 header keywords. Linear (WCS) uses linear coordinates where the Pixel (WCS) coordinates are multiplied by the respective CDELT1/2 values and added to the CRVAL1/2 values.
Reading other data formats As mentioned above, a user may write some Python code to read a data file or set of data files. To write a plugin which is incorportated into Veusz, see You can also include Python code in an input file to read data, which we describe here. Suppose an input file "in.dat" contains the following data: 1 2 2 4 3 9 4 16 Of course this data could be read using the ImportFile command. However, you could also read it with the following Veusz script (which could be saved to a file and loaded with execfile or Load. The script also places symmetric errors of 0.1 on the x dataset. x = [] y = [] for line in open("in.dat"): parts = [float(i) for i in line.split()] x.append(parts[0]) y.append(parts[1]) SetData('x', x, symerr=0.1) SetData('y', y)
Manipulating datasets Imported datasets can easily be modified in the Data Editor dialog box. This dialog box can also be used to create new datasets from scratch by typing them in. The Data Create dialog box is used to new datasets as a numerical sequence, parametrically or based on other datasets given expressions. If you want to plot a function of a dataset, you often do not have to create a new dataset. Veusz allows to enter expressions directly in many places.
Using dataset plugins Dataset plugins can be used to perform arbitrary manipulation of datasets. Veusz includes several plugins for mathematical operation of data and other dataset manipulations, such as concatenation or splitting. If you wish to write your own plugins look at .
Using expressions to create new datasets For instance, if the user has already imported dataset d, then they can create d2 which consists of d**2. Expressions are in Python numpy syntax and can include the usual mathematical functions. Expressions for error bars can also be given. By appending _data, _serr, _perr or _nerr to the name of the dataset in the expression, the user can base their expression on particular parts of the given dataset (the main data, symmetric errors, positive errors or negative errors). Otherwise the program uses the same parts as is currently being specified. If a dataset name contains non alphanumeric characters, its name should be quoted in the expression in back-tick characters, e.g. `length (cm)`*2. The numpy functionality is particularly useful for doing more complicated expressions. For instance, a conditional expression can be written as where(x<y,x,y) or where(isfinite(x),a,b)). You often do not need to create a new dataset when. For example, with the xy point plotter widget, you can directly enter an expression as the X and Y dataset settings. When you give a direct dataset expression, you can define error bar expressions by separating them by commas, and optionally surrounding them by brackets. For example (a,0.1) plots dataset a as the data, with symmetric errors bars of 0.1. Asymmetric bars are given as (a,a*0.1,-a*0.1).
Linking datasets to expressions A particularly useful feature is to be able to link a dataset to an expression, so if the expression changes the dataset changes with it, like in a spreadsheet.
Splitting data Data can also be chopped in this method, for example using the expression x[10:20], which makes a dataset based on the 11th to 20th item in the x dataset (the ranges are Python syntax, and are zero-based). Negative indices count backwards from the end of the dataset. Data can be skipped using expressions such as data[::2], which skips every other element
Defining new constants or functions User defined constants or functions can be defined in the "Custom definitions" dialog box under the edit menu. Functions can also be imported from external python modules. Custom definitions are defined on a per-document basis, but can be saved or loaded into a file. A default custom definitions file can be set in the preferences dialog box.
Dataset plugins In addition to creating datasets based on expressions, a variety of dataset plugins exist, which make certain operations on datasets much more convenient. See the Data, Operations menu for a list of the default plugins. The user can easily create new plugins. See for details.
Capturing data In addition to the standard data import, data can be captured as it is created from an external program, a network socket or a file or named pipe. When capturing from a file, the behaviour is like the Unix tail -f command, where new lines written to the file are captured. To use the capturing facility, the data must be written in the simple line based standard Veusz text format. Data are whitespace separated, with one value per dataset given on a single line. To capture data, use the dialog box DataCapture. A list of datasets should be given. This is the standard descriptor format. Choose the source of the data, which is either a a filename or named pipe, a network socket to connect to, or a command line for an external program. Capturing ends if the source of the data runs out (for external programs or network sockets) or the finish button is clicked. It can optionally end after a certain number of data lines or when a time period has expired. Normally the data are updated in Veusz when the capturing is finished. There is an option to update the document at intervals, which is useful for monitoring. A plot using the variables will update when the data are updated. Click the Capture button to start the capture. Click Finish or Cancel to stop. Cancelling destroys captured data.
Command line interface
Introduction An alternative way to control Veusz is via its command line interface. As Veusz is a a Python application it uses Python as its scripting language. Therefore you can freely mix Veusz and Python commands on the command line. Veusz can also read in Python scripts from files (see the Load command). When commands are entered in the command prompt in the Veusz window, Veusz supports a simplified command syntax, where brackets following commands names, and commas, can replaced by spaces in Veusz commands (not Python commands). For example, Add('graph', name='foo'), may be entered as Add 'graph' name='foo'. The numpy package is already imported into the command line interface (as "*"), so you do not need to import it first. The command prompt supports history (use the up and down cursor keys to recall previous commands). Most of the commands listed below can be used in the in-program command line interface, using the embedding interface or using veusz_listen. Commands specific to particular modes are documented as such. Veusz also includes a new object-oriented version of the interface, which is documented at .
Commands We list the allowed set of commands below
Action Action('actionname', componentpath='.') Initiates the specified action on the widget (component) given the action name. Actions perform certain automated routines. These include "fit" on a fit widget, and "zeroMargins" on grids.
Add Add('widgettype', name='nameforwidget', autoadd=True, optionalargs) The Add command adds a graph into the current widget (See the To command to change the current position). The first argument is the type of widget to add. These include "graph", "page", "axis", "xy" and "grid". name is the name of the new widget (if not given, it will be generated from the type of the widget plus a number). The autoadd parameter if set, constructs the default sub-widgets this widget has (for example, axes in a graph). Optionally, default values for the graph settings may be given, for example Add('axis', name='y', direction='vertical'). Subsettings may be set by using double underscores, for example Add('xy', MarkerFill__color='red', ErrorBarLine__hide=True). Returns: Name of widget added.
AddCustom AddCustom(type, name, value) Add a custom definition for evaluation of expressions. This can define a constant (can be in terms of other constants), a function of 1 or more variables, or a function imported from an external python module. ctype is "constant", "function" or "import". name is name of constant, or "function(x, y, ...)" or module name. val is definition for constant or function (both are _strings_), or is a list of symbols for a module (comma separated items in a string). If mode is 'appendalways', the custom value is appended to the end of the list even if there is one with the same name. If mode is 'replace', it replaces any existing definition in the same place in the list or is appended otherwise. If mode is 'append', then an existing definition is deleted, and the new one appended to the end.
AddImportPath AddImportPath(directory) Add a directory to the list of directories to try to import data from.
CloneWidget CloneWidget(widget, newparent, newname=None) Clone the widget given, placing the copy in newparent and the name given. newname is an optional new name to give it Returns new widget path.
Close Close() Closes the plotwindow. This is only supported in embedded mode.
CreateHistogram CreateHistogram(inexpr, outbinsds, outvalsds, binparams=None, binmanual=None, method='counts', cumulative = 'none', errors=False) Histogram an input expression. inexpr is input expression. outbinds is the name of the dataset to create giving bin positions. outvalsds is name of dataset for bin values. binparams is None or (numbins, minval, maxval, islogbins). binmanual is None or a list of bin values. method is 'counts', 'density', or 'fractions'. cumulative is to calculate cumulative distributions which is 'none', 'smalltolarge' or 'largetosmall'. errors is to calculate Poisson error bars.
DatasetPlugin DatasetPlugin(pluginname, fields, datasetnames={})> Use a dataset plugin. pluginname: name of plugin to use fields: dict of input values to plugin datasetnames: dict mapping old names to new names of datasets if they are renamed. The new name None means dataset is deleted
EnableToolbar EnableToolbar(enable=True) Enable/disable the zooming toolbar in the plotwindow. This command is only supported in embedded mode or from veusz_listen.
Export Export(filename, color=True, page=0 dpi=100, antialias=True, quality=85, backcolor='#ffffff00', pdfdpi=150, svgtextastext=False) Export the page given to the filename given. The filename must end with the correct extension to get the right sort of output file. Currrenly supported extensions are '.eps', '.pdf', '.svg', '.jpg', '.jpeg', '.bmp' and '.png'. If color is True, then the output is in colour, else greyscale. page is the page number of the document to export (starting from 0 for the first page!). dpi is the number of dots per inch for bitmap output files. antialias - antialiases output if True. quality is a quality parameter for jpeg output. backcolor is the background color for bitmap files, which is a name or a #RRGGBBAA value (red, green, blue, alpha). pdfdpi is the dpi to use when exporting EPS or PDF files. svgtextastext says whether to export SVG text as text, rather than curves.
ForceUpdate ForceUpdate() Force the window to be updated to reflect the current state of the document. Often used when periodic updates have been disabled (see SetUpdateInterval). This command is only supported in embedded mode or from veusz_listen.
Get Get('settingpath') Returns: The value of the setting given by the path. >>> Get('/page1/graph1/x/min') 'Auto'
GetChildren GetChildren(where='.') Returns: The names of the widgets which are children of the path given
GetClick GetClick() Waits for the user to click on a graph and returns the position of the click on appropriate axes. Command only works in embedded mode. Returns: A list containing tuples of the form (axispath, val) for each axis for which the click was in range. The value is the value on the axis for the click.
GetData GetData(name) Returns: For a 1D dataset, a tuple containing the dataset with the name given. The value is (data, symerr, negerr, poserr), with each a numpy array of the same size or None. data are the values of the dataset, symerr are the symmetric errors (if set), negerr and poserr and negative and positive asymmetric errors (if set). If a text dataset, return a list of text elements. If the dataset is a date-time dataset, return a list of Python datetime objects. If the dataset is a 2D dataset return the tuple (data, rangex, rangey), where data is a 2D numpy array and rangex/y are tuples giving the range of the x and y coordinates of the data. data = GetData('x') SetData('x', data[0]*0.1, *data[1:])
GetDataType GetDataType(name) Get type of dataset with name given. Returns '1d' for a 1d dataset, '2d' for a 2d dataset, 'text' for a text dataset and 'datetime' for a datetime dataset.
GetDatasets GetDatasets() Returns: The names of the datasets in the current document.
GPL GPL() Print out the GNU Public Licence, which Veusz is licenced under.
ImportFile ImportFile('filename', 'descriptor', linked=False, prefix='', suffix='', encoding='utf_8', renames={}) Imports data from a file. The arguments are the filename to load data from and the descriptor. The format of the descriptor is a list of variable names representing the columns of the data. For more information see Descriptors. If the linked parameter is set to True, if the document is saved, the data imported will not be saved with the document, but will be reread from the filename given the next time the document is opened. The linked parameter is optional. If prefix and/or suffix are set, then the prefix and suffix are added to each dataset name. If set, renames maps imported dataset names to final dataset names after import. Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset. Changed in version 0.5: A tuple is returned rather than just the number of imported variables.
ImportFile2D ImportFile2D('filename', datasets, xrange=(a,b), yrange=(c,d), invertrows=True/False, invertcols=True/False, transpose=True/False, prefix='', suffix='', linked=False, encoding='utf8', renames={}) Imports two-dimensional data from a file. The required arguments are the filename to load data from and the dataset name, or a list of names to use. filename is a string which contains the filename to use. datasets is either a string (for a single dataset), or a list of strings (for multiple datasets). The xrange parameter is a tuple which contains the range of the X-axis along the two-dimensional dataset, for example (-1., 1.) represents an inclusive range of -1 to 1. The yrange parameter specifies the range of the Y-axis similarly. If they are not specified, (0, N) is the default, where N is the number of datapoints along a particular axis. invertrows and invertcols if set to True, invert the rows and columns respectively after they are read by Veusz. transpose swaps the rows and columns. If prefix and/or suffix are set, they are prepended or appended to imported dataset names. If set, renames maps imported dataset names to final dataset names after import. If the linked parameter is True, then the datasets are linked to the imported file, and are not saved within a saved document. The file format this command accepts is a two-dimensional matrix of numbers, with the columns separated by spaces or tabs, and the rows separated by new lines. The X-coordinate is taken to be in the direction of the columns. Comments are supported (use "#", "!" or "%"), as are continuation characters ("\"). Separate datasets are deliminated by using blank lines. In addition to the matrix of numbers, the various optional parameters this command takes can also be specified in the data file. These commands should be given on separate lines before the matrix of numbers. They are: xrange A B yrange C D invertrows invertcols transpose
ImportFileCSV ImportFileCSV('filename', readrows=False, dsprefix='', dssuffix='', linked=False, encoding='utf_8', renames={}) This command imports data from a CSV format file. Data are read from the file using the dataset names given at the top of the files in columns. Please see the reading data section of this manual for more information. dsprefix is prepended to each dataset name and dssuffix is added (the prefix option is deprecated and also addeds an underscore to the dataset name). linked specifies whether the data will be linked to the file. renames, if set, provides new names for datasets after import.
ImportFileHDF5 ImportFileHDF5(filename, items, namemap={}, slices={}, twodranges={}, twod_as_oned=set([]), convert_datetime={}, prefix='', suffix='', renames={}, linked=False) Import data from a HDF5 file. items is a list of groups and datasets which can be imported. If a group is imported, all child datasets are imported. namemap maps an input dataset to a veusz dataset name. Special suffixes can be used on the veusz dataset name to indicate that the dataset should be imported specially. 'foo (+)': import as +ve error for dataset foo 'foo (-)': import as -ve error for dataset foo 'foo (+-)': import as symmetric error for dataset foo slices is an optional dict specifying slices to be selected when importing. For each dataset to be sliced, provide a tuple of values, one for each dimension. The values should be a single integer to select that index, or a tuple (start, stop, step), where the entries are integers or None. twodranges is an optional dict giving data ranges for 2d datasets. It maps names to (minx, miny, maxx, maxy). twod_as_oned: optional set containing 2d datasets to attempt to read as 1d convert_datetime should be a dict mapping hdf name to specify date/time importing. For a 1d numeric dataset: if this is set to 'veusz', this is the number of seconds since 2009-01-01, if this is set to 'unix', this is the number of seconds since 1970-01-01. For a text dataset, this should give the format of the date/time, e.g. 'YYYY-MM-DD|T|hh:mm:ss' or 'iso' for iso format. renames is a dict mapping old to new dataset names, to be renamed after importing. linked specifies that the dataset is linked to the file. Attributes can be used in datasets to override defaults: 'vsz_name': set to override name for dataset in veusz 'vsz_slice': slice on importing (use format "start:stop:step,...") 'vsz_range': should be 4 item array to specify x and y ranges: [minx, miny, maxx, maxy] 'vsz_twod_as_oned': treat 2d dataset as 1d dataset with errors 'vsz_convert_datetime': treat as date/time, set to one of the values above. For compound datasets these attributes can be given on a per-column basis using attribute names vsz_attributename_columnname. Returns: list of imported datasets
ImportFilePlugin ImportFilePlugin('pluginname', 'filename', **pluginargs, linked=False, encoding='utf_8', prefix='', suffix='', renames={}) Import data from file using import plugin 'pluginname'. The arguments to the plugin are given, plus optionally a text encoding, and prefix and suffix to prepend or append to dataset names. renames, if set, provides new names for datasets after import.
ImportFITSFile ImportFITSFile(datasetname, filename, hdu, datacol='A', symerrcol='B', poserrcol='C', negerrcol='D', linked=True/False, renames={}) This command does a simple import from a FITS file. The FITS format is used within the astronomical community to transport binary data. For a more powerful FITS interface, you can use PyFITS within your scripts. The datasetname is the name of the dataset to import, the filename is the name of the FITS file to import from. The hdu parameter specifies the HDU to import data from (numerical or a name). If the HDU specified is a primary HDU or image extension, then a two-dimensional dataset is loaded from the file. The optional parameters (other than linked) are ignored. Any WCS information within the HDU are used to provide a suitable xrange and yrange. If the HDU is a table, then the datacol parameter must be specified (and optionally symerrcol, poserrcol and negerrcol). The dataset is read in from the named column in the table. Any errors are read in from the other specified columns. If linked is True, then the dataset is not saved with a saved document, but is reread from the data file each time the document is loaded. renames, if set, provides new names for datasets after import.
ImportString ImportString('descriptor', 'data') Like, ImportFile, but loads the data from the specfied string rather than a file. This allows data to be easily embedded within a document. The data string is usually a multi-line Python string. Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset. Changed in version 0.5: A tuple is returned rather than just the number of imported variables. ImportString('x y', ''' 1 2 2 5 3 10 ''')
ImportString2D ImportString2D(datasets, string) Imports a two-dimensional dataset from the string given. This is similar to the ImportFile2D command, with the same dataset format within the string. This command, however, does not currently take any optional parameters. The various controlling parameters can be set within the string. See the ImportFile2D section for details.
IsClosed IsClosed() Returns a boolean value telling the caller whether the plotting window has been closed. Note: this command is only supported in the embedding interface.
List List(where='.') List the widgets which are contained within the widget with the path given, the type of widgets, and a brief description.
Load Load('filename.vsz') Loads the veusz script file given. The script file can be any Python code. The code is executed using the Veusz interpreter. Note: this command is only supported at the command line and not in a script. Scripts may use the python execfile function instead.
MoveToPage MoveToPage(pagenum) Updates window to show the page number given of the document. Note: this command is only supported in the embedding interface or veusz_listen.
ReloadData ReloadData() Reload any datasets which have been linked to files. Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset.
Rename Remove('widgetpath', 'newname') Rename the widget at the path given to a new name. This command does not move widgets. See To for a description of the path syntax. '.' can be used to select the current widget.
Remove Remove('widgetpath') Remove the widget selected using the path. See To for a description of the path syntax.
ResizeWindow ResizeWindow(width, height) Resizes window to be width by height pixels. Note: this command is only supported in the embedding interface or veusz_listen.
Save Save('filename.vsz') Save the current document under the filename given.
Set Set('settingpath', val) Set the setting given by the path to the value given. If the type of val is incorrect, an InvalidType exception is thrown. The path to the setting is the optional path to the widget the setting is contained within, an optional subsetting specifier, and the setting itself. Set('page1/graph1/x/min', -10.)
SetAntiAliasing SetAntiAliasing(on) Enable or disable anti aliasing in the plot window, replotting the image.
SetData SetData(name, val, symerr=None, negerr=None, poserr=None) Set the dataset name with the values given. If None is given for an item, it will be left blank. val is the actual data, symerr are the symmetric errors, negerr and poserr and the getative and positive asymmetric errors. The data can be given as lists or numpys.
SetDataExpression SetDataExpression(name, val, symerr=None, negerr=None, poserr=None, linked=False, parametric=None) Create a new dataset based on the expressions given. The expressions are Python syntax expressions based on existing datasets. If linked is True, the dataset will change as the datasets in the expressions change. Parametric can be set to a tuple of (minval, maxval, numitems). t in the expression will iterate from minval to maxval in numitems values.
SetDataRange SetDataRange(name, numsteps, val, symerr=None, negerr=None, poserr=None, linked=False) Set dataset to be a range of values with numsteps steps. val is tuple made up of (minimum value, maximum value). symerr, negerr and poserr are optional tuples for the error bars. If linked is True, the dataset can be saved in a document as a SetDataRange, otherwise it is expanded to the values which would make it up.
SetData2D SetData2D('name', val, xrange=(A,B), yrange=(C,D), xgrid=[1,2,3...], ygrid=[4,5,6...]) Creates a two-dimensional dataset with the name given. val is either a two-dimensional numpy array, or is a list of lists, with each list in the list representing a row. Do not give xrange if xgrid is set and do not give yrange if ygrid is set, and vice versa. xrange and yrange are optional tuples giving the inclusive range of the X and Y coordinates of the data. xgrid and ygrid are optional lists, tuples or arrays which give the coordinates of the edges of the pixels. There should be one more item in each array than pixels.
SetData2DExpression SetData2DExpression('name', expr, linked=False) Create a 2D dataset based on expressions. name is the new dataset name expr is an expression which should return a 2D array linked specifies whether to permanently link the dataset to the expressions.
SetData2DExpressionXYZ SetData2DExpressionXYZ('name', 'xexpr', 'yexpr', 'zexpr', linked=False) Create a 2D dataset based on three 1D expressions. The x, y expressions need to evaluate to a grid of x, y points, with the z expression as the 2D value at that point. Currently only linear fixed grids are supported. This function is intended to convert calculations or measurements at fixed points into a 2D dataset easily. Missing values are filled with NaN.
SetData2DXYFunc SetData2DXYFunc('name', xstep, ystep, 'expr', linked=False) Construct a 2D dataset using a mathematical expression of "x" and "y". The x values are specified as (min, max, step) in xstep as a tuple, the y values similarly. If linked remains as False, then a real 2D dataset is created, where values can be modified and the data are stored in the saved file.
SetDataDateTime SetDataDateTime('name', vals) Creates a datetime dataset of name given. vals is a list of Python datetime objects.
SetDataText SetDataText(name, val) Set the text dataset name with the values given. val must be a type that can be converted into a Python list. SetDataText('mylabel', ['oranges', 'apples', 'pears', 'spam'])
SetToReference SetToReference(setting, refval) Set setting to match other setting refval always..
SetUpdateInterval SetUpdateInterval(interval) Tells window to update every interval milliseconds at most. The value 0 disables updates until this function is called with a non-zero. The value -1 tells Veusz to update the window every time the document has changed. This will make things slow if repeated changes are made to the document. Disabling updates and using the ForceUpdate command will allow the user to control updates directly. Note: this command is only supported in the embedding interface or veusz_listen.
SetVerbose SetVerbose(v=True) If v is True, then extra information is printed out by commands.
StartSecondView StartSecondView(name = 'window title') In the embedding interface, this method will open a new Embedding interface onto the same document, returning the object. This new window provides a second view onto the document. It can, for instance, show a different page to the primary view. name is a window title for the new window. Note: this command is only supported in the embedding interface.
TagDatasets TagDatasets('tag', ['ds1', 'ds2'...]) Adds the tag to the list of datasets given..
To To('widgetpath') The To command takes a path to a widget and moves to that widget. For example, this may be "/", the root widget, "graph1", "/page1/graph1/x", "../x". The syntax is designed to mimic Unix paths for files. "/" represents the base widget (where the pages reside), and ".." represents the widget next up the tree.
Quit Quit() Quits Veusz. This is only supported in veusz_listen.
WaitForClose WaitForClose() Wait until the plotting window has been closed. Note: this command is only supported in the embedding interface.
Zoom Zoom(factor) Sets the plot zoom factor, relative to a 1:1 scaling. factor can also be "width", "height" or "page", to zoom to the page width, height or page, respectively. This is only supported in embedded mode or veusz_listen.
Security With the 1.0 release of Veusz, input scripts and expressions are checked for possible security risks. Only a limited subset of Python functionality is allowed, or a dialog box is opened allowing the user to cancel the operation. Specifically you cannot import modules, get attributes of Python objects, access globals() or locals() or do any sort of file reading or manipulation. Basically anything which might break in Veusz or modify a system is not supported. In addition internal Veusz functions which can modify a system are also warned against, specifically Print(), Save() and Export(). If you are running your own scripts and do not want to be bothered by these dialogs, you can run veusz with the --unsafe-mode option.
Using Veusz from other programs
Non-Qt Python programs Veusz can be used as a Python module for plotting data. There are two ways to use the module: (1) with an older path-based Veusz commands, used in Veusz saved document files or (2) using an object-oriented interface. With the old style method the user uses a unix-path inspired API to navigate the widget tree and add or manipulate widgets. With the new style interface, the user navigates the tree with attributes of the Root object to access Nodes. The new interface is likely to be easier to use unless you are directly translating saved files.
Older path-based interface """An example embedding program. Veusz needs to be installed into the Python path for this to work (use setup.py) This animates a sin plot, then finishes """ import time import numpy import veusz.embed as veusz # construct a Veusz embedded window # many of these can be opened at any time g = veusz.Embedded('window title') g.EnableToolbar() # construct the plot g.To( g.Add('page') ) g.To( g.Add('graph') ) g.Add('xy', marker='tiehorz', MarkerFill__color='green') # this stops intelligent axis extending g.Set('x/autoExtend', False) g.Set('x/autoExtendZero', False) # zoom out g.Zoom(0.8) # loop, changing the values of the x and y datasets for i in range(10): x = numpy.arange(0+i/2., 7.+i/2., 0.05) y = numpy.sin(x) g.SetData('x', x) g.SetData('y', y) # wait to animate the graph time.sleep(2) # let the user see the final result print "Waiting for 10 seconds" time.sleep(10) print "Done!" # close the window (this is not strictly necessary) g.Close() The embed interface has the methods listed in the command line interface listed in the Veusz manual http://home.gna.org/veusz/docs/manual.html Multiple Windows are supported by creating more than one Embedded object. Other useful methods include: WaitForClose() - wait until window has closed GetClick() - return a list of (axis, value) tuples where the user clicks on a graph ResizeWndow(width, height) - resize window to be width x height pixels SetUpdateInterval(interval) - set update interval in ms or 0 to disable MoveToPage(page) - display page given (starting from 1) IsClosed() - has the page been closed Zoom(factor) - set zoom level (float) or 'page', 'width', 'height' Close() - close window SetAntiAliasing(enable) - enable or disable antialiasing EnableToolbar(enable=True) - enable plot toolbar StartSecondView(name='Veusz') - start a second view onto the document of the current Embedded object. Returns a new Embedded object.
New-style object interface In versions of Veusz >1.8 is a new style of object interface, which makes it easier to construct the widget tree. Each widget, group of settings or setting is stored as a Node object, or its subclass, in a tree. The root document widget can be accessed with the Root object. The dot operator "." finds children inside other nodes. In Veusz some widgets can contain other widgets (Root, pages, graphs, grids). Widgets contain setting nodes, accessed as attributes. Widgets can also contain groups of settings, again accessed as attributes. An example tree for a document (not complete) might look like this Root \-- page1 (page widget) \-- graph1 (graph widget) \-- x (axis widget) \-- y (axis widget) \-- function (function widget) \-- grid1 (grid widget) \-- graph2 (graph widget) \-- xy1 (xy widget) \-- xData (setting) \-- yData (setting) \-- PlotLine (setting group) \-- width (setting) ... ... \-- x (axis widget) \-- y (axis widget) \-- graph3 (graph widget) \-- contour1 (contour widget) \-- x (axis widget) \-- y (axis widget) Here the user could access the xData setting node of the xy1 widget using Root.page1.graph2.xy1.xData. To actually read or modify the value of a setting, you should get or set the val property of the setting node. The line width could be changed like this graph = embed.Root.page1.graph2 graph.xy1.PlotLine.width.val = '2pt' For instance, this constructs a simple x-squared plot which changes to x-cubed: import veusz.embed as veusz import time # open a new window and return a new Embedded object embed = veusz.Embedded('window title') # make a new page, but adding a page widget to the root widget page = embed.Root.Add('page') # add a new graph widget to the page graph = page.Add('graph') # add a function widget to the graph. The Add() method can take a list of settings # to set after widget creation. Here, "function='x**2'" is equivalent to # function.function.val = 'x**2' function = graph.Add('function', function='x**2') time.sleep(2) function.function.val = 'x**3' # this is the same if the widgets have the default names Root.page1.graph1.function1.function.val = 'x**3' If the document contains a page called "page1" then Root.page1 is the object representing the page. Similarly, Root.page1.graph1 is a graph called graph1 in the page. You can also use dictionary-style indexing to get child widgets, e.g. Root['page1']['graph1']. This style is easier to use if the names of widgets contain spaces or if widget names shadow methods or properties of the Node object, i.e. if you do not control the widget names. Widget nodes can contain as children other widgets, groups of settings, or settings. Groups of settings can contain child settings. Settings cannot contain other nodes. Here are the useful operations of Nodes: class Node(object): """properties: path - return path to object in document, e.g. /page1/graph1/function1 type - type of node: "widget", "settinggroup" or "setting" name - name of this node, e.g. "graph1" children - a generator to return all the child Nodes of this Node, e.g. for c in Root.children: print c.path children_widgets - generator to return child widget Nodes of this Node children_settinggroups - generator for child setting groups of this Node children_settings - a generator to get the child settings childnames - return a list of the names of the children of this Node childnames_widgets - return a list of the names of the child widgets childnames_settinggroups - return a list of the names of the setting groups childnames_settings - return a list of the names of the settings parent - return the Node corresponding to the parent widget of this Node __getattr__ - get a child Node with name given, e.g. Root.page1 __getitem__ - get a child Node with name given, e.g. Root['page1'] """ def fromPath(self, path): """Returns a new Node corresponding to the path given, e.g. '/page1/graph1'""" class SettingNode(Node): """A node which corresponds to a setting. Extra properties: val - get or set the setting value corresponding to this value, e.g. Root.page1.graph1.leftMargin.val = '2cm' """ class SettingGroupNode(Node): """A node corresponding to a setting group. No extra properties.""" class WidgetNode(Node): """A node corresponding to a widget. property: widgettype - get Veusz type of widget Methods are below.""" def WalkWidgets(self, widgettype=None): """Generator to walk widget tree and get widgets below this WidgetNode of type given. widgettype is a Veusz widget type name or None to get all widgets.""" def Add(self, widgettype, *args, **args_opt): """Add a widget of the type given, returning the Node instance. """ def Rename(self, newname): """Renames widget to name given. Existing Nodes corresponding to children are no longer valid.""" def Action(self, action): """Applies action on widget.""" def Remove(self): """Removes a widget and its children. Existing Nodes corresponding to children are no longer valid.""" Note that Nodes are temporary objects which are created on the fly. A real widget in Veusz can have several different WidgetNode objects. The operators == and != can test whether a Node points to the same widget, setting or setting group. Here is an example to set all functions in the document to be x**2: for n in Root.WalkWidgets(widgettype='function'): n.function.val = 'x**2'
Translating old to new style Here is an example how you might translate the old to new style interface (this is taken from the sin.vsz example). # old (from saved document file) Add('page', name='page1') To('page1') Add('graph', name='graph1', autoadd=False) To('graph1') Add('axis', name='x') To('x') Set('label', '\\italic{x}') To('..') Add('axis', name='y') To('y') Set('label', 'sin \\italic{x}') Set('direction', 'vertical') To('..') Add('xy', name='xy1') To('xy1') Set('MarkerFill/color', 'cyan') To('..') Add('function', name='function1') To('function1') Set('function', 'sin(x)') Set('Line/color', 'red') To('..') To('..') To('..') # new (in python) import veusz.embed embed = veusz.embed.Embedded('window title') page = embed.Root.Add('page') # note: autoAdd=False stops graph automatically adding own axes (used in saved files) graph = page.Add('graph', autoadd=False) x = graph.Add('axis', name='x') x.label.val = '\\italic{x}' y = graph.Add('axis', name='y') y.direction.val = 'vertical' xy = graph.Add('xy') xy.MarkerFill.color.val = 'cyan' func = graph.Add('function') func.function.val = 'sin(x)' func.Line.color.val = 'red'
PyQt4 programs There is no direct PyQt4 interface. The standard embedding interface should work, however.
Non Python programs Support for non Python programs is available in a limited form. External programs may execute the veusz_listen executable or veusz_listen.py Python module. Veusz will read its input from the standard input, and write output to standard output. This is a full Python execution environment, and supports all the scripting commands mentioned in Commands, a Quit() command, the EnableToolbar() and the Zoom(factor) command listed above. Only one window is supported at once, but many veusz_listen programs may be started. veusz_listen may be used from the shell command line by doing something like: veusz_listen < in.vsz where in.vsz contains: To(Add('page') ) To(Add('graph') ) SetData('x', arange(20)) SetData('y', arange(20)**2) Add('xy') Zoom(0.5) Export("foo.eps") Quit() A program may interface with Veusz in this way by using the popen C Unix function, which allows a program to be started having control of its standard input and output. Veusz can then be controlled by writing commands to an input pipe.
C, C++ and Fortran A callable library interface to Veusz is on my todo list for C, C++ and Fortran. Please tell me if you would be interested in this option.
veusz-1.21.1/Documents/generate_manual.sh0000775000175000017500000000253012260623255016564 0ustar jssjss#!/usr/bin/env bash # Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## # generate the output manual files from the input docbook file # uses xmlto and fop # DO NOT EDIT THE OUTPUT FILES! # this is pretty ugly xmlto html-nochunks -m config.xsl manual.xml xmlto text manual.xml xmlto fo manual.xml fop manual.fo -pdf manual.pdf release=$(cat ../VERSION) pod2man --release=${release} --center="Veusz" veusz.pod > veusz.1 pod2man --release=${release} --center="Veusz" veusz_listen.pod > veusz_listen.1 veusz-1.21.1/Documents/veusz_listen.10000644000175000017500000001207012275421435015712 0ustar jssjss.\" Automatically generated by Pod::Man 2.25 (Pod::Simple 3.16) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is turned on, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .ie \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . nr % 0 . rr F .\} .el \{\ . de IX .. .\} .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "VEUSZ_LISTEN 1" .TH VEUSZ_LISTEN 1 "2014-01-09" "1.20.1" "Veusz" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" veusz_listen \- command\-line interface to the Veusz plotting application. .SH "SYNOPSIS" .IX Header "SYNOPSIS" veusz_listen [\fIWindowTitle\fR]... .SH "DESCRIPTION" .IX Header "DESCRIPTION" \&\fBVeusz\fR is a scientific plotting and graphing package. \fBveusz_listen\fR provides a command line interface to its scripting interface. .PP \&\fBveusz_listen\fR opens a new window (with an optional window title) and listens to stdin. It executes Veusz scripting commands, writing any output to stdout. .PP \&\fBveusz_listen\fR is now deprecated. Please use \fBveusz \-\-listen\fR instead. .SH "SEE ALSO" .IX Header "SEE ALSO" \&\fIveusz\fR\|(1) .SH "BUGS" .IX Header "BUGS" Please report bugs at https://gna.org/bugs/?group=veusz .SH "AUTHORS" .IX Header "AUTHORS" \&\fBVeusz\fR was written by Jeremy Sanders . .PP This manual page was written by Jeremy Sanders . .SH "COPYRIGHT" .IX Header "COPYRIGHT" Copyright (C) 2003\-2014 Jeremy Sanders . .PP This program is free software; you can redistribute it and/or modify it under the terms of the \s-1GNU\s0 General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. .PP On Debian GNU/Linux systems, the complete text of the \s-1GNU\s0 General Public License can be found in `/usr/share/common\-licenses/GPL'. veusz-1.21.1/Documents/veusz.10000644000175000017500000001527212275421435014343 0ustar jssjss.\" Automatically generated by Pod::Man 2.25 (Pod::Simple 3.16) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is turned on, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .ie \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . nr % 0 . rr F .\} .el \{\ . de IX .. .\} .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "VEUSZ 1" .TH VEUSZ 1 "2014-01-09" "1.20.1" "Veusz" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" Veusz \- a scientific plotting and graphing application. .SH "SYNOPSIS" .IX Header "SYNOPSIS" veusz [\fIoptions\fR] [\fIdocument.vsz\fR]... .SH "DESCRIPTION" .IX Header "DESCRIPTION" \&\fBVeusz\fR is a scientific plotting and graphing package. It is designed to create publication-ready output in a variety of different output formats. Graphs are built-up combining plotting widgets. Veusz has a \&\s-1GUI\s0 user interface (started with the \f(CW\*(C`veusz\*(C'\fR command), a Python module interface and a scripting interface. .PP If started without command line arguments, \fBVeusz\fR will open up with a new empty document. The program will otherwise open the listed documents. .SH "OPTIONS" .IX Header "OPTIONS" .IP "\fB\-\-unsafe\-mode\fR" 8 .IX Item "--unsafe-mode" Do not check opened scripts for the presence of unsafe Python commands. This allows you to create or open complete Python scripts with Veusz commands if they come from a trusted source. .IP "\fB\-\-listen\fR" 8 .IX Item "--listen" Read Veusz commands from stdin, executing them, then writing the results to stdout. This option is intended to replace the veusz_listen standalone program. .Sp In this mode Veusz does not read any input documents, but will use the first argument to the program as the window title, if given. .IP "\fB\-\-quiet\fR" 8 .IX Item "--quiet" If in listening mode, do not open a window before running commands, but execute them quietly. .IP "\fB\-\-export\fR=\fI\s-1FILE\s0\fR" 8 .IX Item "--export=FILE" Export the next Veusz document file on the command line to the graphics file \fI\s-1FILE\s0\fR. Supported file types include \s-1EPS\s0, \s-1PDF\s0, \s-1SVG\s0, \&\s-1PNG\s0, \s-1BMP\s0, \s-1JPG\s0 and \s-1XPM\s0. The extension of the output file is used to determine the output file format. There should be as many export options specified as input Veusz documents on the command line. .IP "\fB\-\-plugin\fR=\fI\s-1FILE\s0\fR" 8 .IX Item "--plugin=FILE" Loads the Veusz plugin \fI\s-1FILE\s0\fR when starting Veusz. This option provides a per-session alternative to adding the plugin in the preferences dialog box. .IP "\fB\-\-help\fR" 8 .IX Item "--help" Displays the options to the program and exits. .IP "\fB\-\-version\fR" 8 .IX Item "--version" Displays information about the currently installed version and exits. .SH "BUGS" .IX Header "BUGS" Please report bugs at https://gna.org/bugs/?group=veusz .SH "AUTHORS" .IX Header "AUTHORS" \&\fBVeusz\fR was written by Jeremy Sanders . .PP This manual page was written by Jeremy Sanders . .SH "COPYRIGHT" .IX Header "COPYRIGHT" Copyright (C) 2003\-2014 Jeremy Sanders . .PP This program is free software; you can redistribute it and/or modify it under the terms of the \s-1GNU\s0 General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. .PP On Debian GNU/Linux systems, the complete text of the \s-1GNU\s0 General Public License can be found in `/usr/share/common\-licenses/GPL'. veusz-1.21.1/Documents/veusz_listen.pod0000664000175000017500000000232212263564313016335 0ustar jssjss=head1 NAME veusz_listen - command-line interface to the B plotting application. =head1 SYNOPSIS veusz_listen [F]... =head1 DESCRIPTION B is a scientific plotting and graphing package. B provides a command line interface to its scripting interface. B opens a new window (with an optional window title) and listens to stdin. It executes Veusz scripting commands, writing any output to stdout. B is now deprecated. Please use B instead. =head1 SEE ALSO veusz(1) =head1 BUGS Please report bugs at https://gna.org/bugs/?group=veusz =head1 AUTHORS B was written by Jeremy Sanders . This manual page was written by Jeremy Sanders . =head1 COPYRIGHT Copyright (C) 2003-2014 Jeremy Sanders . 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, or (at your option) any later version. On Debian GNU/Linux systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL'. =cut veusz-1.21.1/Documents/manual.html0000644000175000017500000037673712275421417015270 0ustar jssjssVeusz - a scientific plotting package

Veusz - a scientific plotting package

Jeremy Sanders

This document is licensed under the GNU General Public License, version 2 or greater. Please see the file COPYING for details, or see http://www.gnu.org/licenses/gpl-2.0.html.


Table of Contents

1. Introduction
1. Veusz
2. Terminology
2.1. Widget
2.2. Settings: properties and formatting
2.3. Text
2.4. Measurements
2.5. Axis numeric scales
3. Installation
4. The main window
5. My first plot
2. Reading data
1. Standard text import
1.1. Data types in text import
1.2. Descriptors
2. CSV files
3. HDF5 files
3.1. Error bars
3.2. Slices
3.3. 2D data ranges
3.4. Dates
4. 2D text or CSV format
5. FITS files
6. Reading other data formats
7. Manipulating datasets
7.1. Using dataset plugins
7.2. Using expressions to create new datasets
7.3. Linking datasets to expressions
7.4. Splitting data
7.5. Defining new constants or functions
7.6. Dataset plugins
8. Capturing data
3. Command line interface
1. Introduction
2. Commands
2.1. Action
2.2. Add
2.3. AddCustom
2.4. AddImportPath
2.5. CloneWidget
2.6. Close
2.7. CreateHistogram
2.8. DatasetPlugin
2.9. EnableToolbar
2.10. Export
2.11. ForceUpdate
2.12. Get
2.13. GetChildren
2.14. GetClick
2.15. GetData
2.16. GetDataType
2.17. GetDatasets
2.18. GPL
2.19. ImportFile
2.20. ImportFile2D
2.21. ImportFileCSV
2.22. ImportFileHDF5
2.23. ImportFilePlugin
2.24. ImportFITSFile
2.25. ImportString
2.26. ImportString2D
2.27. IsClosed
2.28. List
2.29. Load
2.30. MoveToPage
2.31. ReloadData
2.32. Rename
2.33. Remove
2.34. ResizeWindow
2.35. Save
2.36. Set
2.37. SetAntiAliasing
2.38. SetData
2.39. SetDataExpression
2.40. SetDataRange
2.41. SetData2D
2.42. SetData2DExpression
2.43. SetData2DExpressionXYZ
2.44. SetData2DXYFunc
2.45. SetDataDateTime
2.46. SetDataText
2.47. SetToReference
2.48. SetUpdateInterval
2.49. SetVerbose
2.50. StartSecondView
2.51. TagDatasets
2.52. To
2.53. Quit
2.54. WaitForClose
2.55. Zoom
3. Security
4. Using Veusz from other programs
1. Non-Qt Python programs
1.1. Older path-based interface
1.2. New-style object interface
1.3. Translating old to new style
2. PyQt4 programs
3. Non Python programs
4. C, C++ and Fortran

Chapter1.Introduction

1.Veusz

Veusz is a scientific plotting package. It was designed to be easy to use, easily extensible, but powerful. The program features a graphical user interface, which works under Unix/Linux, Windows or Mac OS X. It can also be easily scripted (the saved file formats are similar to Python scripts) or used as module inside Python. Veusz reads data from a number of different types of data file, it can be manually entered, or constructed from other datasets.

In Veusz the document is built in an object-oriented fashion, where a document is built up by a number of widgets in a hierarchy. For example, multiple function or xy widgets can be placed inside a graph widget, and many graphs can be placed in a grid widget.

The technologies behind Veusz include PyQt (a very easy to use Python interface to Qt, which is used for rendering and the graphical user interface, GUI) and numpy (a package for Python which makes the handling of large datasets easy). Veusz can be extended by the user easily by adding plugins. Support for different data file types can be added with import plugins. Dataset plugins automate the manipulation of datasets. Tools plugins automate the manipulation of the document.

2.Terminology

Here we define some terminology for future use.

2.1.Widget

A document and its graphs are built up from widgets. These widgets can often by placed within each other, depending on the type of the widget. A widget has children (those widgets placed within it) and its parent. The widgets have a number of different settings which modify their behaviour. These settings are divided into properties, which affect what is plotted and how it is plotted. These would include the dataset being plotted or whether an axis is logarithmic. There are also formatting settings, including the font to be used and the line thickness. In addition they have actions, which perform some sort of activity on the widget or its children, like "fit" for a fit widget.

As an aside, using the scripting interface, widgets are specified with a "path", like a file in Unix or Windows. These can be relative to the current widget (do not start with a slash), or absolute (do not start with a slash). Examples of paths include, "/page1/graph1/x", "x" and ".".

The widget types include

  1. document - representing a complete document. A document can contain pages. In addition it contains a setting giving the page size for the document.

  2. page - representing a page in a document. One or more graphs can be placed on a page, or a grid.

  3. graph - defining an actual graph. A graph can be placed on a page or within a grid. Contained within the graph are its axes and plotters. A graph can be given a background fill and a border if required. It also has a margin, which specifies how far away from the edge of its parent widget to plot the body of the graph.

    A graph can contain several axes, at any position on the plot. In addition a graph can use axes defined in parent widgets, shared with other graphs.

    More than one graph can be placed within in a page. The margins can be adjusted so that they lie within or besides each other.

  4. grid - containing one or more graphs. A grid plots graphs in a gridlike fashion. You can specify the number of rows and columns, and the plots are automatically replotted in the chosen arrangement. A grid can contain graphs or axes. If an axis is placed in a grid, it can be shared by the graphs in the grid.

  5. axis - giving the scale for plotting data. An axis translates the coordinates of the data to the screen. An axis can be linear or logarithmic, it can have fixed endpoints, or can automatically get them from the plotted data. It also has settings for the axis labels and lines, tick labels, and major and minor tick marks.

    An axis may be "horizontal" or "vertical" and can appear anywhere on its parent graph or grid.

    If an axis appears within a grid, then it can be shared by all the graphs which are contained within the grid.

    The axis-broken widget is an axis sub-type. It is an axis type where there are jumps in the scale of the axis.

    The axis-function widget allows the user to create an axis where the values are scaled by a monotonic function, allowing non-linear and non-logarithmic axis scales. The widget can also be linked to a different axis via the function.

  6. plotters - types of widgets which plot data or add other things on a graph. There is no actual plotter widget which can be added, but several types of plotters listed below. Plotters typically take an axis as a setting, which is the axis used to plot the data on the graph (default x and y).

    1. function - a plotter which plots a function on the graph. Functions can be functions of x or y (parametric functions are not done yet!), and are defined in Python expression syntax, which is very close to most other languages. For example "3*x**2 + 2*x - 4". A number of functions are available (e.g. sin, cos, tan, exp, log...). Technically, Veusz imports the numpy package when evaluating, so numpy functions are available.

      As well as the function setting, also settable is the line type to plot the function, and the number of steps to evaluate the function when plotting. Filling is supported above/below/left/right of the function.

    2. xy - a plotter which plots scatter, line, or stepped plots. This versatile plotter takes an x and y dataset, and plots (optional) points, in a chosen marker and colour, connecting them with (optional) lines, and plotting (optional) error bars. An xy plotter can also plot a stepped line, allowing histograms to be plotted (note that it doesn't yet do the binning of the data).

      The settings for the xy widget are the various attibutes for the points, line and error bars, the datasets to plot, and the axes to plot on.

      The xy plotter can plot a label next to each dataset, which is either the same for each point or taken from a text dataset.

      If you wish to leave gaps in a plot, the input value "nan" can be specified in the numeric dataset.

    3. fit - fit a function to data. This plotter is a like the function plotter, but allows fitting of the function to data. This is achived by clicking on a "fit" button, or using the "fit" action of the widget. The fitter takes a function to fit containing the unknowns, e.g. "a*x**2 + b*x + c", and initial values for the variables (here a, b and c). It then fits the data (note that at the moment, the fit plotter fits all the data, not just the data that can be seen on the graph) by minimising the chi-squared.

      In order to fit properly, the y data (or x, if fitting as a function of x) must have a properly defined, preferably symmetric error. If there is none, Veusz assumes the same fractional error everywhere, or symmetrises asymmetric errors.

      Note that more work is required in this widget, as if a parameter is not well defined by the data, the matrix inversion in the fit will fail. In addition Veusz does not supply estimates for the errors or the final chi-squared in a machine readable way.

      If the fitting parameters vary significantly from 1, then it is worth "normalizing" them by adding in a factor in the fit equation to bring them to of the order of 1.

    4. bar - a bar chart which plots sets of data as horizontal or vertical bars. Multiple datasets are supported. In "grouped" mode the bars are placed side-by-side for each dataset. In "stacked" mode the bars are placed on top of each other (in the appropriate direction according to the sign of the dataset). Bars are placed on coordinates given, or in integer values from 1 upward if none are given. Error bars are plotted for each of the datasets.

      Different fill styles can be given for each dataset given. A separate key value can be given for each dataset.

    5. key - a box which describes the data plotted. If a key is added to a plot, the key looks for "key" settings of the other data plotted within a graph. If there any it builds up a box containing the symbol and line for the plotter, and the text in the "key" setting of the widget. This allows a key to be very easily added to a plot.

      The key may be placed in any of the corners of the plot, in the centre, or manually placed. Depending on the ordering of the widgets, the key will be placed behind or on top of the widget. The key can be filled and surrounded by a box, or not filled or surrounded.

    6. label - a text label places on a graph. The alignment can be adjusted and the font changed. The position of the label can be specified in fractional terms of the current graph, or using axis coordinates.

    7. rect, ellipse - these draw a rectangle or ellipse, respectively, of size and rotation given. These widgets can be placed directly on the page or on a graph. The centre can be given in axis coordinates or fractional coordinates.

    8. imagefile - draw an external graphs file on the graph or page, with size and rotation given. The centre can be given in axis coordinates or fractional coordinates.

    9. line - draw a line with optional arrowheads on the graph or page. One end can be given in axis coordinates or fractional coordinates.

    10. contour - plot contours of a 2D dataset on the graph. Contours are automatically calculated between the minimum and maximum values of the graph or chosen manually. The line style of the contours can be chosen individually and the region between contours can be filled with shading or color.

      2D datasets currently consist of a regular grid of values between minimum and maximum positions in x and y. They can be constructed from three 1D datasets of x, y and z if they form a regular x, y grid.

    11. image - plot a 2D dataset as a colored image. Different color schemes can be chosen. The scaling between the values and the image can be specified as linear, logarithmic, square-root or square.

    12. polygon - plot x and y points from datasets as a polygon. The polygon can be placed directly on the page or within a graph. Coordinates are either plotted using the axis or as fractions of the width and height of the containing widget.

    13. boxplot - plot distribution of points in a dataset.

    14. polar - plot polar data or functions. This is a non-orthogonal plot and is placed directly on the page rather than in a graph.

    15. ternary - plot data of three variables which add up to 100 per cent.This is a non-orthogonal plot and is placed directly on the page rather than in a graph.

2.2.Settings: properties and formatting

The various settings of the widgets come in a number of types, including integers (e.g. 10), floats (e.g. 3.14), dataset names ("mydata"), expressions ("x+y"), text ("hi there!"), distances (see above), options ("horizontal" or "vertical" for axes).

Veusz performs type checks on these parameters. If they are in the wrong format the control to edit the setting will turn red. In the command line, a TypeError exception is thrown.

In the GUI, the current page is replotted if a setting is changed when enter is pressed or the user moves to another setting.

The settings are split up into formatting settings, controlling the appearance of the plot, or properties, controlling what is plotted and how it is plotted.

Default settings, including the default font and line style, and the default settings for any graph widget, can be modified in the "Default styles" dialog box under the "Edit" menu. Default settings are set on a per-document basis, but can be saved into a separate file and loaded. A default default settings file can be given to use for new documents (set in the preferences dialog).

2.3.Text

Veusz understands a limited set of LaTeX-like formatting for text. There are some differences (for example, "10^23" puts the 2 and 3 into superscript), but it is fairly similar. You should also leave out the dollar signs. Veusz supports superscripts ("^"), subscripts ("_"), brackets for grouping attributes are "{" and "}".

Supported LaTeX symbols include: \AA, \Alpha, \Beta, \Chi, \Delta, \Epsilon, \Eta, \Gamma, \Iota, \Kappa, \Lambda, \Mu, \Nu, \Omega, \Omicron, \Phi, \Pi, \Psi, \Rho, \Sigma, \Tau, \Theta, \Upsilon, \Xi, \Zeta, \alpha, \approx, \ast, \asymp, \beta, \bowtie, \bullet, \cap, \chi, \circ, \cup, \dagger, \dashv, \ddagger, \deg, \delta, \diamond, \divide, \doteq, \downarrow, \epsilon, \equiv, \eta, \gamma, \ge, \gg, \in, \infty, \int, \iota, \kappa, \lambda, \le, \leftarrow, \lhd, \ll, \models, \mp, \mu, \neq, \ni, \nu, \odot, \omega, \omicron, \ominus, \oplus, \oslash, \otimes, \parallel, \perp, \phi, \pi, \pm, \prec, \preceq, \propto, \psi, \rhd, \rho, \rightarrow, \sigma, \sim, \simeq, \sqrt, \sqsubset, \sqsubseteq, \sqsupset, \sqsupseteq, \star, \stigma, \subset, \subseteq, \succ, \succeq, \supset, \supseteq, \tau, \theta, \times, \umid, \unlhd, \unrhd, \uparrow, \uplus, \upsilon, \vdash, \vee, \wedge, \xi, \zeta. Please request additional characters if they are required (and exist in the unicode character set). Special symbols can be included directly from a character map.

Other LaTeX commands are supported. "\\" breaks a line. This can be used for simple tables. For example "{a\\b} {c\\d}" shows "a c" over "b d". The command "\frac{a}{b}" shows a vertical fraction a/b.

Also supported are commands to change font. The command "\font{name}{text}" changes the font text is written in to name. This may be useful if a symbol is missing from the current font, e.g. "\font{symbol}{g}" should produce a gamma. You can increase, decrease, or set the size of the font with "\size{+2}{text}", "\size{-2}{text}", or "\size{20}{text}". Numbers are in points.

Various font attributes can be changed: for example, "\italic{some italic text}" (or use "\textit" or "\emph"), "\bold{some bold text}" (or use "\textbf") and "\underline{some underlined text}".

Example text could include "Area / \pi (10^{-23} cm^{-2})", or "\pi\bold{g}".

Veusz plots these symbols with Qt's unicode support. You can also include special characters directly, by copying and pasting from a character map application. If your current font does not contain these symbols then you may get a box character.

2.4.Measurements

Distances, widths and lengths in Veusz can be specified in a number of different ways. These include absolute distances specified in physical units, e.g. 1cm, 0.05m, 10mm, 5in and 10pt, and relative units, which are relative to the largest dimension of the page, including 5%, 1/20, 0.05.

2.5.Axis numeric scales

The way in which numbers are formatted in axis scales is chosen automatically. For standard numerical axes, values are shown with the "%Vg" formatting (see below). For date axes, an appropriate date formatting is used so that the interval shown is correct. A format can be given for an axis in the axis number formatting panel can be given to explicitly choose a format. Some examples are given in the drop down axis menu. Hold the mouse over the example for detail.

C-style number formatting is used with a few Veusz specific extensions. Text can be mixed with format specifiers, which start with a "%" sign. Examples of C-style formatting include: "%.2f" (decimal number with two decimal places, e.g. 2.01), "%.3e" (scientific formatting with three decimal places, e.g. 2.123e-02), "%g" (general formatting, switching between "%f" and "%e" as appropriate). See http://opengroup.org/onlinepubs/007908799/xsh/fprintf.html for details.

Veusz extensions include "%Ve", which is like "%e" except it displays scientific notation as written, e.g. 1.2x10^23, rather than 1.2e+23. "%Vg" switches between standard numbers and Veusz scientific notation for large and small numbers. "%VE" using engineering SI suffixes to represent large or small numbers (e.g. 1000 is 1k).

Veusz allows dates and times to be formatted using "%VDX" where "X" is one of the formatting characters for strftime (see http://opengroup.org/onlinepubs/007908799/xsh/strftime.html for details). These include "a" for an abbreviated weekday name, "A" for full weekday name, "b" for abbreviated month name, "B" for full month name, "c" date and time representaiton, "d" day of month 01..31, "H" hour as 00..23, "I" hour as 01..12, "j" as day of year 001..366, "m" as month 01..12, "M" minute as 00..59, "p" AM/PM, "S" second 00..61, "U" week number of year 00..53 (Sunday as first day of week), "w" weekday as decimal number 0..6, "W" week number of year (Monday as first day of week), "x" date representation, "X" time representation, "y" year without century 00..99 and "Y" year. "%VDVS" is a special Veusz addon format which shows seconds and fractions of seconds (e.g. 12.2).

3.Installation

Please look at the Installation notes (INSTALL) for details on installing Veusz.

4.The main window

You should see the main window when you run Veusz (you can just type the veusz command in Unix).

The Veusz window is split into several sections. At the top is the menu bar and tool bar. These work in the usual way to other applications. Sometimes options are disabled (greyed out) if they do not make sense to be used. If you hold your mouse over a button for a few seconds, you will usually get an explanation for what it does called a "tool tip".

Below the main toolbar is a second toolbar for constructing the graph by adding widgets (on the left), and some editing buttons. The add widget buttons add the request widget to the currently selected widget in the selection window. The widgets are arranged in a tree-like structure.

Below these toolbars and to the right is the plot window. This is where the current page of the current document is shown. You can adjust the size of the plot on the screen (the zoom factor) using the "View" menu or the zoom tool bar button (the magnifying glass). Initially you will not see a plot in the plot window, but you will see the Veusz logo. At the moment you cannot do much else with the window. In the future you will be able to click on items in the plot to modify them.

To the left of the plot window is the selection window, and the properties and formatting windows. The properties window lets you edit various aspects of the selected widget (such as the minimum and maximum values on an axis). Changing these values should update the plot. The formatting lets you modify the appearance of the selected widget. There are a series of tabs for choosing what aspect to modify.

The various windows can be "dragged" from the main window to "float" by themselves on the screen.

To the bottom of the window is the console. This window is not shown by default, but can be enabled in the View menu. The console is a Veusz and Python command line console. To read about the commands available see Commands. As this is a Python console, you can enter mathematical expressions (e.g. "1+2.0*cos(pi/4)") here and they will be evaluated when you press Enter. The usual special functions and the operators are supported. You can also assign results to variables (e.g. "a=1+2") for use later. The console also supports command history like many Unix shells. Press the up and down cursor keys to browse through the history. Command line completion is not available yet!

There also exists a dataset browsing window, by default to the right of the screen. This window allows you to view the datasets currently loaded, their dimensions and type. Hovering a mouse over the size of the dataset will give you a preview of the data.

5.My first plot

After opening Veusz, on the left of the main window, you will see a Document, containing a Page, which contains a Graph with its axes. The Graph is selected in the selection window. The toolbar above adds a new widget to the selected widget. If a widget cannot be added to a selected widget it is disabled. On opening a new document Veusz automatically adds a new Page and Graph (with axes) to the document.

You will see something like this:

Select the x axis which has been added to the document (click on "x" in the selection window). In the properties window you will see a variety of different properties you can modify. For instance you can enter a label for the axis by writing "Area (cm^{2})" in the box next to label and pressing enter. Veusz supports text in LaTeX-like form (without the dollar signs). Other important parameters is the "log" switch which switches between linear and logarithmic axes, and "min" and "max" which allow the user to specify the minimum and maximum values on the axes.

The formatting dialog lets you edit various aspects of the graph appearance. For instance the "Line" tab allows you to edit the line of the axis. Click on "Line", then you can then modify its colour. Enter "green" instead of "black" and press enter. Try making the axis label bold.

Now you can try plotting a function on the graph. If the graph, or its children are selected, you will then be able to click the "function" button at the top (a red curve on a graph). You will see a straight line (y=x) added to the plot. If you select "function1", you will be able to edit the functional form plotted and the style of its line. Change the function to "x**2" (x-squared).

We will now try plotting data on the graph. Go to your favourite text editor and save the following data as test.dat:

1     0.1   -0.12   1.1    0.1
2.05  0.12  -0.14   4.08   0.12
2.98  0.08  -0.1    2.9    0.11
4.02  0.04  -0.1    15.3   1.0

The first three columns are the x data to plot plus its asymmetric errors. The final two columns are the y data plus its symmetric errors. In Veusz, go to the "Data" menu and select "Import". Type the filename into the filename box, or use the "Browse..." button to search for the file. You will see a preview of the data pop up in the box below. Enter "x,+,- y,+-" into the descriptors edit box (note that commas and spaces in the descriptor are almost interchangeable in Veusz 1.6 or newer). This describes the format of the data which describes dataset "x" plus its asymmetric errors, and "y" with its symmetric errors. If you now click "Import", you will see it has imported datasets "x" and "y".

To plot the data you should now click on "graph1" in the tree window. You are now able to click on the "xy" button (which looks like points plotted on a graph). You will see your data plotted on the graph. Veusz plots datasets "x" and "y" by default, but you can change these in the properties of the "xy" plotter.

You are able to choose from a variety of markers to plot. You can remove the plot line by choosing the "Plot Line" subsetting, and clicking on the "hide" option. You can change the colour of the marker by going to the "Marker Fill" subsetting, and entering a new colour (e.g. red), into the colour property.

Chapter2.Reading data

Currently Veusz supports reading data from files with text, CSV, HDF5, FITS, 2D text or CSV, QDP, binary and NPY/NPZ formats. Use the DataImport dialog to read data, or the importing commands in the API can be used. In addition, the user can load or write import plugins in Python which load data into Veusz in an arbitrary format. At the moment QDP, binary and NPY/NPZ files are supported with this method. The HDF5 file format is the most sophisticated, and is recommended for complex datasets.

By default, data are "linked" to the file imported from. This means that the data are not stored in the Veusz saved file and are reloaded from the original data file when opening. In addition, the user can use the DataReload menu option to reload data from linked files. Unselect the linked option when importing to remove the association with the data file and to store the data in the Veusz saved document.

Note that a prefix and suffix can be given when importing. These are added to the front or back of each dataset name imported. They are convenient for grouping data together.

We list the various types of import below.

1.Standard text import

The default text import operates on simple text files. The data are assumed to be in columns separated by whitespace. Each column corresponds to dataset (or its error bars). Each row is an entry in the dataset.

The way the data are read is goverened by a simple "descriptor". This can simply be a list of dataset names separated by spaces. If no descriptor is given, the columns are treated as separate datasets and are given names col1, col2, etc. Veusz attempts to automatically determine the type of the data.

When reading in data, Veusz treats any whitespace as separating columns. The columns do not actually need to be aligned. Furthermore a "\" symbol can be placed at the end of a line to mark a continuation. Veusz will read the next line as if it were placed at the end of the current line. In addition comments and blank lines are ignored (unless in block mode). Comments start with a "#", ";", "!" or "%", and continue until the end of the line. The special value "nan" can be used to specify a break in a dataset.

If the option to read data in blocks is enabled, Veusz treats blank lines (or lines starting with the word "no") as block separators. For each dataset in the descriptor, separate datasets are created for each block, using a numeric suffix giving the block number, e.g. _1, _2.

1.1.Data types in text import

Veusz supports reading in several types of data. The type of data can be added in round brackets after the name in the descriptor. Veusz will try to guess the type of data based on the first value, so you should specify it if there is any form of ambiguity (e.g. is 3 text or a number). Supported types are numbers (use numeric in brackets) and text (use text in brackets). An example descriptor would be "x(numeric) +- y(numeric) + - label(text)" for an x dataset followed by its symmetric errors, a y dataset followed by two columns of asymmetric errors, and a final column of text for the label dataset.

A text column does not need quotation unless it contains space characters or escape characters. However make sure you deselect the "ignore text" option in the import dialog. This ignores lines of text to ease the import of data from other applications. Quotation marks are recommended around text if you wish to avoid ambiguity. Text is quoted according to the Python rules for text. Double or single quotation marks can be used, e.g. "A 'test'", 'A second "test"'. Quotes can be escaped by prefixing them with a backslash, e.g. "A new \"test\"". If the data are generated from a Python script, the repr function provides the text in a suitable form.

Dates and times are also supported with the syntax "dataset(date)". Dates must be in ISO format YYYY-MM-DD. Times are in 24 hour format hh:mm:ss.ss. Dates with times are written YYYY-MM-DDThh:mm:ss.ss (this is a standard ISO format, see http://www.w3.org/TR/NOTE-datetime). Dates are stored within Veusz as a number which is the number of seconds since the start of January 1st 2009. Veusz also supports dates and times in the local format, though take note that the same file and data may not work on a system in a different location.

1.2.Descriptors

A list of datasets, or a "Descriptor", is given in the Import dialog to describe how the data are formatted in the import file. The descriptor at its simplest is a space or comma-separated list of the names of the datasets to import. These are columns in the file.

Following a dataset name the text "+", "-", or "+-" can be given to say that the following column is a positive error bar, negative error bar or symmetric error bar for the previous (non error bar) dataset. These symbols should be separated from the dataset name or previous symbol with a space or a comma symbol.

In addition, if multiple numbered columns should be imported, the dataset name can be followed by square brackets containing a range in the form "[a:b]" to number columns a to b, or [:] to number remaining columns. See below for examples of this use.

Dataset names can contain virtually any character, even unicode characters. If the name contains non alpha-numeric characters (characters outside of A-Z, a-z and 0-9), then the dataset name should be contained within back-tick characters. An example descriptor is `length data (m)`,+- `speed (mps)`,+,-, for two datasets with spaces and brackets in their names.

Instead of specifying the descriptor in the Import dialog, the descriptor can be placed in the data file using a descriptor statement on a separate line, consisting of "descriptor" followed by the descriptor. Multiple descriptors can be placed in a single file, for example:

# here is one section
descriptor x,+- y,+,-
1 0.5  2 0.1 -0.1
2 0.3  4 0.2 -0.1

# my next block
descriptor alpha beta gamma
1 2 3
4 5 6
7 8 9

# etc...

1.2.1.Descriptor examples

  1. x y two columns are present in the file, they will be read in as datasets "x" and "y".

  2. x,+- y,+,- or x +- y + - two datasets are in the file. Dataset "x" consists of the first two columns. The first column are the values and the second are the symmetric errors. "y" consists of three columns (note the comma between + and -). The first column are the values, the second positive asymmetric errors, and the third negative asymmetric errors.

    Suppose the input file contains:

    1.0  0.3  2   0.1  -0.2
    1.5  0.2  2.3 2e-2 -0.3E0
    2.19 0.02 5    0.1 -0.1
    

    Then x will contain "1+-0.3", "1.5+-0.2", "2.19+-0.02". y will contain "2 +0.1 -0.2", "2.3 +0.02 -0.3", "5 +0.1 -0.1".

  3. x[1:2] y[:] the first column is the data "x_1", the second "x_2". Subsequent columns are read as "y[1]" to "y[n]".

  4. y[:]+- read each pair of columns as a dataset and its symmetric error, calling them "y[1]" to "y[n]".

  5. foo,,+- read the first column as the foo dataset, skip a column, and read the third column as its symmetric error.

2.CSV files

CVS (comma separated variable) files are often written from other programs, such as spreadsheets, including Excel and Gnumeric. Veusz supports reading from these files.

In the import dialog choose "CSV", then choose a filename to import from. In the CSV file the user should place the data in either rows or columns. Veusz will use a name above a column or to the left of a row to specify what the dataset name should be. The user can use new names further down in columns or right in rows to specify a different dataset name. Names do not have to be used, and Veusz will assign default "col" and "row" names if not given. You can also specify a prefix which is prepended to each dataset name read from the file.

To specify symmetric errors for a column, put "+-" as the dataset name in the next column or row. Asymmetric errors can be stated with "+" and "-" in the columns.

The data type in CSV files are automatically detected unless specified. The data type can be given in brackets after the column name, e.g. "name (text)", where the data type is "date", "numeric" or "text". Explicit data types are needed if the data look like a different data type (e.g. a text item of "1.23"). The date format in CSV files can be specified in the import dialog box - see the examples given. In addition CSV files support numbers in European format (e.g. 2,34 rather than 2.34), depending on the setting in the dialog box.

3.HDF5 files

HDF5 is a flexible data format. Datasets and tables can be stored in a hierarchical arrangements of groups within a file. Veusz supports reading 1D numeric, text, date-time or 2D numeric data from HDF files. The h5py Python module must be installed to use HDF5 files (included in binary releases).

In the import dialog box, choose which individual datasets to import, or selecting a group to import all the datasets within the group. If selecting a group, datasets in the group incompatible with Veusz are ignored.

A name can be provided for each dataset imported by entering one under "Import as". If one is not given, the dataset or column name is used. The name can also be specified by setting the HDF5 dataset attribute vsz_name to the name. Note that for compound datasets (tables), vsz_ attributes for columns are given by appending the suffix _columnname to the attribute.

3.1.Error bars

Error bars are supported in two ways. The first way is to combine 1D datasets. For the datasets which are error bars, use a name which is the same as the main dataset but with the suffix "(+-)", "(+)" or "(-)", for symmetric, postive or negative error bars, respectively. The second method is to use a 2D dataset with two or three columns, for symmetric or asymmetric error bars, respectively. Click on the dataset in the dialog and choose the option to import as a 1D dataset. This second method can also be enabled by adding an HDF5 attribute vsz_twod_as_oned set to a non-zero value for the dataset.

3.2.Slices

As Veusz only supports 1D and 2D datasets, you may wish to reduce the dimensions of a dataset before importing by slicing. You can also give a slice to import a subset of a dataset. When importing, in the slice column you can give a slice expression. This should have the same number of entries as the dataset has dimensions, separated by commas. An entry can be a single number, to select a particular row or column. Alternatively it could be an expression like a:b:c or a:b, where a is the starting index, b is one beyond the stopping index and optionally c is the step size. A slice can also be specified by providing an HDF5 attribute vsz_slice for the dataset.

3.3.2D data ranges

2D data have an associated X and Y range. By default the number of pixels of the image are used to give this range. A range can be specified by clicking on the dataset and entering a minimum and maximum X and Y coordinates. Alternatively, provide the HDF5 attribute for the dataset vsz_range, which should be set to an array of four values (minimum x, minimum y, maximum x, maximum y).

3.4.Dates

Date/time datasets can be made from a 1D numeric dataset or from a text dataset. For the 1D dataset, use the number of seconds relative to the start of the year 2009 (this is Veusz format) or the year 1970 (this is Unix format). In the import dialog, click on the name of the dataset and choose the date option. To specify a date format in the HDF5 file, set the attribute vsz_convert_datetime to either veusz or unix.

For text datasets, dates must be given in the right format, selected in the import dialog after clicking on the dataset name. As in other file formats, by default Veusz uses ISO 8601 format, which looks like "2013-12-22T21:08:07", where the date and time parts are optional. The T is also optional. You can also provide your own format when importing by giving a date expression using YYYY, MM, DD, hh, mm and ss (e.g. "YYYY-MM-DD|T|hh:mm:ss"), where vertical bars mark optional parts of the expression. To automate this, set the attribute vsz_convert_datetime to the format expression or iso to specify ISO format.

4.2D text or CSV format

Veusz can import 2D data from standard text or CSV files. In this case the data should consist of a matrix of data values, with the columns separated by one or more spaces or tabs and the rows on different lines.

In addition to the data the file can contain lines at the top which affect the import. Such specifiers are used, for example, to change the coordinates of the pixels in the file. By default the first pixels coordinates is between 0 and 1, with the centre at 0.5. Subsequent pixels are 1 greater. When using specifiers in CSV files, put the different parts (separated by spaces) in separate columns. Below are listed the specifiers:

  1. xrange A B - make the 2D dataset span the coordinate range A to B in the x-axis (where A and B are numbers). Note that the range is inclusive, so a 1 pixel wide image with A=0 and B=1 would have the pixel centre at 0.5. The pixels are assumed to have the same spacing. Do not use this as the same time as the xedge or xcent options.

  2. yrange A B - make the 2D dataset span the coordinate range A to B in the y-axis (where A and B are numbers).

  3. xedge A B C... - rather than assume the pixels have the same spacing, give the coordinates of the edges of the pixels in the x-axis. The numbers should be space-separated and there should be one more number than pixels. Do not give xrange or xcent if this is given.

  4. yedge A B C... - rather than assume the pixels have the same spacing, give the coordinates of the edges of the pixels in the y-axis.

  5. xcent A B C... - rather than give a total range or pixel edges, give the centres of the pixels. There should be the same number of values as pixels in the image. Do not give xrange or xedge if this is given.

  6. ycent A B C... - rather than give a total range or pixel edges, give the centres of the pixels.

  7. invertrows - invert the rows after reading the data.

  8. invertcols - invert the columns after reading the data.

  9. transpose - swap rows and columns after importing data.

  10. gridatedge - the first row and leftmost column give the positions of the centres of the pixels. This is also an option in the import dialog. The values should be increasing.

5.FITS files

1D or 2D data can be read from FITS files. 1D data, with optional errors bars, can be read from table extensions, and 2D data from image or primary extensions. Note that pyfits or astropy must be installed to get FITS support.

To read 1D data, choose a tabular HDU for to import from, enter the name to give the imported data, and choose the columns to assign to the data. Multiple sets of data can be read by repeatedly importing.

For 2D data, choose an image HDU. Enter the name of the dataset. The data are imported with pixel coordinates by default (i.e. the pixels are numbered with integers). Other modes can be selected under Image WCS mode. These include fractional, where the pixels are numbered between 0 and 1. Pixel (WCS) assigns the pixel coordinate calculated relative to the CRPIX1/2 header keywords. Linear (WCS) uses linear coordinates where the Pixel (WCS) coordinates are multiplied by the respective CDELT1/2 values and added to the CRVAL1/2 values.

6.Reading other data formats

As mentioned above, a user may write some Python code to read a data file or set of data files. To write a plugin which is incorportated into Veusz, see http://barmag.net/veusz-wiki/ImportPlugins

You can also include Python code in an input file to read data, which we describe here. Suppose an input file "in.dat" contains the following data:

1   2
2   4
3   9
4   16

Of course this data could be read using the ImportFile command. However, you could also read it with the following Veusz script (which could be saved to a file and loaded with execfile or Load. The script also places symmetric errors of 0.1 on the x dataset.

x = []
y = []
for line in open("in.dat"):
    parts = [float(i) for i in line.split()]
    x.append(parts[0])
    y.append(parts[1])

SetData('x', x, symerr=0.1)
SetData('y', y)

7.Manipulating datasets

Imported datasets can easily be modified in the Data Editor dialog box. This dialog box can also be used to create new datasets from scratch by typing them in. The Data Create dialog box is used to new datasets as a numerical sequence, parametrically or based on other datasets given expressions. If you want to plot a function of a dataset, you often do not have to create a new dataset. Veusz allows to enter expressions directly in many places.

7.1.Using dataset plugins

Dataset plugins can be used to perform arbitrary manipulation of datasets. Veusz includes several plugins for mathematical operation of data and other dataset manipulations, such as concatenation or splitting. If you wish to write your own plugins look at http://barmag.net/veusz-wiki/DatasetPlugins.

7.2.Using expressions to create new datasets

For instance, if the user has already imported dataset d, then they can create d2 which consists of d**2. Expressions are in Python numpy syntax and can include the usual mathematical functions.

Expressions for error bars can also be given. By appending _data, _serr, _perr or _nerr to the name of the dataset in the expression, the user can base their expression on particular parts of the given dataset (the main data, symmetric errors, positive errors or negative errors). Otherwise the program uses the same parts as is currently being specified.

If a dataset name contains non alphanumeric characters, its name should be quoted in the expression in back-tick characters, e.g. `length (cm)`*2.

The numpy functionality is particularly useful for doing more complicated expressions. For instance, a conditional expression can be written as where(x<y,x,y) or where(isfinite(x),a,b)).

You often do not need to create a new dataset when. For example, with the xy point plotter widget, you can directly enter an expression as the X and Y dataset settings. When you give a direct dataset expression, you can define error bar expressions by separating them by commas, and optionally surrounding them by brackets. For example (a,0.1) plots dataset a as the data, with symmetric errors bars of 0.1. Asymmetric bars are given as (a,a*0.1,-a*0.1).

7.3.Linking datasets to expressions

A particularly useful feature is to be able to link a dataset to an expression, so if the expression changes the dataset changes with it, like in a spreadsheet.

7.4.Splitting data

Data can also be chopped in this method, for example using the expression x[10:20], which makes a dataset based on the 11th to 20th item in the x dataset (the ranges are Python syntax, and are zero-based). Negative indices count backwards from the end of the dataset. Data can be skipped using expressions such as data[::2], which skips every other element

7.5.Defining new constants or functions

User defined constants or functions can be defined in the "Custom definitions" dialog box under the edit menu. Functions can also be imported from external python modules.

Custom definitions are defined on a per-document basis, but can be saved or loaded into a file. A default custom definitions file can be set in the preferences dialog box.

7.6.Dataset plugins

In addition to creating datasets based on expressions, a variety of dataset plugins exist, which make certain operations on datasets much more convenient. See the Data, Operations menu for a list of the default plugins. The user can easily create new plugins. See http://barmag.net/veusz-wiki/DatasetPlugins for details.

8.Capturing data

In addition to the standard data import, data can be captured as it is created from an external program, a network socket or a file or named pipe. When capturing from a file, the behaviour is like the Unix tail -f command, where new lines written to the file are captured. To use the capturing facility, the data must be written in the simple line based standard Veusz text format. Data are whitespace separated, with one value per dataset given on a single line.

To capture data, use the dialog box DataCapture. A list of datasets should be given. This is the standard descriptor format. Choose the source of the data, which is either a a filename or named pipe, a network socket to connect to, or a command line for an external program. Capturing ends if the source of the data runs out (for external programs or network sockets) or the finish button is clicked. It can optionally end after a certain number of data lines or when a time period has expired. Normally the data are updated in Veusz when the capturing is finished. There is an option to update the document at intervals, which is useful for monitoring. A plot using the variables will update when the data are updated.

Click the Capture button to start the capture. Click Finish or Cancel to stop. Cancelling destroys captured data.

Chapter3.Command line interface

1.Introduction

An alternative way to control Veusz is via its command line interface. As Veusz is a a Python application it uses Python as its scripting language. Therefore you can freely mix Veusz and Python commands on the command line. Veusz can also read in Python scripts from files (see the Load command).

When commands are entered in the command prompt in the Veusz window, Veusz supports a simplified command syntax, where brackets following commands names, and commas, can replaced by spaces in Veusz commands (not Python commands). For example, Add('graph', name='foo'), may be entered as Add 'graph' name='foo'.

The numpy package is already imported into the command line interface (as "*"), so you do not need to import it first.

The command prompt supports history (use the up and down cursor keys to recall previous commands).

Most of the commands listed below can be used in the in-program command line interface, using the embedding interface or using veusz_listen. Commands specific to particular modes are documented as such.

Veusz also includes a new object-oriented version of the interface, which is documented at http://barmag.net/veusz-wiki/EmbeddingPython.

2.Commands

We list the allowed set of commands below

2.1.Action

Action('actionname', componentpath='.')

Initiates the specified action on the widget (component) given the action name. Actions perform certain automated routines. These include "fit" on a fit widget, and "zeroMargins" on grids.

2.2.Add

Add('widgettype', name='nameforwidget', autoadd=True, optionalargs)

The Add command adds a graph into the current widget (See the To command to change the current position).

The first argument is the type of widget to add. These include "graph", "page", "axis", "xy" and "grid". name is the name of the new widget (if not given, it will be generated from the type of the widget plus a number). The autoadd parameter if set, constructs the default sub-widgets this widget has (for example, axes in a graph).

Optionally, default values for the graph settings may be given, for example Add('axis', name='y', direction='vertical').

Subsettings may be set by using double underscores, for example Add('xy', MarkerFill__color='red', ErrorBarLine__hide=True).

Returns: Name of widget added.

2.3.AddCustom

AddCustom(type, name, value)

Add a custom definition for evaluation of expressions. This can define a constant (can be in terms of other constants), a function of 1 or more variables, or a function imported from an external python module.

ctype is "constant", "function" or "import".

name is name of constant, or "function(x, y, ...)" or module name.

val is definition for constant or function (both are _strings_), or is a list of symbols for a module (comma separated items in a string).

If mode is 'appendalways', the custom value is appended to the end of the list even if there is one with the same name. If mode is 'replace', it replaces any existing definition in the same place in the list or is appended otherwise. If mode is 'append', then an existing definition is deleted, and the new one appended to the end.

2.4.AddImportPath

AddImportPath(directory)

Add a directory to the list of directories to try to import data from.

2.5.CloneWidget

CloneWidget(widget, newparent, newname=None)

Clone the widget given, placing the copy in newparent and the name given. newname is an optional new name to give it Returns new widget path.

2.6.Close

Close()

Closes the plotwindow. This is only supported in embedded mode.

2.7.CreateHistogram

CreateHistogram(inexpr, outbinsds, outvalsds, binparams=None, binmanual=None, method='counts', cumulative = 'none', errors=False)

Histogram an input expression. inexpr is input expression. outbinds is the name of the dataset to create giving bin positions. outvalsds is name of dataset for bin values. binparams is None or (numbins, minval, maxval, islogbins). binmanual is None or a list of bin values. method is 'counts', 'density', or 'fractions'. cumulative is to calculate cumulative distributions which is 'none', 'smalltolarge' or 'largetosmall'. errors is to calculate Poisson error bars.

2.8.DatasetPlugin

DatasetPlugin(pluginname, fields, datasetnames={})>

Use a dataset plugin. pluginname: name of plugin to use fields: dict of input values to plugin datasetnames: dict mapping old names to new names of datasets if they are renamed. The new name None means dataset is deleted

2.9.EnableToolbar

EnableToolbar(enable=True)

Enable/disable the zooming toolbar in the plotwindow. This command is only supported in embedded mode or from veusz_listen.

2.10.Export

Export(filename, color=True, page=0 dpi=100, antialias=True, quality=85, backcolor='#ffffff00', pdfdpi=150, svgtextastext=False)

Export the page given to the filename given. The filename must end with the correct extension to get the right sort of output file. Currrenly supported extensions are '.eps', '.pdf', '.svg', '.jpg', '.jpeg', '.bmp' and '.png'. If color is True, then the output is in colour, else greyscale. page is the page number of the document to export (starting from 0 for the first page!). dpi is the number of dots per inch for bitmap output files. antialias - antialiases output if True. quality is a quality parameter for jpeg output. backcolor is the background color for bitmap files, which is a name or a #RRGGBBAA value (red, green, blue, alpha). pdfdpi is the dpi to use when exporting EPS or PDF files. svgtextastext says whether to export SVG text as text, rather than curves.

2.11.ForceUpdate

ForceUpdate()

Force the window to be updated to reflect the current state of the document. Often used when periodic updates have been disabled (see SetUpdateInterval). This command is only supported in embedded mode or from veusz_listen.

2.12.Get

Get('settingpath')

Returns: The value of the setting given by the path.

>>> Get('/page1/graph1/x/min')
'Auto'

2.13.GetChildren

GetChildren(where='.')

Returns: The names of the widgets which are children of the path given

2.14.GetClick

GetClick()

Waits for the user to click on a graph and returns the position of the click on appropriate axes. Command only works in embedded mode.

Returns: A list containing tuples of the form (axispath, val) for each axis for which the click was in range. The value is the value on the axis for the click.

2.15.GetData

GetData(name)

Returns: For a 1D dataset, a tuple containing the dataset with the name given. The value is (data, symerr, negerr, poserr), with each a numpy array of the same size or None. data are the values of the dataset, symerr are the symmetric errors (if set), negerr and poserr and negative and positive asymmetric errors (if set). If a text dataset, return a list of text elements. If the dataset is a date-time dataset, return a list of Python datetime objects. If the dataset is a 2D dataset return the tuple (data, rangex, rangey), where data is a 2D numpy array and rangex/y are tuples giving the range of the x and y coordinates of the data.

data = GetData('x')
SetData('x', data[0]*0.1, *data[1:])

2.16.GetDataType

GetDataType(name)

Get type of dataset with name given. Returns '1d' for a 1d dataset, '2d' for a 2d dataset, 'text' for a text dataset and 'datetime' for a datetime dataset.

2.17.GetDatasets

GetDatasets()

Returns: The names of the datasets in the current document.

2.18.GPL

GPL()

Print out the GNU Public Licence, which Veusz is licenced under.

2.19.ImportFile

ImportFile('filename', 'descriptor', linked=False, prefix='', suffix='', encoding='utf_8', renames={})

Imports data from a file. The arguments are the filename to load data from and the descriptor.

The format of the descriptor is a list of variable names representing the columns of the data. For more information see Descriptors.

If the linked parameter is set to True, if the document is saved, the data imported will not be saved with the document, but will be reread from the filename given the next time the document is opened. The linked parameter is optional.

If prefix and/or suffix are set, then the prefix and suffix are added to each dataset name. If set, renames maps imported dataset names to final dataset names after import.

Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset.

Changed in version 0.5: A tuple is returned rather than just the number of imported variables.

2.20.ImportFile2D

ImportFile2D('filename', datasets, xrange=(a,b), yrange=(c,d), invertrows=True/False, invertcols=True/False, transpose=True/False, prefix='', suffix='', linked=False, encoding='utf8', renames={})

Imports two-dimensional data from a file. The required arguments are the filename to load data from and the dataset name, or a list of names to use.

filename is a string which contains the filename to use. datasets is either a string (for a single dataset), or a list of strings (for multiple datasets).

The xrange parameter is a tuple which contains the range of the X-axis along the two-dimensional dataset, for example (-1., 1.) represents an inclusive range of -1 to 1. The yrange parameter specifies the range of the Y-axis similarly. If they are not specified, (0, N) is the default, where N is the number of datapoints along a particular axis.

invertrows and invertcols if set to True, invert the rows and columns respectively after they are read by Veusz. transpose swaps the rows and columns.

If prefix and/or suffix are set, they are prepended or appended to imported dataset names. If set, renames maps imported dataset names to final dataset names after import.

If the linked parameter is True, then the datasets are linked to the imported file, and are not saved within a saved document.

The file format this command accepts is a two-dimensional matrix of numbers, with the columns separated by spaces or tabs, and the rows separated by new lines. The X-coordinate is taken to be in the direction of the columns. Comments are supported (use "#", "!" or "%"), as are continuation characters ("\"). Separate datasets are deliminated by using blank lines.

In addition to the matrix of numbers, the various optional parameters this command takes can also be specified in the data file. These commands should be given on separate lines before the matrix of numbers. They are:

  1. xrange A B

  2. yrange C D

  3. invertrows

  4. invertcols

  5. transpose

2.21.ImportFileCSV

ImportFileCSV('filename', readrows=False, dsprefix='', dssuffix='', linked=False, encoding='utf_8', renames={})

This command imports data from a CSV format file. Data are read from the file using the dataset names given at the top of the files in columns. Please see the reading data section of this manual for more information. dsprefix is prepended to each dataset name and dssuffix is added (the prefix option is deprecated and also addeds an underscore to the dataset name). linked specifies whether the data will be linked to the file. renames, if set, provides new names for datasets after import.

2.22.ImportFileHDF5

ImportFileHDF5(filename, items, namemap={}, slices={}, twodranges={}, twod_as_oned=set([]), convert_datetime={}, prefix='', suffix='', renames={}, linked=False)

Import data from a HDF5 file. items is a list of groups and datasets which can be imported. If a group is imported, all child datasets are imported. namemap maps an input dataset to a veusz dataset name. Special suffixes can be used on the veusz dataset name to indicate that the dataset should be imported specially.

'foo (+)': import as +ve error for dataset foo
'foo (-)': import as -ve error for dataset foo
'foo (+-)': import as symmetric error for dataset foo
	

slices is an optional dict specifying slices to be selected when importing. For each dataset to be sliced, provide a tuple of values, one for each dimension. The values should be a single integer to select that index, or a tuple (start, stop, step), where the entries are integers or None.

twodranges is an optional dict giving data ranges for 2d datasets. It maps names to (minx, miny, maxx, maxy). twod_as_oned: optional set containing 2d datasets to attempt to read as 1d

convert_datetime should be a dict mapping hdf name to specify date/time importing. For a 1d numeric dataset: if this is set to 'veusz', this is the number of seconds since 2009-01-01, if this is set to 'unix', this is the number of seconds since 1970-01-01. For a text dataset, this should give the format of the date/time, e.g. 'YYYY-MM-DD|T|hh:mm:ss' or 'iso' for iso format.

renames is a dict mapping old to new dataset names, to be renamed after importing. linked specifies that the dataset is linked to the file.

    Attributes can be used in datasets to override defaults:
     'vsz_name': set to override name for dataset in veusz
     'vsz_slice': slice on importing (use format "start:stop:step,...")
     'vsz_range': should be 4 item array to specify x and y ranges:
                  [minx, miny, maxx, maxy]
     'vsz_twod_as_oned': treat 2d dataset as 1d dataset with errors
     'vsz_convert_datetime': treat as date/time, set to one of the values
                             above.
	

For compound datasets these attributes can be given on a per-column basis using attribute names vsz_attributename_columnname.

Returns: list of imported datasets

2.23.ImportFilePlugin

ImportFilePlugin('pluginname', 'filename', **pluginargs, linked=False, encoding='utf_8', prefix='', suffix='', renames={})

Import data from file using import plugin 'pluginname'. The arguments to the plugin are given, plus optionally a text encoding, and prefix and suffix to prepend or append to dataset names. renames, if set, provides new names for datasets after import.

2.24.ImportFITSFile

ImportFITSFile(datasetname, filename, hdu, datacol='A', symerrcol='B', poserrcol='C', negerrcol='D', linked=True/False, renames={})

This command does a simple import from a FITS file. The FITS format is used within the astronomical community to transport binary data. For a more powerful FITS interface, you can use PyFITS within your scripts.

The datasetname is the name of the dataset to import, the filename is the name of the FITS file to import from. The hdu parameter specifies the HDU to import data from (numerical or a name).

If the HDU specified is a primary HDU or image extension, then a two-dimensional dataset is loaded from the file. The optional parameters (other than linked) are ignored. Any WCS information within the HDU are used to provide a suitable xrange and yrange.

If the HDU is a table, then the datacol parameter must be specified (and optionally symerrcol, poserrcol and negerrcol). The dataset is read in from the named column in the table. Any errors are read in from the other specified columns.

If linked is True, then the dataset is not saved with a saved document, but is reread from the data file each time the document is loaded. renames, if set, provides new names for datasets after import.

2.25.ImportString

ImportString('descriptor', 'data')

Like, ImportFile, but loads the data from the specfied string rather than a file. This allows data to be easily embedded within a document. The data string is usually a multi-line Python string.

Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset.

Changed in version 0.5: A tuple is returned rather than just the number of imported variables.

ImportString('x y', '''
1   2
2   5
3   10
''')

2.26.ImportString2D

ImportString2D(datasets, string)

Imports a two-dimensional dataset from the string given. This is similar to the ImportFile2D command, with the same dataset format within the string. This command, however, does not currently take any optional parameters. The various controlling parameters can be set within the string. See the ImportFile2D section for details.

2.27.IsClosed

IsClosed()

Returns a boolean value telling the caller whether the plotting window has been closed.

Note: this command is only supported in the embedding interface.

2.28.List

List(where='.')

List the widgets which are contained within the widget with the path given, the type of widgets, and a brief description.

2.29.Load

Load('filename.vsz')

Loads the veusz script file given. The script file can be any Python code. The code is executed using the Veusz interpreter.

Note: this command is only supported at the command line and not in a script. Scripts may use the python execfile function instead.

2.30.MoveToPage

MoveToPage(pagenum)

Updates window to show the page number given of the document.

Note: this command is only supported in the embedding interface or veusz_listen.

2.31.ReloadData

ReloadData()

Reload any datasets which have been linked to files.

Returns: A tuple containing a list of the imported datasets and the number of conversions which failed for a dataset.

2.32.Rename

Remove('widgetpath', 'newname')

Rename the widget at the path given to a new name. This command does not move widgets. See To for a description of the path syntax. '.' can be used to select the current widget.

2.33.Remove

Remove('widgetpath')

Remove the widget selected using the path. See To for a description of the path syntax.

2.34.ResizeWindow

ResizeWindow(width, height)

Resizes window to be width by height pixels.

Note: this command is only supported in the embedding interface or veusz_listen.

2.35.Save

Save('filename.vsz')

Save the current document under the filename given.

2.36.Set

Set('settingpath', val)

Set the setting given by the path to the value given. If the type of val is incorrect, an InvalidType exception is thrown. The path to the setting is the optional path to the widget the setting is contained within, an optional subsetting specifier, and the setting itself.

Set('page1/graph1/x/min', -10.)

2.37.SetAntiAliasing

SetAntiAliasing(on)

Enable or disable anti aliasing in the plot window, replotting the image.

2.38.SetData

SetData(name, val, symerr=None, negerr=None, poserr=None)

Set the dataset name with the values given. If None is given for an item, it will be left blank. val is the actual data, symerr are the symmetric errors, negerr and poserr and the getative and positive asymmetric errors. The data can be given as lists or numpys.

2.39.SetDataExpression

SetDataExpression(name, val, symerr=None, negerr=None, poserr=None, linked=False, parametric=None)

Create a new dataset based on the expressions given. The expressions are Python syntax expressions based on existing datasets.

If linked is True, the dataset will change as the datasets in the expressions change.

Parametric can be set to a tuple of (minval, maxval, numitems). t in the expression will iterate from minval to maxval in numitems values.

2.40.SetDataRange

SetDataRange(name, numsteps, val, symerr=None, negerr=None, poserr=None, linked=False)

Set dataset to be a range of values with numsteps steps. val is tuple made up of (minimum value, maximum value). symerr, negerr and poserr are optional tuples for the error bars.

If linked is True, the dataset can be saved in a document as a SetDataRange, otherwise it is expanded to the values which would make it up.

2.41.SetData2D

SetData2D('name', val, xrange=(A,B), yrange=(C,D), xgrid=[1,2,3...], ygrid=[4,5,6...])

Creates a two-dimensional dataset with the name given. val is either a two-dimensional numpy array, or is a list of lists, with each list in the list representing a row. Do not give xrange if xgrid is set and do not give yrange if ygrid is set, and vice versa.

xrange and yrange are optional tuples giving the inclusive range of the X and Y coordinates of the data. xgrid and ygrid are optional lists, tuples or arrays which give the coordinates of the edges of the pixels. There should be one more item in each array than pixels.

2.42.SetData2DExpression

SetData2DExpression('name', expr, linked=False)

Create a 2D dataset based on expressions. name is the new dataset name expr is an expression which should return a 2D array linked specifies whether to permanently link the dataset to the expressions.

2.43.SetData2DExpressionXYZ

SetData2DExpressionXYZ('name', 'xexpr', 'yexpr', 'zexpr', linked=False)

Create a 2D dataset based on three 1D expressions. The x, y expressions need to evaluate to a grid of x, y points, with the z expression as the 2D value at that point. Currently only linear fixed grids are supported. This function is intended to convert calculations or measurements at fixed points into a 2D dataset easily. Missing values are filled with NaN.

2.44.SetData2DXYFunc

SetData2DXYFunc('name', xstep, ystep, 'expr', linked=False)

Construct a 2D dataset using a mathematical expression of "x" and "y". The x values are specified as (min, max, step) in xstep as a tuple, the y values similarly. If linked remains as False, then a real 2D dataset is created, where values can be modified and the data are stored in the saved file.

2.45.SetDataDateTime

SetDataDateTime('name', vals)

Creates a datetime dataset of name given. vals is a list of Python datetime objects.

2.46.SetDataText

SetDataText(name, val)

Set the text dataset name with the values given. val must be a type that can be converted into a Python list.

SetDataText('mylabel', ['oranges', 'apples', 'pears', 'spam'])

2.47.SetToReference

SetToReference(setting, refval)

Set setting to match other setting refval always..

2.48.SetUpdateInterval

SetUpdateInterval(interval)

Tells window to update every interval milliseconds at most. The value 0 disables updates until this function is called with a non-zero. The value -1 tells Veusz to update the window every time the document has changed. This will make things slow if repeated changes are made to the document. Disabling updates and using the ForceUpdate command will allow the user to control updates directly.

Note: this command is only supported in the embedding interface or veusz_listen.

2.49.SetVerbose

SetVerbose(v=True)

If v is True, then extra information is printed out by commands.

2.50.StartSecondView

StartSecondView(name = 'window title')

In the embedding interface, this method will open a new Embedding interface onto the same document, returning the object. This new window provides a second view onto the document. It can, for instance, show a different page to the primary view. name is a window title for the new window.

Note: this command is only supported in the embedding interface.

2.51.TagDatasets

TagDatasets('tag', ['ds1', 'ds2'...])

Adds the tag to the list of datasets given..

2.52.To

To('widgetpath')

The To command takes a path to a widget and moves to that widget. For example, this may be "/", the root widget, "graph1", "/page1/graph1/x", "../x". The syntax is designed to mimic Unix paths for files. "/" represents the base widget (where the pages reside), and ".." represents the widget next up the tree.

2.53.Quit

Quit()

Quits Veusz. This is only supported in veusz_listen.

2.54.WaitForClose

WaitForClose()

Wait until the plotting window has been closed.

Note: this command is only supported in the embedding interface.

2.55.Zoom

Zoom(factor)

Sets the plot zoom factor, relative to a 1:1 scaling. factor can also be "width", "height" or "page", to zoom to the page width, height or page, respectively.

This is only supported in embedded mode or veusz_listen.

3.Security

With the 1.0 release of Veusz, input scripts and expressions are checked for possible security risks. Only a limited subset of Python functionality is allowed, or a dialog box is opened allowing the user to cancel the operation. Specifically you cannot import modules, get attributes of Python objects, access globals() or locals() or do any sort of file reading or manipulation. Basically anything which might break in Veusz or modify a system is not supported. In addition internal Veusz functions which can modify a system are also warned against, specifically Print(), Save() and Export().

If you are running your own scripts and do not want to be bothered by these dialogs, you can run veusz with the --unsafe-mode option.

Chapter4.Using Veusz from other programs

1.Non-Qt Python programs

Veusz can be used as a Python module for plotting data. There are two ways to use the module: (1) with an older path-based Veusz commands, used in Veusz saved document files or (2) using an object-oriented interface. With the old style method the user uses a unix-path inspired API to navigate the widget tree and add or manipulate widgets. With the new style interface, the user navigates the tree with attributes of the Root object to access Nodes. The new interface is likely to be easier to use unless you are directly translating saved files.

1.1.Older path-based interface

"""An example embedding program. Veusz needs to be installed into
the Python path for this to work (use setup.py)

This animates a sin plot, then finishes
"""

import time
import numpy
import veusz.embed as veusz

# construct a Veusz embedded window
# many of these can be opened at any time
g = veusz.Embedded('window title')
g.EnableToolbar()

# construct the plot
g.To( g.Add('page') )
g.To( g.Add('graph') )
g.Add('xy', marker='tiehorz', MarkerFill__color='green')

# this stops intelligent axis extending
g.Set('x/autoExtend', False)
g.Set('x/autoExtendZero', False)

# zoom out
g.Zoom(0.8)

# loop, changing the values of the x and y datasets
for i in range(10):
    x = numpy.arange(0+i/2., 7.+i/2., 0.05)
    y = numpy.sin(x)
    g.SetData('x', x)
    g.SetData('y', y)

    # wait to animate the graph
    time.sleep(2)

# let the user see the final result
print "Waiting for 10 seconds"
time.sleep(10)
print "Done!"

# close the window (this is not strictly necessary)
g.Close()
	

The embed interface has the methods listed in the command line interface listed in the Veusz manual http://home.gna.org/veusz/docs/manual.html

Multiple Windows are supported by creating more than one Embedded object. Other useful methods include:

  • WaitForClose() - wait until window has closed

  • GetClick() - return a list of (axis, value) tuples where the user clicks on a graph

  • ResizeWndow(width, height) - resize window to be width x height pixels

  • SetUpdateInterval(interval) - set update interval in ms or 0 to disable

  • MoveToPage(page) - display page given (starting from 1)

  • IsClosed() - has the page been closed

  • Zoom(factor) - set zoom level (float) or 'page', 'width', 'height'

  • Close() - close window

  • SetAntiAliasing(enable) - enable or disable antialiasing

  • EnableToolbar(enable=True) - enable plot toolbar

  • StartSecondView(name='Veusz') - start a second view onto the document of the current Embedded object. Returns a new Embedded object.

1.2.New-style object interface

In versions of Veusz >1.8 is a new style of object interface, which makes it easier to construct the widget tree. Each widget, group of settings or setting is stored as a Node object, or its subclass, in a tree. The root document widget can be accessed with the Root object. The dot operator "." finds children inside other nodes. In Veusz some widgets can contain other widgets (Root, pages, graphs, grids). Widgets contain setting nodes, accessed as attributes. Widgets can also contain groups of settings, again accessed as attributes.

An example tree for a document (not complete) might look like this

Root
\-- page1                     (page widget)
    \-- graph1                (graph widget)
        \--  x                (axis widget)
        \--  y                (axis widget)
        \-- function          (function widget)
    \-- grid1                 (grid widget)
        \-- graph2            (graph widget)
            \-- xy1           (xy widget)
                \-- xData     (setting)
                \-- yData     (setting)
                \-- PlotLine  (setting group)
                    \-- width (setting)
                    ...
                ...
            \-- x             (axis widget)
            \-- y             (axis widget)
        \-- graph3            (graph widget)
            \-- contour1      (contour widget)
            \-- x             (axis widget)
            \-- y             (axis widget)
	

Here the user could access the xData setting node of the xy1 widget using Root.page1.graph2.xy1.xData. To actually read or modify the value of a setting, you should get or set the val property of the setting node. The line width could be changed like this

graph = embed.Root.page1.graph2
graph.xy1.PlotLine.width.val = '2pt'
	

For instance, this constructs a simple x-squared plot which changes to x-cubed:

import veusz.embed as veusz
import time

#  open a new window and return a new Embedded object
embed = veusz.Embedded('window title')
#  make a new page, but adding a page widget to the root widget
page = embed.Root.Add('page')
#  add a new graph widget to the page
graph = page.Add('graph')
#  add a function widget to the graph. The Add() method can take a list of settings
#  to set after widget creation. Here, "function='x**2'" is equivalent to
#  function.function.val = 'x**2'
function = graph.Add('function', function='x**2')

time.sleep(2)
function.function.val = 'x**3'
#  this is the same if the widgets have the default names
Root.page1.graph1.function1.function.val = 'x**3'
	

If the document contains a page called "page1" then Root.page1 is the object representing the page. Similarly, Root.page1.graph1 is a graph called graph1 in the page. You can also use dictionary-style indexing to get child widgets, e.g. Root['page1']['graph1']. This style is easier to use if the names of widgets contain spaces or if widget names shadow methods or properties of the Node object, i.e. if you do not control the widget names.

Widget nodes can contain as children other widgets, groups of settings, or settings. Groups of settings can contain child settings. Settings cannot contain other nodes. Here are the useful operations of Nodes:

class Node(object):
  """properties:
    path - return path to object in document, e.g. /page1/graph1/function1
    type - type of node: "widget", "settinggroup" or "setting"
    name - name of this node, e.g. "graph1"
    children - a generator to return all the child Nodes of this Node, e.g.
      for c in Root.children:
        print c.path
    children_widgets - generator to return child widget Nodes of this Node
    children_settinggroups - generator for child setting groups of this Node
    children_settings - a generator to get the child settings
    childnames - return a list of the names of the children of this Node
    childnames_widgets - return a list of the names of the child widgets
    childnames_settinggroups - return a list of the names of the setting groups
    childnames_settings - return a list of the names of the settings
    parent - return the Node corresponding to the parent widget of this Node

    __getattr__ - get a child Node with name given, e.g. Root.page1
    __getitem__ - get a child Node with name given, e.g. Root['page1']
  """

  def fromPath(self, path):
     """Returns a new Node corresponding to the path given, e.g. '/page1/graph1'"""

class SettingNode(Node):
    """A node which corresponds to a setting. Extra properties:
    val - get or set the setting value corresponding to this value, e.g.
     Root.page1.graph1.leftMargin.val = '2cm'
    """

class SettingGroupNode(Node):
    """A node corresponding to a setting group. No extra properties."""

class WidgetNode(Node):
    """A node corresponding to a widget.

       property:
         widgettype - get Veusz type of widget

       Methods are below."""

    def WalkWidgets(self, widgettype=None):
        """Generator to walk widget tree and get widgets below this
        WidgetNode of type given.

        widgettype is a Veusz widget type name or None to get all
        widgets."""

    def Add(self, widgettype, *args, **args_opt):
        """Add a widget of the type given, returning the Node instance.
        """

    def Rename(self, newname):
        """Renames widget to name given.
        Existing Nodes corresponding to children are no longer valid."""

    def Action(self, action):
        """Applies action on widget."""

    def Remove(self):
        """Removes a widget and its children.
        Existing Nodes corresponding to children are no longer valid."""
	

Note that Nodes are temporary objects which are created on the fly. A real widget in Veusz can have several different WidgetNode objects. The operators == and != can test whether a Node points to the same widget, setting or setting group.

Here is an example to set all functions in the document to be x**2:

for n in Root.WalkWidgets(widgettype='function'):
  n.function.val = 'x**2'
	

1.3.Translating old to new style

Here is an example how you might translate the old to new style interface (this is taken from the sin.vsz example).

# old (from saved document file)
Add('page', name='page1')
To('page1')
Add('graph', name='graph1', autoadd=False)
To('graph1')
Add('axis', name='x')
To('x')
Set('label', '\\italic{x}')
To('..')
Add('axis', name='y')
To('y')
Set('label', 'sin \\italic{x}')
Set('direction', 'vertical')
To('..')
Add('xy', name='xy1')
To('xy1')
Set('MarkerFill/color', 'cyan')
To('..')
Add('function', name='function1')
To('function1')
Set('function', 'sin(x)')
Set('Line/color', 'red')
To('..')
To('..')
To('..')
	
# new (in python)
import veusz.embed
embed = veusz.embed.Embedded('window title')

page = embed.Root.Add('page')
# note: autoAdd=False stops graph automatically adding own axes (used in saved files)
graph = page.Add('graph', autoadd=False)
x = graph.Add('axis', name='x')
x.label.val = '\\italic{x}'
y = graph.Add('axis', name='y')
y.direction.val = 'vertical'
xy = graph.Add('xy')
xy.MarkerFill.color.val = 'cyan'
func = graph.Add('function')
func.function.val = 'sin(x)'
func.Line.color.val = 'red'
	

2.PyQt4 programs

There is no direct PyQt4 interface. The standard embedding interface should work, however.

3.Non Python programs

Support for non Python programs is available in a limited form. External programs may execute the veusz_listen executable or veusz_listen.py Python module. Veusz will read its input from the standard input, and write output to standard output. This is a full Python execution environment, and supports all the scripting commands mentioned in Commands, a Quit() command, the EnableToolbar() and the Zoom(factor) command listed above. Only one window is supported at once, but many veusz_listen programs may be started.

veusz_listen may be used from the shell command line by doing something like:

veusz_listen < in.vsz

where in.vsz contains:

To(Add('page') )
To(Add('graph') )
SetData('x', arange(20))
SetData('y', arange(20)**2)
Add('xy')
Zoom(0.5)
Export("foo.eps")
Quit()

A program may interface with Veusz in this way by using the popen C Unix function, which allows a program to be started having control of its standard input and output. Veusz can then be controlled by writing commands to an input pipe.

4.C, C++ and Fortran

A callable library interface to Veusz is on my todo list for C, C++ and Fortran. Please tell me if you would be interested in this option.

veusz-1.21.1/Documents/manimages/0000775000175000017500000000000012376130063015035 5ustar jssjssveusz-1.21.1/Documents/manimages/mainwindow.png0000644000175000017500000042257211662000553017726 0ustar jssjssPNG  IHDRσsRGB pHYs%%7!tIME;b@ IDATx}w|TUNz%)҄JuWQ`]]]{ݵ])S$H&$Ly3ɝ;3޹3$32g=is{!DUQg淈h*' tp;fu:q>;Hآӑ^7Tf$kp3!"O|*^!̼NB: r:+UF{x a*2.U!aE Qp35Dt.3W0} Dt R^p-gv4X =fJv:d˲)BuP&^a ^"TUI#(RG1E`,mRe]+D`7F0sj(`2"Z D":Wz6t/0>P\jQB0>a濄3P  W\3*HaT2 fm"J"ʂR K3&QR hQ|ѼT$r.k~̛LHYX^JfNhg׿OxLD&dN60Gw2x] eWD+iJ{6D1z6$Qgm$Drm:d<[?ە{ŘgQ[M6\s39L6"*P 7y !h3":DDQoM"T^"zzi=DD(ށ- #"#-'34i|"/V%ѷ'o`x."z[nD-t}CD?AVODh~ @ ۵3LD+x"CD E[-v"JuBm0W  :$mH?e#m*;g"-k1#M~|L5<&U4#mpۡlr8@>CE#r|` $/GMpE> RHo=GZH hiҼ&сGI[^_o5H V-W! ʖt `3 e!U%"ס=`x@ uQwNo)'elѦ~^6(}MmphqR-gEY}0_r]Ya8Tq(}'" ~T&.yP='VE~U/'ܭJFU}B0^ e UDX0"xFgDW(Go?#2_ ѯE B/g7_TaE%BP]-ʉxaTώa BUx_8w(٢y~*f1=*:a4?uW(niPxI@* {?(_Uc[ctxUǨڤKK4:jl.UPċhw6hMjD~qڲ5ܭ޲u_zhG2U[`p|_YF*w;:w*?naSEcjzQߋE^߉x? h^ @*;DbEN1fT-rwAt]jB9흼+Ue*"{Qwe  a}&!q^&J "D T|S4iL^ceU7"gɪu ="~UX )UC "D#[C}H@ab` @*n*kDݲmȏI\$%_-?SJM!oDׄ_!~5L ":I]0vIE, f(i6罓S*  ?k抰yF ާ1pz R*~`-GSkdw6Ro"YL`Tz=,&a<{|8~Q̮"Rj½@~0~Gk?x"sd:Fī_5$X;y{ޡii겷=Fda|{iH[߈SEc/Go=?&ES",՘v@؏i~Pn~@] :QcWn(X]F$ \n{gzՉ@NxoC9K4߳4$x&"fM"Fm Ṫ;?TC7ĽG'}~N5$8C ''DT"'DF( 8rD^)ߟ!Xg8]ĻN'^0WޚYc4ᯩU$x&"|iV''U_l!t].~Kt!8B]](MS|v!'OO{|+f1rQk4XM Q} 2oRgy>M~LC$ZVO*¿TmwX]C}NC.7"j5<1ME?Y;1K lk4ߵXajo =[:L1 [[AqZ~P?L#?8Z*]~Ga٪o^`5Dc>C+"\(p"M1rLF`9$^cۈZ8{W!oB9$Yՙktv@gLL+s(D4B aA}``I4ҶjP"x`a'H3{ڪܬ3(0>&YPl"keU > e a;4ƁFlN?kq0Zlj Bk +_ ~"ӄQ}᝝_ {NLFa"RU X1ئ ϼ+ZD4LD 6ɹ=@?S4z?HUVm8g߆AU;oRMDuӇ4Bql 9S܆Vb'v2D*3S(lN_+HAf8ʑ £t(*iz^{DlCk1EvIsLMCX+Ɵh漶&m(9rqG0[58#vWPՊ߮ V]`\NrS"Ɠۈh8'O ȕ뀒:wqb?&f3vZ=m2~z 1OF(ͩ9\"P4ʌYxn&"".:3'fpdwJt aPU/N%fޠ"PL3/\:aP*ҙoxW'g^U:wPFΓx B/Vۯn?"NЄBD럘3 ehݒUOQFZ `F3tzx OO}f(gZqΝӠ8#c {!CDX;תp'K⪨?kf r Hp U\R}6fߨ"[`=H' ;lÍ1r惢ߙsd+^Q=2 ن$ F DtBy!G C 1}DNkr` xif"נxԱN (U^J$(;bۜ7n>s+` r]geKŰJTt~DSo;9TiBYYj2a\5BUv[(~0Uř^7yf/B0] r GĊ XV]?tÀrx a5>`1h5 W!>+H]e)cƈliB4s- X{꽒DA1r%J|UWk h0K <*^*Z'J?9 UHBn "|!T^V e *MP;։^(kCZ5XJ{C{Zo P~&}*`O.{D_.(N.U7Ti~0 21= a)/h |3Z:[o2IsUXm^޻Ux->W!~B{݆Kyn>Eo5^?/p]Cgx"([ w8Uqzf.^W1~<w)͝hFj7yfQ^^Py@3cjX{=Ҥm[*$MWwE:UH{WHf(?6N޶6U-hx=Z#^8_U&kE>F3C\E`(Rѿ7I h * ?2(1աX͝N(;WU^5ޡs4W!~c RrkEXB_CQ-G6:kYt jNjr^x _D|8b[(~F%/3>M%Ĭi0&`RUZgPuxR+4~.:6Y~tP.—zWׄ '3gE<ÀeS"~[օy*mkP\ RJPzWz+uY-(7h0n~Qׇ:3-ŪV:->fbb;\BDDy ŹF&'9/!f^CD#E~bBl7ZA 1kwQ|)ȡwKn>bP^x}VCz[; L* eI3ss e2D4Ex_)-v5+zX%cJ#DZ07:i};{.u0 ."E{;AAwq#0'Hw1Y=16iE=)kN@ܠs7OfRn)Ƹb !vU'+v)tep^ȻA=f3uɎ2!;z&V^! 띢sz]c"*LDD]S_mhQ?{>ua觿FZvN;Q_unEM "|Fkx ט7"w!?5<-]KĎ+$@x1slAb&ABBBBBBBBBBBBBC $XBBBBBBBBBBBBBB` "$RKH7G GSǡ%؆(^jBB`Ό$ !!1^%$$}x 6AP$ I%:301EщBI%$BYPmP~q$̼rRr%XBB! ;1f I%pTDHmHHH,!a(j$X+;S$$:^$ !a D R-+Kt,P@DR, (=R-+Kt 0sR'HmHHH,!!ېD\ $XKA G(ڐ ]]BV I% ` c` ː+`'3*$$ *(0z, @D1v  <,!IDW[`ԆDC`IH6$!a7@$X B lC]r%XB`. y.XB"ADIrڐ$XBFȕ` I%y ڐZRAD һr%XB`. ,!ݐ+Xa\)U!!tSjCB`y.XBB` ن$$r,U!!IDW& %$$/! h yXB` f e+LԆDtdBْr"IB-K,+Uq/CRpL $'%f±keΜ9={㖊X$#h"/O=5_#K-%q}tp1tūH0%6c 7v٧HD8L}t{/IpW_$#w8bbltlc c9;yN6)¯H6IB6 2KgsC;Y+KnOKIV$^BQBmbw6l*~Z!C#>>l^pHeK*n/XlgKQI, IDATV ȡW1:d{ʁmdAyFXVJΖ"*K"Lp"!! qnv讀czgΰ`8(-]N 9tL N*Տ l!!H!>0 *Vp B['w6-U(+@nn=$veDsc 1$+ 9) 憈 H))5j$-['m ˉC&!lx_Hl ,.*FaaA8WlKۃsPFd8=5+ a1UEm(mSOEVhWkkP_;L:!#6rG퓝r6"11Yn r([#I& 1u6r455b_quu5o@bb"RRR#5%U w)"NKHmۆD3aYfaϭA!/}s2/?*LZ;#L%E858evMhnF D!moO-CEErK[Ό.#v(E.A3t t#NAMM>t>ƍ76Iw#z>yD\7&ۋ_~Y͛7 jkx@DHNNBFF7 C߾ҹVrE]ZX֥(d p:w6ؐ`]$8 UPHq;a!OUWWv裇FJl4JXT@D+++Spp f$]%rs ~Y+Q>$6{ 3{Q\6 }߿ݻw@Ŗr?='N +;5ص7:,Y y8rPE 12Ϝ$v>UT\‚SvJ"!vApHd27\ iFrǃ N*SR\ѣFAXyDS`[55LlBy`E N#ᖝ% P3h6mŮ]x<@l0mX1`0\3ݴbbbp `b\pBѪش 't2 Fvv62|p:薞^z\ҹxr^ 6"ApIq N FtG$€Uad_¬z4esoIqۧ䃡_$M\6ӪUE(Sfk W]3ڋ |NFBL#ŇhK _ A\ b.46z0]{M4 ,M_6s|x7#(UՖp3rTJ|&L/px@\ j3l 8p0Qomijr3Ρ wF`۪)yX}=aWZy Po%}Y7Bi#2FM7 .ѪJVK?eGSVVIJLMT`VF998X{Xld#i/] Ǎ:[O_nE^ޱ;0k[>ؓx'~jӴKB|-|Co9Sg/1-5Dl۞Dk_޽. G 캢mV5u`9>lR`Imk*+nA{rС66m1`@6ldi{q]⥗^_;Ͳ)wu;|a03n6|||>֣;i% 뗈AtnD|\*M1`mS&O‚ n຺z$ q 11Ng3\.7:,[Kaz7Vd\0M){ɰ'd!Ƥ$ ANJDRJ&tI]#vEXoX/ =B |!1Xei SEB'bRDsSMmY%ȦMvYJ80Yr% t0תm jh@?6jގ6obō9j: NBu!$lw`̘q睷kgD}≧OcիgpY'muZ/*N:i yY&e^k\,^#/^W\1%Y` @Áӓ1kV"|Ess ;?szp鍆;I&b"!{<>ښZ~P_7x ..P~ZRGD(VZBͼzN]w|LՒ ֋odS\=k~e]2%Ĭ!&nYY|)**AaAclPp{ #Qz FX#2vD}($АEEE(,,lZVa;io3Ş:=$;`{x$0}l~};nC=xO'݅=3d#륭Nƿ_M^VpfU8Xx N+fN;ȕ."&&8gcϡXHw.nK!4ԓ97NDsc#b} Pb"?GgBs H[}$ԬS)XihW"Vuz;t0rl9 ˟[KkW` fj1&FL0|[; 466G F#3=Equ3lknɃ{c;tJdB`Un466NVHͼ3VHӧ+GƲf=>k̓:"g\ЙHYYrssUCjеal;̈q --Ng<+?/ŏK{Œ^c+޾cxiy͝8;l#w(zyOǏ?.)w?`q,sI%Za8\>}'5 =]QVn@z\*z6f49Aúwرskvi||3AZZ:* DͤbDؠAf w;1J7Lavh3z&92;VCZ2&x56lzuϴm :`;-EZQlbKKq㐜ZX47G`۵$ắmLD#xmñq9gaÞwdfe66򬫯@*+0|l`6^}UKгgf]}=8lx<ߡ婧żŽ܁3.?sy̞u% Ipbw.?Mݝ8<% NcԻqˑ@%ˋ=nL<AŖ >ҰvBZz:؇$w oJX}$f zMlAt覥ݛV8Y~&ZV9|>wKw]%SZdUHNd uHJjy>I__̺[9g"m_qf߳Ϫ0Zxe՚S@ +/A~(47ִ@c-lЭ%9ȮdQ;^cE"iftFFF7?gm"1.d0wI'$b6NS40w&r[e`[;${-׵vM8cn*/e:U[ë;vbS➻oq㪫fcpgg&egy&XM(Uu{׀xu,ݹ}{!+Bpp{Gz v k׮x{3O| R, mVNZuĥS]n-9yoI%4AOG,c1 ctTи<~,z꣚A뜱\+ihv zN@2lqѪ")4pИzW= MPc)Z?j:aqGSĖ-4r].J6rZj*Ǝ;ޯ9vac7p&Qa;!- d L0[▛o1klsfߐm֑sAۗm rLI#Xaom " [kӞ={p≓QgKm( \D=pĂفUiu6r87"!{BR-ъ,TTUՈkapn!6 LOJѣP[Wc՗`a߯?Z;inj}{~8nD"1!nW,WnFdۀK#?-9Cic=b"".gB!{sg}.8pDa 0sRQTJ)2KK?ɉ ŦU/n٦LmF65-<8y=N@p8lÙvG֋E W;?o87t %aXCu;syuu;;,Y;~AzzpXdefHb&Ȱ9I}#|wxf矄.̌>S1QO{ʧsMp޵81AژN8%h;kL >_xvpϣDbδ;^J//>q8]1 )ޡppPp#q [0;ѳWOOtHJJBBB"bcc  % Cd:J\VdU@&'hTE&24̃3`oOcIqkIٚDf4 C m`\4'Oɭ֭Cnn$d3ۏJkFڲr zF_#p9ሉ δ;^Zp!lFyr0c Mn7l\TTˆ~﫛f{^7ȯ[0~X_D9iwdH,va8sv;v;b:6*)ȰBllu84B\\\YCϑm$ zĘlb [l&ʑ"-EE8mIhRx9v2kI+~\1e<87 `5i#wd;Kň hnvt6k8}V t ^<3~X|w8ԓ4 9ܖB@yUfݱxlv.>#JHX@uu5뇪*8v*8L ֭p)`~becr(:de{ `@9}6!6ׇ yV:J]M> I`Ap(//HD(({ErLax˖ /o$#+(lM++bԨqUadq{m-aQ 6lXATkDC;\rzS"b&8u,5s4?kU 7wP[ 61 HAKD 'NHet92gW'n 0Hʆ ѷO_Z6",x֖mM)@-fQoźSFgUTTYaBll,ɴeFO"ҌC Y @XW,1{k?Z*@-EӐo18^o6y_Fqq &j1a6RU`nE|6Ao3ٕ*NQޟ؛={lnF~ø,G@pr;?.U!U 'gɅsIz% r%fRҲ|Կ)' C~#=ymU^3 +p̈́{S|̠5~xgS3u^rJfي )$8:ʋ(z[tdRXxVrd kЏ j$ڢbX,0J_00 # c3I%7Ϩ}{ѿ?CC\N>$I Íb `=yaF^;u'arS]m%j+Q/>fp$CìT\RBC)Go"a%fI}u4vDH3E(,(h0|$.K"HzMfL֭Я_?8!%(N#_XşC*TQ*AFCFo-/ߢC/!I$L'JEHHblxMMM "$%%s+f ]#88-[g nq[_c-8`} P##*1n\@_\@2Ɓ^z 55% ݺ RSЫ]; AlltFD#&6" e+GItmGbbRSS2$:*3!@۸q/Dfill6mm 5$?8rՌX?~,,Xs_#z<.~&DQ[`<8tݺ|Сjdffb MU0>>!Šhn'#Km߃ofD=tٷo?һaժո+K/@՟|sn|N;z?ۇnPj%1#aKfp)o߾83pB|HJJرcѫ`͚u:(%o-ɠnS裏"7Xt9f;iHLLDx㍷[!7''z$Wlեصk'@}!?/gq <C dV5>b bcs ..Ś5u6TU).;; G #{p6v{Lk$5.>!33G ѣG!SRoSpmڵkp6G&&&#%%Mžҟ0p@du?O>(rfTp""B\|2kobɒxf͚N: v퐉bȶ)ɮ(9rc$ I6 :[Mcq7bV0֭ؖڌ[n}cz/X;bbt9|Q~~>Rl7cbc'XQqm wq2=Ȁoݶ#۷ć'466b9o6\.W @h$866>(vXNQVwng8n{\qTl FƯqx1vXLv  "P}'ߦm;P8=Q^QqO#v<Yٖ䋋OµᅬlS6m뮻 .DbBfN[vvڅ /8nס%8cq7Ǐ ㏸'O ef_^zF||<وEOÂDZZC*RPz nf,Z}rCHcm~3gL}#F R[U[N'amxQVV d#Am7~y~} z$.7Φf[ T 6m*k#Þ{a&އ8ɠȁ'|̌^z_9i+/@vV232d߻ c I\GL,@q'#a@!Uo[n7 ޹hjPGհ"K*+y` I%:[[McąW79q S߸oعvɓ&`u'/|x657ct6xr6@8I\㔄ɓZ]CC.[Cb -[ ÁXI'MFYY9x]D)d56.*^{5̛7/`5k`ܹhjv"!B[1"ySp-GVTc嚃Dw 3?5 3g@LL .47ֆ\_ׄA Eؼ;~^W~nzE|0sLCfIذafΜ[oհ*␞_xgq^x~sÍqERrTjkk1atM+W hS2*6p:5z}8jkkq!p -- Át?~})ȁTxd\:u שeU`R2d;j w߃;͍`X_0!!!qĀ8唩HK~a&4;81ؼ?(i8?Ҥ?֬Y4yDhMMm2]͘={6{9W\_/"2Muuojy"<[88 NoÁX8jHo㪻,fzT;f\~ݎ.Ae>TT_\t޵hljŢE1p6غ(WܨDE,!!ID FC{??ޚm2d0\nl Zfسg7n3_?o)#9V1c`ƌh?Vž={}sq% Aq̰cr"^{ \r ;＀q-ZO>P0"p6++9MM+:AW\jF.uk<^b \uULOs]r A|Ijyq]^^J3&SLۢzDϿ@||PSSݺu@ll,: } ,&b_}˟>Gxv*+159Pp <8?ql۾/ jf 4_'֬Ya&!--0?|8pӧOCSӲk2LM܀| vXd F[owqbcaߒR#V|4 n'nki"=Gs4éNEsS ӕx0i3n^cQSSϿz;ێf=W_{hjA{ <Wh@Yy9.<NTAׄzKiq>y.~-V(e 5ΤÂE(= TڳW^y%*pDEnoh@s1׭X|9.Ӊw}\r \ڈLdff{m~;gBP#jV̞=CNժU5k_7-- w9 ˅gn>w/~5Ì3pWs$$&MMMp:HLLDRRwJJ 0e'CyGQuawoz!4H)" Ҥ  JGzoR)' (i(Nz:$$!e><3sg̝{=t-7v~{o?7:ϩÆM?2u4ZN@벐GM6g:G{n8~[nGKf&b;{<.\z'ePg$IV & +m۽dbʔ)PF%YEv4BCtp d=>C|8kpYLbhxk?2#!-u,}Zg)ܹzuZO*47bL1ҥ*2eb +VN뇣7o.RqhU鳖A툌g+˚`/dM'hÃ;hٿoe%Gr)Qes>q<>!B\\BQaWQi]AaѢEP(Dl1sP*իWSjUJ|a׮]$&&Һu맚gݽܽ_/M дy $I"4  kˋweik$I\ Lwf׮] 緋 fmr]hJr|/~wW_} l2ztl})T2CRRC IQJ j5DT$+֭(dgZ-nnn으999R*mqyRL6.s$}w|f\6+Jyi;MUV*TgYtJׯu.^ggL ԓy.Ԩ ?oF)P0[*e2(TDx4I eYٔDN5k7eʕ2v(.]HƍСs!;&cfK\L Fʓ&?>!Р55ydel gұݯo1VBAtL<̦{ױ|\Xo1Wf}a3{/z3i"~Hvoȑ9xLQ?;Q2} IW^'00P /S`!f!)!D^M=i?K#SSި:vk:ޝX,V=vaDE'g$6ބNyiv֨QT9rN$oxbKQ</ߜů|~*4 `gPkU^ Zf =U7ߢ[jMZ?PL#X%ՂJbdʔ),Z0Ji6 Nc͚5*U[FR&sxP1Yj؎tT}Yn;kugK&UCʞs,Jqw{aԫ?;7b@q+SCFm8ߢywG*!_P(e)%Cre`}NM0Äˤ{( d*yϛxKPH[N>}c14f4WXѣ\z|jtt4?3[lSD #qa9w7EX<./W1YIgb6hdUd-ZJh4X`+Vb՜tz*?ooo-ZD'ҥCd5>d( *YΝ A[FRD₫k*"g>=KlB+hnۮuqoAZSӤO%j bQ]ܸO,FP(…K:8*~Aٳp\\ӝP|2l2\t9C'kaxR#Rgl3hժ%5kȑ#z&͚1aXv^z%;*U21>:vYHRݍ6S՚7 .;b9*!L:t9}Y,kNsYiJP,j2}gƤyNn]ӛÇ6X-O/_-Y@^S$hRT*%{Ga\J 3 h)\[Go.~xC~ q=9`r*Hb($q; yDDD>U˶}9f7GblJ#"ͨTJV}QT3*>ŠIw}Gƍy 6IQ(D6lLɏ-9t܅:tR[6ǻYC[W.xj5m2;q6*J\\\_8p ޞ,y9 IDATJkMozDEEa4>9>_Ec絗'Nѷ>2o8 .JJEpww'$$VC8If֭)MOTz>[eU/Di; ?/mF]2%ʫB`2R6TrY<<=  ONݺY"JU2fS"ӧO^z,_;~~,f;og >|J*1tPƌZcsZ^{oV$$Ń#͡%`ёU,sUlňz@ַhuO]) ,, O@Ξݿlb뎵y7z{nqh:f::}ʟ _>nݺeÙߘ ؼI|O @₌( =Cx) !_`'+$X$))ϑ$)Eȓxt*HXsvn:w'r3@T 8~ ?g EXʹ}ŋ߸[ڵ[N?ogo]jbb"^C͚ΫRoNI{.ez}DDA\\\jVk+IoڶkB<5Vq?g0Yy11cHàpϗ'z J*Ãk׮Ѿ(|9=ޞ k߿?]v%&&&C"tɗٖvژf4Z c 2ɖ;( &?=_ǐxˁC~gq|c^ 2r*VVˁHJJ5f 'OP Q|?損f foƢ(qi qqz+2F'%%r^^ kׯ斦}۷(_===v˚0 !‘QԬY-[2df4k2S6M6eǎ|h!#GI?M7:u;{߯iӦvZ-ZDF HJ}bGz;vs? ANm lG*39V1 UC(^RZͨTjZ4yk/qԨVQf3sHjՉiԲ,N.L+VkX̨Ybqsw_~,\Q[(m۶fO> 1ehvI„B$3`- Eg n@3} t+ Z.䧈[;.űۙ/6Xu6ձQgT*>2Ɍ>‘gro}6Goᗓ+[*U*٣7h٢+ֳ8ŅiHpOƎS~ڽ搒pdƍ0r?S|Eb!V\=BNkF(Q sE6.[HϿȖ|OFaԺu~X(K:y7={68Z;7; 7_˿zfՕZCvfӷdPk,=_(U$Xbcѽ ;w/+Bpv.>|FމgM oeHls CJPmdK˭[avyFU*DFFM.>>!@eɂ&_ EX-ʡwg]G T38o23uTvVѥk7 ),0mb\ I&̚5O}rˣgٟg@TiH]|>>n2rI&]8J|tJҙ H V% .>_y."**+W/`4.̌)R؟Sp1<`$D9sڵ#Fs3,_.&_>/5kƌS_}yHի[L_LtRu^vr@~ˀ@=``H+j@smWBB)rSdr{djV^P"<}ʦ1zG&HV) +o9n_ǙF*:B߼.Tz+2d@l6P|Y:t5k?o.gz"]ʕ>b:wIkϕR*%ɥe:T06eV=8hD\,*?* VNY-&5jV|Ku:j̠Y7C0}R'>!`2[S'?Ӝ%0` !!cNRLY̙#`\#ɐI !D1;+I#/J@!DIRmNj$IZd?Tq x$IS<8E#9ʗ%SSӰ 4 MT=ą:w'2F*(蚺qq~9O~aR9P"V899+ZhI/?5m

рNcӦ &&=wrʼ|TqIC5&o @ pL C*\=+Wmoh4Z* S;;wo245K=-ٻ6w·C=ΐkVH 6_L>ڏ~5ìefٻzfU$<=={^1gg'ʗ/60)ܒ$/o\\c-# r!'n:6m={\D4'G} $)1goaDƯœ@8QvѢCa6iڴ G Jz-xm7#vmh1 T$1$jڔ7')!;y`bϙ2e >@F ߆bV-|̐!]V@`}ش)1X}lS_VA6o|8cɒ%l}xz &PbE͛L63k,4 UJR`Aw tͫ9w_/[Jnj5SJU~a[a5K$U~k# Jڢe>a$qb UWj5纼Z ?r4"Ed)VJ+%Ng '2d<7K޽Yp!sfd] _~oql\N2=%J=H56lYٌ5M_FQ)J(O?0]uoWJ @?sx>I2e Zb>>!=2c )S J%c'ݺDRP| SЃ}ͥKjhYFZh<IE9oѫݨ^܈)ޕ(ފYfXT/+bRrc{/_6[>+bd1,TX.]rBs"Cf0=z4NNNۻ'l޼߻ ǎF+iqY>4b^5. ,㡣Thׅmؒ6dL䭷ZӫwO}3Nr~:u+fSvdQ|н1Z*a՚ԭ݂A3xZT^ߟVYqsѮm\yG^ƋKS,2 ~a(j4i !]=*l}pH9,g7<2'ӮA]3,C $N ZٹwbTZ,+PBvM-?Dp1˫RAw:+_| rw"l V|BqFz=899a0<.{7ofŊ#AѢE9r _[ТD:uO:KP(F  xfG`0rlG@/C^ aYo[וNHcPAce5oJ"2jZMɒ\x%.\|{ze#${{StteΩ g&>O gpEZ5oJfMt -FR1uT@ݻ@ q۷Kf0vRJpe>tLoHa|ԧ7 &o٢Ӝ<^`P""ݻf,Y;T9!1(Y- 2P9</}ã9JL?J% ~i@++i+dդy©!)^2}$_Nӵ[o$ -Ⱥ;s*&"P밞;Ë>|TlBUuH[$J4u*BNb8%ᔛk ʔpX"Y-J݅P?EY^tŊN[d(% U?~˗l2z=QQQᵢD6mbǎ,_*IIj92$Ofk yPǍ%f fLtއ+WRTInV]{ H2l1g̤zjTh_){^cC:JWO>SZùh_F6&ϗ tp36={tdZNĻPk2#2ϐ!CXx1ǎE7ύ;-3>ɐA8p jm~:V3ΞO-ѤYN$+h&NC5j4_|9 wwO+b۱J:tiiݺ Lڭ3b93f;v 1iՃˡ)UJ2 ~ €t6_#1~Ö́$]MNq@=!$H x^j$+/$q\b@z9#nr%>]v})L/CxݜuN3{fZƉQ#Jf]&#(nnnX°{3כ"\Ϋ7رcsĈ!=rW^upl5kIȗ>ݻСi:vu$z$߈ʗúuoprrl6R<6mÆ cΝTRTi,K=9{?b ̛?E6t@xt///֭]E&әe[Fʗ+ر=s>S.U+H8㯧#f,cʔ,QCRWB~jє{QkdJ~8ڶ`B # [5g׮O ɈV`Zpr3rHl֬ f݋߭aԘL4]EF1Qǁ*V3By/II1T\_>|0T*Ԯ]g' 9sf3dP  8 ]i=5j4SN E+j\2 ~2$!63@ k!D[vXN~z M%I)P %rmFTB((hWf8Wq$S>G$^/񑹖Ãrl]<-G9ˏ+SؿٛP*XBa:c[\MݻD߾hĩ ?}3gfzO^OժU0&侼vh6mӿ!\IH`/51o߾lْ &PB:v븓FJ9̞>݃ `ٸn~?Tqj4Qgeϸ~+Q_ޛ w5]@Jllz:tb޾ Ij՜~05F8eܿ?:ƫ5$Pk4$ѲES>x JŒ2d!}yfX|9*DV^Mn]XZ|}yݺuݻ&ItZ;.ߣȿ?[|v{=J*bBP!C&/B|JvX!I!D`p]q(M|pOA !6yaE#wc$Ѷpzxq&dPpaBB.P̜nԨ>~Th4jrZBJ$!rtX^ÐhU YRHܲFwy#pG}u@Y[KdHβ ];z&iMk (\!UǁR $0dBB.ԫW&RhfK$d >uxyy: $ 1,+0l<S @ѻiR]sJ72eg߬4+$AR];;??oކoQK":: .r36ڶJ&95{LxϗK.;L& |5lݺMI7Sb y5 uIZ0g`"IX/^HttS?M>)W:ڄK`l2  F> h"rfȐIv#?Ia0~$Iq$-B*I2lb4v!Dil׵4_DJ Jn#$ۄ/#?z%.hL`ە虯*UTs0^O||<{5W3%;>.'>l20~`Lr9/k(ͬc6etQt&/ ))I"Yr:jlO?AIF =NXs' Ƹ<?|kROR{ !N% WWW0g[;ZOL&]4CωIIؾKҸQR\]]=lZlφn|(솳I0pR ¢hڬ ߝLAUNԾ!2II7oJX'ه h:4hGe٣+ rHww7T*%OK!228ːiES7 ]f͚%O8M8QF( ^xX8cv0n])V,'$15kV3lpfO KT3rիeGK !;Kxa&Y5V\g#>#.1k]]ݘ1}f$̆=KcIH+h5Z_7~N; SjZ$ 01fջp&J@-eimpVM=]ukہ'-x080^ZǚӣG9R(|7n;LGvȑ[{ƴiӲUoCCPF]ԮE`?w<kNzv$J j7ob۶m?q[P()TUҼys)%.1Q*"9},׮ 2F<=<(V,`̓JV ˕ BpPj1}fׇ͚T^M&3CC)]*:C9b ZǏ믿&::_X #[غm'k_˚Vș!㥃S&U `H`TiP8yLyB@tph4'8<֜8 Ib4?S/Y-Dx=i$AZ'>HRcu:NZLƄo3۷d .ζFSRQd0kעUfԩZʳRAP%|4af13 RL-eOjb1[W$Vkۦ@FM&'Oqx)\"E ?ZҠRiQu0!#;B"""8a2d,%ɦ-2dȐ!_ĵk)OLJə/CFqAT v g)9 9KdȐ!C  xLVx||北!#z䌐B%g_ 2d3&gg'ZɐU|9ŋuFLe8B/eRےȐ!Ct:}m2d>d 2 X|-R0\WGd,C 2dȐ!#/!BːI<=v8[nBte L&2dȐ!C 2r+$XLe<;r oG$&ɾn GoZ&9seȐ!C 2d</_ !`/Av!ѾQB)^,ŋQ[pgS 2dȐ!C+ԯ_O/58B)@s$I7v$Il$I7^,b>+^Fh\Jdd+H?"E 2dȐHJJBb$ː]o،3Pje93dprzn5~@E@EiŤ!#x!H$IqVAi:DN>^ɕ6R|)~\+DEEGbkۆwi'2dȐ!#8 S+s2GkeBs&We:WFQB''$$z*W\ŧ@Ȑ!C ˕Vɔ$g Q!pww3CL5$I B3:/K{ !t!ޓΒ$Dn! H"||8i˕|r-nb0QT*YYYYJOjrxl@uF7Z- qHVMZFTȸ&,f3 JRB]+وj~&2TPgLZj=ᨆ1 7 2^|,lVI4&$Iz3 y< $!\-uI`*,!fPx[D!6l5XIVH[X㲛&6Q$%Ӭ/"(IR10 a.`$I{!xiVRǎ5cNx4nܹK;n-I۶D`J5+ͬSݫ{۶m'5jT#| C>2wds.'N}x.NNNx]Ç`En%" $dԭ]۵α:;Nc׮ٺfL> !G۷N`6O\]xٔSҠ:zm۶q nHTR-Zlt@^Ama֭8q[wBP/UTyY!*Ko0ψs...s2 v,@{`Lo"$Ieet׎oX!Y`8ހk5BZ$h~ x؂ml&@y;~4ӬӬ[9'5͢crtd ,?{<I~߱ #G egC$N8E*=g>;LH4%qn}+T%Xlc .Fh4]5N:Q#;dY[zi}!*W*7Jbꔩ|,oMb1իWMfsi-zN:͏7RrEey 2{T%)O4B:7N:ðC ϏO+ll߿ξ;8p gQ| IH)ÐamA O \|l];0` 3*Cf`Z!CHNǎ0|0~yc`JFj{ !$YI&Iծy,KIk'oǛ7ePkcd/B-iGBsi* $mXqm's$IVc'%IaG{@t$B@u!D$IמǏ+iVVͦU*YFIxCw,irW\tO [ 0jvlۘ>KL$>>??R* h61u}ݿK0A`kK#ٺp7{߯;7,ɞÇȑOZulڴʕ+Pp,OgMZ&NbiTj+kތȏQ:Ga$%lF1c0jd2^uFѠz->mB(uEb~ RtEE21݋E8ϩ\W5zJ5HVLƤtͿzWjz#Gݼdzʉ)ch5l AҚ4&И4&$9lEch ho3{>)'7= g)}xCn 1s_'hmכ[ݵɶ>6 >t8gWONK ~h6m̾_CRb4z"̻}BGغm'ϘNkhntޝ-;~)Z:ITo1w,߸Ʋ%KIJʑF ]wgkd3io\gWKҕ+ N&>~OMnXaIGhPy{Б~%_+W;ٷ?Eaذ%$RbyLj5ZI~!$x7`xDTl' )LUO:Φ!U쿧I{2~ om!DzZ$iCn@O(6 }.S "]&@Ro،v;o g_$81p1s$߯%2oF$>850v<{̙Y/Yuf ǎ X-R&ˎ>VZ3ŋN`MS$EI$Z1KX @@@-!y?X`<ӥ}";VEQ"LBf.qqO|M:ܔJmazKrP՜:u_tj74z5_wl+J:~w.fzHFn0HLJ3ުZfYjƬ'Oh@dmY!`y1E ?UM?o%n,k|P`E$ ˡWUuƍ[غm'4.WW:u|%K0yܸqRK;ku6֝<>OZz"\PzV6?A da$%\?yN< :g'#IB\x4* fUhJVt~VXh}0:c *^mUS4m4!6S述)(cS|iM9CGm,/cv“7jI„5.@[lT!ď@7I3 [a3J7 y JZG$;ٻ^=2ml6kñqqq̍Dd7}vʵ5"΢\dͪ/N?̙=&$ufw!HN̻תj*֮LڞmK6=#e?% h:u m)n_j+Q3 naWK8?Qn+[&OAdK1J|h+$ Zg%.J\ ̠)̣3I"(8]]扴hDɝxyX$RJ釱_>zBPdp_qe2_M&<,y[/r|_268;IO=@^mV/C& )S?`ie\8]njh>:MeKZGz<d1㳡}NTz)ŋ Eb.]reKTR%K,d0`Vi9y ӧMz;y1 F{vQ0qDZjErL!C9gjL6VZS"/Yw抓eM0khz'ex{ xƿ'`jr,4tZbM4亞5zNjc۶m?~re =@Km6nHd|!xH@5; > IYM&ILs 9642HRi Q<`6֟`s v6Ӭ)_jY)IpFi7><#:&6$9YpYn\2{l,胇3&L (I@P /2L ]|zV4F6mң߳|P b#Aycsf~vާ6 ɘӨUCϓ2,3$c-DE`0ZШ)`6**.(L#z=hcYYƉ5 <$rY?J|ڠ]~STQ&@3'Nر7|_+As9fVR ĭk! ^R?{O9VjF׈v6JVWE$x'@o fL_G]lތz$1!2[`+S Q"8(eKseʖ-o:3t*5 H) cfĤ%彩ПF g-$fk >JMjB6m[h ݻ}{X"cѷLXܳτB܉}w`- }S(>O3laVJ5=fl ?$u~4ZUH7MI%I,B`d]y/6Ӭhtg ߬`J֭[ILLb`6;w|߳Ws)@;bA ~;(E DOAQDlD@"%qɑJ.Ɂy8.;;7;;e?;<3٬_IB[tS&?ӪSWޜrܥ :1tȠ *uz'wA~_iDmD4w51y/^Z *zaƌO{7QB nr]IɃ`-<7.'MFh2,yIo1lL*#,z7Ety!9Sh𛭬ޠ?/"x# "Hff&ӧOgȑdffvH=Y:$ ibfMZ8y\&©e;˜;;wF6\TUkÙfvƁ9IN<&Í}zul܌vfĉ$:*\nW~C+RUUej<4*B>] G ըUt5%n$i;y,r.zJV3'T!^ _}ߙEL\U7c>NǎiӪ .Wxi³eoѼsEG3~ ~֯l\̠,!7sǓۇr^71\'i3>Ӆ3FIQ(=N'WzCkp<UeiykuF.; | 7rXx1fDEz|\xI!V<)mB"XPWbb0IQUQy6-޳W`Y8`O\flb65kuVn;`M/+^3wAS>8<(hJ&1羜=qLͲ?0d 4Zvgqj%$4H3(h J@ o>|:icԳxqlI>?N'O 9̜ȧoeqU +W^v a`Rzv;V""x$/<\Þ$$ӟ>o>9{?2b᧯}rtTFB58HjqEXeĿԩSmݴB޽0-%Q::ӧU~G(ၹT\Fdp~'.>jVXAT.ݑDoBՉAԬΊ+x>83]MУϓ;{W;$OA6)ܗ{z!n7dff+i`v ,:)]7 +GL !MB@9ˁAN%_X /Ӏ/;kOu,4Jqhg٣x&.`7W?-xg)QB@K<$/LfU.Ce"]-(5 ԫ[3)g8ra322d2g?9YתU=+S#ޝ4nȍ79i~bnw0nW\vp{kS+_ctk޻et /L 9 /SFPf'/ѵsg>(*&И@ zVp:mƠJQ IDAT*FB@)PAoh,zpDKX57 wzsDFV Ch8]EKW_MNN!!!HN}!z.n)gA*9{5\m?.rٻCWӷ/dgˤquvL%x#gj3f |^Lݺ 2cǎ%((Tƍ0yzGhxM{|o2fδt/iƣyox>z~ƨ~5 ) o~{DٯR(k׶$шl6Ӫes8߶MemD6]#k5 a֭<>>iٶm5S~0{֊OkU>۶2 KU5؏&8v[u؋Kc=ӃJ -]gD||< ,`ȐRK3%QbK-TU-n8L!Ẃϧ =pWm?"8PSBkOw S`?p~xa%hg98u222Xg{~"'N?$""Wq:U ӪEs?;G͏Rf]RӨy'VOn᧯ތW{7aFWeKa_Yɏ[Ncih .o6ʈۘ &ϾEDh_eTAMY7? II'i> 7p2P|NS'4֦¯ݖ]<{J4 3~[N;(fգOY_`?\z>i x7crJ3 dr."˥?Rc([Gm*$NqLLUx 3 ! ܢ.;_P/z\cUU%CESkNpQs ʌ_LK?\.|EcCnβoK3?Cګ߱c7/W&䗴կ_fDҥS3k&d~[ yýc"+voJfMyaE*B?0/}|z4 gGD]{^6xmSؖ !D 槑"m14D Xn. Ң(*6ՍͬⰀ"и~/TE` 1b PցzGw1C3fhRN=غu˹&no#Y1⮳7[z SطKc. *+`, MRpt؞pÔ8 6w9-̝xr*5EEpehޔoõFqd߄tat_|>Ν:5.< a$edKs 2sU. &2sM+M7mք5Vyn&4iħ}?!Po9pUv;-q4gx1 @6z+e…}z+K֭ٽ{CnC")t\d +Fq3!oYt+Ԓ;ztHVS:5믿ݷ{nMԩS1L ,@ h/ RUI Ig@/ tW+ PQЕ"2++u6r f5tk&&'"XFSzQ֜€NΏ 3d% xIش;k\#)Sw3`@ -M2{Kv3>&z֜+tNh0cڿ*BSAuP^ZMB-zxWgu DƳSynL.Z;x|rk&<:>Ĭ٤e*̔e@\׷V/6ƏR+M:nNWYl][ϊ]KYqfn`pjj2ٹg^>mzO:>=zӸ ^ǘ?IZxzD5hG.LOcv(ny݇]j^Fã'h Jygs0qNl<ģZVݨ9?Ý0k{^{/Yݻ VbĐu$`4yYOǯ`t^o\:$($'fq`n= :{?pq p:!:)$wdlβQv@;w:{vu"^{RnٲS&.ɕKXEp~g4^OxeWU5CCw)C׫WfEme)N~)޼#s1u*OOP$iqP"/:hB̒o,S_*hq2?,M6~4,m p!ju|=[գjTA2~nBAb4 B2X-.TtzA`]@&aMV}]xN&;Ѳn7t` vqcz;o01YTܷټVύC4j\g&|25ux@K)5 LMfK]iw8&LX72&e,A[vMH[ᰢcbE3i9<=yw ʇCQ[ŋÝ#϶?_'kO7烌'&ގf~w`ؠδnVBEnn3^}EQB0qLnlǑ(ہK-Ynfv`&皿N-N'uٹs7.;wS^]r3Z6fHV+yyy_C¾/r0`O<7Ё7ԫȺM 0ul]N;dܸqϡz`pi8913'2+|(m" C(awfYgYN'oMBuZ|w/ޙ?VvF._Va||%|?/7'8vt{? PV-F^5[~`fNf*BK׺CQw,/I(9-oEK%nV+-7cn2ODG&''ْ/$]_uvG7`}Nbcӳ ll_8Q0eT( =z^oJ,!\M7XQm5#gCFC{ f-G窳B9v$5܇+#ǧ0:/f忯R6lX ksNKLyzK"Eh e|,{CWtDRwm%.6ڶm͊Cc-øwCe=#FeQX[16$佨;< ߫ʟkC+GnO>e洩MH Кìಫ .@ (*_Z:Xmka)san`ct:/ۺi0tCѤ]Ԋ<~ʳ/0mG{,)>z\aQ=.Hao1ܾi֫`,?C' BPmA)v>eBt\Yl޽;_5NiZ5kpuy^?ξ}i%G%Q$W*oltelec]LUaLymVp[e'nW_[[ח&ԓyM_ui‹훱cn{S-hEC@4E k)R(^odqlc ϐ ֞W/k,)TG]C{ 3Cde^ddb>]plNm)EA՗OL( 1+媸KUU3mLq)_GoW[&Re,PӅrҬi֭4wʣ"77ӰQYmZ1?G[N֭[KYd$U{zډ)S^YF _ &pK]h &44mҰi'+P6^RK {,CIv$L!PjvlP ǜB3w}76k.sBnݨS7lFRT1E#+9|Pd"G|5*<\ّKy$nwYhUWxIGcҴysԉ.7l\KV*{_aBܼY'5fdmΝ \et1VaCK:NE <ë `.PQR$Lth,3[~1xv7*KۢI4_-@jz=SrnJogh žIzSv-yv\+"tx}ɮ".3}y"+GTСDN>5ihӦ{^,ټ=/!/v~k|1#eo੧c5WE͒̈́_豣rIia:iPLߏ0 8>U4ӅpژX˷_b[ލHŒ3vڌyVsf3w\ڶmKPkuyÎ7q3yD,f^ZH` f-_cǎUiwއK$^{M:wHZBa (NNV͚EYlVDEպR֭V-䊩;^Ǯy݌l"։#'NUM5'?8o"TɅ]?"`8o8w]K; kЭwZ?wT틢63Sf `q*%C·+/ +N&Q58xry6t+ bca$nwCfMر=fSN{ puA(8+%ZUU,L>p.1cyɴYS#/H9ĞG2|Yi˲4|<#O~6=ҐGr&N= ن-4jY*8S2qkp QN:Q5tTՍ͒UOv[-5fve.IoBJNi7^Ѱj*VZU_h4jһpWlذ!o:@Vǯj;Y]7113{&/N;o;¯O>`<̙=Ⱦ͛g>RL] eg[0[-O'W( bh i8q"~\Khk㦟IW;. VU23ٸq3t8~lHOp\m#Yn2|Πgrص tu~+*ddb^?-/F݇;QM6B0&ĤiKٵ(!&~^a;ӣ{l֨w`hu ۯSX\^:y  \Edpj5 !(3nAӴIC*O*H?͠Axg !pc~>M nߏ۾YFNǦ3{DD|Ki,a)yj?,Gv93.g2IUrTrSKtgM.2vz mmÏONok<,$>NlÖ~sOi9r2 2 r|B9tgRw(܍X2_ٟb!]yf@t:M?;tj?S荡6lpB0要<9q,yRNˎhӦ p8Ic0qD+V`붭 *쑤<';v{QQDhz( ps!΄\.v[u48vOWQ-ZEqv9p:mGąJVg@73*S ` $%D"H$Jr$D"H$D"\1H,H$D"H$)%D"H$D""X"H$D"I< IDATH$)%D"H$D",2p::u,NBQPfDQEA @ExCP4UE;~!@qPj3|o*wyU,Pn:&'|3sߞEA\7ī+=FAP蹪*8R͏Ǜ4z p@KGo#C?W7 yu,w =t᩟'y]Z/Xl).7R j=n*ۅ \nV]4TVh-U0D"H$9N.tS|$F[P-#jbD†jѿբ{JQK+d<]^NEۅc^(QROMT<˗/jJZ1U-%+Z2ł\#D[fBVvBDs:%I *[~o-|;mDՠVdr+T 9HtX\.nUu̒H$M(VϩcXf4hf..LRs¨ȌH$DR&V `"G|ԐAVp:m]N)ɕ*V/X\r9PUȚQ7B'3jd'oD"H$r "+Q(nJBv$G^@jԨ^Wkv;jteL'huȔ7N"H$Im.H$+[+8x2331FBCCiؠM6ZH`S%׹je%H$QuDqE.9JUYٙI IYs~_"$PYl۾͛7p8_5$==cǎsQrԱ-A2GqWD"H$?r`,<)@%Er1/ի7aX;z 5lܸpZlOq]ӽ5keڿ|K"H$< ,TШRG ]|_Yqv"9GK]_Wx_Y"\lg?02d0fn&N;F`6kS αYF;ݮ+~rmΎ;ʍK"H$D`PZL*Up}×_yZ%MiӺ5~pL&7o@UUF\:wHzz:V:̙cXt71c)7.D"H$󦇊i!$")S2oi}bWri`poo~{olݺ+Zt:Tr\Փd̘0-̛Zܸ$D"H.rN9F2J ኟS1_1jaQLgVg6+#ja8y_~ ~oH$3nU%0Dpp0Y t/_C=̻dGܹ{"7/@BBQUD/W-gMi޼9Gax:W{SH$D"Ig-b#G%YIOUέ), Drtn7n̿GEQUݯ# Frp"111vʍ+77wC@@,3˯D"H$ɅB`I(,/ W5yNкu+sr8t8NLJsf3qf>ZӧΡaÆJ AYx4hiTt׸++_OyUy3ϖH.6v #:t(}Ǐqݛ,?NNN.{&"" s\u ==S JppУGrH$˖-~+`0H&M۷/cWfɒ%5mVwQUr] ̜9}2h O<-[GH""Xr+. +/s]cRJ.UTՍÑGttmKBBHӦMiҤ sNg\#--DBBB0e/^oAV&11J͛y74iyfDܾ$&&. D"EDGcДHn5 ЩSGnE(iaAU4.Drq2zhzQ,Z 2e-ZFS) jq̘1㼉lD"ED3RJ$jBVӍ騰`g\@QN6l@bb"4nܸBUlEF/4&f͚3rʽ˅Vcbh4"\.N""X"H$V8֋..DriP~}9u9E޽{HHHnɵ^]w݅^`ʔ)ܹS7o^B_$559s(J'N`ҤI4h^~NM7İaxB0j(f͚Łp:2j(wGoa֭f6mѣK\ ֮]KFF4i҄zX3',2`ۼ|MN߾}eAH*}<H$D"Tƍ.3ƍyꩧ8y$z+>( 6dɒ%7@Νiذ!:twEnabcc9vd}"  ""NJJ $$$0~xRSS2d=z<쳜,f֬Y(ᅬniӦ  !!=yfիwΗtoEG~G~.H$ӦMo~ʣ>ʚ5kaeqݽ{7 <ի96|pL&{ԢE BBB۷I\\{tz#@nʍwE۵kxF3x׮]h+y%[v- 2hnFٳgB:wѣGIKKd\.v@JJ ǎz$+[ ED"H$4nܘ]z?={d<3ԭ[s?5s.h$::SNy(BNHHH5o߾VZaZ9xWGFFҨQsƩhy  x̚N:f+ׯ_eb!558 =yԥK fРA! D}y#tc, iil߱6SUU-.Lvm DrQ0?2ddd\@n7yyyTVBq￳}vbcc9s ڵiӦ<:66{ҿr% .6L y99ۣٺu+}amۖ0bccqK$)%ɖ-[Y312C$IdgguV:uHXD"ڵkp̙R9s!!!!ChZmf͚l߾t:f:+ cnרQFS浧xſ`]vر4=M7x̲.]JJJ ;wO>r%D`IeIH8D6%I9tAfD")%6nXb4$%%ѤI+pm۶l߾lbbb ~xEd2Qvmv܉f`0x>hkԭ[DRSSK̉.0k.fq\\6mbҥ\uUo?.K$U@ (Ԭ)3B"D͚( y ޽WfDrҺukڴiڵk_3229s&B"CyN:qqq$''{cv:_tܹL/ӕaddd0{l\T>aGngƌEdZl۶m}E̚;w~5jx?nVK||<<&Mp"c0hѢvsΥ:ڒH$ye.\横e|͇ȩSLj3#,DCb'###l63H:u ))oetFGrrk9M`̌,vڴm%<DDDЦM\אGdd$zNG~5jTA˖-by^RHPP͚5G`0HfիW( 006mPV-ƍj6h۶m;v䪫B'H$DOH䜤;[X(Wp^cŷˋGrebZIHHerZVe;#00#!ݳfnh)9D"H\B%&SW,D9ŒN$s芔_˳aq-[LOh2b2B(B"n3ilظG\X.3㆞=ܹ_?ӯ_?]?d%;vPV-jժfϞ#<ⷼ믿"tE`` ;vt9N233^:h4S[o L&u>fx莋^deer}%ݥYV+VB{VJKҍ7Gb'RK$@Q![{?T fw!`УaGQDGG?w<0z+&]q/rrrSUʻ=z@Q*v=zz]Ru6((.]-ݥ5HjԸʧD"MvN]vh @Q`0о}rG%9F)V%$$įrFѯyOs{^+R+n5c2*D\gEkl]흢(~o?WWȑ`y$54X p0a6lo w}򝬿(D"9vmrJ 5@%>>^fD")V-bK{D|3=_ ਨ(8ÑT_fEHMMz 999_r!00o]ȋhëI|>B4ZIv`*ĩdUU7\yEV-7D\vfK$r9j l ""OZOf͚U4l,Zߖ3T tԋk!?B K`#pXѭ e pjUJfeJ$UDќs>F|$IٿE3ge$̙3ԨQB F}Vuk0O?$&ʂ"&a},źN"vŜB%c^-<cJɧsq*H$yGQn3C - !FjB+X~`JHH "z^.rEEE EDB :H 5@=@yy63;3g'ssLo;֬.هeaz#J)3 MQQxeY6$)bƠ;X1_ob;X6k.Wė$+ڟ*$J^^ey,eY>*r4J!P>zmDWjGxoS{I n$$$pR˒?qxN@2ܹCznk)'z]郞%|xvIכX(X3f"+ HT*' {eY.Ͽp4&$)T!s$IzWa$"%IzM$Mv-I$Ia$-$i$I}Kk S6$i$I~2{ t21"8??_KP8<׮]+gϦ]vFRֳ,[AŢ>J6+WЬ񁊵HLR ᓿ`ˣ>\*8Q(Ӄ$i$IZD@Y%Ԋʃer0$`1_%Irhp$IE$; x_ռ O\f.wؼT2mcJ%jumW!ME1 c -jtR<<ϴrJJ-[t0h%##bU)Baw@n2tX|jfSo1A,?/-IH[%0u5jۦ,%Ir#@= km[ڦ=dY4@k 0S/o&Ȳ,ɲZ\f;""4;+E*)RK^__UU8m*`Au%))ٳgl20… J ac˒ ={q޽E}`vw@>CuiYd[|=&$IN"@/Yɲ|CO, hLԳ(D=+fYTH IDATl20U$0x[}UEi^+ҽM\fH[_]V0$8E,^ܿlzaebb OpzzBڵk4mjޣwz/rAU{RUʂgIqc Vw7gGvv^C#:ac({w&;ۢ틊Z5'NX\WXB ;;+V/l8\a' OPz|ՇX|)=kx(xa/>3_ YECKh r %Ij LFl[ Wu@WL۞EȴڣԬ׀@7=7ߞR̒U5ɕiF#a˷PPP%YWW\POz[o֭[C0c DVll,9998::ZMDjj*NNNXiӦ/6϶}D{D>5Vprr25k h߹~ӵtՃ1?FhPNč7hJG *HmQ>~S-ϫ>sdY]tk5,Kt!r(dA$-$I-rF ( \y/OpɤJ,j"lZqqvvɑOrs$ E+ $$P(+D ]vQFL>˗YYYL<sQBBB,>˗DXXJ_>}qe ԫW76%%{Ѳe wVZ0pBB-Z0Xf/b;QH~}{3Yc=Ŭ> cT*\D*$IeY~@6Sg`>0MI–(ÙPֱ,g I>V Ӄ$j뀾Es%Ijr (6,W&#%I@Y+j?e9[@_IVȲ_xņ/kӚӔLQ(ť'rsrp(jzÌy!lZQ!jOCV-Y0 o ?d̘1E-[ܹs)e.\9s>}:˖-3Igff_>۷رctլݲe ڵ*5СC+wkkk˪U`ĉBask wWe҈WyvY- _Pq.L-T˲,K _%jCT(ǁD%P(DYW}?2F !Jo[U(k/)Ѷ@` 1U5k>e2/w^eAgѦukz/JÆ ص7B)n9 k 4!ML2UV[lΝ;F ͛ ݻ9rH4DFF,Xz/Q5n@lqVtXjSLaҤIVwqǏsUz쉝E\!| 5jdѹHDZZj*N`Ae .ey,mdYJLβ,w"ev YȲ|Dպߖey,v%%bYe),*˲}-eYU,֏YAT `6[ `5F ro͔)S2n}˖-ܽ{'g֭ dY6IX.\vrr]UgժUYK.Yg}d!LQEx{ Um|wĥ%K F#1i$m]V|͙S`U [a@ BxڴimٲoZpp0۷ogFolٲ2Եk׮M||wܺu tR)G{{{x7nȈ#x7OM7PТ$',ʔ_N>[QBüRpsǘJ畐!\qrIItڵXP^xnܸQjN}8p^zY8wFoKڵK- ԩSe(qpp ++KȰH~H@uys²G8d~V! ~drތAEʏT6lXfYfͰ^zs2B5jٳgM5kZƍ4mڴ2aooOvvv `\c<=uOş2f֘2B8nm/uy8;;J Pe{L9 'gɐ}-V C={\۹sg:dZjŅ ,iӦ\zWe~2:d(W 23 {uşKnfn~w1ѵ@ K=طoUu֜;w*5mڔ+WX܏#g͚NNNY/4F'%%e5 7ɡejںueFƲ~^"Ox_$C,3gE5 1ÆBLNF } ꁓ-Zɓkj/k$Y͛7ʕ+4k> k`2dV_~ڵԲڵk3zh@߱OΟukm'HӥK0yQy{{~z.]ēݟ|׸932292EXX{&L&n @i-$<kY7ȲCӚ+5w9[<a~QM$zl>"Z XB&MV{V nٲUB9{lÙ};^ 뢰CР~}~,M&99uiǎf!TZ%IR , zeY>SIDqK5!)B).-lo1 Ql$ÆPjW_.5߻wR>>MibBX5#u%0ށ}CBI>%Xt֍뇍UjI7niܸ}xyy/lllx?<1W_ݻ'0˗[Ż((**"77`ض@P9W*@MUmW@?tey$djBw X%$IzXBNgG ١+ D@ ( wNll,={Z0x噀H21 |enjjjJEEEٳ!C`kkWh?\к5+DWL |]}w)4- 2[0z!xz $ TJO7]*;U[ܚ&}!ȟΝ;йsG&PplL2,]L%@ t-ZJlْsgs ܾ}&v>|(߸q2I҈>+^ϱcǸ{$^iiixxx|666Z[_Ѩ[?S4. ~M r aaaFѣ|A Uĭs͹s;T*e80@|l"U?~3gLO.>kޜ85kҲESs@l )CEo aͿuG7?dbѦMCzpuuJ-Z`۶mh;;; d׮]`'''rrre`X;;;j(]P舛?~nڴpAݻ4jcwqq!33ww*y]jkf֏Z⅌He#x-IK4(e[/[Y3J,N@?,'4o Tn@1oJ/3@,46P p%7ԱKM%I ece9 ]uۼSN٧7 jזOt'8 ]:%aTT$O@ *&!!!ڵOpC j޽{7k֌+Wмyswww륲UMxx8ƍvuu%))ɬQeE59s **Y%- ׾ͳ%UH8r;a`!p*q?=`F3(=c@$I_H T@H_$IiӚ9t`7?~_&='GG>]/u?u9~D`@ Ύ:paB 99ڵkɓ'-y\|"X^n=֖l 3]USu!UH@Ɂ=O 抲6Yter  "Go[S(M?2VɶF UH`,_(|#˲$E^tuuw̞=&zSy}85& ̍⧟޽{D[#@ 4[.ܺuj}v֍X܏- 5JVV5_߹su asquu"XWq'ЫW/vlΝ.PKc);w>d·|axAB[,j %EJ(?hH$g`*pR,J>ֲB0Rt=vrCeY,rjғ"Y3;Ư] O@ \p9s37 X;riBk};v̢>8sFɜNU-[ضg666{ZDw5\\\0 Yf*wnr#Ɨ?}=QSyѶ %442ߏw@q$&&2c f̘]9=V,-14n,ʱlˠk lѝ|~Բ W,rFo|{/J/$IA%' pPҿхa}3{?o_c˖oIN<Ũ{gϙO?m2b t7#K[^,Qżiժ%Fha+qIFϞ=ٻwkݺ5 +ORRRMHHۏΤ},uzԭ[HLRFoٲ$F]F+eի 4ݻ&#999f8j8B/$7oԎ.t Bpp Zdմ@ x|o>x (,,F׽{w"|~$-ZSh۶0?T0'{dYVv>(e9Q\^U-IFw@I 5 x&]R0AL6+'9w~;o}I:4&S|uٳx2dm5kP(޽;:lܸhڴ) ,(NHd 2EQXXh8Y{l/@\\dԩ:y뭷0aBmMZ53+/Y7uL=$D͚5Tl+q-.N`XcD?/ILf5补lYM(ѡ|WsՒ$.R5,_Q lI ^"nq~@#ߏQ(<S糳Rk[@PXt)))K6m әG4bE_()p }dذJͿ˥{@&F `͐fuA7??bn:|M֭[GÆ /;;;ڶmˡCڵ޶xH۴iàA6m+V(%&n7`cccV길8~VXALLټ ᅴ, vvvfǎeDDGGWy!\vm@}TPhm< IAA'O.%4i҄˗3i$n޼i1|233uvgĉϏѣG3m45ټy3>4N$$ڵkS߸qCBa1ĜXY (QXeƆkLaț ewӃ _||<:u28>g2֮峝;ōH Dc`5X |Vf`,@-("@_+Ͱgmۊ Ae更iF1cSLޞ+Vi&OnnVi&w_/[޽ĉfGGG25ח˗/MԫWmu`M!\Cmll=?JalTGcǎۭ3>͚߱PTXվ}>x0=NyKh$rX ^ Pbbbpvvf͚V«WfرԩSd TfY@@:ٿ?Bsj޼9yzz [.7o4Idffo~WذaC$ qqq1͑#9H5Ժ>}קƏ<-$DPT1@ xt}v郳yzz~z"II\wO}z9֭[aÆܼyӤrT ¬M /\´iغuYYyyydggnvOEMnJڈ]o @`< Jb.e_}i 64vu:Cd*XP1pppgϞܹYk Dž%I4EKe[nѠAGjj*ӦMcҥBtm7KڨQPTOl\Z`#WB 0|C2$:m'0qB^i(U^zIzwww:uy'E%Y~=www{=VZe6ܾ= 27o2, uwvvfҥl;)W*j.&n:,;;;бN}x8so#rbd,1|LIUXTDAai|0v7_=huΐ&Ϲ|&KޡgyH&13c+oº6g硄R"Q۠*QX$ #T1.\W_o &бcG\]]6.ٳgK`}X- LСCZ&ЎɰJRՕȉ&h쳥D/Ɲl?}QB̡34iZjyE"Ӭ]YChҔSyWѫWYz5³Z B 1%;taaarNv_mO;e pµVDu10c/00D}{z5 5iWlP!ԇn*% jjՒE q0уǤ/(ҥKԨQggg:vHjjQd޽{̝EEB^oׯd̘eiӐ7;;dڷooTӷNc< `'BUmݷ a$\$ѺMS[ۺv^CK^glG,2ڠS$B2kڵ`cc#b޽{YСCG,\h/fΜ)Y ^xxxxTDԡש,ZǏ_a͚4iX\XV@!L g=X,,,BN_F'Bm;okRN9ۚΘL}*I888Oga +аaC̙CDDYYY Ǐ]xn05χϙ[wӹ,^̏?F_1ae!,'XO\) йSN܁}d'P]DM 7u‚ѯ;iBWFT O@ www%$$DB5jĬY ::Zk$AyOg@d6#ovvQ]v(LLC.hݭ?oASݺuaTۣ%0tOHʸqLzZ@Uxl֭޽34ctb`޹]QoK=K (°m|BжgYML2UHC Çhܸ13g,Ž(Hpp0ʗ ƖR%F 999$%%^ˉ'֮tA~~~^r5Z7&=+ Zb㭷>`׮%@ٗ?EE=EZj!Y`&e(AINM??=-i-ޮruPG>bq# )%(Zb*%D8@`U+E a=,\ÇӢE -[O?͌3OxK]˛ׯKwui˝ӽ{wC-mFDDUҬnyG mkUnSSdH![Dh .*]NSdv;M[V|yo%Q/\eVs̬)/}KeAcaL~hr jg鄇oӲeK]vŞ>M 0qDar'WJtqqq1{G*ŋ&{bёK9|ÃB!0lk#r%lӎ?cGoĪߖH !ڎ{/6.ϸj!lhQB A]COOOa͔)S4iR! zLԫWwiHNήeݺьgРAfݻdeecyܺu{CZQav)))g ?;{|XF@ ЏFuILEIĒU딂WRR%jlvsև5}T>|H LN'3sju|};68"X 0)١aϞ?,LWPep{{aDcΎCnݨY}ԯ_{r x ~i֮]˪Ur]UG?,ܽ{(RSSuaVx6ڳg=z7npE_6MHH !!bkkˁ]Yt,ބfXZ;ի$$$зorCHII_~1b('O~#o/ؒ!e{}\rz݁Mt>]?C.?~⑜i>}W㨊H -WX$!)l 6nܨ wΝ;&c߾}ڵ ZިQ#fΜIDD%8W8e7Ҷm[CsOpp0vvE?~ ex"nݲ@E^^+Yrkr%c-+&Z%^©,oܸS#FsQE V3=-OZI'vvCE;;G(?aFݼyx9{,4jȨcccٹsgq̜9pV^RE;v5))UŅn]q?Kk׮Dݺu-: .дiSsHKKh!>|Hll,zjfddΡ ۻxt&DGWa\mqb'8-=Gʴ谤MDT\nt]͆yO/^ftr*0uO%Kn~'Y)&+ ͏?vH&H\TD6mQO6lк~ܹߟBCCIHHիFSiҤ 3fT ̛GQn.#Fҥ3P%K\ߗ.H_}(ٳgԩEv-޽KvN";;]M/_ѣG8p#rÆ;#\Z ,\O{wƣ7m z60Y)dgpӘ~um[٠__:t芇;6£wZlCցzXM^u.]2o>}ڿ͚5_RӤ |o|=^_|Qf3t7dsof.Ե{as=v, M --R-~:Qi]->Sr@sC 6N5I Ki%?u1G/ *=DI~^ْD]FxmHaA,I6HDVpptײ ,Xƒ8y4 oBxÆ BXV¡C/@PD-ΚEԩlȀ}?}~hx-sZV+..WWWņyyy޽XBzz:w7)K +5joFFʕ+tTIZ:Gx ؿ8$gISFmOn3O=- .f:7HKf=SF]Ro:yx*֯_Sڵ+ٜ:uJNPm"Jcؑqw} ٳPٽY-nԨQ!$$\4'X 0@IOOMF*_YG@2nQ!өSGap@ȨU}/vԩab"a!ފO~,+r"xK-tercۯ$S%+ }+|x/$x G)22(p˖-e޽SUխ2i IDATPY1}Net4D:YЪU+M;ub8m5-Z3u8ޞ3m>< MJJG={9<3_TNLܹ숎VzG`D,<7tS1!̊CYXD>!8;;=s[Յ{<#yy6:uj 3gN&**HpfͰe"Q (g<=T=`zjrHo.'-_|{x##C/\͛7%??1#\ccF?}֯_XU AU_vbPd{[N{ٽ8z<)}^V>KMMeTnrLp$!IAHMKE _ƿ9 6iHTnСU ///:wLLL /@zph7[KΝٴS^4n%:*HK˅ 3;FZZaaa"43b'}>|Xaog_*/+,Rdegq-_so؋l?ǠAa88 jբ{_< -[/~0R&ٸ:λq"ߨIRRR8pm۶u-BB ,0K  j(ϜDZ xȂ* :$;#7' ܜ z<{{{ *LܜUOOOzɯJ~ Y  .eTeQ&VB‚@$яc 87yԠ]@Uŋ¿%Ђ*sߐeΝ۩o鐸!Ef$'55=\ѧOywx1йK>]M7w/ߺ}TsHNNpWB@h`X";s&yGUMo$z (**RT+RM^^)A4;qΆf7$Ig?=3g윲y}_QsW?KӦKxNNٻw/۷F*mBQ]\f "}ÆjBg\xPWc0lXD&,">>'ФI|*^~[R/+Z5E9'='=%+\J+ spA ͆7DҾiШm0Eѫ(xyyѬY+<=HW"<ϮJF aٓ9{,u)0_BB&26#Fh"8#}nU 4T]7~GKϑPvUT/g63dϞͲ_d޽ 8>+_mhBHQʂ_~n `UWm `cG7ƍӥKq̚k^RoJLd״]7 gzDBQ5NIIw.D~¿ONS)f S^{M,~5;$&&+o+oʏr-YNŋ/Arr22228uo_v/Qu fUϤ%TȵZ( R$n$gЪJH<G/^NzNKr%S5 Ą Xb1Zj2tH3Glݺ ¸_':wɓ'ٴi{fѣGfӿ֭[Ǚ3gxw6mf"$$̷ǰaL0gl 림رhX}{́ZE' ̗ڤ gϖx}BBBX=5N~wgoW8q"g@/%%ǎ㩧)( @š3~<|ZZvAy?B |.~FYwzHɜ >>͛71Y\x"$$(fz|IOOF*XNMִiچx?8РA޽;+۶3f +V`ذalْ={ҳgO˅h7ciq|P]7d(\FN=~j֬i7oÆ yqӧa=t eP7;-[؍;YnDi |g BN>^/[LzJC+J4~U\(~gAbb"blڴ?ٳgYBg#G£jIQW! yM7o6Oy/PaCWHj8?:|/:mz9cjt`.gHA 13s[oqUVƋ/130{//O5k6] sa֭ѫW|zxx0p@Ǖ+WIKJJb̚5UrEwNNf,X9" )?Ϛ5Ol:+O| k32_ KYѲ8Mtm@ή 7o^1ƒs➯ޅȨgGO~_5O V]VEe&C}T ,K(?'B2?gd49)SyV*ӦMcϞ=l۶͡TZ7ERR&MbƌTZ=zM7i҄ &0i$\{.qgq(t jJQvz ItXi[FoES!;nG nݚ픶~!~zvTBj&X7Fcj;e&ח]$2yL&BD )%јCyM&#ޞ! #- I=kJ幮;S&g̘P0@s ߼y3fPZ56mʄ صk{v } G.\Xpތ xy-5(\h5 QǁWD-"۵K!N͛n,bcl.Zdgg믿Odd$~f4'fsw0|r<#%+ jDJ*pA 2gPjժtU5\a2IRRS eIvv)EzNMa%|KW2h ~RSS Nxx}({ a }FpLٸQ)'з\,xU?_},c6lפ jϒmo!xIh򇑎7 :MK)?9w:tcǎ >*~e?1ryVA$ }F#~~xyyPY_N||<|=C̞=)C3ߟpe:w\ڥ/ d,_Od$Ԫ aGh3óf-М8y>p@XgmfmѦ gƍ}IܜtNC˖-8p`/?ҡM(S1T ڕ8BB13vؼQP(W^" ɨDQ9sիWSn]*Ud79zh̖;vș3gظqK;;B?E~fk ˗/Ӭ uy3?^LƑۃWSre!ՊåK8tg ۋ P"XF9~/T ( H ߸I4hNVfjEҾ}{ڵk̤K.yfdKرc ]~ݺu bӦMuvQ" x,l3|xE4+Vkv &h&Vldc ]DD,-nܸ Z(X2Qfŝdga@AF܎eN^!nZH)B}gi=wԻ~xsyg-Abhy.zo,?甙N')o唫( $BOrG@hs2z~=o>hyvaYh0cO@ rErܪ4ߓ<#9C{W [ L4Ʉi2dF$&$$5~oA.0EBɍ7شim۶VZ?ƍ7WT(-ZD5LG6܂$tpUZ׮+WvDxA-c{9hf.G)))۷___z꥖1Q !d'Mfuuf*yDžz1ϕ[ +,s }m򓰰y/s.e(@J/-2X `MZQ/9-or}*H~AiUFlH {+ Ve˜AFzJpp0QQQc+Rz `{,fۇ/mF;5Y?ƕK˗ahؼYx_vLk)s9&>FӻKo(p+QBB;vP!}Vg(>}V'_IBp^j/ca4Yreά]wU! /E={BP0eS!ˊBy3c.CU4w6^A>aaaí;ԶݷJ`kKpEڵkGhhjD'+au*BIxxKP( 'b q"""h۶-7oٳͫL%9dy!lmXX*LV60p.Cπ쏊Hͅd/ gOJJ 7n$00~)\B ;l.1˄p3`05p  cpӼZ !aH)4;Ҏo~QDC*a< JJUa鞶:X9L+LKJ+_XHK9ƪhK V\IavevHe.֚vyϡgف▏+ᕰpx˵F>g=nzVJJ+pd8K@@u4?WG^sܓ:EgV'_VsC+K<#?[6msˑc+X޽dy9JZnCy~ sT"s/[H2Rq*'OҠA8^9}y$++ ѱ8зS[Ҿ}{زe -Z^z\E; `^w~җ3GŸ{Q֮%%p+⁽ ) 'yʳTk^d̠1L&oDRQbzH5AÇӧ@1f( E!7wOjԬt OS^c%+8ǎcԨQ6 L8D2h :֭[֭[w5TRfA5=p0ěhέСFi(uGb&~?TPU"1tW袱'Ҹ Mc.^3.))Il +V@J~xA8qBpZ#"h+tp0ڻ($''sE4oSSW^]7[˰{ ndggP(*5]Kԙk9̕kIdgKlc77z'K./ϟ'((VZߟ:uyf9b޹0h RRR&#dwww.\ŋ9zh9QE#UρWqR!Sh^| BL8űZR h c@*G?7/^,߰wLJ?Ϋ_7O\\}eƭ"<+V0h f!j*9qիǠAbÆ \xߟ(ټy3 |rbccu:x{[mh lJ NVL * 7jL IDAT'`moT U 1fz._Ύwupi%^{<ѾH;<)ի3g̙C>}G_H] [\;JjQu<<9w{FjL$ԩӓqcmݻ3g/pw{;;sQ0›:TVE, O{BѣX z(BYf 886mč7J컶jՊ۷(n===Y`˗/w{IUDm X RhMnXq Ap Wp z*vڌ1RRR0\?}=ϫx7m]'V] ( Mh&9,H-EHkK:WFAim[r;z?[ J+Jap#=_C &9%F>__OVӵk\ǩV?guY]v˝Xx!)kԯ ߲*· {{.^|9w(Y7ls`}FOZZ;_1p@Yr%yCsseΝի]Z9cim ܺ^ 瞃ٳ˷H;zTs~b ĉc0|8Bɠ>Xb'u(ŗgupvY&L"v_,mUXs;bQգ;l{T=,˗/ӪeKxQw|r?2|DDD{vL#~lceh|Oxf51~?zzeUT!(4iBJ/Xpa ѴiӢuvҥ ߿@dd$^%KDO:ܹsYdI8Q۵| V j1/hRW0YO? ɀ'q_ |4s1K!!غi5Σ2Vk/kQVVTAl!R]Ei#,Z5/İt"Yl16o!R%<<>4nܘӧҰA={wOs<]""+Y;'O^zbN(5ח5k>b=<,Z8|*2}c///ZjEV|2;vӓmRȑ#mٳgYp!ǎcʕ%R'N`/^\B| ݐ}!!!scHJJo߾~ڷH||>gP)0agϞUP&sΘL&bccټy3UVUVb*o>fϞMA*U} wwwϟ_S 4V YVh1_8YeirVXjpްA3zm)`r)wWCZZ4i҄Rp?www7ZH`MVS"c;KH;6M ^Eib2ev51fO;FVV4mڔ&MNj|9 ש0| 5kFf͸r ;wݝmX*\va49p;w.Je(BrR O=~A)zfd /ҥkW+f˗9|pγ0((nޱώuAÕ=ұ5T}^0fe" "")%!0ɤ$]FJS% x.gyzmpE76SjUz\?h36*WFQ,yk ߞotf* I1XzRR<pmHMM1yg@VcJuw;}xu<WuSn}M!R(eC gLzZ"),a›B, ӿصk;v̙3'߲?΂  vڼL*6MHpc{ Wh772DR߭> %0Sl QQFOqqqDGGo>Zh_j"h, md !,M}R=_@ĠY ) Ba[aJ޽ԩlٲSNihӦ sε+-ZĢEr4׮]_~)S27Ull.WJ i43k_~ox4M'lԽ;TVJN6L]E‰%2ڳ3@ߟ:5@eQQhoj !Z[]߬-'3d"0!: !"BBgq5lBB(!D!U"sMB({VrΝ\4i҄Pn]~G|||[. ,(P[ _|@sjqM&ѹN:ncӧ5:{c _}+t{{;ܠDpqRݠi]JJQ\@  [`5hC]S$ 6|Qka!y]|_B? B%Ro *3˖-cÆ |7lذ-[wԅ@ C/yCtVE{X#ROG0k`;&p5GBr\gkzW][ЦM *гZwe˖QfMx'>}:&Lf͚pM '88%ˋH7n̰a  y;u鮙Dge9{D &-~vM~Qc䦧ԩ|&\oHWڴ W _ʍ+Saaa,r9mϷ-vgΜnݺ }n DۄͺB !*I)frv#^G@0@Jin1Nw5jlo-RҟwK)[sp x['=rGY,T ^0u( $%&믿ѡ-+)gk׮{̙3www-Zs=DŽ ;HIIдiSVrGf[~^R,!zyEp)׫CP( ?si":QQD,g}-#,plQ~9111Rh-#""BsZ:tooo4iBI. ..%̋KЦ:lS/?&NT~pP;"8"dc+ >eO,C1ּ7gyb&/\ć }Ĉc޽+##Cֺ;e7 FJy6r Vkh3>d⢅`B[}Uò*ժVQO,BժV!1q'NѪxhpZhB4iڴ 3gLtvgBO8Fж6All,ӓFQvmu!w@a k/cItG3 x &EaM.YT͛wwWE61B]E/غi+aul[X ۶Vʿ~Q^z7E$BМb)gIp78͍hc}ceE3xF]f "޽ZIFHKAf#Ya wB ׯO޽ӧڵ-[p!U#8r.7$^hɫp~"gIRSf͠ysu}|Wx$PJ<|hW=wT دcۛaii+qb1?43곺h=gw#PMQb`=9*&!D8:ֺH>(F>>IB]$ߟ6mZT<ǨQAВtqzZ"]숧'/o/-H·3tA 0T& !Suh4rU;EwR $Yڠy F[S{j} qǤ$bbbחǞwGF; kW :!*E<ǰHO:a$a- )8)i(7خ-Tu)J v0իUQƪA&!!~;L\BXJZjJ॥sGD+4T,~]U 6Fɓ'r9NܹsTI]ZRRnck-/-z+ʗ+3e|B+\8yp轌v"Cx Đ?'L ;/OY:)*]c~|.EYĉ4j؈5k+PLKߟ'ҡ}fIKPmm#F~0a3rHNo2,`̖C)<[4F/6 #$+/B||LpP um\Dr޽@%(^^.dmI#3<B<"Ri+AX@,%!B+*Y%v0a/@'`wa4~& v 8gϞ gDvM*x Z)9DS|>`mJyP  ̢X[X ƒ(RI͛`ѢEN3?{m~6 ؍Qα^__$W$$$;w~tܨVaaazc4oޜ͛1"4h%K 7W=zq* %b[6Z|lJ;xE7 f !pkx>\"XώZOaD)rI mW;viu ۱vtFXQ60`̚9k׮Rj5^~U  PK[Dmw=tu~׮]+R@@@&K.q!Ra]ĉMY]K6fQH d;ipsSM t2E(:%''wfGI:thDp#Ԇ\6H)kv]OZ !vqkmp8e)-f\["2+0v~֎lcOTಂۃ'Oa o/7![娠$zZOOOɓ>>>T\Ǐ۱S+>BRavcX)'ƮSObxIB½k|,#GU VEMR^B)噼*QRKBV}{rA}!ۀo!Dg}B  !wlKKN`3pQ.t- X"XB!hgChBG'z6'S@ƣۯ?6!ŽO!xN}2^J)cRZ0,0  \EL1d)"G`!l4f({-enH>mƞ={`" IH{6nKuXij\j'5֭70fd .y( |eC6A[ 6;:K/G%#tV9U5k!DEjHӏBԷ*7X ||| 郁Qt&+fǡj"70Z7\ 2&<,|[J S8}Gv?oMBBӏ] Vpk?zy(o aE `y\t-[EyUwyZ5X1\ <"4 o@.d[*gљ6K)-WhBN޲:h)W:=׀ R:IJKq j3~B)V.׏R;z]wH)͏LZRϕR~b,&.b]Jl4fpW8y=?ƺu_%4oބ֭3|t|ꔖa1fP(2YYF<=.(*u 4Ӹ J7CyJsΑ@h޼9;ҩL]`gݻ_mۖ(X BhNs7}taJfģͺ6BWqm=5Mz0f!| EQB_K)I)be^*4lP6mK4hήXշ<=EnLWӃ޽zPJCUBIHGU=ԀPH)6fxUUZ[km-/XJɉ'"xFD]휧AZ+gذ,0_JUM>}~槥qy.^HVVիӢE lэ1cZ?"VNE1i= CJi)'ZbwƏa2S+K+'< ]'Fw!DUBcu}ݳ5n@CIBGb!bf?R'ˋ=Fj4lX)lݺoS^A݃}{1wQ~fwM/BI#WXz "EH]{"r-1 !Fʶ1 aly,w33ssKzMXN7X603sG$c7({rQ1hr:ob?ڀBHVhh׮%/t9v{s,7.ɩS8x 'N@Qf3IIIhɓ~Mzc0ƈIej Pu6uiKFrؽ$>n$sKIFulhphɴ>$v^=]^smG?2N CKxU3M⚿.wOyX>J/|}YTU}xSQh`Z;  1pq xr*Goֲxgig+IB5&Aؓ'XH\ PuBM@oX9SW\b"P}tc|FN5pбG,Xۆ ^cԨ*E*=-UQnPվC_%Et]j#EyNQDEQE ANr)?E:BW@#`(E1)r)Zy}T(%^>v1]FQpEQ^E ^Z|J(ʝ$((h/n'NG]ADp5Į'oy?3 `:ɏP*1OeY`4͚-.7EE5P-/hA^N-+4 .-%Oe/@UUurKv~so]g:/׍ m^B+1F&pkeq q}/ CQܖyL_DDDETT`?a4hDG*wbShEu4Z?Rgs..U*bm\:c]"w(]<[UUejE+7|{O]0KQw^hSU5\ yؿEIu.arEy TSKjv>+ m,)W , .Hr]g4iBll,QQQ5_Geرr8硅E::%jU:qqqr K\/o76}%;Z|Xh?@]ewC%UUs$ ""!.v=1M4)999 ,,җh,Uhމ`26^B5׈yJ||<ފgHj2fΜ[w:}ܶ=4[DLuD  T Hk!N֭iX,N8Ν;)^_F GFFJZE+2BRjzLr/5'Gh-fr[L|mfGQ , {sCl614k6Nvv6۷WvDRTT+5jk֬p[:uBf8m\فJ׃vدJ6@ϋ``V]yxtzO3 ]pjbm̀P%yV,Cц&%AEQ| /ևg6jԈFmph_=˗/g7N Dtyypت;b|][oW٪;֭0iS׵ZX,X,3NGtt4iӆݺ1do'Pm g-Bg}IYab=ѽ"Wq8:gϞ| wZu]ǧ~?~ AA ~Cݮym6_ ZxAxEO )'ooZȁX,dggp8JیFci"VZy[\rђf_/&FTz%ލ۸ql\'a0x{ ٤ML![(XyʞDڼy=m\}Te醻:7ޖs_SP?Y5=VUU-zAGՉp賑kITnBvc&~`ۿ'S\տ?C.YK4,w1D L*Izڶ/}TvI{ Ywi9 eL8_^XmΜyLzt*& L17 /?̈W^jj+`!`/򂴦>Cmo|Wf~vwhƌiqK O<4ƍ#2ҿc2NO` 1B`STcZ4́e0`)5e_V ^B\111|So vZoIrI#]`> NvUۮMM܌*/y#FǤG2+L68n6f8[1ի)a*ɘ`!پzŋ]ٟzmj!Bw} =ؐfs^g%;}4}a+k}S xqՋ:'OT/Bqڼ.f<ejח0|صIiŪUHmӚΫKtZkD'w!|yx5 30x MOG?k8CL]v;gxύdݲF:xdv]D ʊH_ԾնkxuϵNEsЧORSpEt5u&B aZn:z`C=Dv-ע ﴾K;N<-OYK d_b qbɴn11}4=)_K>yx ڦ,cuh}omĮ/mכe{oBؘHnzm>EZZ}ov"l66brrrcZl8Ng͛(3?44h$++ HRRL&q:۷t1䖥>p3ϸjbzCaFx}ܕ!E,Ⱦ_q]1A-**ncJ=}O[V qy݇vCq n6oFQD  I޶$ a%_C!!!`6گMۆ ns%(DGGcYfMz#F8+ɌP8y5/ (@ lpZI%)\9{'3o/e5§eDHHٌ` $$lo:K\qŧ&Z5?,9 IDAT`oS{BK/١}< 9k]\=rFQFXYGxdVϢSװO=~yܕa0c zvY?m8E`!4Ԁf'$$D!T͎XIFAL) X 4a ˿VS7ԞSΰK`قRoM[&{>ϠE B(S4Fo/Q!PUFn)'4V_CI*J'U%=nŭ45H飆ehLsBX<9lzmi1i|8C?)O\K;^ I E BfF f#&o#?D Ba21bnE Bp}7X -9Ul0мc?/T @Y7m0}[k]5%[Y  _ŭ`A߬]\="jZw]Tcڃ`$""g'<on[و'XA;K/.^Dpl*VʁyxD1ݮe9U7 <?8pڿݯXqpeZ^!Cfddopi6n4i`AApGUUDp0H*I\U2Vw^wqOCh޼\|~`~1֯wm<} A0e ̘Q#:0k֬Ϙ1# >C ~ %‚lspB1-yz x HpX B #< B`wﯷW0{=YgϞ-RP 7@+?hIN113ЭFD  STΨ!U7̒;o1 &VSw=큌 AvC`K'xB.J_`\pddhb#>% o=ٲEL-AOpx\"k[1B.pCP\ tn>(ha~&#Ȉ&|l`nw`B `Зfq¸!ѹsg+..ncZ}t~?Gn |>>`*5haӀbհt)$$K坅pWb2\ۆ2ӂ`A*l6;D$(Z "TlrOb6R ]Ѕ7| WɄ` $$tttB'Ÿm* s|vХ  եߝ)祠((((}`hٰla0PUQ:m%ߵti Û9yjUU)..&,,HAlL} fnwѝjzz^`d^׌.,6܀VS~wv}  'SOA?3\9Jt:ZXVl6[...&//,q]$"X'7/TIQ`jvLPwتy%}' .|̜9S ၏>ѣ!7-y 0}Vg,U]N9>RR@a2*)  ANvqqbZlvr?,ٚ_:SNw#FpkH_PC2H7#1 "XS&j*6¢"ΪO"\Kqqqb76mJ,[q#Z.b.׹ڃ#=PU$Z8'ѱ=ϱK%D6/3mԖm~*BX[ciK+BjL4IVRf<6U( \'jFkh!Vv۰~ݣ[< |&rU8lro_E!$ĀjlZ16IV@;v\u0t(LJ؈Gmg@ ݹsPy,O"\mDt7ؓ>OE^]U:[:d{OWԗPy5jT*K,`00j(lK?gRq\>;je[V"#}:III >O>T7T0f =b-,ZNj-j%d.ƍ]"#^l#c6Gaxx9),(d++ {]+<Zy彻,/"a*/"S…СC5k6ͯ5뵱:Nw&=$ja) }4_{رGF0{GBà%)0X dwZ)2SظaGfҵWO:vH< ,Tw"D!_$5&&37111gEGGY}?Om/6W_]„Ǔ'_~9mZ֑9/Z +kW 5_|NQQ8vƍ +l>}6lX<}=׌6kL.{wXn=~O sҖ cbI_^Y1ʡrΉjwo}g5111UZDBBUsxg8y7aECj;1֎;yhpr:dggcX"77Yqøq㈍%66A{D6nŸwr͸xFh\VSxڿ]i V -g@p82vSJOj _ޱe~*oѣ̟?uq ?`ڸ'rw6o}xKye<٥}]Ж.LIʷ A !L8kKEס/ڷodzzibX,X, QUn#&&XڶmKTTT~W=%" xV+!@B{4?+̷x|v1gqDf͚3n"DauVQ*_Xk$̪~֦q6tb)}%˗NEmBpcZKB=CR ӧ:Keggp8Zmݺ5f9hΛGx, m\.vkVS紒J=ݯer5//]Oʹ&"X!p:!p޵k+W,3ppafsKѦM暠=GG;xD܄#ZMǁ۵>=Tz]HLlڷoqe=?3YBMgY,IyO  o@"--49P\ Ӧyڿ ~@_Z)p Xf^AQ2 ۶m+)PD  N{IJjU&${bs w熂nc,w#Yܩ%TR?u00$!pFv}8\y%3gBhh4Qv2"¸K̳;$4oFFb߾}( K+d~`t aܱFfvB~ OsvڤD L֭[Lj#t?~|P N$<ïk5۵kxQ}t$'si߯o$11DqwENNNiEQ'668, ]41Q|KڵHV0Bٱ#vnyDbb\bX@ۢ|) ;pKu5o>DG|:NgYY 4MቱwcѲu.N@ݽ鈋#.sӧѣX,I("X03-` bDEEҢES7oFXY "vA4P ѨՊ T&S ݭ.S]JDD{3f`4OtD;糍?ުM6 '))ɫP,B鈉"h$Q<XD Bx8$1("XӼ ,ډIJ_z)/T!"XD0dYfСCFX #6ҺR%Olٲp6mJ.] mhтnfD<qJSC\v̟/ǽ@3f>%>F{x5MNʲu6rQJ%*A.g->Bb4i͊wbDAG~1ŧ1͵*c+&,,]ҿÖNQuJK-sy$xfTKw E+6Çgl`Xsؼe+MIMm+*MNN7oG󉉩 -.ΧD~.g/ ++)A "sϳ#"+x gsNlz0%Z6 7*,,d>|( vGjT5k&#W@DDG1HP‚Bh!_k"*z+$[sQXVfgF`A McU"""Dg5[$&6bk#GдiSMoYvIbbb0䖨!h::`jg[4^dq$-h!@PLddk֬ ..d5j󶢣?~/#`",T_È)50.]taȐ!e?~{n\̭w\=lx1 ޽"h4v)S8rQPPٳ&/ICKԻ}ˣM6t:8ڵkKE@R%^< ˕Wbs3ԢE믨*, '\˕*#K=+T.3*/y#FǤG2+L68n6f8\a!?7W^ᦄ^8Æ1L@~@¡J sيN-G¡\6x} =4?YjmZ֑yt_=:Q: s[^Ft:Pc_~!#FzEEEرǏc4i׮;w>ke˖1w\"##! ;e̙C>}U\C~%\wfȟ>Y{ 6uӦ',W\U-`9<3} N:dCtҥM2k#<"_ @"p딄Dluӿ'<7IVC,>v;b!v Fqn3{6˟~Z &"Xi1[^z躇KOI.j-ؿ7 fȚ&>2N  آ r@p`cLV &0i$ZlY:?//?lС=zҶ5j th!*T"K(NÇ1 V^?r$,a5fHN'> 44n: )((`֭Fݻ,O!\dZY/Њ"ぃII|~4mʆX~ݲ]ҳgO~iO@oQb!66V '"XPvzQPhlvFi-! םDFWJшfK.Dx9JrUD\"gIIr7ǯή'sh.[Ο}ƩPvu@VеkW222X,qyUk`A$ =EHHc27p.TUa{ կ[ڭwZ\TU|w60hP(..1_2\ U" Ȯ9Y!ѿ=7pÆMy7|Zo}m汭I<ڱ#>[n!w0~6t~)^}9"""իdddi&BCCi߾=M4#"X03:deoP%l6afBC-&77i? h~Ҷs3p߇8XŰje<,\cǒS`@ jH7I(t凟?ܡΫwp'Mbw~uNOڹxrat8S?/ެY*6UUq:#"XɌdCuJtt4cƌaѢExE7oʵ\,ƍ#"2"hp^uyZn CZE;37|۷&Pi"xݺAJ?Ac B6niOi}>ϦVm۶a0HMM%))ɧhז/ӉSpE˖S]BD WTg`X/yb,>ʫM||CgSxTP۩>x~˧`ݵݱczm{*+Vcǎjꬶ : Q@n% 5O`&yT󳾇z*fqsWy_F#;vcǎvvŚ5ktвeKtsM7_駞f8K߉Z'/"XASTŒ/3qddɚ5ߓڦ5ii=u-~KHJjw om83y2BbѢE̝;W.  Wr= NᝓP 6*fVf޴9 :tC8w}jՊ֭[yt;r$mkjF {/Eұ#5㤓SUA?*" ^E>}HMmE]Y3eR2220Ջ0@ˌj4@Aȕ*eɀoSNoߴ4B32(nт=))ӿs)"X yyyB;v0BCC~06t;F˖-iժo+]fĉL8ch![Ӂߎ@&IUpO ZغBϚיΥӱq׏t6oLrr2:t8+Ks]лGZ̟A8v-}LhAtXgdaFBʼnLjn/d2(oN8JdD$N=?g&;;-[`ܹ3ݻw'##EK/YvF O8p)e%3;{)[!!!vmU`ݺu`YvZRDEE1iS۲®]tr예`abмE+v'^*AmTdg璝}6 ⓗ"+f޽ر(z]Fp0wܳ@d޷o0Z_DPVG9NQGGkI}ӏ9snw؀ae޷lْ-[ˆ ltԉ:˴{a̙gn59b`aa2izGfrAEѡtM+Zf;EQ\ctNց꺙U3YV!{zjSJ9-2Z뽫UUQU6Tt=mZ9M6 ZêkZ9ޟ T9(%'y5J{Eծ%Tt-S"Rҩ3hJuQq\%m% M,LۙϨ\Ѐ' NŪ:ՒB:QQp*8UTT,杪NOLJJ[t ۊN!\֭[9v[fC;vX1A%VuBC}ip7 ΌI3`%46ԟXz%gv_.gA9Ⱟ3 ,XwP<3jOTTnJlk_}k׮4jȷEi,>UMzO8"=]&TETzP(W@*EHu`#i1F?Ҟg}ٯ&::VZŚ!X*viל8s >pW&""nݺy~N䑾3xRqu~\cM"}ZZ {Uoӂ xۘ`ٳ;wŅ^lEWUE(B7kPk'hĘǣ( ,[8݂2$f6-ZXi!%?;v6vUiҺV;99PiJNhڴ)Pkw>,A E;R\qZuUzڬVkisJJאgA nv+nht N j6l`ق<~8ؑw䋳hܸqB82*o-ZwQ=Tz۶mlٲܼ\VkZKWHQ,0ЉPyPD x=z4ڕPdee1yd,Xl&++-[p8ҥ ={C;T(Zzĕ4jUUy她GOĸx:$%#S▿3!mȦUNRUKIJNbƍlٲ>}Yus ]vk׮|hnnΒYv䤑rc2Ga;8vN%PtuZB3I^ѕWQѡt J׸?\cVK -16۩ ӡ%*IUv%9g.f8Nm_U#<"Ħz  PYf1qDz!RSS^xÇcbcc袋}(**_~ .0N_ӹsg6m*'`A1pٳȈS0#Pt*-{Kc X%1RTT@V BL4CtEٟ%3h䥗^bݥ, 7|3?mڴaȐ!,Cixuz$4!'MÆ^/ǥؖ9yΡ'xӏLpp+#/}ͳ1&~?@dd$={T`/%}4n€ضmpQDc$22[cRT: .Co0ҴY2ʑ:-> IF;"~GN']tqr Ncw&U^"_DӵU Qagn\חFsх~t GCRKbKs a3 Vdd$]v'N`͚5$&&ҥK7njw|wkCGsJ "/rp^'ŧۋ(JWTՉV:Ihܔ07}QDzz:;wc 2"l  ^*ǣ&IKK㝣 58S'8|0:7d jY&2%11w}k-̈́ Bff&Vm۶o߾Bhu#9 ?ޙEU}MqCQA5wD33[l~-󧶘ii߲2V݄Tw}cfpf^ddljGJٳgٶm* v1nTlH˖-3f rriZ!ueCZG A`D"򷴢Hp%}D?Q/'.y*,,,Ԗq3gҹs璲vڱl2N<)HrKh,F?%l6ƫdTaX' k[ӱw\8{NXXC=k׮qFRSSoYJ}"nhפQ^ v܄Wʮ^uu|S%z/-,).V8HLL m6\9{,6t-ے=IIgJ-bѢExxxذdf͚o]n`x"Fή S6^#FPt҅~CYʚ'N!lUV)Bpp0*MGǃKg,?2^IJ065W v<`Kjj*_~%ь3J`I!;)@%aie˙WƷUK3B~]-Ş={ C E&׿>7$[_݊Xm>}G֘6^43f̐jID ͊ (GgYR%ܚyO3><*I$((-[Z_>q)75kpppas^SgcٴiSf_aRK(bC!{6l[v"2ú˖Ud~=vEQvyW}ImaaaIA'N`]w&''ɓz+ 4m僃al?l>XȑKFں݄pyuBf2prrZ.LFFFɵlVI5cE(45V %iNƣ;H:ͺӱ$&&ҿ06:OO~GJP* a: oESVs- K )))t҅l|ٳ;O=˃MU_ڃqtBUrsse< ,~o_}5<Ӥ]&%U㧟~"55ロׯСCXj:2yZH&f`zMLvtp9uI׮]ph4ƗږL !##M6ѵkWZjU#}8q_geʴ)34N>MNwwo2d.Ko"X`ɭz]czԴ ؕԙV,(*.^zNgg'>h)ƍ 6 kkk QJmj̻ѩS} VVThK"1{ !G!))P<==KM&IR!cU&4,@ 6Bx888 QJjcccŁ܈СC$$$u5Cyw֕g?|_n5+_O{F9kS3hp"v9dJ,D٭~cӼbm)%8::LVV6c<ϲe3/гg(Æ %77GGG*cǻ)yZ :pvɰaC*%ɓ'IHH ((`vɠAHL0c ZJ;Ю 6 ]]7_|SlhJ網е38gvwNVV7osU_c P"_Χ~ ;G2d|Aܖ%_#ONM[%oEƥju1vδoߞs+>fS2pPBCC$) ~~~8;9QeTjy3o} X t.Y{ mI$qy=ZvĐ7J,13E08%/ŶڠX#--ٳ8{`Qg¹sop`8BDDGa۶macS,׮Ãa#G%K\q K M e8%[r{ KK5]t!;;dxWx2,JVvm۶Rmӫw [-lܸׯAHMU]% ƫFgg9ɂ9{vbNɏ?n4nPP={d֭$%%T7_yr#Kh rCK)o,4ܮiVSZU~ƢaK)Ûdz~z_H 6lΟHvNÇݙ4mJzz&ba+%"&&ggg^צ]vr$fɌma]T^]@6)@J$.Y5jǎc˖- 0[[[?K3a2-jS8̳Oa7*G)'[zsbU]צlTd:)x%uF ͛y3e8}4ѱcG:t耝yW+LgT$bbbP0*u?.Nb6cefcgW.g*5+ǻ1еkWٶm&3O/8}'{{:ct؟\ IP+D*U!9YqtjB޽ E`aPTCnXդ-z=ؿ?'''9(Z%"8˘WDgPpwX(**u?@ F=w232cʰQ51ޏ IDATKZȵRK,]z4i"ER't|sfciYΔ@}Gucŋ4o͠\-cqϽ\΢rg~>@nnM"X/bXRXDrۋa59gRT[-Z$SNa6mʈ#9c/KscK.OTE`.lڴFMߖ*CD"Hng.\  ""-ZA"jBkr Mq | &BCeI\΢K?H@@ B+/_مg%<9RK$D"i̞=T6mDzz:2hf>`W%o+BӧOkŴn;͚TX{w$-- {{{lll8qZ~߿5A·5D"4B˸f޽2t U~ & F^P ^i~vvP om,,K\]]Yj87B4eVXУs< 6mbȐ!XY/oM{ 7WWX8YZQ\!ED"H$}# XkBƆ%KK/*_f 999RK*"8;ӱܷ/4k&Ƿ.02;6l`9xlAZfmdœ>\2I"XBV)**EQIH$*QBxƌ6ͥgϞhر籠^O:U^ xQf7Vڇj/px~ʨQضm۷M6f?~XQ}֯ݵYKޕ-ERPk,lꇝíy'-V*MSH$IÇIKKGuPH,1 `Cv9^`E"A ++FIMMW^&@$h"rkVO|}&x\|k;Z:aUfXCXׇ~JD"i\h1w\vA@@vvvôi_cƌaҤI~zy$&3 "I r\ ={H ug|猽SLvV6MFhTdwƣIK2Ɣ>JEA&-rr㖝IZ䱮/$&f{~:qq8w@xdT膉 l߾m[G@@44hPPXJUhec"4ΛZc]PMnv-cAAN^7yS?D"iԇHz͛;Sgǻ|2ŋZl,XDRcw,A{B`kk[} ''gggʊÇsK޽KOFF\EQٳ'...$H,iȂX+8C_J';7X_MIBSOz~^:^~%kl---.H,*ݻwcccѣٴi#Gرcپ};EEE2MR ˄?@wu |k8ǸccB2Ϙ8t(oT $$IF%Kر5-RC۶_W_͔1X4P\vheM]֨Fe=Ի~G)%Ǝ~[W/ra[}ژ֣_\Vb„ IRF?v{+-{zvN>}xwoΜ:0DZǿ?xDx'M٪U+\]] }ӧ.1'/˝wn{Fʓ!EI2ײ]ˎQwƉ?,Ux}{+a|4utˤzTc@%>r96mD i0^fMm0.h>/ RyۣܺS^uĘJ#,V'$Tc$)ANh`Q4obŊX֬ -* QFؗѵ6fJyMgm H$ bpss#""XRe[nʕ+ٕ*2e ;|0ǏgҤI7%$$}v41|L@\Nhuu;ۍKV3h yC{ H]+ HysrKćI۬4n)%ׁSN8oP[}H$Brr2Ǐ',,rnRTܹFëa͚5pBBK.eҥfn7`*vW6(H$"mFff&& &22TKUeԩL6UO)umQ"Hn%:u0ϟ1kk*,grE{=rssKUҘcy"c[VӲ_K̫8Ԯ]o:+"SNbM eDK1=TyU0(2(zȡr_b{9x%?+WCB;9r(Aݺdӓ}sti?EE `?!E! IUbz{ݴ6gҾ"Ou+!s况~V6ڔ~߫ޛ^R}H$[Aaa!vۛQFq8Jb\fMͱ:u VZUc=FӦMMVI:Mډ̔Lrs*k9gɿ/徻ƣNDT g6EdImq$6ooMZ"~3x%?|իY:gNNN&^z6c͗_&7Wtڔ^{OO "xub GBD#m gH!ĆJCޯ"`݆~w7VdNTA|['2wpVO(7!%@BB ѱNOԶm[ڴiS#6y晒׻w&::{2u_K5,c9~J)4k;k )ŻB )..v!-^ڽ;w=OM61`bXXX0i&M#QkcV[BtW׶ nk[EVe1IЕoϛau]tFQF=u&ؔ\,H9QQQF/_NTT[l:YYYlܸPxʔ)&{͢E駟ig4^%ۖX|(}zɵJ>S"縒"Z;Dk^rH 666Mzz)kY_ilCxG97kQ='`n40?<ߝ Jv5 fe_%?4i\ DR7V\}{ոbȑٳ49 5Pb Ă lD8EQ\Y꽸t:X(OaJE\D SH,HҥK~:'O4nM~4M5Fbb"|F@O:լ3gΐ3ydĶN+kgg^˜9s~ KM.((`?q͙C&MB0h慇 Y`YdȑlٲKIyzy7D~1h3:voB_ u t;EqA#@b^u"!u}ފ!65l'b~VGD"-!+}_Rm6:t耟_ɓ3bĈR55PիWIJJbС޾sEF5 s!X)vvvwW_%y-,U۷x0y|zz[ɡhXXX0|pn݊JErP%D(gt@Xa!U]}s𫁨݉6xv xx؈@e/p܉609`AЮ+V6IB\ V]?\ j/ƱCڮ>J$̬ۛ,33k̮7ǎcȑu*3338pIoBll,禺l{YYY߿&#&ٵkaaa& Օ6?TkiT70l0v؁ZWJ9~8.]ݥ[ <(J{!i{H/pE9z7@[`?F. @;8A !.GzBD*rlB<а.Q<~ٻ@AA! j^Zehe%Kn=66XZZm&/3g˫˗/LJח]$''s)FUksFQBBBj^aa!;w$<<$ZhnL  ܿ?9uqcXOD;]Pr{pwk.T*r@ (**")IiFFFxOnx NV9xǠF࿊tBĢ 7b i0ⴢ(0~hm(A;ِ9 xTa>RQEQIcR3acG`;^OͻEza#.y]_iGVYfn!>>ތ?~ 111(rX`/FæM:th"6gffrelR$&.X_,]ѡkĉágCp[1`كJCr@tr=qqqRWmh|a*nx| h=XAr v=K̶?6BU%` dSx-r>)km@<%g,3;ׅSquuR ΪUJ `=z!Dpppm۶@֭kٳwww:vXc6oN޽qpp6Y/;ޏ%yr+T֖ l>o|Z$J>}ؿ?qqqu^E"UQ:Ng4 }E BU`Z?窑+e*h ڠWzѮSt0˩v(.ʓ 4ϵ{ <$y4p&Md4J^222صk 2ypUصk͛7m۶5fsUmq޳gOfSɒ+s8չ3~%9Kb[}U{E~syaKnKBCC9tG[nr@1/ ! r(@G BH=71RW6%M!tf>5 !B%#H$5C~^:M<}8{&S6Μ9ӧku/;hӦMz;mڴsaggGӦMMj?fHˉyaZ[^3ٖDr;н{wbcct}{% -[n9pRW*]Wh8v]O#uAeOy(t4 /V% TUYF4?6"wB^VDR9Bh"F/d޽XZZ_![nSN52E o߾ղSPP@|||Ǡ .pOH͵kRKFv*REQBuhn/׵hp\ѣqdeDU>g,Mypvr&((w7_Wb˗/w)UV\\\"LksFa˖-t [6puЧ*{O>x,I]g<=ߋP?ͬ,v͟Iw3_ivV/^}?%Kv-Aq_޷!CZfo!Ep*mcQEvRCw(Ю['6K'(r6hA?yVe3Z#ucC,Pl V?!g HOࡣx7muK"Tl( v BEQ&rsgƌ)ȿƁKx6BUT@zF&UQj͍ׯ抵M_gMV~$Db .$##T٦MPTܹTiVyfjT<>|0iҤjhO7$0]V;0ɓ'i޼yĴ>h4tO\\4RK7Hj8S0ի<؁17nX/"BT%mhaP4$W'k77Wos"// WWWl(Vpss $]3EEE8cc[+7coLlzI$Axx8Y@HLb֬YFohF*M4a̙,Z>x s￯NLLdq旛;ҥK <6_nnGh?p+W筷gϞCdd<'K/=̻~INm233/8st2i$t"5p̄?Tg*xv+VV3â矗"X%~R\C{D~n:wyW)./iwdmT:@ȿ{ү %H$O=J0}7o?ccg yޜn~RkkGaذaܹ}͛"A ~cժGӤeoߝ/V~xAihybg^{5,--K5Ǐ2g^3>#Hdd$flXz5'Fżyh޼9 IIL;//`)%D"H'66Z#(B;?:l#zo4_599890vXzw͌gPZ/kgvi۶I?>eCZ@>w&~~TM8}MemnL찣Τ,Oa^}—ۿ3Гں~qqrࣕ/aÆa bV ̸!CX lٲ?-^̩^#ތ"X`턫{ 2/\^vD"Jrr2+V(Uw^ܹsK}||6mZ62Qf΃? !B~G'fuN(qK"o&UT9sQFU6))>+X[PTdXKBB<2i{wꬪ,x~˭.c~4;hיb`Mxө}'ǭ3=9rp+ҒÇi&M^wl uh׎-#''G]B `D"RXpav||<8;;ceeł D< aŠ~&M>tKߌ7֜1KCYdY"xϞ=kZ䯿|yEEejp=%#S~mH :[E_~gf׮+.YV}JGKLŅP`HOFLX 6|6e=y TiD"Ԕ֯-,:a(UgGNMɉ.9 ڇ'9*+>]G;C|[gabiiIttju3\\#+kM"++s˥O/qp1~@ѯEt<ב?GGG>}???aw3uZ&N0dH<$/4V ?4 {09>MH9A?ޝӑ58nܸr'om6~'ÂywP,ݺt.*dsܦ }ʼJŻά6}[n%44u1cƗك^LaaTsMB|<z oG4M=)JMu+b>38 ?\ȬT.8?? #X9M.#щv%|lx]`ԩ6xc@ ˅ͅ{) Hc%D"zqssH 9sLj!|'L6[[Z xB;;==Ge}L M==ͶieeŅ ӓޚFϞZ(Jjs鐧?}ⓒWo',CWl.}ɶȾɎ5')+SLif׳/>Gqn1xstQ<zitu 8F#jKKKqwwvn&uϻn*$X(Hw1\\ H$VXnsg0g+]=pqqݓ'O4SS$OL~\^^}:waI$䕗c[<>.Re={;VfUsK}B>/alE(بG{^@A;CN{ EXrEQ !y#'E$v~'tF+6}t}8 8붝u@?]_@^]-5>6v#H$;t`kl,tBߘT^ll,#Fkb2Ag?GQ 4?|u8xjJgTG._\eqbРAl۶#Gʋc! ,m&C(JO`>Zng!=BpJQsmpz ! zr2`"@ |Ն?m7^ !,ҽ#B;$ KWVY{bV'')++{O%H$?yh۶TIk$9c+W- Aqq1>\UJ(5~mVս;KYϚE3>z%fif4ѣa̜E{-H'TYGM4mw.s"p>//M^r\X-_n"NgؤBl2h.ՉMK|VA?TݦO KP;V*;ZMӷ!+gY"H$F)((`ҫz ʬqVwȾK|&=rݺu|Dј399\z]C rhBV?0ֱ-J-\uW^>܁#GXܞ(ڥk]hѢm*;BFs6˔^|s?amuR-D(tWQEQ>։qNîIN]k_}=}NF./-D"EEEr$2L5}II8@YN6{Wo\5YMHr[i3^kgaUӦMr*gJU_MM{9 XXz D{V2'';F6mB= >e }y2-<_NyPne'A.c\ fL^cnȕ|фOD"u|<;"4r@$|ATz|f͢SL ;lbrpNJ RS5–66 ˋ˗/+l ZiWFxnˮ\յ899ae”lgggZumE|MǮ];jCMEe3O^ytPQGAڠ B?1u2uqI~r$IbccCǀ.P/EDRM6?- O3CZ|7\p_})Sd hӡ`Ǩ]xw֋iҤ ǎ?uDGG,---۶ s 99???i֣b̲u/^KF~!xp#^~.O>Igiy7Oe*c' TVQXMC lBҒH$#CK$wufO$iFRRח1}g]Xl{sknxu<Ν;h{ѬsR Dnnkliii ƶ '%1xc6/5[H@Ű^8(v!B(Q+r z@`?Ћ*NH$9Z"1oooa~O>&$)j/MrACP.D {a^!m}|}}K}J<栏ln';00'Ndnac݆aԻdz,N1ݡkǎ5jGч7?Uo{k'-n#''G&%epOK]7l>Կ7[Vpp=ކXڵ˯;#**Jyyytk{Iڳgbʹx MN֢eK'_}]ãt猫eҥQ}kg1ymW>?~VZ.k/BgNf;4c TcW"$wqM6SMar8 0(%]"CgOYYNŧ3O?rf/zU#{QۊR4N|ޔh`x 6ͫtvv]%iذaڹs&Nխ?Uǿ[o[7ԢO>R)^/*̚2e6lؠ.Co{[sѡC KSהutiU%L̼EM7BpqP?$]m2*ʁ3 #jjvJKM]c"^0Oyٺ(0OTeTg;w^\o^ܼ}p;ه30L&ӏTyKQaIѢ9N_!r.?xxppBB}EEb BmΑ#GջwvJJJS/XƍU||6oڤl\ب(rFcǎ@_ìbEfԇ/ BgVWVJJ_ MNւ%y+11/ k*>> 8;3U\a mFa# øܾ*"\Keer8n+++aH1*(,2mї*++KvH7Fm(iP3F={TLnw}w.JjԨQ`*-111j]ժ 8p[ֹޛԫܚk \wƍӶmv,N͛7rqb-IDT/9im޹t:> Ckn`,o5SOݭ^zU=6O3.dV\@ ZWIM)3DuU11뤰-7hoXX[]7۬re)fZruFgMsڸjnFe L.ԃO=is+v]|nBbg7LճgOʊaGu%$$ $P+vm;4-?2rMVϞOﺫTnCz .K<$i% B^mt6mhDaV v%p(Wjg*d48>0qcuIťQPή|xW!׮Ç_|s*[׮]/o.ҪUt+007 B0p{o@{lNog}6]˗E,х^v-LY=s5kgrД玈Fˆyu`ZT']}S'IOu-שGge ғOJ))M/t_ɲo_8Nɵ%s C\众^IGѠi52l6֦7\z杏t5&qXXLcƌ{(OkY{^队O|i^xAzmN چn»/hw2tMTWTThֵ;o5A-|t6VTPPp;v֭[AUVV&_)裷K.7B0&4EGfp:)%@EE:x M'l6Q#==]_|l6=9e=gF%T)%-}c*nҒK<* @zeilXZmۦ's[ Q]u (bW_g &MI'cSNՆ:#y=zT!!!5{.]UѣW_iڴi~ }߶yv|( #gY@@"##ukr\n;p֯[Q+ڲrYVnke/Sb9+Ue/s;KҾ}ڵ{N*+¤cgw qR} {>6@e>^s_OWM[صkk#9Y U#5G^rd6U\\LJJל3G(RCl+5e:Z߻Lf]^7<}Zz.aaңJw)%%ڽ{ &[.ZtB=vm ZKj#՗_jemP]w5?}9ѣGkǎ1ch?~_ p6nt/Aޛ٬ȈpuZ-&Zenk),,L6[L&B:9ҧ2غ(vzL&&8X_4V^"Z?Am_몁%IAVx}N^ XlĀAz m2_3tw+([mw)Q!!!r\*++SPPMz5_j[cbb&G)ܤ UȪ@J`c3L f G& pN:hq:zWZw͆5aRք'ܹu{r,?q^Q\.CVKrjBpiEl!m>snY; /~SR/5l05jvء'jܿ)==Nǫƍec+ue+$w:2 Z?0#ڻwRRR|*#??_lnb% -[Q<+Y7wrss[_'+G3g+(l!^H%-ܲǍIxjֻFxWA0SS*//תUkw$eeݯzVtnV@q :0ZPvMOO׀p_Oa\5t&?\6}_׿KU5+^=K.sر~$߽^PαK~19p%IB:M6hʴiJOO%\SY.˫[+~Y5UGfYӧNeonoM۾ԂWH?PvVs-gӭ3g:L58&&F{է+W곏VKM?:ԳجV],oMŋ%I[nՍ7^G9#R!Xnn}`^[{uqpzfGz鷯ĉr:YgN}F;I $5x?}OΟL˖ h,66V͓$͙3C![%Ws5oR=peddh̘1>Slөs+j{+9kǷ;Z/{д)Zz2W VdknTqY)a`T*,, ? @PE~NRCd6 ٗ/Rrr嗗K.>mcEEO >7mń q~9 è3n۲EXLY 5g?! ӥq}uခ5Kݫ=}ϲ"rۈ'S!ǯq'er۽G.[ GU[c>UTZh + {UnFFϣKv-..&!6M__iuJJZ믿rzwWew٫ڶoo˹ C&ݽ5]ph ?~\C 񩌓'O{>hu:_?7=--MG7-22\^{ [=m;[;{o?,{U?t%eݺɇx -7_wO=m驇7qaF7Bp?o'Iwcǎiɹ$]]oJoT|@ΤV7.a~W }g5Jg*M?ѫXS.v߯={hذan{)}yyynrYVUILm4j ̩^n*`4+DK4^$0 ^8;[?]T8~zݺj%Z}*@aaa>oݩqv<,'?"@.҅#Gz]G|:0IgL-k4*QPPKE \{-eiIL6ͧ=޽{'NP=oh0ueq`Жp1pdffꢋ.򹜓'Ojر>r<&84t i05Ocz;F !|~]QQ@())QHH!W\\ܦBgaa@]q:tH s+TL ON8aÆ\ɓ'խ[7)**h%&ҠAכLsB08ZV`Kouׯ{Mq B0h˚ -B=U~YNJrsssNsT0 {~؞ڴie/vॲ29Syl^mt3hIǎח_~%I:s$'+Wj~َ{Sgy%%%>#G(<}C5+"׭[7]u~+o xnNSw>y,BZwc~0گ-rm5lӰa)ԝ?4p"" P.V7`@n? ^z=zXfBBNSw>y,,_.::Mם? XIyy0{p,h 4i6WS  a ҌPm@Jr@B0h1ѣZ>|!N @ !6[$ق#k~/-ɣBqSk O>^ Cwh,>S꾣!: B0p ]B`ZVSVާ:!|v[;Z@pꝖs$nЁ@a=uߑi` @ !-8zRf[߶m>X$KK:TvwknǬ=K@]t\Zha=ʛjk>&B0 tc\>. [2jl:msmmX~ST9l;붴_CsǤ/5ZNw7cO8h ە~60՝qK- =ose5}q~5 lظO-v`t\@d]om_~y/ng r]@whxvmgðִ:Nwr αrRh Y R\WeoFnk1pw掕崗!LKACm w[= M=osyL::_uw]?w!=;4@' M H ji֦7wjk\8km@#7^eiCS_xn8A ;/˹3}v<˓uNo}"=;4 !B0`F7}E%Ў @ !B0` @ @ !B0`  @FapR|p8DEB911*)-Z DeDݺES+8&٤BY9Uxaw($8X]t2!m[PP]t\ 0ATsKP23*̟"p3  ־QhvpVYTiQ߄2s kJIIV#$'?S~KUB0  )WfjۿRUa롞q 1 ,r: W)(WDDVbJpVlA $ɤKrs&ɩRRTYB0*+J+ҿ'PXX̕QA! (/@yyզg`+%*p*.&OaPKQn޴ [NCGURZ,Ӑdld$L&L&&I&Ld\yudQad\[;J@C2 d2dz 2TUyejWaT.TΪz\50d&TQ^zFtdTZ1~7.o-Y@IDAT#IF9o2j.w|H2L5e2d2*_Kb2U=fd*U3j~֮Sz4Uͫ~}Vϫ^zkz՛WF ZK. Ub5\FB 2 e0I%yͻ l#Vfs R/W4KY1QV4qUTt!`ɤ@MA5֋oUE@Z`:S,j4jr͇}Sjay@̫}&N9˭R9̫ $fC£א)_[FRma;꾎=M_${{^kW~MMLk2rd(kwQ tğe(ipO !B0`hú hܾ~B0|mɬMB0Z qCj[2W̦~z>@-vpi'7^k-kԛzotfѯtZQ/dP,@M!A2ݡцx5,G;X9-CK0Q_ZcY_g#7%d9ƙ []_^^~博//xz<|e=M.R-k8#ݯGtwonow}H%&&<%:*:As̡:89$M?_-`Iz7rj\KeV:vTXfY[p$-AK-D++URtf 4 3ge7KKp'pZ-vuM%1p\ldM>|v;!30.'&&El!w.J!*.6R=cz\wxf OyTNn+00+t}MKpgtdw>&Q$%! ZT({C%2\""B$)ظ^b)P?TIENDB`veusz-1.21.1/Documents/manimages/customdefinition.png0000644000175000017500000004501311662000553021124 0ustar jssjssPNG  IHDR2sRGB pHYsHHѾtIME>tEXtCommentCreated with GIMPW IDATxyFy$Wosߗϙ5v Yc.6&!x׀mpƉ |6 8mc=xOO^jj:J%{O[T*U=ѣRNMM0JST:TbLgV@[%m6-|<ȓ-|ؘui(ҖJrBSL$+j*`Q/A BEKCT BFT(+![SP@#Թtd,Dd%~n'/6󯐐;^w o,qIZo4)\٨q,UKtBjB!ro''fft?;44jlv jOYG; jC +` /XCf ty r+ed RSgff.<0<J(;GP0Ģǰ 1 "ؕGI]ISO]ﶌ+c,q`>$#!JN]Wc\!T?ubnN&[VZȄye2~ݻ8bjZƴ&)mUNiذ?z #1!I2nJWk,<ڽ{ŋ,[btd0?y=g&&.se=nw8p B,(i>1b!UX7aa{fK "z ޿Χڽ{K˗-'Na.\8-^'R,._bdtPȟ_>YƏ^ŹVL=ԓ|^ sؖ-?{߽ZG-[;Lz!n,.DݔUM)f5by%|u7]uP.H7 $/?/^|qmWێ8bѱ-[.:p`w;F^b3gJgLt`ttltt˖wLFW,_eR+ٳO>Y 3dtldt˖,_7P6fkʖYl0HmH$֟80QJ4BtJr,X_^EUU}_xa[K.L5 7-J8Tfc-O9I>WFT_"1Zp"1V&sٿ-1"y'guc[,S9nB? o\(ehqxZUymom=Џ땯>vum ~{8ߞ9LTuZ&^:~z3٢K&ݻ.͑ ew-a:tHUkI:a[,9)omD䩜q*/iӧ]׽nrA''<= ѽ##c۷o5k֯Y]J BJsyB!ĥ' y!6! =hkW\a*_*媕DbٳIIU~;_xÎ5%NܓRPȧR!^gggk$IJSjEUUIuܚŞ69NgL!.!Ʋ)4BRri:0bc)^’,!R%$;,h2g`zĦ4[ mD2] 69L'jdشAÞp螯&8F/>pݼYku?#7o ru},)}Z+~/߯Jf^TdI՘N釆UUT2)Tg{:+ \6ӖK>Q&1MmG-W+w\~1L>"i]AR:r7Xcǡ9Dj]X$[$ _b.7ڵTUVr*ekl^(U+W|S_ a{%YRI3gZ;λn{׿Y[Ť2/&JpQ7G~׽○,%ŵIe|UV!ԔxP'r2KI_}b$ JTJ$L:d"a^]KRÿP* w7ǕJ:Ŷ(ӍNt3Fƅ:fOS뉦 ƅY0P < @RjG~E(1LjlJ\2tvrdRqelxdDLn gݒL&l1jJ%y` 'QI˦5:l8X Ll QκbNJTejgI)5l$*kM3Fg;97ݷ0Bc#NfgrO###^lvaYjl<$5f?099Ϳ?jSVj:`Q]3+ǖ,Yl?Et]/15 Plv={?{7Wʘ~G>d$ieQL&'??2V]?Z(Vgչo.`L+J!:WZ}[uŋrL87J !!Blx|g].b>\ݖpeFعL8)2U2R&}NK70O/Q,j6^K0ZegQg(5XTu 8t`֋sss?Tyut~W-oy%Tf~uWύ?K)}V/I.%UY d~kvznrjRX,>[  fZ%x^d~(E:3hHp739B]-dj_4s<l:U0~ųd~ceuXksVe1w!nd^F0_??g,| |d>æsz#T^V0gy7/73 ۷ɤKaLɚYk?\u`xduώ ,,OLJsb2Fuc7hvvŊϮ{ϻ2|C?]hX,>s-[E/ekֹؔ?:?ֱ>[o-=$%TJj40/FdIO=3;W -KO=y_|4h &vzlQFZ\1ƈƝܿ>0DJ\ e=J(.fqaHXm$%u 3e'qi}^ *i3W:[Z?JO>y_|3)E$Y73ٜK&RZ&.PIu[IJₘ$eD cXUYPd2/HLNS298y>.oAaND4(2)Qk\|$lhիSɔ,W?{ƍTBӪu7_i[eAF3f%Qyzqa*m3ҳ6V=o~?dnOũh׭%fH\|ɥ$lh˚իX*S]xS_&kK(mYzu*5B&3i{&Q%i$=/&:1I'kbjE7Bt]Լ=WtJѴ*'9S<W,;Tl^SEr}P%3̙ӏ>𱣇u]d%z˯Y&Z]̰qUػ{W)XҰVh>e5|]}ԝ4£#"^Gd%s3QdZ9KtV-leNa$i 'T&gp{v޷.-J߹x% 7QɔH~-ɊTUUjT,JL&J+,Iѫ:庐ZbKMchsF1m\F# WJ| TdjjZ߅YьOCEIRd9ᚉ$IN\ŜPI(D S\.ӒjƴfUaRCPVbY!DbIu]תVq܈(7 [dpFbC,BΨ,1]uZoΒHB(%:Ęoa$YR$IҙkG7lJrBP(5Mz\ $ .`4.ZLB}3b9MJxN;])/ۇP&a{$kO猹js-Q4Wbdz7kC v%bD6PN݌,)c,Jڲ&MCkR2By.Vzc,8lUn7p 1o6f5u=jtEj%QBUU[Q*u+(M&UjNeh!E4in!J%+ ~҅/Lա?p5NIcdVV-V˔X*U R~%PBX:}{1*6T;|^,kK6#e8`\Yn?:o`9ؿBފL{ f<״hDm|!tpSQgjD?U418TNˇy/{y1`eaEL!uu/ bHE$BN`Г#K.X7oUmx}Ny$c✆!"eQ]5iRYStwSv*x]\rA,FؾJkn1?Xt;5~tucGnW-s0rpY@Hm8vx2D BwvˉHֵH8qa9hazY<@~V Ïq"7_g^;4}oN,d Jcx[o 3F(Ǧ@QJ⣲(sn> mڮ]"CtjHvI)~?k7vbbkPq"a~$p<9j6 ҈T$b cɋvmftBo*HyW|;KiE1UUj̛%6[EC Nc0|}?[ގD"( 븄Tj:7WGzBp/H$ªNNV^)ϵh7Ft.s|㨦a-)}\O_____ї )J`Q …Ba 7ܰ)J4Yӈ3F QlNX$*e5  KAw4mj*?<`Z_8XX86۪K3m7(3ϕAKOKu0ڳ4j$In=g_Pgk3ZvJ+ה8_IAOfvXLӱ[A[^chlmuM+=m b~ш2Ѻ>3 z3@y#d pEz=^ulW{#UZRZF/K &9hW/<*sSFPgUυ .e%huA5Vd^/XV[yT: og2-h@\0h4sݵR+\0'R$,/r,* [wbOHm70F@j 7L`Bg{{F#"]m$+hV,/ϡmA=`6_ #|a f TTCvu+_C8tV &h"DQ۳v_;TG؉Z{;Dnٙ_^C pj n:ى 7Jh\mޟ(ׁsm&ql* ̾"; "LT*g+>|3 Bw ~y.Ʋ4 ͹ՙ7 IDATĿ*mA HʠE)9uw=A*xh-vD%D7k/l0zKnij r - }=DaΝZ社F{蠅lDGn]EQ[8|%.Vne[rfF!㮝!'FUViQmrw7^E}Dz(q3ng\;&UgMZT4fg18OC9\뤈oDLlAđ I|#".`}m|S:h v '-{:u+Qs~\2?hwǂ)WJ mNX@,4Npc@#8w=+r> zjVfZUW2M cnG$Jʳ;ܴiS" @K&nzY:Z@@* SU؜e?FN(ceӚ--acN#o!Q힚"gsޝ#xBfؤx8U)R{kOmGeklxk'W@ 1b# TB V*Pα6'~pjR|FSءSO/jP vNE.wQ>μt?'%RHOmCW9D½S9ST̰SXU@Fv/l{r& qഁ͹|Hvq1mi%1{ʨS wv"5*Æ`QM9m usS4(}&,p'س|amIZaSu6e&0,YBAy#Mgh>vٗ,5{3ّٙ G2⶯JU( mc>;KJg2&*/q.x\2ZHݮDʂ-$DcwJ{x xr56ЅDGgg&<]Qs9KJVs9h>֕ 6XZߕ|U/O6X@ dU<"^p-$l+=-n1cf囟LhڸV4vJhr$IRٝORA;"b`7Z3T tMh#Z^.Pa h 3V!$@6?[BGLe/ܡj(6yp낕(!dfzš\oώZ[]w '*gD7Sa@@}[6/l Js)UEV)WU05ΖW\ teQG\D[ݥ#y` &N;7esܦt@,ԵdmYq2wspZ,asBP3:b5!)-ifaNAŏNBpmB;b}_ ԗb%ud]"pS[Bng#;5Td!,[g V&N|-R0+m[DZ .%DUt;jUm׫EMWq7ʇkFr\INHrjӿشqc"X7Zhm8* * T8,ξb_ㇰ"Oߔ֢+LOD)M42ؚw ے;tJǕV\0-V{;H[vM3WWK4/7g//?yP ^%Iل__$]EGtp%.h>1Wvρrzs,37pD;^\ { ^9Cg뚹:+h0h•V)ߺ{DN]5LD"]@pI1u Z8hy3.2b'L5kd.|n8]%ľV$ą?ߩtݥ5RVu`0p3%*C4M+[Ty<څKT%ip~ӪVq>؏Sp0Uӹ@>4ӹ$և|DhgAٷT-V6*/> & ֍pW [̙!.[<]8m.6ӋOiŚu"+ Àĭ$$egί`&r=A|l])k[2`>$d1Mxf 6 움)̕ĂDBc 潌iK@ntW'߂ RE0L89xȭA+?bsDR5oǙ,U3Pk̓.ڼqh@N1e^afKt"*̨lQ9!˩xЙ- :6hmup`e@ASW\&0@* kT|; D1F|y$t@`.r(V6u+?C_)| g!d0>[bK6]Oϼs  }[VaT8^rKp5*<[2]XHE6C.,Jjd]>5t G6PN@m []3'N2T_@NyK8 :?lB6@5?(L m/ %Ͼ2 A1a* $§N9zt")/oU˥-]MUxSg D"]˕//vߒ851-ItvT*ryЭٿةU8ʸ(ù~$@3WdFtNQ$@( 7V:ozDf:WNLL^ǹ6-+pw.5HD|єT*+VVCMа48)F`JQX6ڼy\J}Wq+e+l4/빦di7*r$Y'v{N7oޜJr`벡]imeh^a"swc6Ũ}:g,&ۛmRʊ;e"vom tf[l\Xool[k֮k#"(8]PZ.Op8p+^V4j#GF6Kq\Wa+bmO=s5MO=qD?kpnpi6pzj]ߍVg\-qRp,_leNT|tg6dPqRԽhfOꕾ𶊘FXU1 qa vnKϞwI.&3[͛q#kb֌nXAߙ :xj \8;q'Dō| 4́%TipX۽o7"a}&xح S[4ZyBiU=7ǘ#J_/ Inn/ ,VNyK8NÇ`V@i"Y:p޺aׁb'h / #hv艽'qqpû%yͅaX#۫.YΣ^?]^2F0x Qjg5p*nvBTU;\OOU?}pov'Wun&XƁz]C"-} FI0GD /b|[l]&^ W!>||Hie4~uf9y:Yg%&VƷ֖vl&hBfKP@Aͅ}an Fl jz'.TM3w /VRE4+|fg]{1g/s;>k6g\sQeΔ6=#N,JЙ~92]vq& /%xmwqČobg&]`:/뙕Vo< w/`U-EJ&gHBd/vv*n\?Oz$#O$a^Oi5aiͭoPҿW3ltd\Ӊvf6 egmpPSa5(#}e^dZ-'pUU8tWW3VnznE#qIR?HU8b2/?^6x0:|u33]i$DWO=q"c`5wx x_ V/xF/5X75}'֕/MB4I{݁"KU*KA&+>kF|+h$ #:+g33ϩd"'}aB!bi#J^>;#],w*L&Myً7]' ,-0;po0xc0/ * Da34_gxcP/ * Pa h>#Vah74Ny & |k@"sR00@@Pꇷ9h 0/.\ 2`F</H@+ Okbҍe޾- TЫGt|ah/g3$766j*4_ 9h-ॐD00@* * PaPa  ga:j_ T ߙT `v򅽴0x$'x8-RqD$/ (@* PaH@a|ayJ8F<SHJ00@@* * PaPa  TT00@@* * PaPa TTFilw& u00@* * PaPa  TT00@@* * PaPa  TT00@@* * PaPa \6t1sfDEOwh?SB -U*ՕˆTsY*їξ<1>)elMU8U T^@ Ƙjj+H$ENDy6k UX -YxQB;oϤ#Ub$ w37\Ș^.;:Ç^P&ͳ)RkpW1===<1Tj:^<]8=<< Ubu8t4}j022V`Y45U4G&ͳɯ5|BRTVu]grB0ƪjTu]Nr3-|IMۚvk"/X۱M=* BGPOOPpOtExKW٫j"ZӞ&T#mo 4 D${|>i9cٚ'c5We3.+U6S<[rMd4f]:qn+#i0A[K OWؖ5YlUka-5HjA}UF[{]j֕:fy&''9f۹6L'0Y#aTX!:c1#(^]l^+]nŭ*Vd9&rQb/*F՚j 3! SB%IuF(a4um9kUbt5#$I) *QLg!"(#V^&ٳg>H$#G)DK"4ȈY딢F^kөԴ$%sq$2=ύ-ʤ3uzEٻg׺+ #.u1(r{_Cb;M$Ƙ$292(TyoYv}򥋆]f*wzlk#ɡ|aZ&Aٙ3nҪ%M4 ] 'Nyzzӫ30]dKsIz]lC .xaBA<"^!/ӗIwfJH&ͳɯ5c1I)%$]qcW.6QoM^a}a]' #24_BdQƻsPahsޝ30j2_Bą,˺TB(u]e6gN5J)%Lg F k/Lg$4Wb `IDATT: cx::) DOǤЛ0#I”bja q@ T[(diVt6Ʉ˅UZ|͡\$ / Z*#c+wɳgeҊ(TZL͡\$ /w!sν1]U;["B98دrԫ)@$lrvY"T |w2]]Z׵rAt*52rWwW-)|SUO1Uͬ\ꫳ+W}#t*t tN|0:\- ەsU n c 5/osk_cGH/߶~ѣjz\V3s\(Ze&Xet'hr\Tui<ȫf[YЌ"k&DŽ:{ٳs ի^,ٶ"u lg#|4;3ƾo*~'s䘉?~׻2/tGIrahD׈iFӷ>]\ā,iYWZӴ?~%Xg2cy]]oo{͕w}-O|bffe+?+/)J3Ki2S)LZ_J]x䓟 G 5謞Ր 3lr9[JcٙA⫗W|Wͥͫf 'O+9BM7)@3aַe)1TgOk^'6nLo ׹ Lekó&3+2{D"Zl)Ϳ] u٧MIt&̇1vwLNN>>[;;::cǎ|+jM̝=[|%i&u=+oe|cz>}Vd׽MZUXe4Yl *JՂ`UU﾿?޽{~߸a\x@POL1屾'ûgӹய}X\&~&'Z0 :CCCSSSm뇆̬;uo6wpMf])R6,g8:w~_8sŋ|S@D >d9IՈU#Sȥ׾زmoaŧ%"$i"+WqZF4F^ YS:*tn%1EI|ow_G$Pخ$bE\$)rxbZҗ~yK~4V %${t7WiI“ֺMNN{''']$Ó Mvmyrrҵ5u]>U&3j÷5֕^Moy*AK:T&3pU1?KOSsV7ҮC@K]rn:7xV-Paqޚعs/j~k*ڪqN."uBR∄(K\PJSkpJt2Oɿ^$F]}2o^>SOZ tٕWN=O}WeD͓DPJNfvY5+^IF!|&;@aTX#G^FU: E>r};^]H(|Sv JRk?#/ՙIRH"J2JHbǎ_Z_WV? S*}ٱEtSLQyfvv Ϯ[RkEQd[33SŸf7|W*7/g!J_Ի޵ off~ٹ#G:5q]#CyvұkWe^]8LO7'_u]u]/DU3 =[HssG~hK,Z<<& ^(̜8yj|La3^ d,[li. -ubjq47~}AX-;u&f34PWZLMG._ëٱ;et&Dgv֜0]\XM._C~軳wkj [__&Iw4 ^1^ + TTndnIΝ"u>XLXZvT*|_8I3L4,_b#*dvvZJ$33332KΝJR__[uUMtreF驩5U_j),1Q,^>3%+edmz=mIENDB`veusz-1.21.1/Documents/manimages/defaultstyles.png0000644000175000017500000007246311662000553020442 0ustar jssjssPNG  IHDRn-sRGB pHYs+tIMEqntEXtCommentCreated with GIMPW IDATx]g~Gemr{lc:I1 N $p(JB3 ئN NGMbv ؾƸ+wMC:j{wFi43fA]]!ǻ\.B0!TPJ borRW^KʷRB2bQAIT\ ˉE³Ȧrr" w@fuBAd*+ddBL9B#]$#s'TXgsZxRIINro+8B,9.^{b1 ~ bbm\˶o%%nF"&LL1|P]Y _r)CXג$`,N% 1 F!A:;8BHuc94*MľLJ "iσɕ2̼v߄#|5ٖIՈQiSĈFt jX01KL9ư}lrc9\=k9`lTs/رsg7 `B/~ QW,:ΐg֖uk?ˍuuu{U5О]U>:6s &isKq*ۚ[ŶDPnfѾjhw>w Q,rbmIr9NΊ*n޼qgkrc]ݐc#ݻ6|>OÓ&N}u-[6%3;o V_U]޵ U& BL떖k26:٥Y'Lfg5:ٝJ_'=!B8b57svI"0_m޾f͛Oַg:G9TSUSSU3m!K UÆe[t1Y[0`ps\͖Z/ra$WdSBmmV͛O8ۇ:Wߵ{w55ӧyK˫(y"ĴT2}\jkMeW_3^Ou&mW+zڴ[d-գ/-{_5l0a ; #b1w|](M_nذN9k6Rqli-ш<ǯ|V?uJ kOp*Ypz17peDxޥֈ&_Bd}YA҂ْmu.S/row`vDRBO" nņ N;YXzw?⨉~ׯ4vl㹽tc$Eōz39P>ql_&HTQ6m21$U&*ƈD#Ҧ֭[1uƱc|OZn޼)\=c{I "ɧ:vQfoh;٧M?q<ַeTWUϟ?_64m='b0lX+G-پ tSX ̶B"*g(3PHN`B0&& Lt$2G.%e. fP# H2ҋ:g'ZOOy:Do[Ɂ md\> (}g~6ad?ݳwY3oG}nzf#~:fl:sr&1ʫV2=flZ&ʕ46kg>#dѣ5,vRkW_͜57\yr޽t_~v!}ՠΉ3 JcL|Ct\eY#y(ڷow,^Ɯ֯og(4!E!X$f[ȍE26  YʈGi|衳++{0*T`&CNb,};._~ߺu°HUWP(r:`cI}'owފE_j}q1!r+WZr]t~Zw$ .0=^/  3 pQ[g`cF='\c0E,6&h(R7ώ*3l vDGJ8waa7iCp%̽NQ) ٴHm!<iaɫXHdeHTŒ%w_]{AQĂ ξLxow \~%on!urVLNCJ$ ov /d2]l}]U+W7S% Wk^ u] X:.QKdo'[z61uxxC2za B"<soec)E&q:ɉ{e[2V4Z-R Djykp BL6,$,~>Ϫl&LrMEiS lȲ躺з͒0M+.aQUviZ7Qn 3MX,}־qꮚH^Mؾ;eroZ>[_xO32Zxx%?'wK8C Qtcv%&}}W~7q;]I_Hƃ:zoum@k~wh|L `eD*t84 a}>?2vdc[~]fȳa+Pzi5Rܽ؞IAjf5\!N  T5*V?c=[(k 1{jجlΐc>!UHOP++x*uWU)0XeN%#=]J&^rQGG#ꫮ):;è߱*t}?]!uʉ^r駝R[[$ Mưgn Ӈ~w.#_v@z/1 $bRӵ닶MaxEu8XRxN۩E `"#Gj:}tڽwW]PO.ObΌ"!V2rVB.ŘFRU4/̐@pzeU JX]t/փH5(V%Re^ >IzIWjCR f{-blb+c8'#8"`nٺeJH$rY?k/ }쬫 A̤uC]w)S&_f"Tcc5ioԕ{dz4ڱc=>p?\ K_|!&ΥzCЗdкD'?|g[j#*U9@$$lشwtv`}Duk;uʠBadSy'6.w@mU^Ɂ7c0$g&(uɄptg?&bpy<۝ =n5z6}z6CͿl+[uӔlMٛ3ϻdU0JR>`'-ssnܵƱ̭+TIGwO4QWVU9T X\>c"+4Zj;?V&[~2}<$Ib\M7(Ek߻az!>_u>> "Ja !'GD ð0kg5G}xmYz&Oq:T3TMA@/|f\ͺDDvƔ`"Oj%Y jԦZ*&mȤ6=-)Vc@(՛1ڮgPM˴Eb$ c4|cի3qҌIJJ裼~LbؚL\k1,З֕>pTkKT:ҧ}1}Qdsv'Mծ֎֘;cp$1'\JXb9nƌY ü[:j()}ymjjr;yIr{WfL!QٳI4):W{q*dncr"z]N)sd[lMNEs0g.Mb墲E Y \ӥۈ\%1b"q~ޑ5^f݅ Ƚ@p:^rU ^ XNh͇BrI=-e;}ZW{WmPDs1q\}9jve1TRM?YVH5!b+XY Glb9u#KrZ#A%F W!\yY'_6?#fye KIVj^]ko!97G%Saޜ7'е\#[[YL6Zf5S+b1<LTm!^K4Ip[8ڵ¼yon%=^z-Rb@ Ԡm!\;fdWbI?# ^ f*kR,:tDjm'\; ; o/:nDSb)H{KާF(iԯ'{9ŃYİO%E &dOsm3޸R6dN`)8O96k*7ZL^UTkOy3E5 Y?(_"+|HeF'ۼxXl*26_ݷ<Ϲ~&f9o3J ˑ"bBN嗬DrkWGJay8Y"9S펽95m*d>o޾kߐ4[ == ytwv|1aJEUѥ/b{Gȑc!-Y[v!E# b׭UA˙9-j ;,8X߀^V0 H_Ba L)܃4IaD3XY)ʻ(>O)I˧r#K7m6y!kPcٵxeeuggHʶE-ֈ1Nr1+JFEQHy3nbN= i>RFeW֤{( Xc̲lQm4i#*YY3Wd/%e;<ʹOAѹʌ^ Z {Vvt!JHȆJJdRnR-b`[ʤ?mR09))~/ 5TqW=!Rm)u3xRa[f=%Rdp bv-UB5G s¼UXI7HwE0ө+ItTm"o)&x5iх -B@fXIFaud~BUj~g{_3϶iր(s-tlhORv6̀psIT]މLD)J˔X+|NQ\^1֎FQri%I?M4+DfX32bFbtS"*BDmsEՏ5C_1r޹VX&OR hE{K өpZ`*J^4>D/NsۍA] 4j&d1j2U"{a4|%PȉٖR-ҽ贬BC$1_/MawTįNGn/D\PA^tHlȃR.(W|IR0EmМI);~=u,[os)gXS'M#,S O& $ Ҝ\XP3>M{T]w}/!Qt˟H沶h7e-/GY7%ŀ4 Xl'N8]wu^ye}Cx#fbБ'O~gi*[, vPm{^L ]6BBwY5kfuuIf(c͚5C;v p8f͚sTVVR4663VB}EeܹsR7Сu(c,+7orտ^sJEE@1vWx>{ɧ??vjY DοHg^F FH\)t,$鷿 KiSӦ$ⱻ\15MsMV/1EX 5-]3OΥMM| Q&vMsGx\ǖgTDQ!kJ9'.}AϪ>sᴩ!6cԳ}Njjs~[dM/Vbc/]o rԪJ1 h dpA\1=?Bϓ8bp8X1Y{T5v)z^Mⅳ~Q`޽m&jV9R-] ]~萡MMhmdE(ZvYӃ>+Rgkx);F 0ztI:Rm.~4|0Ck#P2z[=+j]]`-!8GEԄٮ|LЊJk7]^)l7tٗ ^С3J jhF UU5wn },ژO $f (i3\rn/\wxM=>_/$ 1&@h,17RE/QILj)(((cD"eإĄ:vJR>$i 2/d /w}4 "B7GxuuuuM.R-Elæ5 {<%-LQ3KHzz===GM g-Zdgn yBy#I8swNfVvw.kƌnjM[;hִIܿ}ێN?6$I۾޽i[Q JԀ w/_[o^wyZ`wb}b՜ (r5Dd&r'vEۻ"~/|Ȕ q ᮈ@iA0ru_4Oip\lh^NA?<W\i_qS^3\Ζ sх2Lnu;l{kt'x+JlX2Ա_='ARBl3l;n_r!7xʖYvFTqvR.XQX$yGv$|*Ipw.Jm0=OB4 Ř?Ȫ⯾{w9فX*-rPIDQ앵ʓ  u8I37ՊwoJ#Ǩ#s7V5ߴCd<d(I2d%2qh&鴄ʠ3N$lJV4iUv.13'ͩcK(b Z䫭ODŽ4 (*Ү|U U jɉ1ljb#!9|I=9朓SPӁkq3 ֜*`&;ښh45|n+m$aW>Y| d/kUFiZ`F/)V1?=H\:-JG <239L,j(JWMR[ q%zf'V/+އTS#'PR6Dh%Pb+ T[(UX$,^ad[-1m@ALBXա0E{:E* jUՎ8+܎S Ek; s{v@\V(`L#цa~ |+@&7V!ga,baXO(t8;Mq2aتj>JBz#@!BNsx y[?iQ2MiX SsΡ|(̓Ib_蓣(#[D1Ǫ[vVN#DX8>3(UkLv!8a0a8a0qϹBsl؄<ּ%ꙉUj, :bqqUyFV;֯"Սwy|y22X\- Aԅ8pB{xG["kܞ :c@ǣ]]N9hFPZn!C(;BkϞ5=́m EP1wa '$fj &qE|PO*]ެ̀5@f8kg9Z[jjl>|QvF#^u׳g@n@6Z~>=3+)rp7tGŹni⫤,((T}Klyk헲UmYl"+2˵|Vf`LRZTk +ʳ9AMXxo] h@lĀx~q)i\)G L1#%/TBzQc/xl%I[,oAAvDvY@2 6jD#p x+/#/?}>o) _&lz=UG>N|s\>lh|.A. a/ =BEeYM xFF/x^6O|z[̤}-ȅ^ 4/\%>{Ҏz_]c`uuՂ -c+Y+Ov@wcZʃmE1dz1ُLdzY6tUٰy8/_zh[(>,JȪ6/Sn;pُ>\޸uI]{4M1H9TbOI E1tr z(_p;Xk7xK+^㏞#"Ai̜"}1hS^VI[DR*LiښPMOQHmu5%yy}jO׻n3Kpa"!߻{ڔ%;+ŏg)xjਹ^F, w jB.9RPPPy [(,R&ֺӦN6#E D:!( @<ټXT4oA<#\ Fq&@o@<JьLD, cHTQ=؀JZՖSs&P(C`cX!y.I: Qd>]|ڀe!x,2~XKXpRCA(lz-&tMߣ{WΓϼO[ z t_=nLJa{zĠ֭]C+FV_?qg J". E,TT'#5ը#˶ Bˆ~Q[Shns]f `b: l[/3}8hy֡p)u?nP+B&&pb_ އMռ^WٿXWSQ9j͟HmvX2>XXRZ|,a]/ӦJ'}LJ@h^IO:ױs[AE h(jk'IeNݼi}]BȤ3äv.| 6;{%tt X g/ϟuC&fZ2ݚBP3 l0֒2bɉxODo!vt)qRPP&D1jjaI$Z*bXb]rBL~~{@CԀ@1H  yWKAAAQpk@hmzsЪ(cC(J嵁 9-#RV^ƘjTT,KQ1U5tCWWgfB]A{}gwuݱ̥͂ Rgkx>irzl|&o Å%޷sϟϾgy`yK>~ڵc¼j*Gfj̔ pXr@Rj)y:BiNc= .TfBy.\եSQqy?;LHD !D;(\K2=zǺg~)׆%LV4- %kU @  )ܧX~("( Hia%<]Pp8|@,[hy=zZ1Bȼyge$!3ѿcaSzmxxQ#vr^5<e]/~`Ŋ2.ZHY%DD[HDMشC^ gT<|/P}yUA+\tiGG[oyܹZ`wY^ 7|i 'v#on:kZ *r ݥA)@32kNS裏\+7moj3ywߛH$ʫl:xw>u)t0@ s%ǎ3dHͪj3iL4{0X! (I{O  bs(RPX2  +lXSOJIsg)(j/8'd|;h-D< PZ;Φȓg[[pZ9gL-jݞsVO*id*YH ҄<ӕ3``={ZYxg7e8*>+ jey|Z[Ɖ)(((XBZlO$ yX\x<  2$-ys,8e:pܣ?GgއQX!Թ'$:eBas7UH{\|Uc?n_rKZ_gB1Pꘜ9 ={ ]Zn0rn)YZHIAQ4.+ARZ"H5 ]lkm)(Q={VZ 7I+ܢYpw?8y|f$-E?1$5@AAa[Yr=S&9n\pp\zMaQPP BK.sԨs<4e$Z`9V=!I`Z)jk1 B;H1\pՈA H=8)qo 2g!l)((lЪj $6DWIJrV&ST"@"Fڄ;Ѩ891Tr@Y)j.V&fF*))(((jŐnqc=a[&Lw0N7 4j N35dFyLIAAAQLCͪATQTxuKGqkXwmjѧ1B^͂Q!QRxq,BV85 ҄B|l])FhGp"L=]\N (J:] Ţyyv?2\ckpT!NQ, TóX3(ucNB"Zвnm[7VǖDivۦ]yC_}m lLgohJVKK ClnlidKvȮex5\\s(?mo\PTr Mu 4Y,͏bLfY]zHT7aB峴2_:n>*]lit|--(? 6Y2)LYV/+Y==`ZQ [Y=R'i~67m?K3`ډ )t|xkl\4EQmjR2r鯖>,VuewjxG[Z֍03-0H yG CĖY<=F; P}qԷO{e0?W 7 d}ɱ3`K˺@1kŋj5G~љnxf_xۤ{W;Eqew"ϱ]v-ѳWju:XK5ʫgU,.kE=Y `"jܿmbf¡=y=9~کغeES%mV͍jU0_-o+m`-Q= ulyRĉD.8?V1 cib팾齽;06OZl@`eeeަހm yϞ7ݴt*1D_t8~:~B]fRPP<񮪪];[ڣ-mx7*Lu>qǷl"Sb/%%W07} Q09c{1 Tg}>rD[[u wuBBw~9󐪪ʒ"mUT&vĈ;w)E᦭J~@UpAɧ R;fhZepWO?;ٕRVō7>Rb-,6o2vh=RZ6oٲf9v$Gq\yV囶a!u( CjCPx3mڔ~Ty!MFVegP nEyx^?ܹ?ym]mmT[_?۵77@ggZL6"\h4Z]]?gI]fT{g*ɿzЏ(J 'xbs.ci)?'H,_~78JB{l b(/jg(4n\%^>mqMMԱ8Q B~ ͛;;!9#0N %mO|z[̤}-Dy46PYo,E2wl D{BDhٳg}YRY8a^rʿńdF&BWVxՑt9Y:@A_@ٸac,. 6pOc%IhǣmZ'ODHL ?|~'N_B<÷'w5_*W0WDꥤH^T/ ?&ϊА$<̍U-Z\c BLÒH7ˑ6 1Ȇ}HH$dcs$<\ 0FWwWhۺFH3Sn_wUqƙ젹p8g:+v|zHxiLB!:,MuF~b O[UUBS:Me^dbTJɆ7qLqgeBou/jqfP!a !i?EsSh}_=b?l=|һEN@(RIau9z']Xj (I헿>3JvaG>,%cK:ZVm0P6\p*XЦPVVlq/-P@5|ԃqe<2gގEN mj$(曯ָŤ@*RɁ oMuj ߹bqQ,@bZR;kf8 kM9ٱMqRA94]UyfI8:TVj8|L+TU^Kcpp88<(&jI+Eep5l<6N"LPsJ n 5MGՙPLy~$O~Z2}(‹< > ,Oh'8'^_`83!#s/7!,Y+Sm3gN.;< fBk\ñ@P0جZ9 :Yv1!t9!g` Ǖ.Z_@eѡ3do{`Jji Zx,WUb㜫㢈W[5Wbry{_ג Nr.PRa4Μ7g:UbRDcDMn*up27hpxԛVjּRKErґ>~߿4 }L/r;JjW'!P+W ?}V~`O 0GmiP9q@W[ctzw6q}U:^XCmdo#z`P-uɅW^! _gc[75%'`ۨ%PW㵽~rUEWA׈`I @:UD\mUhhƌG1d 7 ۷f|f# (jo0xu Bl3NBݾYQa$1ݾeꔉ{8XZ9cR /?%Ilvf6ٳrʋ.쵿فS,ciE5fL4pwD̀Bƍ7eYퟓr~[W/Xp髯|8*8:@w3{;;;NUgB-[sI(vi͵lܷ%+-MXpfEqx<Eq`*Jm*\&ɓWX:!yӟ fslX:[Ɯ9!RSQǰ'+JdE[[ے%K!֭{ f5ndK07BF-MjuBEbMl6Rfcań. IXΦZ{Kl5@HZTaJT60:kUfXF6UӜ_{ (Ij-B:B1AD[@-Ү@a x2oZujz6lPe+==Jhj.>|'vϞ௠ԎNRsٶu텳}3# C?ikmꚆ0PhƞSKP A 5ɮ]{vut+ab޵{sOjU$Ijkku= v{X0Xma13cykd,2k0MvgdhpDF8=/~sۖZXhlŇ k!f:y6 k2(>Vy$:ƴo_ZFfxJRUd׬CC5Ba=+?%^\; ak^akjIbj3&H9Mmz~akB1f4 |IDATL/04h0c^p0dj K0tkB}VfڌQcᅺkA1aFEӴlyq& )cfxd0|:xe d33+3a9!\9s榖,pnټVDQ/Lؔ]@tшAU yEuC?{MӏPB&.[DӻZy_ 927erNkq_5M^ !;d{N8T͟H$)B:P@ %TQdAx"4._M/ fVES?:e{j47᰻DR*4}*dRM&pwbX*D<r"sŧ׊{& g3]T,V;4<=ػ/xw~Ěe0iRI1\`==J؏ϱ$xٳe;sRR9%'tpm:VU((Y͙'#|o 3Da`3Fޚ]gSY3g|5jRO喥UeR F&c"rKMZg?k/<%~ )wjDy=BW\9}x|pƌ/7!oN"ujM5Ml8MAtl/iuUWJvJ"!$C}nAQR @^ѡ,G'164in. $N!t6E">S9gduJ"L)Eu܀' ,r&CىF0ȲCQ &mE:nR ZZ ?_< orW?5=5:?5`x;!DW+.s]p0"|>_O‹g}!7t톫9@`ݺzz5MML$G7jd2#^|тS&/,P/={wuM=3]h1Lsi 3`$I,o4\7^{/Kiћ R[7P*I64c,8cBCVIQuZ,[}hAf  ?Sfˡ]j2ic,隖7qLga1MltH9sc3c7!㚛cѨl(%NMK,86B%e4k.\1#RDԐ:B!svܰ?oA $ki9:!Ps&r:R4BA`z$3q4 ;dفz֢" g{?֖0AP@mM;7bv N8pA o3F)ݷoGgD2ZfSA)x,s."B(pn|WF˹m˦&QU(P:kw%ѭ[6M4AcgEZPR*O |^׳n'M89O|-[nmok dN\j˗.]DnVdۯ~xN EWI$9C zqYn1w;%)}F$2BԆ }ș3BW|61\L^tʰe)-OPnj !zs=9}wBn&U74mc5W]wKdϔz3O 9I[1[}LRz! |>9ӟ5 %~LBO*t p@ n`~ϟw iсG:p["/xשl蚾m{VӘ>Q{59zO(kMFJr(,iڣ>Om[ŮO:iOV=:$s.ɄD3q*Wn۰/ܽpfHa?xpH;`Ǝ~?}@O#7,rorf3&SR- |>YyF;l`{ߏъ#G>7Z U`Lh'iDIPk寥|)3yo8[,Ռ[rFҺmoҜ͢,u˕+[@ι$ne__ .JgLH,H7λsߞ^jhʪ?QDU %} #S9 ~Nऱ Ʋ^@ АMrn8́@ YҜ)1[ꧦ~ưH1&sgK$c_̃(N]{irq9\;զ-q5wxArO.3as1ב ޫ1B_b6ƶhNP1S|08IZ,9+~mH'|*a$ן;DI˒%vYw|WU(TB UR>o^sh!bsJɧ*8s_s 2([rV{wO0Q4 l$ݻK-Z.ZUa W,O潄=W#2("A%$i"6~;],~RKt9mpXeRI#CC[|8yR'B͔"BD T,^ne>>,㏻yGHN8dyѢ /twM,B^|޽>?ٓ&ODس{׶5Ot嬙rvѪQR:;}:.ጱX,)--~rbXh4/O:NK܆H-clp0rι#ak Hzܭm^;_$ȊU*@K/eJmbHd(aEq.Eq^rU*@K/KmZ-FJIZpѪZ"%|21lصZ.t*##Z.UR ZZ PD1H-TWg7nb1v5 7i.JVQXDD uF Kу$ v+X*7qS9{4MXp0 ǵ`$bώEI54vT3t8BIENDB`veusz-1.21.1/Documents/manimages/importdialog.png0000644000175000017500000006076211662000553020243 0ustar jssjssPNG  IHDR}sRGB pHYsII1\jtIME StEXtCommentCreated with GIMPW IDATxy%U}N3ӳ/=,3 0DH\<1&jđH cG;Yڀ0Q5bl憩_(4Aw^Jkg٫%x 0(x&d2H7(J422\U(F5c-i3"L N` d? M+X2@ i(f*IjB/n$xf&/WJ($idxZh?lRlZ^*D*Br\!jщr"H3]]=K.dA[9BH9vYE̕kEl2h0q:jFW77AKZfPZ;2|rlLKWdyT.72b$UXly6&Lz/5$TNٖ,uT6ET0Z.h?~ܽ7_Aĭ+V̳ ?v^ؽ{ظT*Օ_z-^1)qʮ)21>E',l8:AɌm1Ww`>Ļ!! GmCx͙K`BLp;vdnLU&ǎc,Y1 ~ e;cLUNS#6a/xW!!DeL1v͏Rb+};^7oޛ/BOoixvx7se=ls8HpB)i>b.Õ[n)Moꩃiü̠7GDg;^7o'  R믽c.~:EwΩL-QOT*y^4dr:8¾{n:EBit5cM=͵~.glRtvSXF'VZkW2*RBˆH@)!??eθlszvo7wwݺ~ IJ>߿15 s 3؟_)W5nMj+QG٦ ʼ6|$xpӿ]{Oިm{-\X([w޽у^hqcΔ C'Lf@[([w={zHˤh"uF&5|陙0ML =uܻwCSbmglxD{T\OMeVX|RRQz۫ri2)ZUU՝vw]UUZ྽Y.OXT"O.Odl3:K,VX<c% Y"yOf nk|>x|;cpyz3su ^=$/]~瞧/~ˋti~UUYr>۳剉 Ld坓XU+L&Ȳs( \LvNe!\lWUXZrR,W6ElE#Gs)TLӓu?8ַZ_.?rʪUΑBO뷿cٲ˖;x\Z.D֓c"%Qk Mk (W3J+cDe*SʘTպ/RgMl׆|Ԫ!xSUs̬-4-rdt:9#]!n1cэVѮ'ajcQ3?8SG=٭~ӟ~ !л~!b*S* 2Yj z&Wd*ZWLJxMĤ1 dp8xs/O_k_tͷx O+۲eVlYw,6,ɽaoER(cG~eopm#rJӉTR(%R9mMeH!r/VRBH"3G !qW+ƃje.έ2Kkq¦DyRdz'MQ+kVїR֯!RetR"PA("C)!zpxcx ش.7k՝qx4jPZ,;v yj_޿MЇ>|B05) X9velRcy=JfM^JcG/d\k>n:SLVl/go◾2ɍ6o_>7WQ .ЕM/#6htА(UBGeeT(x7}3qǵ[ܱe7 "Oe(}_?ƝJg0ӻu7;ͨl2!O|1_'~oG>4,SG5/0xߢECJe2RR#'N\A7LUUkZ68Wc||q?q"1R˲*R<s+E@*%q7(Ě::OjNXc\sۿe7[Ӳ3czΊ.&tRL-_oݵu2O\mڔ(3͆K"Gx%L]g -1{kO2]z4auTNesy}[ww_ɿڤoդGUX._h~ry#~_"5+11u{%uo|w||wsWo̘`<"*LQLnr3!>2ڋ;ktT}剏xBq+BTK,ݷߙδ?׏6oA6c25fS\\4ۋ6@ֽ*`bA<ᮃLתjX<=+Hn]cEc)BTjL2f8szҠ3 VY p|61jCxgy~bb=W\ѩW-r̠0*S3f|_׮]c|t&3ў1}jj!}g-l.gxxڋ#1S~w|떛oЇ>>a6}|f(ES 6V{g_8Rv߼tOl!dF[J6hmȰjM\~mg;kljdi7gSeQ-!lgS+BQBHC.6"<е8*fc2*00)3{6:m S|kxwq])!fګAgQ+͍y!PAQ633]SGwgd\A٣guҥK㱸惬֪ǎ{d`` *4(uT)#uΆ#s5bRnFUL*t<=ezK٨j{"9yyzY-]'>RV;:5W1UMUHd*=zY.]O@zl:d>ȑHLMIbJL*3$ɇ4O)-(d4gr|rS {IB&TF4+XViD00őH\eɓ'^=|@U\.;11!FK8z{D(r1lʃ?ӫi~6S[BTKk+] ^La gNQ* …1"SVGHLͤ'"XcM"qUbH$VLg;l'l*#Lj6œ9MVirxuPw90O|=aX\ 1a7-؞7e㱺I(%lƦ5Z-yNWq*r#1B [@Gz6MΟQfc1:>j/8c:*2U2cMnez*JcѨ$K{@DJQ;i'ԷBcZV>$Y[+B)aZ6R^Hg&џOflF<,6ZUdjJK$u^oHPB0n 57ffg9&ZμR-@|(ucS[:ڢf+Ӗw_ެ`ݰd`/sYjuql:sb 65,oS&@HDHK08^Ar&ctv&Ek] zx-SNƚy4Su|!{X`Ywj(6: h&2o3ˏ l06l$9fI_#' _y כ.׽Ӹ}p9l?0V#LCbS&}{UAZ)2Gd,ӾX1z E\gԣYǸP&g_jrs%:f!Ƕ QdJx RAlHx 3 @ሯ1M|n-&Vo!ǔgD^-TZʱ.PY;8"/67|96m!!%l_'Jݟ i11~խ0ڋAEg?xPd8kISic]! cWG&4~۷E:w7 @SIEv6iÛ` ssmZs~ P|*oŷ¼nٝ}4gzq6:B Rߎ5E [md.]x؛dP~b6H!1T BED%'O>(т2BS(p_,˒$>FHzNH=6>B_BȈ-3GFj|aNmlrh:M; }MF,Hknۄۡk_svFl޲ /X~6\Z IDATM7 0_tuuij\&tu9cy$84R[+qtniʱ҉L&%OMH[:fibmA VU{ڴnrr{7VDV=qZY),^O?^zi".Z|f d~[بÍƘF6Mo{xxtf1aHY>yv=W6:U_r U$Iڻw)lv$yϞg}~V^Y b4}a۳WDW#M}zQkKgL*@<dr&ر҉6?Ⱥ HpHtPúCkð|-¡C.]*˲}ʞhCCCmޣC˓6w*kJ"eCO q h&޻ggЛL&d)цyd4۳{zٜH̛7Z*bQk"+f(Օc??ӗ-[>b]{v\+/9BHRT뙯.$d"O8TUX,Gyr\-s)LuBW:)+˥ѱDyd*fɤOs$jt3O΍ N0l틔T*L&ct;řj̀~Tuh':U@h*t<cHR8E"":X%IIJ>_J$;rqd, ˲<OiIY cAn1^T5~KFϏy~/jf+6|0 Qu{Ғ:4Ƃ Ta6Y6?O넯(y->U[x]Mژnʹl6%[cg5Ln `bzH0DƒIE&h? !d<>7>$f*[ݍʼn,Hϲkܡl\oqC 㬃 =7YM+#˲UTê|Wan1- 3Kl\316JezemAWe}AO{YcaΰVZw` 2-_{` Ɲ2#u v7`9֡ zF݁ƕuoSЬ @0*C\9Y˓W|Hws&3. FcLW4O5Lo٪,ӂ$FA4ƕ~{)W^g)msB=X.g\La|lpɌ Ln.w'& H&MA)~ u'>gB:S0ʝq LJ3LgtN9n1%jBաqeOy=l[<j 7J`:[06A=A]Jb1sվϯZ}k?WrOaNde\ToHf ,:<8@V2IcWZeVuFM @K۳meutji̳[i\72\uuyk ~Htl#eT)nzMacL[٘AWCAoĴeQFOt8Aq=v~0<rtI fzyŪu+brUqn=PWUq>8? S[6HUס0-@#A:LĒ!RBD*(M< jEͬ I"ن+RE&om$yϞ=gFE*+ԹM !X hi0Lr\qI{URBO*Jrpw+FLf9E r- Ґۏ+~6S)Y*ɯ8B3 T>+@39_xxqMhIւB,mWn'Վ`maN]mUrcӔ* F4<n2κ-Y9ژgi)MWj˥\=g}KkC׍}MXϲ l)KyL U~UO{&B`Zum kgc عkJ E#(F;ҸҸ4rĶ86l{Ckb<bHh 00|w?_*B(1_6Kfsv{~+VhsJ>{uwXS={Zbu8/ٻkm5 0#&LLL|#(O =BGr9$[T)XSk_]U3׭onmV۴UUkUJs@ Wڦ-y_vV ?dK[_sj.$vna7|O>!+wյNįVa;=- \8T#`6șt:7ҋ.4.}MaltIq/}6;z$ӹ[gJtn9phU?,]$ hDy"[򛊎U,@su.;l.ԇi0x<Hx8Hl.4웎ykCt%NYGMŅv Ե] YF#[GA #aU4V5q11,'qԪnSg1h ̚NHȖ!9e?'^%qxiڞn?yt< n*xYx\-{laa /(–Z}W@Z'z<^=Ub7Vj }a[V1`:519JEuDx G=1hC[!AZss"0m*Y#VlsP.@k+o{x9@n0BZ}4`t0 pͶ*jŵ pQ"®{(>ʥ”RKЗ1܂ȖQ"FC wG:#ё0t#e ۨpOz0H4>Ox=䏉&[*‹#?<} ! K0lr SHđ`Y;đh~LaH0kΔ@H0b !#-L 3K[漅8 !CЦ i!єj`l֨pM PY6*xY-gPy K9Lx$x=<T[O¥7}3 9o 3s[i9Sy5 |·FeiU8xxfIrr lfK[?a!WQ. w7ᬙ `.z1:K=Yw7ݶ)h¤)~VN@ё M@ f+ $b$Ѷp_`; }1*](MAU ʨÒa(%\0PU5t#%><#M]>]G0[eu֮=gZР0FJbcHT %pIZy2"DDQeSA4kJ}LR}FWt+#[6!$7o[U|.122>(L˔BbQ1Ig3IA ngf c^}aVֱ]@;8!J$ IVkRAGq"AP'h4 3 ez0@masyo_5H[sMx$!͚>''0M- * Pa#flxi0k ֿ1i 3(4 011AtZDm/H4 Ҡ]bJ%pYd0"GZ @ GZ <2HO7=l%3?ܸϕʜ+٘[~g}nz ?6y0;<᪰qُasePw!Oݭj/u<|:d2:ob'Tsph`6mTf<(f^&(hME:0cH;WVݘqHtGʓ߆5UvMԆ' -lMܒК+@s<bgjϕ{<ׇ/ _<29tA9GZT|G~V8#Hl\M[C 3 UL5,թp8(̣qm?lBM?A,ғDԶ.99Vgk~[٘f?9 <opma#qq4%㿽7aDNײ`|x 4ӻZ؉!4 h+cl1S@0?&JG DelP˼EVs(/ w!FDWZ ܠkU*NiY>k, u(>}ԇ[7`p*/C(%} <Ϛ 3ȧϚ4HmzV v5SB~aI hϚ<_ J!0Œ{ưT0I4LbM !h$KB 8[XYR{RTS b4r#hz L8e].?|x\ST@T.{ښ'N@ 0jqJRDDcccxg#W-Ia{x-(c ڙjuzbɅŽ캍l?lcsv<6p"%6tq>xcg1j[Π9lDd Uo3$!+h {LNlNw~S\Cs%V| Aw߁^wYh cǯrpƖ nB7}:"Lx8cN\q߿wvlF)xk cLՒqUz?*LL rP)z6\sc\Hn€l3Hc%]ElZ C,@Qԯ߱0v溵w3׭ewާ,BF\>V aٲ8`O)^=pǯ9W lҏ睤>iUDDA9??$.m姞=!+w/>?4X`R*3*#JtΏb q‚K@` BHecLVF;#H\2[Y ,VVȖXM/|f1<<$qYQDhtuDMA&_svTgLjﴕ_널^qy&[X~#Ѓdzw\'S07 XJ[;W|Ѧ\-+3n|>aáBD IDATB翸~Iƍ3L""Znr]Jٲۍ~K]So燇}[>k](\ve_8dN ^V?pPkh[*C[nع_WVYv.{$m{#K:T@ 2="| 8"@'ĵm_ /X~[oV""[G/4yK<cw1 f'ӫ?FCTԧ~_\r a ' YV։`HK'1c \*#J"IrTDc rZqJLJ UXVbsSZ?e2k~.OG*pXOW+5\PɃ!zGKcky"),:ݝY3&Ї_UQbT(3 Z$Y.%E ,p`f܉1 ghS+ժ$IEoE.+Jk:ՍI\VSbHV:E6 n)kkzch3lLɳJ>Qll=[#\>w+Fī?VJ0hjFuܾ*l4Жݸe7ha㕀g/x>I(!Rn$ԪkF[e|_l6-&E]se=Ҹ}\ьs:b<Xeb\HA? J>߰a!O?$|{$xƍ$E3qqP` gw[uG+ :[s؛vݾe>r 7>߼P6oMUÝpV'F!v%s#߳i_֮g. #L[?kw|Xb͚wqg fHBFFFW6.pz:#a:ҳQG?x62 [\x֯_ifA`mR.Εl Z[xV-ާzl` p?݅e#iL;=SevhG06t[|`d/]L*244 6n7ZϦR֊88(RΗ%KQm+/40!G"\dž=ѕN)V*ޔ" |.++V!ڻg,sٔ #&sxTX4/N&Sŋ޵gl粙 /HqTȦH Al,-˕6U2dd"Z0Rd2XTW Zg|00SVbQQR/tB0wUXJE] mX:j2 :! ~E)ʫO=JL:ɬݻ|>E N~-"$ LtB07 霢bP'˼_($W,L㇆M\h1V l $DWo/[1RJ};?rJI+*clwB.̾֝MO͢œ nذ?jW:{;nj*U2}ib N-͙?Vٸզ-fXGOn6EUM6>|[o]x[-cO?- Fm78n[TmwR[ѣGӟo|ݶm[&ٸqudP'3#ׁV]GM @gGmƫzrU1{Ǟx۷p կ~_f2//nY+bs ۷@p}\P0XDٷ)UJX%dtmo{… %I뮻z|0N_r%x;6c6èe&739]0?GkKC=J/_~UWyxg_DZw 9&0> 1{x@Suw'XǺ6.uqD,W*PI <șZNræ\Z(sxIW_JFcsȏ{?;=:!DcdX/u •W^O^|u Gڬ/]Le}%KJb$' ?U'_8:!WaJT:m}DG_ڶb"J-+hy'¢(\G~rXi~=;_޶`Ao>E N¾I% ݽk۳>i8,\طA*i* X,[)+D&N& @'PRd2F]:!*mr-F'q&0@С=Dm:DСrfſB!jchz3jۗ/[ w*,B>抏ggv8߽wyoFQUT}cNFa e3-_e7N /1AsXgV<D&ڬT*`@* * Pa{L5Ƙ(Feb(yTaY+w|`.S,l)}*݃: IVp+y* ;3e}  <6QMn<A | G}J=A軭qT*' }x[kЂY2*,J8bjj8Τ^E""N3>ٷ…d ,D&ٿsY*<::=1RrHH]FGOa0׈F" dhd ###9EQGF==$/zzzGF6nY¹sk,Cq=K" d2 VURH*c&|\r$IJEUUQ$+{ޒ1k;SO8s 3 *ʍ7X* {Z(un8W:cٴiSv]Ծg}lϨHںo*[XUM6>|[o]xq&a*{[`V7v悱k`#NT !jkVnr-"!W/t =z?~mwm۶e27[.`ƭV"n7A1m >}%w~{$xƍJ/f c=۷o_d2_~_%;gfe탶04ܴR=YOai]V[R5t#I֘UbuvF:Ch8h[6 ?#pïͻ .lT(J 1 Jt]w=S>`:Kw{ K9WnOiqtRlN+8š0w^WV.Vxy{$<}}}^{_*zT*|򫮺UgAђV~c}LW6cuÎ 뷠iټeW'Ơ7ӟ 4#h>я~$)H|EQsM=+!gm2*2:7Z (J&?^>GP(\yO>_J0gрnSjwWnESSh*,¡C,Y&/1/Y+4|ȡLߚ3u~`+"hX67m‚@; d2)ˊCqlllW\& fVOB*SF ¢(|Wi| vݵk…("%CEEI(Z> $I7`D*\x@{_zOe/ZhQ* ?H$`\9*,B>brl8Lfd2@Pf _0,Rx<*LRd2ƦBD"H沓 .00@ ^[0dY$ Mh41;@3$+y}4M&ϿRqdX2Fb4 KcPJ)c %d3\ݝ ȲR,\1ZA"Φ_E"- 82Rڶ XS,bq۶9g]> /vgLT ^D"=t…$k0D&s>* \(HqCr~eO.3pLӈF d8)d,PGFs$V*UIeYgcJs Ƙ$IJUUV+EVpx$l6x>ma!SX- 24 P3SvNylA!rRD,oX_l5(ƅUђ%'se+<Fi7ql>|F%Ֆsܘh%؏pmƋtqiLPc/Y*nBHZݸq;@yޗdCMCҥuHUk^{r˖;n !}l&қtXu&}}o:qaJ>/N$oذr7?$|$xƍIh<7}!!{&7n UIg9vJ?纛/|Lg_smڴixxG>|wU(\vev[Vl@TT߾eD_Mz7'OMjUA&:&h 2${O;w#Zf-ߡKQv4ACJ{'?yΝ/?䉾;–=u7~`*¸###s`|M\x֯_Mg?y޴y 4D)D*3Um~]SN8`Lи'>y@o =`ROSXvi_ɺ5+@e:pWA[}߹G1Q|[.T;YZ#hl٬:iW1.v:yD3SLDK/4MPUUUUH$JGףT>yYQ~uYɕQIBo`J{wk({uww3X,V(h>102i峖^:bLyRA"16ݻl'/l{jɒEP2/=F!*cE@o/)ͤSvۗL&ec50"qlllVX" a}sc~AJ DHHXbFJ/ߓGB aUQqb:Ji6R??N_s+&Akמ/ߗe( 5xXRDL MQUO)MO Ƙ x EE2"J-YP=_óEE6]h… ҩTGD:E#B,t2[4#ͱ) i8;|.F r d2dm$',b[gLԋO* c*c*sLd*Hq Q)M†6Lՠ8sn"3b<D6_~ 2F*zx4VUB) <M !OT;S"mλsJ;E DeHToSTEQUUBSBwmqǏ'@lǶ6M'MӪ=Ox郶~(Ҧm,8N YyD5/>9>RijYB+-!І+Z OTܝOީfZsw2J+X"w4־*lbX BH,yyب ^0{:@ ) i(@i47;;RDie"$I%`gn/Ƕ<**jQsKdi|Z$I+pmB}l{ޞeJSiُ{L) YI͗  Zh4ݶwҔJGۍ/Jػ;Es@U Ay'IIut 7/ |!Ds T*8VJ ; 7_kI4if-˖O kOcU/v5X"<ʾygZ"8[ OKu. ka`;NR\ ͼ5 M䀹3%wd,[Zf|&Ycw>nQne|EߕXj;+N 3e2o+F5ovҞ^M)?V+T) lÐw֡c;Iz7^2_}aHim߿EK,9/n!lJs]7DCt;IDATy٫/7ο|󓓖繦)E.@u){y\ 7矝B/j&g;;VOq=DZ>B/+2 ^w^ǝ(UU)TQ0YRHa) 0R@  IaX[o04RˏX0^Qtͱկá asD`5+J&Jp9:nFQT;}[7?n :iN_p`I07-G~ؒ#d,ʩNIENDB`veusz-1.21.1/Documents/manimages/winwithgraph.png0000644000175000017500000015337011662000553020262 0ustar jssjssPNG  IHDRUY(^sRGB pHYs!R!RsqtIME(:tEXtCommentCreated with GIMPW IDATxu|Ggvr\܁<(R(m RRhxB "šw_.;\.ݽߛOcwv|g"R$D"CDN#XB|\F9`Xf!>JcJq .9>Bi49Z"R%`<\1_![='tCL!` A)Bу_Nv}t"H+ tN+5Q޹kĢ+5bs!%I%ɈI.!bѠ& %1.P* 1(9aa\.ҲC&8…C79.t/ g\N}cKFTrM/ 1J"!& :u$AiMztXB-IBAle>  d~*޺?6:,VDa zPCIio^fTʜP(xz*BB$Ƅع!i8is˴,i;am6,agjdB.~ Ѕe*hXJ(q12Qio_X/JV6lX6'O6asg-g?؇{IzzFv$IX,GFTZHH""qdEнEj ҂"9:fX, !XН]W3+~*nfC%/JtHu)QBdBײ|:'$d;D!4m1F> Þxv5i<(0XJL|/֬Ybde1(NrG, &fыG Ҷ\*@z,99cA;n-f *M~ Uz\%g@Jư r?I8D~pWPxx5/==ˏ?mղUOoy{{{{{GEE?{z\ɱULE4~<(QZa/p.s6 tMX]¤C.ź['Iz?\{Yϟ'VDa T\AKL$LM'@BI ML$ً@=~aܗ]ԨU$ʕ+׻ϟ=ZD!(Ӧ.m^t,*.mf7ΰ/qɄYw%VUxQZR0DɵeT[P^Ac D%/dbVJR.)Xo .8p'1ŋV1y8)Lwd@P!qSM?omT7oܬPBDD$IڅPwÆ {V.,h2^4G&Egro@t N̵@KKf \hh4Ј+DOss5ÏaXȽJܘ/`wΟ eӜW> ț7u6@\b2b["Аs.D ~ҥ?.Yb̩~&>I 1VC7E|gyb,YidpeczdkLOfPiy"Fx@ vx' $u:X,YP3fX=&:ɛgML ,?K/ț?B)LXF j̓Uo?8n)/v:g%T!iOuȶe;%WD6w7V!fvd|4; aC!͛ _1}J~}b@=yd &_ܭ˗~nLls.|ޢɓV\ԙ >JyS % ?!>ݴG2BېDH"eH/$*'[*[ a^1T*E4-(>E˙F8xE˾ qx%߁xB…`qaFQKFDҠawgdQܾ}ZT&rKMYʮc-4_t5Um37'Ŧ nAa0oXޤlld62.B#FlW=2J]??!G DiDB)ӮNSj7~r^E=t\kJ-9&ʕ+Q$v1]o߾q4ŋ|}}(F:/O;=9'zbQvw zr "07B>n  Gv[ך4i$ j4׮]Tf*U8kAxi@n #+rm]RE,O؃Tr҈:t'B%L0HYRaYuA ctDc11+>:=9YI.lqdAT mU%?QAQ;wrlH##ɜ*?4O~~?,I IE$i A0 ;zxtjaa|4]M&OAM˗//F:I޴> H~s]d6tY Dli5VRWfQTJ oN~t(b)QJd8 ,XD:dĂE UO ,IU[S@C >P" ~n%?qry39,|ܣ" > 6  ^Ͻo=Z=lj|gaGa 裼` BYr6绅ga T%da^bcj1)@*U,3S K-7(YB2!2|*-,FXHsʫbR@J@YVĉ5[rBc9r˙I s۰mr@F¼3 CဂPGB :q,r/;sM!T] 0AN;>iy^‘[Nj8\ffc8ȏUEZu9X կ"ϰGvQv9M_3p| K;{;|\o+3Z+JRbm Ŕsz"֞D3_@`t DZj?mIy1'(жqpZJWg3u( K'';4P312_a(|78T:!h\8́gY"`A㠋DK`ca9TI{ |l0_)>8!*eVd98+7ġS+즈VO=ٞr%4Z7tW3G Ml/4ϕ 2yA*ݲ]QsoX?!ba>Pl?BD}Dg7šh h45Bw%b\خd,*iozOKa׆Kw:̚I  a/iv!Q)SzH}H"yu7@U˧R,f"[63HeB,/Z>N?+lWJ!QSMW=$vٜTX rM4ߵdTWEi=94gφG[AF̱1tpKFHlB%Es+ 7eAaN#ql%r⹙r bA$k:PlNzN,NN50_Iփ4+Gf/:g-Zd ThLL}Ro" J8cɰG/,!Z^f,_}f|r9.`wX;G31qA (]tD,@Ҏ-k.Fwݜ-!JqǩE`̊1RJ IQESX)fGl0l m44rɂAAo !q1|^ {F $6FnA8TWѭ $nE%H h9:Y,4v/ k۹I*vP.M Wi6F'WF%:pǩM0Q3\KSٽRI[ ҅t# @G4Ұ{BN#FȦkD4bw6}+Sɠ@JeVE֢p]zҶOE^8}rxcIZ?wp[)b1t*Qbk郭7-[V)SM;&rm/[N^[P9i&̈́Vk!hZga)dTp3'Bנ2GC w>?+n@EJ"q#7˩8;E`SSB%ksiT.Fc'H:gqǩxčr*Q] 2WCxiZY;G=*ܙvǩċ?N%w=8ƣl R(;Q(* honjN%d'N"#B`\',MYRKIl>>2׷r?NH"t̅B-eAzϡ{CB%l䀏%ĜJmIaNp%X 8 |z-149}$Z(qY_V),}0mK 8):smzͬ,%Bb_T*]}5+GΈqHd%ò^TtR>%<|x'z=JmVZZ+W2+JN~_^]//KV{=h[B'4jUQ`DBȠpGZ+a1ƧXꀁEFrjJ^|EPDbVc0Fh+G8qVX,ѨEEY ɳW&NV2k(/ZQ.1Ba< IҀHҵѣG޹}LML  mbRppŋףo߾V#0 <ΨdtiʼO=,NOg\zꭁ[N7Y ;rԨlNOIh2իx8w1yT_|EHHHVN>}aHԠADj}aĻwnݺSAL7<{rJVSSS+,]pE"B/Rv}m۶ڲe6m@`tE(pʅm9)_"_+mڶMxSHݻ$ԯiY~t钽0 IDATvR#"N?B@ѦgddffU\u  ku ]{?~j3#{tF%E?3ecb2 Ϟ?\t P^}?qݐ-y8Ĝs.[*)*FzDOăGreKUQ"6"'׎P'hyR*~=Me@o%)_vGy|q߾w/|W5lPK#IҠWiԠRTx 7;#ӰrCrߚ]\tqo[p5w߷>lքM"$̙UgmFqBq_uٲe6ܹsgZ^Ɖ j53sYݦ]zx=dӭՖ}8[7)nhxF;q60AyG 0`ML&ۼyU 17۔.F;{9~6;;>3f ʕ+dg CGegggffT* äR)B0LlŪSc|ZɌY̟_u7{}x?֫Wwi'4َa,f@wceþsTbc^_~'O͜c6;j ^AܹsO*V Tf3 h 6짟~ 8p˖-k׮X!RU1SUF*:&?F zJ Jr4)Okwiﳔݻ h11Μ9r 6P0y81^wRJ'OrBB }7 z |Ү\ѵ[-W֮iZa5Gw<@߰ΝM?kdʣG;ĭ{. ֩Yέ W z9 h ӝĄ/_]tO>AG޽ -qRX^ kU*zիC-Iwu`ql@̵l޸ŵM_S-: U('%m޸"ϟucǏ;vLDb0t]mV+ݠAyF;vZǵL4cX}^D"d2T*ȓX,y<ʳ0p[w̟wySO<[>y̿W42ά3;vXBY{Ap4UE7_<|g<<60Jzu?P͚~Njp x<-Ġgɒ%>RW_=mOjժgϞ˖-V:KC@4B-:A|Dd책NMl:W Z NB,3Kt1~}[B]b=}uF_df;{fbc;/il/`jqqbx]ZLj}J 555#3'+[T"9ݱ?ok.ߛ76j\~JĸP.<۶m{`۶m]t |Yю媄rW%צ-:^vIJ?Pyjf͛ϛ7H$"bСW\ڢF6u5={tC7[m1Jz.z=A^^^ S.(P*$i @Oƍ[uYK_ϭH"|ږ|xϦ{aQ>џ0q<#guѣB E gek_߹] ]8KٴsfBqQ7w\U++VCcO ;vT_RNP͹fYuv:sݻbnZ cɛQ| ޽[.~дI.=mժ}H]Fm6KOOY_CG!w$,ĉSL2&fRZzf=yƱ֭[O?n:zp|;ʶ?-'wWqfpk^|٣yo]Qc%OM(wWep`*1eFI<=#Uϥ*3ҍ='TP7{ ūVڼys !ӿk!c<믽WFl첦ԥI C!n (8yzDØǎ$b|U'_~ 5;I;woXiI:ɽ۔9CG~pOxpW:FLv=`wM1Bh8 iOq$W5!;.# rW9hiRT uJ塕C)[2;}J֐8czǩDB, Am/ Q^Zjog۞={7oaZO>z{OGC޳$?J gk&mئ6ϝ⫞iYc\<O" ̄FH[3CiӦufddtn0n?f+2м2+oݍ;qM[)AA8O&8+Gz+nȂ[.K|-#S:thX4_0hiJΝ{^V}@.I;w5npAPD@ p  4mtúu=zV(a7m>z3/\PFqM:NM+=r{qq˗@-:A5WuqRar-B'/ϲ]:ϾGv\;/tK  Դgĕ뗼߾}k1HbHVVqZvr!uُ3f]"N8J!?k?7CN;3ӏ̇δ7_5ӴЖ?tiCo?3Pkw~z/;o22GH ᕫ}?4 !$J˄t#+7S"fmZtz= ҵ+q/}&w>Onx+4VR%OOW^}U7 &|%lOܫÇdeeLoԨ` ϔ5i\X9@V풍O<`\0vUmZ!J3g4wlqjy_dڛ,1ڦ1+q>SݸycֿS[uZz܋$&^^^)V 5X(Q~ l5~G"zmƍڴisȑ_V;eiƍ/]ϟsիWlBVZӰ7 hѢ7}z!Й(ԻέX!^ju ƣ2 48Ѿ^~zzuLt/;)QebɼkxD|@Y\>lذy!!]tڹk ~8qMP(@(tLGr=ͻZc{θ#&>ӏ7xˉ](&f?U\VM*"^;]N=߿w屢?o?S>XӮL|ӏ9dЫ>UBF%F$0b( 5j=~'ٓGϟ0a @&SSXBȺ.YfK.UTWiЛo$W?]hu˭cn*3HIǎ3kҡxx>/ 5T"/Vf+^9b011A/4_I:AO~4ˮks5ZC!x6BZ@D3yөT9 32LU{ ! ުsĢA &Qzj޼yG۶1cY:9mJ㸸֭[/YC 01yh#,dޭА+zݭ׷f?ֲ|Embye?H ;H$+F^04Ԥ\3CJø/^>A l ?|lwpP+NDFF"hJxcǎN4%>!aOkm۶]`Ѡ8qjk?@O LJUdSav"IRaaXŷqR3 M2@"P1S'* ֯=  =wX(o^?  EB׋5[ƜE|j'-6_VQmkڳg;v\|%wkԬ9a~٘QPXsu>"h:V_DL!Ap#-[yKոagkБ_.\9Er/\:ٻX^ߤIͺGȑ#VRuٲeq1I:.~T'62.?(C.;3Q u"ܡ9?陪޽{cJK,7O6/[C?>{=3'|VؾlLnʟku/(8X>g|*V(۷3++߿ܹ5k6h`p&Oѿy `PUeVZ=+ԙYDv:]8NxX@< @nn\33d$ k>ai@9YZl{5ñ5j/WIMݓ_&I:@2omZ{A3~LMST<}be4˄4^tͷO:ҕM.1N뷂s}zUCfA`1Q2p"˾|S3qƩT*k>LJ .RJrr~5auTk-^ĉ+8a?@ǮO>e͏`4ڷoejʛ,Y֌/!x]'}YY _򋌔m۶:`45kFgff)ٖUiMOUe4&Xz5 ( L"O2k2"sĩx4S,>ktmQgh%VLsKIhF#ϣC ޱ{֦YN>} U?r`z0fp%u½{D"R)ͧ8p`r<-- trW~yns>D/+m_ax+ߺ}Ua!6*5%qVkvеLe# nS#Y2alqy3wς&@mh߀шv=kۮMpp I$}|}+V|u*񓨨r/!_ʕ+Za'\ѣK-|Ivm:mUp|޼y+W$t?q=%[- yp?*UxY 5[Ҫ.]L. -G+;4رc׬Ysڵm0]1m_,\xQFkZ{\(>^BC6nXݺmysg03BV9kvN<ǟ^>F$2k):u>{@FNڰp=z^ rL0W(ȴi[մ\Hr\!M8~٬9!2̘V{6h~'MHdsм9P*s]W_~  %?ΟϽիga~OΝ4ip ĉvڴiX, 9h?ѣj ?"h4'޻%3W;~H+1 Ůڴne;]T*ӦM_:w7+$tWg«IWw}y+Dh5v3q;`4C:osĩ&M>ܥ-3P̾rz.-C:ulwI 'p?A1bQll1cm۶ ڰaÐAmmΞP(ZlfSuoE`-AiYjVwI.\HHHq<22QFb!×-[:v2'[3gɓ̛7[8q/Ao盌U ֵʄ)MKOhuGQt[u>V +"uO'e||—_0\Tp?9=x1 s%%e&&t:qf Gaύ;w/^lqHTv-Qo 23g`^ǎ}sqZtʡCva̙ѽzr; 'Olx^= 4ϛM1V-~>_?R Kͫw}9(Ó_f^c-{]ݳϘѣ+Nر]jjٳĦМ\ޠ~6_P鴚ۤ|H F?A6jsr 68Vo>_ ӯ_$*Q9JO~zUg[tA6h^Νژ45j-pذ!w[rԤqF2ݻw\iN8q'eu4" bIcPPP\.3 !ꦩ)MD*C(<㼜,(>Vj{U_{P.o=QyE^r3T3L˩Z9Zu lׁ3A׮B`~< ڵ#g "A ZD@xo7e˖-[;|{21M6Y0㭿hYfvTڿoBUg2eg"~-F!Β_d\7s3zVCiA;ht11}||}8Y\.D̬Ǐ'$vDfGmmĝ. ԩ!6ۨ6ms>|ЩkS5xk|2àw4dsVi4YȘ d/qPtm[ |U.b˽]>m.>8%%ի|nVTRӧ-x񢯯ɦ.\[6+W9|l .]iղŖ_IRye5q:L8~aHBRif}HhӶoTX^tZ:T΀Fi4vԬk_'NJ%^^j233O8Uu@dV8\;wHOP*] !ٹsg۶mM$f͚<9VS M+ʖ+ "]Ǐpƍڥ%edHY7{sĩ zaSþ<\eIJe ,,ht|>/< ~B$ph:8b8X '!nEHS Z3;Ѩ?~Uh]A=>AԫWo~5lղ=7%usĈa_+ġCG+TP|i,2*/Yr̤&vډzB|μǎw~ k" N:}MdRIn_ϟ7ln`_nN{ꍸ3g/լQ"'=w~y B6[nNm 4G!B;[fu Ç+~^d!BPvƝLPyS5yf-(9ǜ=}.(ǯD/O~##GlyE  DƎUe6]A\y-A?tkoA- H%fTEB `|OнZ|Ӄ`Sj !:tG[ٿ gm'*3j_z^O=BV$j Y^E>N[qrkvJϛ-[:c33GھwlfcbbHMM{x u~{toJJ 9: !.\|OÆ6}ڷ5  Df/?Fon"rzN2uX bԩc)a2E݆ DbBCrC&񼤘Xt}r:Xtǎ zjZ 6l˗-/Xb萁ZM6TR-A* 5j`F^PWd2YPffV>W4U\,nOEd6sha }ۊa#֫̓g^}9ºtPϑtT]3};0o\K. I!ZB^*MfA ,R)TE,*U@z!үMr ss3;}3ofZٚkND̥\h^| [ǜGO~ٲKa䟣.-Sѵh\ޱmGt'/m_V$iڤ_jee|$@/&ӆ-[R +(Ta APERBHۚTZH[{vGXPȐ(7i2H]缼}iҥFueӼ{-: V V; .֯#aJŚ޾Xz9 q:ԛa;~æ9OW4RGB^|ϤRk^[?)W^ɘN._\Yr5W_.kT39gKAA2r pCmԏ=ϷũQi{ʕ+Ǎ{c(ڽ\QhXEB+J>v+ؽ(3\qB8{mj'0ދϜ;7}7O>]8_I[HfH)(UL@ g͌FE}.RxcJо;Ve&˯ m k :)#4D WW_퐮{#Gd駟vtbq kHX6-[%/vƑQ۶|ЧWF =is}n-դ  z'y SV.jDjgc._F_QN\ÆM7խZHHhPǎ;{֬wu6]^ax6k8pbiN߿Bb}5؂"ץXg+b6M@1Hg?`1K7g·#ߵ\(b#f>)2Bzh.ik3gt 9ڤq}- fRuwlʳZ (Eۓ״i+ 6kW^}SΥrrhdʕkl`uvnNnF ##k<3uj\||_~꟟ѷ:>zܴy?K>;c6cGQ&AD,&\l< bB R|$)pov*w&}f^NOBؗ-[۲wzџ篙S≨5SL6իIII?p}۔=Dw%?ly޸r6:.X6_~k<ύ|mLTpO?Vv@b(7 |<畗G Yn?##sjI={vmPq/\lܸai*W;n}C߼VO{ ۴zo?ʼn32`{+n_4 o#mX(dzES}VL-P͟&<Gͻ?TT fV2,åY~}7| EA?Jݤi+R-k:ٰlFQAXrɓ[4߿jG6>ڶkت\uPHv1e}@+ÞtLNI?P(Piw•3|2=K !(^'0g zI.B)E'S> \<#7PCz#xأe3 8ÏdvNDaأ}oRG(N=wÇggg\H[hiDM~<HKe/ޙfbVC|Fu* -EͷEC%eJ:2R,dX̷L!/GҋJftSʉ"{w ΙJ=KG`kK8/LE}?zHj̹-rJ._R/сQ~{ӺrʹƵ 4|֭[7k@liLz9Q-[.ս.]sǮ׿Bm%`!ڵԈ@"cN"$EZK}ؤZG7yP}?xvHughڶhՀBM*F`XXz1͠MRQ0F!\ {S{|2W-X" #aa/ԃ& Ļ{ȱD  *ٞ=A#d@-{w'٪lhԦqO+*]tئqxM+8cL6V4;Ê!Mǿo<%41qqΝoѢCR `04nxy/R;zhDb_0AБ#GFx#;zhzX}`1y4dl4 Gd-Z<ꭀ7'YO1ݞS{ }5#1_L- SNք}ر/_>pDVpy.@6Z}n&N`rBgM-D:͓0 vr~gbE#N;@:M\:r<7×M^(  d .4TdogkTSf1PXV\Ia{w3%zoEM(8{ҋ_C9Ze:-AM>>OI߸.q^ PU5 cC¸ʘHuBf'Jl&lV#HFJ `1Jh&f+ڿT5mJ)KգSFg~NS٢E 7Oq{68 n+(;[Lc7KA3rőhT~4GAٿx~S|~PLŢ`\􋁏['.)is \slO v SS_>Z~|x|:1I=?y{j5{ѸA ,V;y}r Μ9k_zС Az7Ro0;Bz$$xQTS7|bzop8XB B^H+V_J)oڴ)#9ޟ< wOFh(kyh}:vg^tC勿,׀w~Gn1m۶p!pF Vk&gN!_r85@VܙC~ $TZVgٻ?abfw{ݧg̝P>HHR%4EJAhe %>wleYAY_~Ltc[#mQb){وSJ'LP08SٴUsM;߽۔M[[ fٳEr7nA2,p!Fwܵj3wNի۱cǏ|Lً3$m;yrٶڵcTJeo4l۾۠~OɱlbBU>=^n/Z; 5\ޚ/^d9^U{Wy yōG1;:2Xn/9d!բڽaTieJ< Lf!$(S( CPB%Nu67[(]ַ!4Dw©̘T;a fy;oHuDGA~߲gSOm~C՞QO]9J/Nɞ8IZ)I#Ѱ'N tsTpwn߱[n<+%ߟg5ռ;~IӢW^?/yu~-k'/MY˒O7D7Y YDI0S;X1D^mdPE&&?}LttlusT*K/>ύT*u}: DڪemJlv>V(SDV PdG v_O[Cpy E-Ӷ ҹ6_*@WrHps((l?*|7&g ?мrW:u؏?{&~;K?1%ZacE 9Nܬ_簹n1 ERS$`pɕ+W%GU$qW-k\uIE s* LFR$@j*jiΗϼjI +% :ZY76==1o֨; z~ph SV}^^$ndvmVN[7Q+n5nzHj"1YsԬ80tbMSAZAVu5팼w_}aȢo6OO߱"8ݛ5iM_yǜOB>Zv?o4v͟0)Lۣc.g`N"-tfʓ ۹k'B'NxɘC?ڤq5"L&^߾cW]yj)frQwяNhմ{OB$J;۷yDү_1cܹ^[^:[9g[DMW0(|wA}2EѮ3Wp;۳etk׺O?L~i&4mwy_yZnт DzV+_OW't'NN8nX=8zpH͔XnKw[GGGM^fR8WRN7P~T侘 8ͬ;6Ovݟtp.XuDe}Z6!@R7nJTvU?lsD6ot?Ə~Ӳ}y~N'WV|󐹑:S#9N:Y+;hpevܽg{ 4!VMݼ7Z{ 9zfGFѾTؤɣǸg^ZuX>oE[U'K F׬_.6ks3-%3kڇ_ƭ`M83u ?K/>_,`}+o5^&D r?_{#u/wx^QNw*x3ٹӧNLM)/|aZ;w@b;-&ݜk׶C11M=ܟ+Om(9k׶1K2uzQ]>~zGʏyRD~ 5fg$vTKo[B}1K`:$ݾecQ %vީS_~E(XxUvҥ \v̙u_ksTLE]dcg'3}sh˻^?2q]~{XQCoB+~$L<~d| k,XA $0 F\en\<"&Z\:+'@/}qErBUZ*Frr|V.\N,ݪeܟQ g^Z6!2 =[Rj1덆l#A[XrN,-"d/XaDADAĄ{}'S6Rӷnݲn:r:`B-M}?fd2筞yx ر Y٘;iī)WCA5eRPUċDT=j0!CoDT.i]gh޾Q=YjQϟWfß1oڢ`ZvҥK7oުU ;[ ֭KII)1o߾}cKMMhm۶iF}Lu+)&d s۴i j[.Wo1H@ڂuƍ%4Wݿ}!?v-R`k>BRh`Rs;v6I5# JYV%?v5,8{7ӳ `G;2iכ&5\Óx > gfo^fK;ԉ)ڞjXsn`0ܼ& T]txSJ쯾\?mԦ=b; _=~9gOm:}c1=;zWqޝ֬GlEq/lMKB})b&w;Z-jU}Jfc|4Qb?[ IQZiSU)_!aXȿ⃔e͛7o޼, ٖ(Xr.c({%HF@HlYhC k.E-!>h\TEؽ?mc۹8o IDAT)ΩdHͨW C@ [{'#㹹yr_/ſ% ZEB?bS(._:PkŋUq&=#b/ BEhc9PN|w-̬CרQ=((Aܜm۴V+KոI+A0KTq #+GCv﮽EQ3re H)E+4Fԩ! .\['>`̹'G޵K9 jM_ll*))U /m> PɊgI3rzARkWQPEժ]L9 w2J> A{KNN;>Z tto!`PHCT(9MLpZn0fSZ1LIvU^ߦusm BͨRWת(۷2eg͚G"##ϟ{8aDS :?V(XlJU#{4k վUa R~⿳k֬=`#fyk T*wu豾}4m\fKU)ն;u)'t{v=u}4L{'3;"<OjѲٔk}G℠Geg͚U[}i߾kg͚ꫯ>/^o߾1~+VԴif͚>?^kۺKɉ l1`:!rS[&lyǐf,wFQ8`0<LO .]:תU…yF(|g|N`Æ(<O3fěZz>RIԨ׿rJuZ刑/wԥM_.YqܥKqqqZmTxLI49ctTg{^,_{֘ 慄T3fdz.)SnsSα8JvzRUxtRiJȒ{ 9c~_ULb3UFlƿnLv-3M6˻x)/,8.-Vnn^ݺu MWڷo'gֵy<`%<)m۶w} oF%xjsME}9Wvb7߷6RZT_3RZa Zf͵k׫fg\{ª}-Zeffi4&I `uVby)<@v#Ts#Ŗ7ibwғH| J%U];} VJJHHhРZ,:{2(J(Q b 2/ =劽rϑDl l߾]6%2 aFVso2Vp  H3RnQ(x jEFcU  ogeyW e dDamE_ _H8aFx 9s[٨Jj=r&$$` kts@+_"YjD"Hyp롤=zuVɩ8o< dJNNNNN,zC) '2Cb% ⛄=   H83  Hբ 1"7 Hl*HA_AP"fR,[W R7##åiG~Ӹυ&ž987*$<+ / HP6faqn ; EJjҤR45$luA!E=QUb, qk5S5Du T|^ycs)ÞJdpBӦMdkIm]! wh@,ٚv%x`pKBe"/ #(G7hk,KlW&I]! HcdolïڧչT&Aʄs~tp9ߓ0⧸-oT  (!iBA\A {A H1k N:zۨAAXGRܾ/AWرAʜQX ^sȑ A| Nw9hȷZD2X ^Ad2%''б-/60lA*Cb% ⛄ӧN~2nb1lJ$ԚlU_;?K>AA_A_uXA_AAE?&hR5!Ζ69!!eXا,j;rZ\ALߐUj`!6dwSѐo# ?)GdaJx.P! ?[涎6Imqٹ  H!GeS=qR䞀JOv8 3uTxydoS0Zpָ2 ù_A{~p vN̋C )/ HiD`!W UV:79}'a7bDA\}PR - Cs|>ΕçT*@Aܞ5 ~ K?#AAp|_?m_ a ˲ ;lBԫ))*jĈW+^aFȑ#Νz@tG9rHZZ/')ڰ}>}( 5E&s~H94o~ĉ!<<C Xu *jСX,~q_BOyޖRJd"d1E<)sd_A{SkB?xyF@rg d0{R)sAI{[Gv &@$\5+>#X ?*\qqnԆ˟J=s/aPAA2|)+xSM_!E׍;O.FQI9  +V!{uä,QT Ui~`x0/ %vr̹ 狙sU:GA߄WA&n?WJR*W(xfAJ' ʆ(I($þ=y}]I2U(<`7OQ88 H+ !rkzwJ=zwJHsNq*'A"m)K>H֛?7NRąVzVvVx&@q|Ep%̌;4ꑮq&Vl)A%E:GRύ" roOEQ ^PK{(g_G{4mjͶmwNPl˯CW@cN @A/}n`l EIRJ춰}䐇[O>J@ԛbd>bܹJeDD0 Յ Ƚ%77Ν;`6/ﰟ鿢ɲl='!Zn Omr?s÷zCSy,;ڵ+<3AAA͛7Bsau!ro~={d!x 7m{z@!}v?7lxPAV~AQF5G> !@Hif{d?<7mS@Q sӜ&l 7lxDPRz%BYjFŰfN(v&fNQhWf* 7?mx[DAP!jQ:r`w֭I a! MLJhʰ?m"'cZYO-re@_A\֮(t3>>^DHKKU)U!B άڰϋ}.We%Ducl J@183 HC_WrqbngUY)jr! ːؚA^f\AL "u sm@`Q˿A^ A'ӪT> @ >o[tcfl TeCచKvq ~iSG~?<A?ZFU޸U5 ]jM)Bb#Kd_A m[K3\n*91  W0JjOo޼df1] w/aP/8 Ayo/B=H"-g cա{cV^FTM㟌snQ"c/ T DsS/cG5nR.hccJ#?j[ءB\LWS2s POww@jqI"wR&3+++NZqՄ5 / +.+&J[^i׮_*0! !@ a#0a`Pw&P @R @)W[h@(b,7J $? %RD@7O)%@.HV KT"@)!$ 6WB S !r%oR aX  D(*D$ r J%J%"IMKD*Q (HTð:0 &W( +sH`6x^UvG>8 ݯ؞*1y mǭowBpJ\m.+R!͒Rfc:@ۏa)ͻ<ڦm oŹZcT&REAu5ZVϜ`X  38ş|e`"/RlذB0,X? * Ү :U$,gy|yCX`B2PBJ2 a+JGARRa@)y?΅ݰ]GB@(l3>&#Amxh&C:E@B ?% {1 qJX$&)QTX(1H FFF,k6%x R㕢(]t.HSW,pEܨMI8 -  @sAA:h6RI:B2؛7S__ He-0\Nnn(EoP!U]<)یJl1#"ty #qAsaX UY1\Τ7JeV8R 3)4%Uv8pAg괩sR{ g4Y/&߸pbvv`PT!!!uԩ_6P!IbfDA +B2 רoέ O5EeY71H*8yСCV56vݺY׮]z^߶} 5j٘uFeHQ/ H)b7O:d3v%u޽1kl6^vȐA bν mPb IDAT@r;Ԩ!={Ɉ  &>J!.hר/8XMeYt&6h2a5;Ѩ g0|)`ܹcZ/^0]Ν;veM(>'AL_2>2:~3g)QB[[[+w?k w[OeiPdjvp}lO ӵH)H$ٳm^׀ԓO\G 7!+)T0N=H'>ɕ+W|hC{>|_s\'O#/np"gUkG<22Z fӦM?յiӦMl4/[_"a+JJ]v;{f%G,?%KácoGt$gCC>7)}u*I6X9sfu_t!Q(߭_wY[4^UPG@CG֯(\9 `&̝3+ QPq<)>fmnn}G#.V|d2.{b )lysܒ:S?_M ?7N `e-_4Ebl|K 29v~(8HDDRȈ,7θW ){w\~"ԴqF @u۷D$ ޽[DV8Ws8O~*\~֏Ҍl"]P:86oT573mMdȻp_ pt,>1z_ԐsyFq ̏Bg|_81/0l>?BD"ڵ'8`:|>Wz?z=Z."p! vJᰈb1V^1֬7)]]]~;nN8sωȶGm0͛6mVWWgjf;^0;^G(; p ^P@@ PfJ_N_tJ_2c/P:p c944$"n;P!&Jai~8+Cuuu~T9qs='""P66mSWW7jf[' @@ PfJ_N_tJ_2c/P:p c/P2vH$ m޼`7;vk喛m;% =|jmƨmzp;'-466._(i" 4G` V2ap8n(T2555Qt:2ΎT*xfpjD0)jc26mB;DD1/,>t5\h!LPZ\á/^LFSww7j6[y/y;};<Թ~7d2{U\88q6wy037}C?x9V2sә:뷀>y+;ʉRsnt*FDt2I&"TS$Ƽ@a/_7p2<'@AWijj?Ap_(_%J#l}hƍ1 W2_PZNFMM-_G66X0J646S?.w,>tD"/^rsY]|1y_oۡ9WX?Q +T{^=_*L^x/:sfرcG}kWx=y\濙|4t艗^z o755cŲE ^G=α揌Rd:+[r"yWڜdgZ󾖼_pp^{D"z-[o#HZk#Ѵ: A'?ٙNЭ޾{BpXu˛Q`H"+N*Z&dTpp%ɓY oy}W9s&H+U`[[=z|?p] T6;7 ThМV>Mi2d2=g?~W O>okZ[ZbRFtm_=#跾v*_v+Whigr*''}AӺB|%\{'N+pûGG#́?wGVt: }\ȑ#?t(_rW_R 9ۄN'ҥK\|q;~z׻޽~xv>|;o6SÅ .?//>߳>_cw-@ XXxyC[0GԫaZ.W5k{vmƓ?ΛˍnPxwlp҉T6ww}G#.wSނ͛7wwwRf```ǎ"r5r3[SDC( ɝpͅV2גwzL:tx41?K_nǎ=hģCCAڕpu*)Sww7-[켟* [U&aQv4Ρ ?M>H4w$Idz|e˖yܑѷt|׶]r4</ݫǪU ^s=ߝkO|J؟ur4*5>|tٲř99oMt?L_* ٳׯZJJGF' rA*̇3T8t0\ ^eL3LD'\h L#,jpF 0Zt:H$gt'\.⺞b/xKRi5kvѝFt)(Ut:.[q6ShHDD{il,Z_.WfX @M߂/ wξHx,<;O'ή3h*:gu&WvcewU ,M~$YrVkRhp踈\c'kّC{2M(bÆ >UD>~GIOk3RImڬիLLRx\kSİZchxp7s[+Nzїi}|a"35j}Jn={.a/T" ;|XV룙NW&٭Rɭiv~h޳HO^V~(m80o+z"/TS;Pe=4f:m+Tz[^,[w7 P'}رH/=w$_^LtloJT`|5y<]j`%znifBmǍ=?Ifƿݹ%3͂=+VۿoML5Jz~Rn@^~z`[֟mlDO/l.T_ج _d4Mo^{NL閞Ezm>== t¾"*Q`ǿ(?`/T$l;ED@SVzGno3i?-2 _/"Hm]5]OZ[ϻi?+4F!ox{ K>_vn`'"W0hg(*=uww[ܲ9=40/ok`o.WF(C@$ͯ70J էNdb nl*(&LEZ/OyM kN8'JË0Nj/G^ookq9h+#k/'򟃅ֶ٦6c$b OwQ{"<=S T&N$$ɎY`:7[>'9IDATx_p8LSGcd2i$ N&X45U+o즾y[ ذ8ևn)^km_h$Lat2. /g)[|^p8}Goe0r&ȂGf_cR$#kWYq\hC<G\_氿ry?DիtM7Kwwwoo/nv!"۶I_w|t~̺Eo ?*Ql3mo&l!>/*򟃹 ѣGzrGq\vȳiRE[qSUކjnY^7nkdd-"à ^!%Ujkkmok޹lEŗطpVW%@s"uBeÇJ[Z]].\y:)?lO=m1 qh4V|a^4i@9J)z&l%RJWS)lPGq_ p0]Ŭ =>uOU?@)G_q_ vwО=#JRJk}uEkt1u{͝dRj]{]qi[[ g!U١CK/Y`6.C\wώ/(TyJOx !'piǭ+2Ӛ֕5wӦݽTرc~km_:>ؾ(ڢ]W$[v}}}T{Fmft_&eRLsh+T& <Qmm_h233Ggs0TN]mז9 ԧol۟Z?;95 ewZ[rZs7.ߺML&d33V`(t}D$Olx5GܖD÷fp ?3|V(Mg M׭ֻ֖[DgwwN9_fFfڼ#CCC/?p[_S7x3TR xߺ}1O~r?^xѪU?q׿嗯khhk^km#neY4 -Ð/?_SsaL52Z[oS0_y<^K=dd_Ks1RO_SUԅzqez+g}vddeߌV~pĉʗݵkܹ*ks?u+y'tˎTe~:ep2Z3gDQg%F8uuuuu-/a;oe R_J^_J-9A9g? w3o%KRU wڳKڈEC,^`|F Oh:+\cM{|9^_[x-o=o\?@U؁7Xk&Lu ˙i kLحϳ?zg^_ @q5 xSھ0T񂓜9*)nNyQ\]@k,$Y[-e ωV֜b}L9\[x2ܦ6'608eˬɗ\'݊}r9 oMXޕoz_$g[y︿9323ge7?>˴~w|sj/kM0IP|1UK徐2-s[f~pxpxfNE' Mzpgf `(t}D$Olx5GܖD÷KZaCDLT"nhF#Ch`|iW$B<5ɰ"V2%tjmiE|{| {oOD WêU(5ARJ WS ?ZgE< /;WTggǍ7GL3Q"#,رñh"m9P1kŪ"#(K]_v|}EO~r?^xѪU)?IGy͝Q*В')1 JD|` uZ:Rj2?ٯ>zs~9\g4MN L0ˏ>>9s?7kzkG#jZuQ_o _ozf$mffOOi% Nř"I^O}_|4T 'f1qy_I4N:gSt:'}?%nu|ƪ֋f>ՉT7@YM]=i>ʥkAOiH42{vgt221Z>0T8v70ju'L5|MMMTx<_9DDeGh#/C6te퓝mZ[[[guvD1j ( ^Rn85x"T2yϵ@^@%ScaC^km.CK JkdddxŋWh3JDcX[k[s0^כyyOD<ޖtZ8V$:jR2RRRJQJ  kk:;XQZZmZh-VS{z)JJkDim[֢Yψm)Z]%Ғ*D(e#RJD+kZVZ)R"f(eW(k}V)QjO,[Ț6ֳ9pb*-hѦe}DM끩Z+iԦ7u0\^ 5r#X,Oput))yjh64xcq ic)jg'>h,Y_"}e>Sg[U_V̂r.?8OΙzR>!֜o+O [@ّ;ʉRsnt*K&b0S;/ߺH2vj j%͙b93!WO83Bp֭Pjl"gfO<̞̈́"P/e4-tP//j?YE"]DQjj8Y|+XH$<3ޙlZjgr >i4z VZ ιռa;S J8(_b;|Z޻姞|_|/^|ET8|?#ss9S`=ظn%3Y3gbחl s.V8~sd+A=*ؽ%rV}ܽ'x_z;޹|ʾbѸ_x+tizRiϞ]s \lE_qV.^xn>NRi|r.sžbit&fќj,J%rOqb6lK:gʔs"S"QJ_oٚ5oosK}߼ ]bMgww-[B^q1zMl&6:&v0+]KGQ谗#8.&^Ĩ'O~ݿ7}С˗W=/3+V:KT=yd\M6ٽ?42)._sO3=sgɤWܴi=/pNs iNȳMv=+]}˖-S6$J(%:H惉/pq&#{UUخ]9={w\?t;Ngٮp5b*SzMT:c$ӹHy"-$iG`:LSyѹ=[/nR7\/KHJl/],l8b7|օ{˭ڋ/> %F?OO[y7={w̌˕jڋf3̤<<.Mwz~6s<2Ya.]3͉ά }{m5ٷo߆gprSevDrH}35MrÔ;vl[/ʽ]E ;y=q)lp l̳ϯY~YbLg !9̔mV}ԍI7`_'SY[ Vke#OCZ&AlAܗ8:#1ι8c{;C]lֲD](fr'C,g|K8O⨕RGDܙ0ԭ>~6ɣOSO=ed):cG{9_w}k}b-o1u7Xo ұǍL6l'Ţu뭦(lM6_ɯ޲eEs]; Wsn+܈}rxxh^xzX7\wXҸ3ٚ9Nz^e%=JGí[uogXK).U*ujbWfJtRU󅁓R2jyfeW˓66jefe'LLs+ZuzN3Vi6&SY#WkW%DBH""hj$R#d6K D,QB乊mė o Axa>ςweDi#[菴+g$LMOcub8w?5>1ADn}֭dūfjkQf ȅ9Lh?+Ω9Rlro{۷rw/Ł~\` dk*֦2je&X?:*z$fo͚N'hku͚HT*MRPsw$)Ui&IDe"ʆF3]LW\2~LRLg O4G]I$J*ͥ/?ˇ~o'>6,sGk;eY[dD]|c|Tj6={䵚$r9Ua zڮZV(dIJG:ӭiLT2;S#U _B) ឳ\n= f$RZԊ5b=$ f'aeB9cV}+7/~cox CFVsTY~c߸K7|o.shN2Z^(<˷]ˇyG?W[,% 2SD^UUVuBqy[Ojdd̬$IB֪J"0sϤS??H&L_pմl6J&C q]viqr1n0h/6Z6%V6|#V !u5*9IRzg9gL$ϚSOۛO(5mg|Dg5m:m_ qϚ۬zO-ӎtNez_'O;vۿqbl6J-e\LȶO5+//}˿`:[I*E3ϹդZI(y7ل.kuޡyIgR2 `[oÿջyO~o}οv]S)>nb=˾xZiefŊ|q[mgF_J0N5IqO}}险L&%UU$ ]$1] U:̿xv߈į^-՝m6ת3ej,r3%D"m8}>YO5mgtFn74C0ܫ145̹,tuώ?}}}֋ݼeIbLO'Đ)3O]sU{2Qڼ\069S]+_ ?u'.y 3\&Tb E4[ӼNv !_Ʒ~۷xõλ[8g>#uݵ#y<]~uyX*׾K7,LK8!qwժ{ݼiq:rpp`5\g3ٞt23NIg{!B|l$=jbsJH*E)^e5 ^L*SsCGB9Y M9熏{2cI"](A;O[&+ }G喇.E0IYwwϪUۻyYfG?WSs"m#BWR͛68o _K/+;РEu6=sLզ?3G/e}aBLT`z!_x{gǘ7r'~æ}=>cs2P-z60ӝΘ}^)O IW6i1V& (|e;o4BI9&ԌTΪKHcV#N$$9!zmxmַlDsXjŃm#9;[xA!(l0ӦN朸 7ozW˸-Wz:k2t,%:[03駟*'LG4239.3 X7~cQ??/O=6m5t]z(y`O>е7=zIW?i!-QTӔ½ "I{O=[zQT+=i}g ;Chd{h\D-?a1<͞C_Rf}֜pϚ$8)⳦7v5[/5Y7=Y >yΟY{Tun\o{Ej裿=O? F%|K&JLL'r*ɌLjR5')ϏD)lhN$"Wp̼&kTK/;U.ɧ}7)1B$3D~.^tYQ>\IуzիSɔ5֪G}/L*F=ABmOGNnJgmJT_3K7Xs]\(L<%sw)+IҏgmZzu26ΈZz葹+}c6dge7iթԜ\TG3ɤg3(ICs~499ͩ,МtRj -1؄TGH$ U5U]x+$s+}AΒ>p34(JMsIQWJrBV k*S.S" ]hT"q%2ƘK!QT{1""V&|rZ5)I eB(ag1M\b3XIRq*IrUYR8$1ќzVrND9u]u8sBs].$; 6PLdvU;6DťZ8.7@V"45v4n.XFsN啙 'pGA3#2;lT*UT(;ܠ˯L[nY̾؝bqddngn[-(' 4'.ږ$u>D2]/`cOAFM9JY״|j& Zf}DE M(IUW14Q^sYV2TU4+,-8"J|6T*eG-s[#nyڑj*s;9/xnLVSrmEjhu5^pNkpgJ$T(V Ӯ,g1yݯ%|zY5[%L½fɳ\.SӲ 2ZG#7!aNŒh$%zK&$>]|C8Huʂz9F7+]2-X,*:/=%vF9 ޻'$ԶGtpi>R}l+FNnڬ4+*i&[@+$O*16E]@xB{#ꟚT8ܳnn]|y *^enސvz dir_-K }VQD(?6<4M}8zo<7ht,{lذ|L&L>߽gnY:^a6>1.fGæ)L ,ֹBݐnlOQbNX8qLO\)55UP5٪Fh+[rEtwuw-c9~]߹'M9!$WGUkW^ysm7|FBG?B!\i] !€3+r8 z>ri?ۺBX:twuo۶r͵w! ޾}BƲ6u*RGbĹ׼ :~ccc>g׿ۋž[޲fjшcipX涠oUm_`ej{+ܵdú38nkD༱q֮ΐF,XMlkju-ht0Dnٹ /ٰe˖"I2U:0BJm wnH'rGԍoK $ _z% @יt|))Z8F2^zyƍN @kB$ ]W9g]ٳ甍#!< Z9Ӵj|Oyt)q*XYH &QdҩpMSTm6diB׵_ Lj]&KCC!Ǽ:JK$mwBEъsq7;p<7T*$ĚNg :> MQh,Q4%4ЅÞ/t)it -ƕKL6!dh^`)X8u@|BLk a" "94]UkhDK"T]+X63S.tu.Sdx IDATRD6 7[F4OY3ZB@]L6Ww(J` #֥R9V BUR|T < F]'z{Tq4[u=vfW*MS)m pN4MT*,z1mB†ǔ簬,|_׽ċ D`sZ X(=<m)QV)& *A}fZiڒuKqͧ\ל^uvH 5X5XLOjqOOql^{Rr 蚳v).j%,kX-U&8/k +77|4"kN&A#Fk,JX,kC&1{5NrE^ VOqI\-`)@uMp{}k:тJ\B%'ݸqcY>X?@ (hrV9 "p`ThLq氬m,k+=U3B!5kXX: b.N# 8sX6SވfnxFfH6S2t]4sN)UEeI XMZKrB ]YYVt]+g' 3,]T\DQUujjf`py"TUZR#$|V=qp>M$1[lTMK˖T*ˆ@Ӵr,IU/ v'^fMJC+4Mj܍Z/OJ1t 4[ժd2OZdVνS'&s?5y0Ml۶W׽ƶ--'33C*A/9SS=EUUyߵZojr4NSMɶmN5s ?F#Gedjj[Ĺ4M\ 2S=(2,kXZ2Z֌1BhZe9$oCyXd&r*`j0'jGn6x $F7DVǎI!I#GdEW-Y 1Z54Nx!<08G&6Ɗz:EojԍP:JdY. :|А랕PJP&gη|/="W#D v&󹁁}ZÇ?ׯsV h-N$CCrwʕk7nܘN91J$IryݯxeCCӃ:4ڮ!Ś:Ej.[j%^ݷoO?44J#O8ͦ׭[r\. M"U.Eܱ'w,gsUW szmYut:U,̔ժ3YRT6d2`/"UiZE) |\$XZb=geS, Śa@`T @;X^*8@p{Ҁ 5@@b UP-‰'x򉉉!R9/~⿄=X,B>ۡukנ5h#Jң /kX8E*ᥗ_Z~ݲeÙL@Q.sK/|.!&N D$:44|~hhpTfL 2,v$ya$-7Ejkpt? '!?瘊W뮹blX]D .uF M vns5a;zt̰[]Y۶8;g^i%S\݊5bS̺9{;ʲmZ]Vg/1{IhuS(5N4y'T_jۿẗ́6sP@_0z^]fH# +]۶m#\M?B7z|,kW6Z{Z|rmkl,_XuK088X8 @ԜJ644wIM`HdTSx@{Z.K&H ̀4 t&21w55@ _0r#t3f²6b kQ`M7 ugƲv0ti9*JB%I ,eB?47rJ|'+ˊk噙 L&-2SUUZH$UUVk!djphEZ9v`>ńsE.M'&&W,_GT.5M34\.K|RiRtLRWfXq7ZXֱ+JCë4]WU5NVK&+N8.M^."h ZRje9PU,"Z8Y:N)m5\E"fO*4\6Xɽ@1ՠq $ґ{LgS+WjäWz{z\GXmhsV8ܭ)_rۆDus&SK)9xf+S1׿n4ZRPHl/2ƫ*ce#V+UƸ(ҸSAnS˹4~ t=*ūjve挱J"Z0ƂJV71W}F>5kU}D%`|B(ǎ$C#GȊB(u-ѫ&Ɔb!C0w }|*Zk1r[s<4k&Uu $-_0Ė\JI:>pա!!{ aFfxwh-uZB3C[l/iM565j{=j3^K/^ԡa]EQCnniA5$XvAJ"BP_03|>50PCzŨ5DB)W׫V߸qc:3($ݻwxm5]o5^2^hնcJ ѐeW\A)={_NRjȑ'NˤSk֮^by.#֒$uww);1>1 G5MW9ͮ\l`X岔bX$&PJ\:.Jt]t*d2`2kXl-I |>3|$ 4XV6`Ch˚77బ,kwUz xh0TU96e ),kh;˺8yr',& !Ry s=׋Dc.ĺ5/=cCC׭] =Q*~c_p~oo/De=ׯ[l8,^)˹\n\F4&s4&iL;u'|~hhpTBc1Nc.Y{9* U!\...d1Iٚ }Gt,<ϯ|ߒh[u)dt]Rp"`f4Oӑ:-kg1CG{1\wڪKC/jbP|z9̛G/bbPwϲCb˲YnB9{Cjwr;lL[K ט'vY5t-4 en.bk- Jɫ?w=!ZmR#oo&|}P!hmUkښZ^CclL )r:#^㓏`\#;E/ZyֿcVwwwm۶r 7I޿~߸oߞ荹NiLgmI w#o,pSqk_{m85ûo)Z7D2tmuqtM|8:b6-7>lO?o[a3555럏 6-yLEG+Cuo"B/t1~nTJ1ò>|p>9|itJ+*cL%q- j=9Vk`J:@g9F|P odV+e֭FXn]Vڀf՜Krbxu<6^iNЂnP~o(;vL$cYg70[͛7$I:r䰢(pkL/[ m/>6u^˲|gybU\)࿈"% ЄL}(RqzfϚ6n<%NscRI/իeYr׿59Cl;G"LO( 6$ScHq|vժ^۳géTZ>|ĉtb͚+W.峮| } pI!fc J!s!{Jš5'&&Ɵ{i䲙ž\.8hq(a3jnJ%IbJGH>MR}=rZ.r*dҙLFQdBDs6$)mƲ(qCKJȅD>_11\Kt/XKp4kpsaM|^BCu{,q8ᰋ ] !u#oK*AR±9C *lre 5p q!jB8%pBaU@ 8!1b$u\1I2Nf4.%yؘX*B!h}k6C 7#R>ʲ*1dYn攒10@\< '0 FNe !8%/C)TN^0@Q*1KR@&HR/槦ZP5 c*$s4`#9 `. @ sz.d"܇>DӴrE[>}u)M̤"l%0g1VE@]\.Oww%"d4W\:\M \itMLZujSDI?A8gJETI4MT*);ŚhHvfēϔe%͜wYLbďܦ ܍pI#ŀ`|U-SnjXKK66t"|}}U묞Q8kut}6M9cېr#йW<Кx!*\7"1ͣh)b`> <.)(qȊxKwM`QUJnr zIׁZ(6Wx7XH#QU;ppu}!L8: 0j d2iss¾>TA<k>y_" ڊd`Y/!_U+'hA[*ݙg#q/3սnk_ACU2v$4mϞjeRkhAdn{+G_G;FT"{7?Y'28khnnKqƒg)^|mAe(5kXXx{ЂpX~*=@#r`!e mXGnjjm|ÌxEGsBȧ'U熑e̚c>o`o+F[hvix֛b3ɝIwԚ?7W@0жUHAkk/Cի vhO)*DuM^67b2x?oXGܧH%E#߀zD-4ҕ[Ou(~5iVxZ1ˑ̃zU{#m%n5 ѶC}HAI^kGsIۑD&޾cFX;Xbroa23ZxXc[m{VN,g ex}+P4[7~5턬#5w;w 30Iz=:z}IDATg~y%|hzu je+ 4M۳wZֳ~Vp9qb2-EVRdld:E `+(E@+-:;Sq&r6b kk\$tXZ޾U?͍Vk9.m-5){P `!1!u꠱>.[ܫ3-k/Y \o~=m! OB@s8lƻWHS`҆@Fi`_.gW@fF"ۂ1]7\O)AAV B`k vkm/Oީ{u,5ŦI}f푠'aXցl'O+zFR U+H.$=ɳxFu?D#¡tqLXE'a x7:;s/[vZțK0ȏT}O$M8Ez.X8SFֳ o;[s,k+& Կ^{"8vhq\q Ww4bq&IӺ d"ZTx6x!A:mHz8h)Rev"vѻpb 9 ֋O#cutƎg(qO)x ~'Rh#X/xa:pA봁!&t}2L"8HsK F~,P BA^9k#!N@EyM3םkEgDlK"0`z1 $rN!hdSҬ5EM,j4?{}ti<eĞA5v )(^R0IkCۍ! *C`Y/\ :@ :E^N:M|k9S):DeT%&)XJ346%>eAљO s.RX[|ʠZ%~㋎78zHNKEaQN~mf;ALgNhdT|nPnYh; ڴv@k8N$WKB@b b b kkk4A''(R9~%s:b~z#t6O=9nX{:22|L&)g}{ΦkޖL1 E L<3ux^q㌜>!f|3 B>_fџyyMVu]7Ɔ *d B늵Ϫfu%b O׽j]֪kS;akwm twu7!K?I)5B8wY/ebɉJ6 e:K #m+V.kЂLJW]9BHZˍ;o 7B> Jw5ƍ WٲZ][DY4y@׃֡{۶mk~@wg(cU-kXAw^z ᷉@koݹcll87u{طu[vXs ǛZie3EVk{]W\q]/#֝qƙv[JgpZt+nA$[v GF6lٲe-HoBZru9E;朸L_Wk&H$JB3]3ͿAhk:N\>C_|q:%sEcPkM熗wq=Wa@$Q&h)|9sVw'HjziUWVU- q^/'&'C4/eJ :W&& XcPA6$rrxBt8eT*Nb LL:A;B5@@`g 眣@X7k&,kh 5V_06g{arrJ)_BI>ۼnu1>^z熇7lDkLLL߷s~C늵M[A( \gy+x>Q+f݅c0A6h0<_0Zmmc5wYE"!>^x柕dæ+ӆ"6!L% ]gǪ5nCm3gѓ&w-kȶHdAypS$9c@6@AӸ !]Is!w|GV.dEG)ֆbڔQYuUF}-P*ұNiH.Ծ|8eR骫>GUkW^ysm7|FBG?B!_A #4uc57TH.kC@սm6B5^}? |3z*5cD[P+ 8Zn&ߺs؃>٫9po~bo֭츙Zۈ{$&A Vڝ"Nj{+ܵdú38n[I|񩿳58#xhލJ 1"˭\Wp$kHeί|']{%;Z;xM7Z4ћI{YѲg﫧zZ"p,W;h":H' 7Du$Y&C46>C]|=94˻6_b+do~6a{kYJMI&dUlb7u!œǕSuR;CXoODUΙmj En]7P cdRDbHQveqsiUCj[W"f$X]~U8HJҡccMXu[DVVC}=t*id3it$$UjɱR97\w^6IrF$pDPD37{WY`'@5Y y݄.)ǜ;ȗ]xr2n%4X+bTBjڕW^ny _я|PȷX˄twuo۶;WZy%wg(cUjCt8'] ~ccc>g׿ۋž[޲fjh_0|}| 0MRk{]{lXٰ;wR7ɲvxuXw_t Dnٹ+_ĉッC^yIj5XkHUʝqlAe/ ʤW}=_NɜX&t!7?1HZCdオZ\ZëDeၮǦ=U0FfO\MnBKJe岞3]OS:|!8H)e|.)6i]\W>v _06g{arrJ)_BI>ۼnu1>^z熇7lDkLLL_y @ǓX97/#'_+}Ɵ5b 4fĶFEߧtg>Y9X:d2B[Zò׌|նm9zUkxuתnƝwMORJg-V;"_v'c-:zV$x 5m|,q&J!V]yFΝ !ȇ |;^[fx8=<{۶mwq5^jK.yw=CoRfLm] h~ccc>g׿ۋž[޲f];/FkLĥ\?ٰ~?a%w3nײy^i35M^@>t8$ܲsW|ӉěpG)֮*p  U#8 @'יt|))͌dYj8wO9TUK$8g)|֘j %zͺgt78>~LU+&6ךV]Ԝ3Uz f3DN<(6 GӴl6J;Gt&Π555@@,$b8皦-H(lybZR1[*Mͤ>&S79Vs|.Wؿ hXONN s*iZ:,AgQ`u6>^W5M0j}}%]w>4r_>{]! Zn0UЂo':J*cw/}K \UJ˜\1ͭuXɓ'm1 U lK8:C|uv\3 tvݜEZ x;h5^dZ>W*-[ .+i\Nb[ ]0^|mkqu t.^7gmVe`T&zOcǎ|k_[reLϿu/?=uDSoAGA_.iEg9reˌy7mS >&a8FRTU $ ;Q2&[ğg?3 :Қgr~bIfb8y|q`Ʌ?R78[4Ob-ū~W@A̽f[Ζo2T`x/B,je+ 4]?37'5`&UVt]NGUUe]f꾜H$~## fLO߇v@Tg_r"c&>RB%IbJlgΉ],?W袋lJmKA c\$Jqm J"Ko5UV}C"n W)D݇SX,Z;^`^9R7H$)a =8d/l Nkz6"OMMzukWnsg/D8fP/%9#^*3^ɏ?Mk׮祗vၞnL |sB˚RJD7Jf+W K~'VB~岁ˇ4%֜Q*& SIq8+,hWw!LVL&e2iJ3+4('ĐPNc =Κ(l&I׵)%X k9 7:HT&İ0iRRk9L kY3F(%]BfYBf_02i5甆:XY L&,ܻ0CdYf9>Q)3dY͙uRJ gpJx,k27E*/ Ts޴{RqF8 F0n0/%%5'K #֔RL:ZB@55@@@b b b kkXXX 555@4M@9uVSۥdBeJi'`ЃX4wmZ@p֦餩j5ugߛi_JiN:mfL%j7b.> # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## import veusz.veusz_main veusz.veusz_main.run() veusz-1.21.1/scripts/veusz_listen0000664000175000017500000000177412237406466015321 0ustar jssjss#!/usr/bin/env python2 # Veusz listening script # # Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## import veusz.veusz_listen veusz.veusz_listen.run() veusz-1.21.1/icons/0000775000175000017500000000000012376130063012246 5ustar jssjssveusz-1.21.1/icons/error_barcurve.svg0000664000175000017500000000630612237406466016027 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_grid.svg0000664000175000017500000000654712237406466015334 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-window-close.svg0000664000175000017500000001066512237406466016163 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)

  • </Agent> </publisher> <creator id="creator17"> <Agent id="Agent18" about=""> <title id="title19">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/veusz_32.png0000664000175000017500000000343112237406466014446 0ustar jssjssPNG  IHDR szzsBIT|d pHYs PtEXtSoftwarewww.inkscape.org<IDATXml[k׾NmTqo*W9(+Wc BfL+9a4| } i#',']z]~@)|Rk֌i0f< .2M=1fd1!4Yxح͓w3 Op?R#1fL\nKu3o&EDO_icLϺ HO [z-=EsknO6@O3`3kpG2 %=Y2Ʈuu=`L?դTt+V%@6]wW| cƣD?)c%J A1#7@bٯw'ݣ3~\݌~wʾ|R}>hd Gp|!ppu@D AOuݭmPhBGz{S Oc &NjYy\F|\[bcsnգ'OLq9}!\%Ii==wnϵG5Bβly}@Ddk[oh4F;( cnmF*;/(lӪ $|4aV3(n- +@..En£ a~(-NM:DJ xeU+(x`+lƴ?yr7n*SUL@H!I/204ӭ#I_1R`fg}͚zwR$/44Ha.%uQKew :q,=s!u.]ݻzC] ?4rV.DF00lRQyQ!ŝsBߝFت~߸+(\:Bhokpk_2:wj =}qOONde ~] (mizT=}F}mj meU텼5\#Gmgn\mnDpGA4 3L =zmWrB+q.HۗV\>y+~7!G=xlLawqd,gd{T@hE7+ Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator22"> <Agent about="" id="Agent23"> <title id="title24">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/kde-document-save-as.svg0000664000175000017500000002217712237406466016725 0ustar jssjss image/svg+xml image/svg+xml veusz-1.21.1/icons/veusz.icns0000664000175000017500000011564012237406466014320 0ustar jssjssicnsis32т Ȁ  ߀ !!$$%%%&&ĀS`kҁz؂ӄ Ŀȭςxto [V_IDEׂ '#(&ҁ Ӂ̂nۂ ۂ ۀ  ߀ !!$$%%%&&̀S`kz ۂȿս ׂ ݁ ށւ(,.JLPRJMP knqlnr ρӄ Ŀxto [VbIDG '#*& s8mk@(#` O ) Sf%= |D-("3J? d[~k.@M.w.?U1 -3iC0N -mHHHH3`361S`r`X}]` l`8rQk`'<.!I`k'k Ps z,JCRk ? ~>k\z/8%D:Q73D I 񥘘wj[M?1#ih32 Hє  с ت  Ύ с؄ щτёфՈтۇтφуЇшЀщрч р҆!"Ѷ с·#$Չ !ы$%"#ъ&'$%%ъ')-17:>B&'ц OSWZ`dhk '(+/38<@ц|Ҋ QUY\bfjmш€̊ ~шссՀ Ȁ ӓр Â̌рՌфόуƂр |zxwutrp~|{zxwuр jhgedb`_mkjhgecсZYWVTRQO[ZYVUTRсIGFDBA?҃ԏJIGEDBAɀ 9864210.с̎8754210)(&$#! ԂӋ'&$#! ߋ  eшпF̘0т҃̿сƁ̊΂Ё΀Њуӂҋ!%#)р۔  ہ ⪍  ؎ ۉ ۉτۑۄՈۂۇۂ߆ۃهۈـۉۀч ۀ܆!"ۈ ہ؇#$Չ !ۋ$%"#ۊ&'$%%ۊ')-17:>B&'ۆ OSWZ`dhk '(+/38<@ۆ| QUY\bfjmۈ€̊ ~ۈۈہՀ ۀ ̌ۀߌۄٌۃɀƂۀ ۾ۀ ٶہہ܃ݏ؀ ܂قۋߋ ܈ۿ̘܂ۃ濋сՁ̊ނہيۃ܂Ԛۀ !""?@ABCDEFG((*+,-.KKLMOPQQ3456789UVVWYZ[\\>?@ABCDE`abcdefgIJKLMNOPjklmnopqTUVWXYZ[uvwxyz{|_abbcdefklmnnoqrՈwxyy{|}  Ɖ Њ  ۈ    ӓ Ä |zxwutrp~|{zxwu jhgedb`_mkjhgecZYWVTRQO[ZYVUTRIGFDBA?JIGEDBA 9864210.8754210)(&$#! '&$#!   tQ8'+)/h8mk  d` &$ %2h', +FG3$g;SfB I 6A7QjqX56z`GBlX'g66Go<o<gv`PC}4 3r^Bj< H'0l&G&xW gMH 1y |k  Z1?*D^08'@BDD%&&јф8:<>>@BDFFHIKMMOQSU%&і̂HIKMOOQSUWYYZ\^``bd&'і΁ZZ\^`bbdfhjjkmoqqsu'()+іրсjkkmoqssuwyzz|~Ѥ' ()+--/135578:<іԁz||~Ѥ35778:<>>@BDFFHIKMѝ́բDFHHIKMOQQSUWYYZ\^ќ̢UWYZZ\^`bbdfhjjkmoќƇѤfhjkkmoqssuwyz||~ћՋФwyz|~~ћњљƁљׁщҋшы шщчӀ؈ܸфЄ҄܀ـֺ х҄Ҁѻц҅ԀҀ ʻ߀ц҂̄ʀǀļ܀؀цфǀ Χ܀ـҀцѿуфѧҀπˀцЁЂ ҅ѧŀђ̄Ѩǀŀ ёф٨ёуԨ ѐφҪѨ я я юхф хЀ~}||{{zzуҁӀ ~}||{{zzyxwvutуЁҀwvuuttsrrqqpponnm ~}||хЀrqponmlkkjjihhg~}| {zzyyxwwvvхkkjjihhgffeedcba`yxxwwvutsrqppoхсeedcba`_^^]]\[[rqpponnmmlkjihъ_^^]\\[[ZYXWVUlkjihgfedccbщYYXWVVUUTTSRRQQPOeeddccba`_^]\[шSRQPONMLKJIIHӰ^^]]\[[ZZYYXWWVVUTшLLKKJIIHGFEDCBBӌаXWVUT SRQQPPOONчGFEDCBBAA@??>>==<ьаQQPONMLKJIHGч@??>=<;:988776ӌۯKJIHGGFFEDDCBAц::988766554332210ЁЋЃϮDDCBA@?>==<<;:ц3210/.-,++*)ЀӋ҃҅>==<;;::987654х --,,++*))(('&%$##ыЀ߂Ԥ8765 43221100/..-ф('&&%%$##""!! я҄Х00/.-,+*)('ф!! фЀե *)((''&&%$$##"!у ߃ЀӤ##"!! у  Ѐ̣т  Ҧ  qс ̝ӧ UсҝƧ :р%рЀЀҌչЂӋӍӀբёэʀѢyЎӂƉЀ̢;ЃӊЉπϢЄҌςЉЁעՎЁ҉ҀТӂҏьҥяҍۥХۦѕѕoх۽ ۽ ۼ ۼ ۇ ۃ ۄ  ۄժ  ۄլ ۄ۬ ۄڒ ۈՒ ۞ܿ ۝ґ ۞ ۞ޑ۴۳۳۲ۏ۞ۏܝێڜۋ܀ۙی܀ܘۍ܀ؙۍݘۍۍ۟ىۜ݉۝ۆ۝؆۝چۉڛۆۛ ۆڀܚ ۆƘ !՜ۆ܀ݗ!"ۜۅژ!"՛ۈۘ"#۝ ۉژ#$$ ۥ#$ !ۤ$%!"ۣ$$%!"ۣ%&"#ۢ&'#$ۢ&'#$ۡ'()++-/133$%ۡ)+--/135578:<<>@BDD%&&ۘ܄8:<>>@BDFFHIKMMOQSU%&݂ۖHIKMOOQSUWYYZ\^``bd&'ۖ݁ہZZ\^`bbdfhjjkmoqqsu'()+ۖ܁jkkmoqssuwyzz|~٤' ()+--/135578:<ۖށz||~܀ۤ35778:<>>@BDFFHIKM۝ݢDFHHIKMOQQSUWYYZ\^ۜUWYZZ\^`bbdfhjjkmoۜƇۤfhjkkmoqssuwyz||~ۛՋۤwyz|~~ۛۚۙ Ɓׁۙۉۈۋۈډ ۇڀ؈ۄ݄ۅۄۆۅۆ݄ۆڂڄދ٧ۆڿ܃߁ހ݁܃ۄܧۆہۂހ݁܁ۀڀفۅܧےہڀـ؁ׁց܁ۅܨۑ؁׀ցՁ҃ۄ߁ށ݀ۑՁӁҁрσۃۨ݁܁ۀڀېҀсЁπ΀̄܆ݪبڀـ؁ׁۏπ΀̀́ˁׁրՁԀۏˁʀɁȁӁҁрЀێɁȁƁŀрЁρ́ۅ܄ŁāÁ€́̀ˁʁۅۀÀʀɁȁƀۃ܁܀ƁŁĀÀۃځۀÁ€ۅڀۅۅہۊۉۈްۈیڰۇڀیہڰۇތۯۆ݋ڀڮۆ܁ۋ څۅۋ݀߂ܤۄ؏ۄۥۄلܥۃ߃܀ޤۃ܀̣ۂہ֝ڧہ۝ۀۀ܃܀ی۹ۂދލށբёۍۢ܀ێڃՉ̢܊ۉϢی܁ډڀߎځ܉עނݏ܀یڥڏٍۥܥۦەەۅ=>>?@AABBCD !!"##ABBCDDEFFG!"##$%%&''(EFFGHHIJJK%&''())*++,,IIJKLLMMNOOP)**+,,-.//00LMMNOOPQRSS./0012234QRSSTUUVWW223345667889UVWWXXYZZ[667789::;;<=XYZZ[\]^^_:;;<==>?@A\]^^_``aabcc?@AABCCDDE`aabccdeefggCCDDEFFGHHIddeefgghiijkkGGHHIJJKLLMNhhiijkkllmnoKKLLMNNOOPQRlmnoppqrrsPQRSSTUUVpqrrsttuvvwwTUUVWWXYYZttuvvwwxyzz{XXYYZZ[\]]^xxyz{{|}}~\]^^_``abc||}}~abcddeffggeefgghijjkkiijkklmmnoopmnoopqqrstqrrstuuvwwxxuvvwxxyzz{||zz{||}~~~ ÀĀơāŀǀɀʡȀɀʀ̀΢̀΀πЀңЀҀՀփӀՁ׀ڃ€Ą׀ـہރ՛€āƀȄہ݀ƀȀɀʀ̄ˀ̀ρЄρрӀՃӀՁ׀ك؀ڀہ݃ۀ܀ހ ܸ܀ـֺ ҀѻԀҀ ʻ߀ʀǀļ܀؀ǀ ܀ـҀҀπˀ ŀǀŀ     ~}||{{zz ~}||{{zzyxwvutwvuuttsrrqqpponnm ~}||rqponmlkkjjihhg~}| {zzyyxwwvvkkjjihhgffeedcba`yxxwwvutsrqppoeedcba`_^^]]\[[rqpponnmmlkjih_^^]\\[[ZYXWVUlkjihgfedccbYYXWVVUUTTSRRQQPOeeddccba`_^]\[SRQPONMLKJIIH^^]]\[[ZZYYXWWVVUTLLKKJIIHGFEDCBBXWVUT SRQQPPOONGFEDCBBAA@??>>==<QQPONMLKJIHG@??>=<;:988776KJIHGGFFEDDCBA::988766554332210DDCBA@?>==<<;:3210/.-,++*) >==<;;::987654 --,,++*))(('&%$##8765 43221100/..-('&&%%$##""!! 00/.-,+*)('!!  *)((''&&%$$##"! ##"!!        c D* Eۦt8mk@y- Ij," VGL& !%l"*'&$" %.,+)('!(21/.- O+6%2$22E.: 0%{ 1? rY+5C"%$ 7K8G$,*)Vk;K.6?P*BTbEX H\\k L`)I*Od /'JRhA3QjUliaXq RbnombN8~c\u'uusrqk_y:zxwv4}b}?}|{A[ e[j+F|"9)imiIlio?a:s[w3v%.y*)=)]pl| 0 J \4 c?()nn&H*f  hsCLvM`#*R^lL;yF G~L\: =|(HiLh#/n L,XR*/r 'p'GB#Xg !*/ H~zzHY d4|< ]t,93Zc;+&8_Ffd1oMS+f+)  V-S7%q EkeUl7? #/_7g_C<LB=+ +ypUsj$pGD4Bq;-'dp_Ny,t RtmG(I e#O!Cc4"{`O>?%k #Jd#UDr˗Bo½rb1*1<]ý0P#9ý".Z 7ýFXL CA ¨ \'/D~zv1P_8|v$$/-|vhp|vhP JyabWE?w 43K :Aviy[+fr.# s9 iQ2XQ,6;f]V˹Rqke_ /b`1Zqke^ oOER awqke_Sf"BPhkeF6&?.-~Ȣ&kDz7BIS; AcvAE|uLSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWURPNLJHFDB@><:86420.+)'%#! 0ſ~xrlf`ZUOIC=71+%  xxxxxxxxxxxx~xrlfa[UOIC=71+% veusz-1.21.1/icons/button_polar.svg0000664000175000017500000001157712237406466015523 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_axis.svg0000664000175000017500000000663212237406466015346 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_border.png0000664000175000017500000000043712237406466016166 0ustar jssjssPNG  IHDRabKGD pHYs  tIME;kgGtEXtCommentCreated with The GIMPd%nIDAT8c`Ƙ3gÇDOw7aepwĪ (~Hg`")))؝: U8 6Wg\%8c=FBAs Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator17"> <Agent id="Agent18" about=""> <title id="title19">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_contour.svg0000664000175000017500000001015512237406466016066 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_ternary.svg0000664000175000017500000000554312237406466016066 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/veusz-view-fullscreen.svg0000664000175000017500000002242312237406466017267 0ustar jssjss image/svg+xml Jeremy Sanders veusz-1.21.1/icons/button_nonorthpoint.svg0000664000175000017500000001066512237406466017144 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-dataset-new-veuszedit.svg0000664000175000017500000003032512237406466020000 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator26"> <Agent about="" id="Agent27"> <title id="title28">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/kde-go-next.svg0000664000175000017500000001154712237406466015132 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator17"> <Agent about="" id="Agent18"> <title id="title19">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/settings_subcontourline.svg0000664000175000017500000000625412237406466020002 0ustar jssjss image/svg+xml veusz-1.21.1/icons/error_barends.svg0000664000175000017500000000667512237406466015645 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_fit.svg0000664000175000017500000001632212237406466015161 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_plotfillbelow.png0000664000175000017500000000046612237406466017571 0ustar jssjssPNG  IHDRabKGD pHYs  tIME &XtEXtCommentCreated with The GIMPd%nIDAT8c`Ƙ3gÇTRZ]Ξvbff8] !0řV!؎"(`śF4svX?!̸`Μ90!3302󟁁L±%x]% h@pXkRUiIENDB`veusz-1.21.1/icons/logo.png0000664000175000017500000003061112237406466013726 0ustar jssjssPNG  IHDRW-nX sBIT|d pHYsBBWFtEXtSoftwarewww.inkscape.org< IDATxyU'$2KN2YɦA  ʦl DQPEQ\AD"F,3&d;ݓ:U=S]U]=K}7TuMչn=瞻B5k~ X}-"ÛWx/ \W`C> y/nqS{4iQ rP2d00 d@wci_.);/ U- n||"$2NL`L(jE"%saY>DKD/'72|0$R$!GfE=/ W׽$=#fKl}'7J:os/)9䖗_:K$;` ydw} vE5ھ7HQ3\~a!  D䝠*+a5wE.oE{I]k ܐoQaG%h5QҊ'6waD$CsPKfW  ϡ>A^ťW4` ӷm|v>gn"@Q6oHo"2y\|.g d?\y2'صYDDv3F_dK%C{in7ϖ?$9}@8l@!x, "#pwտ)Q!dQD6lzcG(CƳyyWDX,Yu\M9p | ] 9d7ΣVv=X`Z3Ol~ժi-{_JE"5hfSe{5Γ;mywxq WoWۺF۠Asrp^ 7_ʻDJ8~sd}M#!Cyq+'?~Jc"&g޷7סgEs([o]{b`q4;#~ Ƀݖ)iawF ̟ثwHHCYv*E§[6poHý2Ŗ-j-D-[8#"a ۀ/ѭ6g!L yHɄtstM{?  ȧBiݺn }׭;eĈW3@.;Q ? θz4& ~ mD7MtW׽+pC `}ꝥ?`%";}vڭ[MޠAlwNygn|C7_|e=׋pwǫ;jgϳ"׵&&b: |.RrmmEZTi`͚ boF-?UI{L\9}iA.Q] t E`Fk ߲p5xmEh/ʑ@ܴNܡ"i,/tPB9 6'׮$?0zEmH/M bCsnWuU?SQ=P[xH! {AXb߳ z^ѩmtN:ZzqMTU3DG߶!"6l'ElXMͣψitȐ'"ן3 fЧzOຯ} R2'Xr͚='BLgyY6| }Bɉijd<Gx5ݳ|s۫si5ZA6vƎqHVWf([@NZ# l~i(nэ>Sa* z{|P/-Ag26\}(,5,>׺Bwo3j]yЍC_8o_ ux7<0;',[^'Ŷ"ShVB-۲nd(ĢkR8Be"fLTh.9gO*sq]~x#njj-nn~zA$~QUr-Xs߻ف8I>En=zի+W<(fEv7 p=>Dd3,aU>4`αg nB:iL_5(H菵55(ك82,} 2Ka|#OV/֨=N^Q*yK~;ؽj1׵^9ń P2À Yt[j+|DnE̗ۥDq'zAmX,R`1&cD. YOt//2 @\SH%gah"%\>@\戸ޝpq"w= njzڕw5508/M袢 rwB!N~QDO{W}Cf'2 Iٶ]wMVXps>PvO T%9Ϲ Kްa/ 2/\v~i#+=>  y]ˎwJfŊ5cnҿsyqܸ=@~<|SR]".wEAcIݏ D/h)hSZ5IܳMXi1 <38t"Rr9yڵ'yy~}Y#q`dEm#g&ϕ+<+tw .|JL.'{qnz3˖LiU3wy'nב~p^5ڑDVʥEE!]bƤȄH8CO~㗶u|9ϥ[(3tvG%S\8;܉,9l&//ͧbO j.&Ja {mi0>yW n̝Kj'˲?+9]ƾ$./̌z춑qu#ލiQɇ,D&5k{։F+>)ǃ0<͠_ۮKisUO\V7YbEUV"(DJec{0x0ފbno?\xQtC/~^2 A O;F?(5dAz[G=dG<(urmGJ@$-%[FoZiUV{uyU+VTe :3fs>ϓ#?LMmיlZ4X`6|2? զ"D]ǿB!5 ;f5 sQSSˆ<> qŴiWb}#_x* {lݺS,Qbm; Cħ* oo1T6}^P]<ҭ%&i^Zw9sF䉔nkOL>}B KW/GB!4 ]?ޮOfl ofapͰaF_.DϰQ~mtSDVr _dn85M" ڳ lSZU{L܇_U"B.D\ ]+͢Pg<ڝt<C9080apN(D|cGOa3 ֕f 㪪x0N}oՋ9]ӿp0(DﮦP=D}F6t,SFx@dx)~’ljҶH*[CeDt=sk={]0/W]4vgz7kk_2 N1 >m 1' y R7uj Iռd @UQU˦j}!\aW, L6{S*TQyѾ=D?lT飝Sѱ%Wk̎ņeD& F`]݅s_J3WU154ѝhy080808081"\n \Yݚ[?&!}DҜHuwo3# YD^=/a.2=-z]]^`/`Þ: "@F;8+Ζsm|X"g.?qRUui+w~ݔbzVwGvϚ55 N7 >e Q1!3eWӮ>bNh+ʥJnގŢCjB@c;@dƌ8T$|C'ZTq{{]_.TW306.Y"p1X+>IU? T+mP1D[Y}c|㩊>gfE?->5O BZ5eQKvr\E~]aF$|;ɣsݿ|y$砬ᦹ猎iPSD9uی*"^u\OT.^2HoV.-8U;W鶸so=0hB6r,m'&} ԏ7nϱb}C`-pqI>Gw^b x? DO#شw(/+:م6^e%>hQvA=zꩯwƒ2qƱϱcl,[ٗ_,qDJ3_\C'aPp!؟b[~GM|OYj"9~a7m ;.hjo֥ 'p/*zQ:jQ9E'`{vzv AD견_ïB~v\v)àY`!D}*nmsks(TUn*ښᮢqEW-*QG-Zyq0ņS"޸AUouzm8sn[q7y D22~ܤё>=Hr6w_55l1 j\P08x ':lv>$S=&VVvra]dKR]nz^nxE V|3OsZ:ػi,X@qq=+ԍE$k$g7q;Ku5KDwIhRWWS2 peWS6751ojK+@NQvZZ[]}1Uys|uݵ Pl4Rﭐ .mg3$;I_oX2`cE 4 MUm*^2V^>?Gwh~5Y.s_H%*ϙڹj>24DO$NgŨ{X<9#2w'^\wCP(00C! LRnҘ'̝ ==c)$ ٗ\BΦM_?ERZoƶoXm!Cx0K=U77tW^tTr\,jZޫMg閥e(sPeO*yW[-Xߪy}+]-1 !4r " hf[ݬEԪ*.4  an,\ %j&^Z9׷z`a0'&V}ݕilMl-SJA|g%RBkGApj ڤR/yV͛S}Q'vb-~>G|M\AX@>Cpi_WrV}UU4,kӭ{6{jn|Mݡ{ۼUf|_C0YP1#P~ϚZBUo]ܻM+"lE+ɨ4-=~SuuvD7ֽq&+Sl9N{IOXQ?E{,xwVէ3R~#޻ߺTU11gyޗ=G}ݳl/ @W{/eW]һ551 aJ҆ L5? |RяirBL%zyCG=2*&V͛|TXdČdT0ɖn*z7m,z U}º =?"RUDѫ19wVV^{7n+R%UAa'byWWW0Е&3 }Uu|]ܻe[b&983pG+]{T劮TtDZuYu(`J6AmWQU5o%H%)(꽊^rs}h8qԉmc^P$S=TsyyRE`<~QE{Ŋ^|5|+U3来:R QPGyEoqEr6\|ϡ_מM0O{<^]O bU xVR>^_  xnR;^#1E?бukj h.G ;* æo*K76x8eOoQIAIMiPxEO'R:+& fo_k ئ; 7hEYirXdKry3d:򧯢hu^s_9r3,zv/l*C8lLi/`64܌u@ >w֥#P.w|ki[tZg~%[j^h=S>[`lIy0O_EuI+̸T>ywtkc"*@魛_u}λӋ󌸻?H6 s|W?cʖ;;J=ts0Ռ+zmEB)T nW#O5$HV=0O{߿՚%]BvT߿~/&R)UYѫd=~3ޔ&{rd<&uKQUp%ȑlmiaaPfaM4 ah|^1tCxMDުquː DoS1[&i81zj+׾,ʯ1ZܥVv-8Jث-D9y:E]B#kn}"`bYT@\*{?~/KWg7>&=IDATc(:ś|0{׿3ꧻԡU''#D^!O&Q#\,pR23PJt1~/|qM̫P:_$ ~}^Z]2-G|ٔ{}[~oTAUcd|1Jbbf4҅~^_O$ђ8V>cTX㊖{+ =|M5~7:hISҌMM Eyy=J4Z(%ue-ZYsr|ݘHdKvUj~鈡Gh=k/'>y'jɑ#:؛ EPS _:K/ƭ+UjEN=(O)}ssF$ZEgΒ};Oxn7ZXEVՁ.{^2(s*h.~"O P^]u+uݍ+zmBޗ06^_W9Mn}l 'c-4P =o,K =9#:l's2/o٫T~Du(iL;{xԙjV(ZSrEFE7F`Uuft~EW5HѱXXEǩj4*NU߼ۻAqޓ牪V)Z T`E`Sѝ;v^/P?OlI6hbrHdM8iJ FּV=~5tP`x+{Ikɸ5{SyfU45}>ggq%wpb!nw` A(q6[› z]m_ vD0״COk[ Eb,/ |y[ю+Z"K!i"*=AW{S{ `丝Ÿi+ XS۝2Z1 Qt" ͩk8t k)5E},.0`b-|xHM[]t=,EMYA,G;L"h_|hC&5о}- R6~jFQcX P`y Į !MO[c]İ`/_u.P>x5<O{܉瞝/_jO`ÿN<~A},^ Ltn~+gW`6Vy6%!%8p40Q%~OEUQJ+IUhz݅X< LӁJ[Z\e āy6 ʳX{gof},^5WH?SSiQ//{)u#wO6P۾swvLNE;ʺHj#U)| N X17d{-c9_"o=8KD*Fpp%qAWZ|k\)ߋo6Sj#KL5oDXvi.X8IJXe ޷y@PWހJjqh{3._E׀Fxo%ѐ&Z!Q/YME84,)KEsFy7{M]AQGgbvkh v]mdԯ#XQxѴEF},{zsD뺻 !cK֤җ?F[Y鶶#r?xڃX0 VW7{ y/NO;.7ceպ߻A0#Àh `JjK) r)Zg|r2٧_ ~ -;+_鎪lz?eXq@*@ch'un]z@\X(N,^W6,3Tdzot]EggwD9Ox~6v,5IX L& x|auԛ;۪O#81}qg[ J8&ZE`ʫX\+Fkov7vW,,rex86{n&eŎI4ђwO.iu062/HZYXwsz6 jk]m$ذ{o'`֓>`Afؿб-o'=88kD'B{q=; bwo[Dۿ.דvF>F,Cp%wТ=ȄR:nI޴[4cdQ]m䀯QE48'rʿ?r;ujƀ6(H%Yq)5XrV[8 HglksK?u!﨏{cYm|QS> {[ycXAK_]5"E9D]AGUr5I3=;W^jyXF>y>VӾ&z+^lt &EiX J7f>ߌEuXA۶qW lVk!6IGN0;Wj6ն^Wٸ"EZH…9}]mx>/IuTWy&fۭ?Dvu5Xc4E;Dž.;Xߋ+Ђ57bO`w`*Vqaw ⺯̜YeArZ_@9,Nn~kT6Rj#sѫŧ`մ), Q[/ A7.9Oۖ"` lĊ)jo mõߐgq{FWa.o82jt&,$>J]uFXZUxvbնUXT"8,NEHŚ7@Mu"5p9ք07/ѡ- 7̏M Z+J|_i緐kÛ}aFaV_0Ntv?į *1X`;VmDdKrZ](rKbSэi^VNwH޷Sދ,(P~Ei{mVgrQ2lH$R -I$븩}hnm&eصoG[]nQ`e}e>c{N(/0zHރf"`L5j+ ˻ݼ3)\AP\s45ۻiR]mYA#A#[Ż W'޳ײa C1{P+, },P<zЃH{Vn'Z/:OzgAwgOU6[y-i,9E+[D3wM[9i1\Tdimں1gdtnZ0`xoE_`wW9*zbqC#k$*ך8dLwb--z EI? i$#&lzDUG` S = :ϿiMuJ}glg&^29\umKƍGsJݘǂ g@@͚|$#M 0LZ^'M=c{_mt(Z ~U rX (. قY#M_]eWE8rhݸ^gR9ȻTۅ7UϺ0m`EӦX2qJoڞy=AGq -.$0Mkt6O9=A|-m݃(>un"Y=A 0u6 ͷ@NzЃ(u6A# ƁEbzV/|PP1I)0M~j,#I"%%RThJDS!)jMfLl4\LC-)cT#Т6x=؟(ϒ 8f %dN;^5Њ$h1TI &L4)$E4b3""HIiI14ib4p(Yl&ld(d$6bО=8@h鹘IENDB`veusz-1.21.1/icons/settings_contourline.svg0000664000175000017500000000401412237406466017260 0ustar jssjss image/svg+xml veusz-1.21.1/icons/error_curvefill.svg0000644000175000017500000000646612311641432016201 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-zoom-1-veuszedit.svg0000664000175000017500000001347112237406466016711 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/kde-application-exit.svg0000664000175000017500000001237612237406466017024 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004) Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/kde-go-previous.svg0000664000175000017500000001151512237406466016023 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator17"> <Agent id="Agent18" about=""> <title id="title19">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/kde-document-save.svg0000664000175000017500000001240512237406466016315 0ustar jssjss image/svg+xml image/svg+xml veusz-1.21.1/icons/error_linevert.svg0000664000175000017500000000614112237406466016043 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-edit-undo.svg0000664000175000017500000000764312237406466015443 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:40:13 2004)
  • </Agent> </publisher> <creator id="creator30"> <Agent about="" id="Agent31"> <title id="title32">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/kde-go-down.svg0000664000175000017500000001133412237406466015115 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator17"> <Agent id="Agent18" about=""> <title id="title19">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/error_curve.svg0000664000175000017500000000640612237406466015343 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_label.svg0000664000175000017500000001352012237406466015453 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-edit-delete.svg0000664000175000017500000001334112237406466015730 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator26"> <Agent id="Agent27" about=""> <title id="title28">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/error_diamond.svg0000664000175000017500000000572012237406466015630 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/veusz-shape-menu.svg0000664000175000017500000000567512237406466016231 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-zoom-page-veuszedit.svg0000664000175000017500000001633712237406466017471 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/settings_whisker.svg0000664000175000017500000000417312237406466016401 0ustar jssjss image/svg+xml veusz-1.21.1/icons/error_linehorz.svg0000664000175000017500000000614112237406466016045 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/error_bar.svg0000664000175000017500000000510612237406466014757 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-edit-rename.svg0000664000175000017500000001571512237406466015744 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator22"> <Agent about="" id="Agent23"> <title id="title24">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/error_linevertbar.svg0000664000175000017500000000641512237406466016534 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_axislabel.png0000664000175000017500000000054012237406466016650 0ustar jssjssPNG  IHDRabKGD pHYs  tIME $tEXtCommentCreated with The GIMPd%nIDAT81NBAy΂PK ;Jj`Y;'4j# 4c^kvv?;M\$> :'d0"a,j?X#[L1(ь$J9N(a3s/ܭ;'^^$x|ccGv.)IENDB`veusz-1.21.1/icons/logo.svg0000664000175000017500000010377212237406466013752 0ustar jssjss image/svg+xml veusz-1.21.1/icons/kde-document-open.svg0000664000175000017500000001206612237406466016323 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:40:13 2004)
  • </Agent> </publisher> <creator id="creator27"> <Agent about="" id="Agent28"> <title id="title29">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/settings_plotline.png0000664000175000017500000000050212237406466016530 0ustar jssjssPNG  IHDRabKGD pHYs  tIME .9ᮯtEXtCommentCreated with The GIMPd%nIDAT8c`\scDLIIA6.ݍ h Q\2{31 cFdC01aߴC/i3q{I3QˈYr%p5` 4Ǜc&$䙙33n33+У%!}x(p&$rIuSIENDB`veusz-1.21.1/icons/kde-zoom-out.svg0000664000175000017500000001343712237406466015342 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/error_linehorzbar.svg0000664000175000017500000000662412237406466016540 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_nonorthfunc.svg0000664000175000017500000000561312237406466016743 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/link.png0000664000175000017500000000035412237406466013724 0ustar jssjssPNG  IHDR Vu\bKGDIDATxڭ1 0 EHn`kڃ;trC΢N괅I__*0U5DA]5}/)%TR ir潷NMZh?-W8ϳLUMDvXmsqLzvORc{C1t˨/ IENDB`veusz-1.21.1/icons/button_function.svg0000664000175000017500000000561312237406466016225 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_axis-broken.svg0000664000175000017500000000703312237406466016620 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/error_fillhorz.svg0000664000175000017500000000614412237406466016047 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_image.svg0000664000175000017500000000655012237406466015463 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/error_fillvert.svg0000664000175000017500000000614412237406466016045 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_axisline.png0000664000175000017500000000044612237406466016525 0ustar jssjssPNG  IHDRabKGD pHYs  tIME itEXtCommentCreated with The GIMPd%nIDAT8c`Ƙ3gÇTRZ]C?33^5Ll1?33X1!{o B20V7 %%SZ 8C,⽀+k9M>/λJz L0IENDB`veusz-1.21.1/icons/veusz_64.png0000664000175000017500000000734212237406466014460 0ustar jssjssPNG  IHDR@@iqsBIT|d pHYs,OtEXtSoftwarewww.inkscape.org<_IDATx[kpuŒ%Ңd=!TA8:t4$M)n&5IH+nڞ%[V4ucT:i:q&jӈJ)LQL߁谝H"- f"33]ŻVnMH{ .@bv/Xu@B Y8mVA̜WUƔS'c p"H P m@45ΧG! dPW𪅼C!%) jpe󫈴s7e"z!] F<6YjzfZmR838<$؆Bwr@44 LX!+!t{{b@4Z* ff"K vNgqw1@!Hp"n(va6  RIYhŋJ8BDhO/|ܻj}&f?&&W B i R  DP!PS[X/bzB,ؖm !԰ݥ@Ng!ݘ "HwZATS)FYHHk4;)"M:MSFJm(ckp( 'z&\v"2h)[)`縩I>/2 {L\1TA>Z w}fTݙȝ l¼)uD ;s/oB}ԱRuY)b xl08^ TX  #Jz3ڑ3GY;Z$ !4|A:=Y  dhr(zsWr.Hm]4 "uٳ;WP 0) <Ey2Jd /&& hR 0t,8mn_09;[+t4VȀ6y!ԇ0P[g:@JX7B\]Ҡ\\ U"A N/ƨ ޸Hۑ[-BhG̈1Cgc 4)] j#@iȭBȾާi_eC+sL)` Xg:uWW9ޤ^1<t͞sX<1yL` b j7V%<( #&T Y0F 7\c> ~5z6ʒ *; &Bq ظqzzMӧ_6)h}vYӎ'R@> 4@@Q ՞,2P3bы}}IzG:;;qSCcE׏=G,TTRLVgEV$+Kh(O᱒_NxŔ)$K]G[ja;c 3g{k^44z[^:##P %کV5̬HU\'yݶpC@$y]VdhR34`N5eRS;H wS-kwFफ़i@+twŔNWpeOoiL K@<ʓ 5z}M0SƇ(C.L*6rJT9@1!ϒCM (A$ KhJ^-E̍ " FL;'$dv@->BʁRQ(8] 0E)!!Չe@zj+W0Ɣɑg=%1ֱ~ۋr@A9V :G}q&2Z#3x9e pDw(`(hhmEp)T ? &󓛎3A'>jW& ζ 87CV=xR&$KŹ}<]0\>{{'\y.x>џ,v)lb[;Zӧ#+ 㽃'[}Sv Ly%ImX n=(D@uvG"G 1WzL']p&w xc@ wW)RO2E5mݳV`} 7ەhHH( Br~ڵgrEhl{=k_]pnxIENDB`veusz-1.21.1/icons/error_boxfill.svg0000664000175000017500000000572312237406466015657 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_bar.svg0000664000175000017500000000737712237406466015155 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-view-refresh.svg0000664000175000017500000001266712237406466016163 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004) Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/veusz_48.png0000664000175000017500000000532612237406466014462 0ustar jssjssPNG  IHDR00WsBIT|d pHYsgtEXtSoftwarewww.inkscape.org< SIDAThřl[ǿō&YHit l*6Aˀ1ѕ+_k&~i؆AGpЦ؎7a?9Cю{s=kREJ_x) Z= |J@ZqZG"jF_(BT5Zgn^3@ 5I$rGpB5s R>J ɪn>xBM>oO!RjU]S=ahvS.<5uʴO J#ogK!Rg2QWpA# *'"'u֭ˉ[,y/_`^X}+#B} wbmj_"mUs3P/<螱f rH#"[6@:Fɀ^79SUU= rn`Z跳OZ`ӑ|'U q2^񠠽BuQ ,gH,րM$i4/] mGD=fp4XfEr|Z?^c!ԭ9,*8@ #@syرo{AEb="O,׶qTh_.B>O:|Nj0Ry/ʀo$dQ~.h[-еHS1+m:6~Byv ##mE&Ó^mhaw*|vlɒ}B83kÃ=o@+ox 8|x {OD"/@K}+p>CDΈN; @J"FE cDMQYoh"z+Fms0NϚ$@¢ \ƵULDz!CQEvfK}Vl|˖%K'c}H'4||5נ~()v{F8l6xCm[n]U"2F%%2*1GhiAFn?(^z(^k8߷#\4KCd̑rqG^|K[[q{:(KpD3Ťk uu(fw# R"EI)dR+)F|@8ʀIw=@}FHO!xƝ@iUuqoKcĖo\x /Di10\W^Gx[k+,NIŧ9(#e1iQnc!6 WD]m,sm}tͣb $da2l }ñm %B[$?d}Z-2nHAʍm7(,~bP"0휺07æpߏw=^E6IF(JSDc\k s.sI]66XzǃI[sWrđ8|IF h".HL' ſO%` 7T!x i2auSc-`cAm5`u- DXG/'7i `u)[mG7){\}zzǾo' Nm]//ͫon؇U:ec~ @YNh W4Q VJ) TU;6q8ȠqsKY5$sە^bbWBmnipw |fh Ő~[$ iLdO?37s]MH]|e?::& Wx~Ǚ)NUÚIENDB`veusz-1.21.1/icons/veusz-capture-data.svg0000664000175000017500000001164212237406466016530 0ustar jssjss image/svg+xml Veusz capture data icon Jeremy Sanders Released under GPL veusz-1.21.1/icons/settings_plotmarkerfill.png0000664000175000017500000000053612237406466017740 0ustar jssjssPNG  IHDRabKGD pHYs  tIME .ntEXtCommentCreated with The GIMPd%nIDAT8R 0< d JW,ɮEa Z$gS $/l߽cDZd0Q%kOJy0խB$!>-TǝvB( .ugf XkqEӏl 9jO"yӹ"_YѹUJ Z FRFIENDB`veusz-1.21.1/icons/error_diamondfill.svg0000664000175000017500000000604312237406466016476 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-document-print.svg0000664000175000017500000001362112237406466016514 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:39:51 2004)
  • </Agent> </publisher> <creator id="creator23"> <Agent about="" id="Agent24"> <title id="title25">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/error_bardiamond.svg0000664000175000017500000000570412237406466016317 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-clipboard.svg0000664000175000017500000001204512237406466015502 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator28"> <Agent id="Agent29" about=""> <title id="title30">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_boxplot.svg0000664000175000017500000001217112237406466016064 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-document-export.svg0000664000175000017500000002014412237406466016677 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator22"> <Agent about="" id="Agent23"> <title id="title24">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/veusz_16.png0000664000175000017500000000151012237406466014444 0ustar jssjssPNG  IHDRasBIT|d pHYs|tEXtSoftwarewww.inkscape.org<IDAT8mMh\Us93;2щ !IYT(XhEqaqSqEBƽ 5KwB E0NM'Hb+Ķ&3c{ y߇p]P#BS-+TB!_6A>ZPOZ_khz}n@iBȗAT=Gţ r*M!}r]?!BPH?XkB8aB9,WJw .5/ |OOP|_)}LҀP3x{yl6L//~ϨslzQ6j5;O>Ftp*oe2lg|~wa?XkiFӰwbi]uZ]_Zybo[k^\Ѳm^|2%[ޛ۷SE [ I )ͳYV*wwJD@eؐv3Ϝ58^;1@uyRGvy|<DIMbދapDf kȱv5wr2TVwVC-ݼIgOWnke1l7M'X&>毟YDf=FNL id>nB'/raо@kuI!E&eIENDB`veusz-1.21.1/icons/kde-edit.svg0000664000175000017500000002201412237406466014465 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004)
  • </Agent> </publisher> <creator id="creator34"> <Agent id="Agent35" about=""> <title id="title36">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/settings_axisgridlines.png0000664000175000017500000000047012237406466017553 0ustar jssjssPNG  IHDRabKGD pHYs  tIME &.tEXtCommentCreated with The GIMPd%nIDAT8S] 0 23/ Yr\;L}fvjg}{Ji%,okBUBǥ0K̋2u*TfVK1F(]?]REf?23v}O)x Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator22"> <Agent about="" id="Agent23"> <title id="title24">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/veusz_128.png0000664000175000017500000002002712237406466014534 0ustar jssjssPNG  IHDR>asBIT|d pHYs  ;dtEXtSoftwarewww.inkscape.org<IDATx}ytՕVWk떬l˻Rˋ$'d2N$ L E1YIːC3$|a!q baKvjc[X.uEݵw$stW˽ޯo[rMC zyY32{)l=j x?sއ ee6 {M>&/-Gē굄Y&B PH @}"opo|32L cU.+U^-:׸&KS$z֟c P:&Oۆ+|b'IuVf޲)#|QZ6zDU&R=dc0.eU@bV1B|B3({\ pg;'qHa1O̞2h)~uXA1A#Y[=C;Kg,V9Rb?D=(k;4* }RȸLdO-2"d2M>7SY;{{}!X゘GH~ G,7{v=W K^[+m}pOIz)vx̎B@Pdnٳ_,ݻKFoZoqwL e1( Hq^]ӳ$V%m@ 4̕ s?V-JQ D;m:<BhYoD"khX0{Wr/@$UnpboVis{#d<` "wHIGq;Veʪ{W ϙfm ~e@!:`7rSk]_"ͱKҘ[]9 pU̯V `l^թwB6]lK>`,!4Rf<\_ٹBmb@ܨݵ3HF֒H𹈽"-PWҠIXg{ ` Hx_3 "2r3{rM08[yd3jA俔ҳ+^dJ؈p$nDnuj _R_7x$o'绺'gJ@ W0ot* '`Dq힓 <2:7 i# InvB zc@=,xqc?*[jdG?˜S ^#U=_X'!fvsѫۃ ?p+YzOE`N7ov\RK!B fsxTNL7~==b ^. u>nM~于exیۋí!LZ|~ڂI7L 4#R{6Htht*-(k }&NXg y /sCA` Ԍw !AYڽ MV۰|2ঋ/6_1Ry @Re~/:!|>=m0cFVx֊⡓ % 1Ғ/5X&^ۧ7,|>\+X`ղX \ςx+ri7].;;4L;ƫbZUpDju ?(}+-Y9%81i=U?|>#B9`<ܡ# rsk5z`FC!"FRSrB0O|g|+XSRAS X!B: B#G |xTWV X6 ;i| IP~χKPW*"L$Ӝ:0fV|Unu`$ξ( d3+5INwݧJ{r{>3ްdrpy|(=[`L;<@ZfGR A1M^4. S$ 0͊Ӟ9l}9 `~fL'K`>zM5MoTNlTL,9IMঀ`U&abgit86u?%`,d$鵧~:2[X0b9퇶pNA]&-g>s^HKX֜$w$3U0ܲk!],C\SV-X3@=n`02% 4w,@'(w|ObIҼw%Q3+-+Z}3# `'}o|fՂIU՝n+%"VSROqweaWڻńIErP'F='F[NdY=xl=#5J5rtg.A|}{0e@!K16-a ` &,7&N}~JjM?=hHb_nμ@#0+n$m"X 7y߷~ A,di8"*,Ad:?(*t~RL3GTB6_3wѢf^q]^~H,U FIPgL,{Xh=UsZ Ht73Oa;=DAL[0"> n-`$v0s˜:2,`+<}!`y2[I ^g^? Hgv> 0.Քmd4_W_ ]^aaR?'3ʆ=TDwnvWd'@u3dOLuuOG3ԛ-,?Xcmvk r\fŢӚaA G-I߄<¦w6U0Fr-c= >oW,:Ţ\ER'<s|E{G51vdp_{ Ӟw\k#%׮?g}CN4X&wc~0G3MotG\\r1]@ $M K1i#݆u,*†s7d^|`kMsbZJ{N*C003"z'"] @^\Wj׾=`b}|a-H1)`ndz̀)𻸭+ra=KF[k='z܇XmpD{RrG*ћTvMkGYDW&ߞe/VCo0%_)S5a= ep1^u:\"7i9~ɤKdw#Y<34I<8*G{%YB4՜O\#E\G 4cV:zK$*G$KMRLKVI>ڻuZ2;2ug䊷с=1+mN4 ,>Z]El἟Wu&K>7`gLn5fW$~WuEM.x6S :},&߾ͩcH9-8m\w@ioK`DE !% @@F?I3z]S>.Gd~NPn]mZgXA*ʴ<++sFpf>oS&fN+yC:HP45SW,d -'@zw/=X_6s2͛J 8Tjĥ3*oU^ӓM8V7/L@DL7M 8[CLr2&n cUy_E"DJ]~:>pKp8&Y߈&E?Xe,+bsߵ}Y7W$_*3֍P!};=ǛEAL|.#gp#B,Ʊ-Npxҷ"KZr3ӫNg<~X @T`3WC72|-m;IFDZ"D`D 7 cB\%=U9 Cf(_qV k㙙DQ<7-Av g(¹zc5x GSpwY$26Q#k#ߟA#0}%e=([[tu$8 wyPy 10I ka5"}T9|LtqĤnQ?EHLe1xcX-|ܨ(Ke6VXZkpпxNw 7 HGשTxO)ěcDCCE?P1&%Y2gF/ OM#C~ò$46ue g/& $u? ?Q#ȷˊJofѡ@3bpA "5M6#$2?DRiLſDrn6V !״;̏7(ޤgr=<#Dq[pd9P~SS@= ,"t aW0Z~g5klĎDC>'pMdб pfϓ~fKkQ ]$Ez/l>$Fsɔ*RNJ\o_#sq%^7,go;e]vNCb(y/9_{rS 'A?gL~E$^VPipX/ Yb~4Wu1x}^Wxk̼4:4 ;5ԯY֭fW#{fZVo rRr*- !jbqKv<^uNc{; 8%Čۛ뽶>zL@ 0rC@)*n]80E;avЌ0,Ej̥zVrf oΊ*ʦ@ɄNy(;1h8\϶~(\[X3ӮFՙ$P>0OYsI!$Զ PWryr۩|!!6eYsa%u+޻poIEya\JN4@>,x ` X~_OHTW!oh x Q9(SV ZGOɉ(jhG.N&8}q*jnar%^D.evD{y Cn0=cBfnn"G l99*[#Zg,@P`}|rl_S '+ĵ)5 H(Ɠ JCĠy|m]~riM7M|j~T!Y\7pr`MGIENDB`veusz-1.21.1/icons/button_imagefile.svg0000664000175000017500000000723212237406466016321 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_stylesheet.png0000664000175000017500000000046312237406466017101 0ustar jssjssPNG  IHDRabKGD pHYs  tIMEA tEXtCommentCreated with The GIMPd%nIDAT8R 0 L+] H5Ӕ#ONڦ{u l &xۃ.=pu7PA 8{,v^@U#[:\8D~߇DU XP P(3t@LIENDB`veusz-1.21.1/icons/kde-zoom-veuszedit.svg0000664000175000017500000001160612237406466016551 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/settings_ploterrorline.png0000664000175000017500000000064312237406466017610 0ustar jssjssPNG  IHDRabKGD pHYs  tIME 6DtEXtCommentCreated with The GIMPd%nIDAT8ݒ1n@EcPsrDJ)>@v#r27%m${4M+,!Jٙ̇r7lړ8 c Zk1,kt>?U"2v sNzqL^1;D,S&#iJtv+KfNP)ű'n`@EtZ9pZnG$cOn\bS8@$ 0HQhYkr eQ\HoFm[ۛIENDB`veusz-1.21.1/icons/button_vectorfield.svg0000664000175000017500000001140312237406466016700 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_axisticklabels.png0000664000175000017500000000040412237406466017705 0ustar jssjssPNG  IHDRabKGD pHYs  tIME  ZtEXtCommentCreated with The GIMPd%nhIDAT8!PE#MH vek@Χm*j&ory Rxa F Kp `熩tPxEpQ9my-o7OplkIENDB`veusz-1.21.1/icons/kde-edit-copy.svg0000664000175000017500000001464412237406466015447 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator19"> <Agent about="" id="Agent20"> <title id="title21">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_axis-function.svg0000664000175000017500000002052112237406466017162 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_rect.svg0000664000175000017500000000456512237406466015342 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_polygon.svg0000664000175000017500000000544112237406466016066 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_main.png0000664000175000017500000000043312237406466015631 0ustar jssjssPNG  IHDRabKGD pHYs  tIME  &!jfIDAT8c`hO^d D1fk@9##rF  t۱+mp>W`lll80c#A1\v|!\۱yY̐:Cātys 'oҒxtd9h9EM=IENDB`veusz-1.21.1/icons/kde-document-new.svg0000664000175000017500000001561712237406466016160 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator26"> <Agent about="" id="Agent27"> <title id="title28">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_document.svg0000664000175000017500000001676712237406466016232 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_line.svg0000664000175000017500000000505412237406466015326 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/veusz-zoom-graph.svg0000664000175000017500000001036112237406466016236 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_graph.svg0000664000175000017500000001115112237406466015473 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/downarrow_blank.png0000664000175000017500000000030312237406466016152 0ustar jssjssPNG  IHDR Vu\bKGD pHYs  tIME 4c{tEXtCommentCreated with The GIMPd%n'IDAT(cd```hnnf f0`THj%> ',@IENDB`veusz-1.21.1/icons/settings_bgfill.png0000664000175000017500000000046712237406466016153 0ustar jssjssPNG  IHDRabKGD pHYs  tIME  btEXtCommentCreated with The GIMPd%nIDAT8R 0|йB! ĨKqKIgt2X5tmųN9ധaJR$03qc\ jXTr%Bz5df%}} y")GPF}]@ IENDB`veusz-1.21.1/icons/kde-edit-paste.svg0000664000175000017500000001514312237406466015604 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator28"> <Agent id="Agent29" about=""> <title id="title30">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_xy.svg0000664000175000017500000001574112237406466015043 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_axisminorgridlines.png0000664000175000017500000000047512237406466020625 0ustar jssjssPNG  IHDRasRGBbKGD pHYs  tIME {NtEXtCommentCreated with The GIMPd%nIDAT8c`ظqrh&J-gkFQQobTUS#H3 %%.@^1/À9s"/0@4H0pL aL6n܈Uu *Fƙ)^.AIENDB`veusz-1.21.1/icons/error_barbox.svg0000664000175000017500000000556312237406466015477 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-dataset2d-new-veuszedit.svg0000664000175000017500000003560012237406466020227 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator26"> <Agent about="" id="Agent27"> <title id="title28">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/veusz.ico0000664000175000017500000003535612237406466014143 0ustar jssjss h6  00 %F(  nmHHHHёkC;  ? 3311..,,k`%%##!!CRkMMKKIIFF==z::886644йѲ,Jjjggeecc``ʤWWUURRPPNNLLs߿ „'||zzk ЈtqPmmjjhheeccϞIΛ͙̗ʔ`kćÄ}}ܸk۵ڳرׯ`Р'ϞΜ͚̗˕<.!ҍɲ`ܸ۶ڴٲׯЭ֭Ѝ8rQ`Ѳ lr`X}]S`ZZbbhhmmsszz3`361''7''''))//55`Nssyy~~>$$Y$$%%%%%%`--55::@@FFMMe""{""""""##]%%m&&&&&&&&'' U########$$$$N{g @!!!!!!!!""1 tF $U?)^$57e,}sFuvxyz0. ޔbhdegh)P,R S U V X !stuwyA C D E G b(cefhiX02356S T V W X Z !"$&s B C E F H I  {/???(0` '!+%)#/)ѺѬџёуugYL=/!2:aP^p\uOДVV~jg uғ[\80F-WH? QFѪ te"Z1''&&$$##!! Aр\88(775544221100˩)) ((&&$$##!! /;Ҙ ]@JJJIIGGEEDDBBAAɓ99W886644221100..))Hңх [[lZZYYVVUUTTRRѐIIGGFFDDBBAA??Pcf5mmkkjjhhggeeccѐZZ.YYWWVVTTRRQQOOT~~||{{zzxxwwuuѐ_/jjhhggeeddbb``__ ȐҎǎnjƋʼnĈćѐ,z||zzxxwwuuttrrppѡРϞΜΛ͙̘gѐ; ƌhʼnĈćń‚$ڴٲرد׮֬ժթGѐϝΛϙ͙̘˖˕ʓɒ 8߾޼ݺ'ѐ֭<֫ժԨӦҥңѢN_`ҁ5Zѐ1?޼ݻݺܷ۶ڵڳ޲ٲ*ҙD^Й0|ѐkѫ  ѐ1y ѐћ ȇѐMH~~gѐնQQ&UUYY\\bbffjjmmGѐ&xW ''H((++//3388<<@@'ѐ0||l&&j&&&&&&&&&&&&''ѐ<ЈOO SSWWZZ``ddhhkk$$$$$$$$$$%%%%ѐ''^))--1177::>>BBB""""""########ь&&&&&&&&&&&&'''''' !!!!!!!!!!х$$3$$$$%%%%%%%%%%r }4##############$$ gv`ѽP!!!!!!!!!!""""""C۶6Go<ҁo <X'g6z`GBlX56Qjqwxyy{|}I 6A7klmnnoqrB ˑ_$abbcdefg;SfT FU V W X YZ[G3uvwxyz{|I hJ K L M N O P ', j+klmnopq> ? @ A B C D E $ %`abcdefg23456789 &U V V W YZ[\\((*+,-.K dK L M O P Q Q ` !""? @ A B C D E F G 9  ~?//?/;//8.</|/ooooooo???veusz-1.21.1/icons/veusz-edit-custom.svg0000664000175000017500000002321612237406466016413 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:40:13 2004)
  • </Agent> </publisher> <creator id="creator30"> <Agent id="Agent31" about=""> <title id="title32">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_ellipse.svg0000664000175000017500000000515212237406466016033 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-zoom-width-veuszedit.svg0000664000175000017500000001433212237406466017665 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_key.svg0000664000175000017500000002073512237406466015172 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/button_colorbar.svg0000664000175000017500000000671412237406466016206 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_axismajorticks.png0000664000175000017500000000047212237406466017743 0ustar jssjssPNG  IHDRabKGDC pHYs  tIME 1#;tEXtCommentCreated with The GIMPd%nIDAT8c`Ƙ3gÇTRZ]C?33^5Ll1?33X1!{o B20V7 %%SZ 8C,⽀+ Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/veusz-edit-cut.svg0000664000175000017500000001262012237406466015671 0ustar jssjss image/svg+xml Jeremy Sanders Copyright Jeremy Sanders Released under the GPL veusz-1.21.1/icons/error_none.svg0000664000175000017500000000422212237406466015150 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/veusz.svg0000664000175000017500000006514512237406466014167 0ustar jssjss image/svg+xml veusz-1.21.1/icons/veusz-edit-prefs.svg0000664000175000017500000001513212237406466016216 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:40:13 2004)
  • </Agent> </publisher> <creator id="creator30"> <Agent id="Agent31" about=""> <title id="title32">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/LICENSE-icons0000664000175000017500000000572012237406466014401 0ustar jssjssThe icons are licensed as follows: ---------------------------------- button_*.svg, error_*.svg, veusz*.svg, settings_*.png, logo.png, link.png, downarrow*png, veusz.ico, veusz*.png, veusz.icns Veusz icons Copyright (C) 2008 Jeremy Sanders 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. kde_*.svg: Icons taken from the KDE project. The copyright is as follows: ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ This copyright and license notice covers all Primary images. Note the license notice contains an add-on. ******************************************************************************** Primary iconset Copyright (C) 2007 Danny Allen This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 2.1 of the License. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA **** NOTE THIS ADD-ON **** The GNU Lesser General Public License or LGPL is written for software libraries in the first place. We expressly want the LGPL to be valid for this artwork library too. Primary iconset is a special kind of software library, it is an artwork library, its elements can be used in a Graphical User Interface, or GUI. Source code, for this library means: - for vectors svg; - for pixels, if applicable, the multi-layered formats xcf or psd, or otherwise png. The LGPL in some sections obliges you to make the files carry notices. With images this is in some cases impossible or hardly useful. With this library a notice is placed at a prominent place in the directory containing the elements. You may follow this practice. The exception in section 6 of the GNU Lesser General Public License covers the use of elements of this art library in a GUI. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ veusz-1.21.1/icons/downarrow.png0000664000175000017500000000034212237406466015006 0ustar jssjssPNG  IHDR Vu\bKGD pHYs  tIME $tEXtCommentCreated with The GIMPd%nFIDAT(cd```hnn@e$Z1`&X؄ƈn#!؜ĈO1.?03(%5040b_SIENDB`veusz-1.21.1/icons/settings_axisminorticks.png0000664000175000017500000000047612237406466017763 0ustar jssjssPNG  IHDRabKGD pHYs  tIME q"=tEXtCommentCreated with The GIMPd%nIDAT8R; 0 }s@ t| ,QRV1,=\kוq)!0des׬Wַ8%P!GOηK $YaL !"DCri!y}{>tEhIENDB`veusz-1.21.1/icons/kde-edit-veuszedit.svg0000664000175000017500000002203012237406466016503 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004)
  • </Agent> </publisher> <creator id="creator34"> <Agent id="Agent35" about=""> <title id="title36">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/kde-mouse-pointer.svg0000664000175000017500000000730212237406466016351 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator17"> <Agent id="Agent18" about=""> <title id="title19">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/settings_contourfill.svg0000664000175000017500000000375112237406466017266 0ustar jssjss image/svg+xml veusz-1.21.1/icons/kde-zoom-height-veuszedit.svg0000664000175000017500000001442112237406466020015 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/error_box.svg0000664000175000017500000000567112237406466015012 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/kde-zoom-in.svg0000664000175000017500000001506712237406466015142 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004)
  • </Agent> </publisher> <creator id="creator24"> <Agent id="Agent25" about=""> <title id="title26">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/veusz.png0000664000175000017500000000063512237406466014145 0ustar jssjssPNG  IHDRh6 pHYs  tIME  tEXtCommentCreated with The GIMPd%nIDAT(c?)4 $aocns0P<8l87(2Nɳ@.vH] q?C˼s9>7:G)ˋ7~uL7_Z $ƆI3ܿnEEs诛 !! '3QFFFD|74t}U߈a5crg2^'ik39b %ʍB=:q3HJg$5yi CK*IENDB`veusz-1.21.1/icons/kde-edit-redo.svg0000664000175000017500000000764412237406466015430 0ustar jssjss Part of the Flat Icon Collection (Thu Aug 26 14:40:13 2004)
  • </Agent> </publisher> <creator id="creator30"> <Agent id="Agent31" about=""> <title id="title32">Danny Allen Danny Allen image/svg+xml en image/svg+xml veusz-1.21.1/icons/button_page.svg0000664000175000017500000000655212237406466015317 0ustar jssjss image/svg+xml Jeremy Sanders Copyright (C) Jeremy Sanders veusz-1.21.1/icons/settings_plotfillabove.png0000664000175000017500000000047712237406466017557 0ustar jssjssPNG  IHDRabKGD pHYs  tIME !=tEXtCommentCreated with The GIMPd%nIDAT8R 07P0C& X uwy!qH"q}>?"8V@͇.:5Mqi[dĜ~f\F$,'\gU_LsB^OhA5 &Gni<}Afa\Sk a#mZ䰩?IENDB`veusz-1.21.1/icons/veusz-pick-data.svg0000664000175000017500000000510112237406466016004 0ustar jssjss image/svg+xml veusz-1.21.1/COPYING0000664000175000017500000004310312237406466012200 0ustar jssjss GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. veusz-1.21.1/VERSION0000664000175000017500000000000712376130006012175 0ustar jssjss1.21.1 veusz-1.21.1/AUTHORS0000664000175000017500000000015212237406466012212 0ustar jssjssJeremy Sanders http://www.jeremysanders.net/ James Graham veusz-1.21.1/veusz/0000775000175000017500000000000012376130063012307 5ustar jssjssveusz-1.21.1/veusz/qtall.py0000644000175000017500000000276712273225057014014 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """A convenience module to import both the used Qt symbols from.""" from __future__ import division import sys import os # disable KDE specific dialog boxes as they are currently broken if sys.platform != 'win32' and sys.platform != 'darwin': os.environ['QT_PLATFORM_PLUGIN'] = 'none' import sip sip.setapi('QDate', 2) sip.setapi('QDateTime', 2) sip.setapi('QString', 2) sip.setapi('QTextStream', 2) sip.setapi('QTime', 2) sip.setapi('QUrl', 2) sip.setapi('QVariant', 2) isdeleted = sip.isdeleted from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.QtSvg import * from PyQt4.uic import loadUi veusz-1.21.1/veusz/widgets/0000775000175000017500000000000012376130063013755 5ustar jssjssveusz-1.21.1/veusz/widgets/point.py0000644000175000017500000010557512327177747015513 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """For plotting xy points.""" from __future__ import division import numpy as N from ..compat import czip from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from . import pickable from .plotters import GenericPlotter try: from ..helpers import qtloops hasqtloops = True except ImportError: hasqtloops = False def _(text, disambiguation=None, context='XY'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) # functions for plotting error bars # different styles are made up of combinations of these functions # each function takes the same arguments def _errorBarsBar(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw bar style error lines.""" # vertical error bars if ymin is not None and ymax is not None and not s.ErrorBarLine.hideVert: utils.plotLinesToPainter(painter, xplotter, ymin, xplotter, ymax, clip) # horizontal error bars if xmin is not None and xmax is not None and not s.ErrorBarLine.hideHorz: utils.plotLinesToPainter(painter, xmin, yplotter, xmax, yplotter, clip) def _errorBarsEnds(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw perpendiclar ends on error bars.""" size = ( s.get('markerSize').convert(painter) * s.ErrorBarLine.endsize ) if ymin is not None and ymax is not None and not s.ErrorBarLine.hideVert: utils.plotLinesToPainter(painter, xplotter-size, ymin, xplotter+size, ymin, clip) utils.plotLinesToPainter(painter, xplotter-size, ymax, xplotter+size, ymax, clip) if xmin is not None and xmax is not None and not s.ErrorBarLine.hideHorz: utils.plotLinesToPainter(painter, xmin, yplotter-size, xmin, yplotter+size, clip) utils.plotLinesToPainter(painter, xmax, yplotter-size, xmax, yplotter+size, clip) def _errorBarsBox(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw box around error region.""" if None not in (xmin, xmax, ymin, ymax): painter.setBrush( qt4.QBrush() ) utils.plotBoxesToPainter(painter, xmin, ymin, xmax, ymax, clip) def _errorBarsBoxFilled(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw box filled region inside error bars.""" if None not in (xmin, xmax, ymin, ymax): # filled region below if not s.FillBelow.hideerror: path = qt4.QPainterPath() utils.addNumpyPolygonToPath(path, clip, xmin, ymin, xmin, yplotter, xmax, yplotter, xmax, ymin) utils.brushExtFillPath(painter, s.FillBelow, path, ignorehide=True) # filled region above if not s.FillAbove.hideerror: path = qt4.QPainterPath() utils.addNumpyPolygonToPath(path, clip, xmin, yplotter, xmax, yplotter, xmax, ymax, xmin, ymax) utils.brushExtFillPath(painter, s.FillAbove, path, ignorehide=True) def _errorBarsDiamond(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw diamond around error region.""" if None not in (xmin, xmax, ymin, ymax): # expand clip by pen width (urgh) pw = painter.pen().widthF()*2 clip = qt4.QRectF(qt4.QPointF(clip.left()-pw,clip.top()-pw), qt4.QPointF(clip.right()+pw,clip.bottom()+pw)) path = qt4.QPainterPath() utils.addNumpyPolygonToPath(path, clip, xmin, yplotter, xplotter, ymax, xmax, yplotter, xplotter, ymin) painter.setBrush( qt4.QBrush() ) painter.drawPath(path) def _errorBarsDiamondFilled(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw diamond filled region inside error bars.""" if None not in (xmin, xmax, ymin, ymax): if not s.FillBelow.hideerror: path = qt4.QPainterPath() utils.addNumpyPolygonToPath(path, clip, xmin, yplotter, xplotter, ymin, xmax, yplotter) utils.brushExtFillPath(painter, s.FillBelow, path, ignorehide=True) if not s.FillAbove.hideerror: path = qt4.QPainterPath() utils.addNumpyPolygonToPath(path, clip, xmin, yplotter, xplotter, ymax, xmax, yplotter) utils.brushExtFillPath(painter, s.FillAbove, path, ignorehide=True) def _errorBarsCurve(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw curve around error region.""" if None not in (xmin, xmax, ymin, ymax): # non-filling brush painter.setBrush( qt4.QBrush() ) for xp, yp, xmn, ymn, xmx, ymx in czip( xplotter, yplotter, xmin, ymin, xmax, ymax): p = qt4.QPainterPath() p.moveTo(xp + (xmx-xp), yp) p.arcTo(qt4.QRectF(xp - (xmx-xp), yp - (yp-ymx), (xmx-xp)*2, (yp-ymx)*2), 0., 90.) p.arcTo(qt4.QRectF(xp - (xp-xmn), yp - (yp-ymx), (xp-xmn)*2, (yp-ymx)*2), 90., 90.) p.arcTo(qt4.QRectF(xp - (xp-xmn), yp - (ymn-yp), (xp-xmn)*2, (ymn-yp)*2), 180., 90.) p.arcTo(qt4.QRectF(xp - (xmx-xp), yp - (ymn-yp), (xmx-xp)*2, (ymn-yp)*2), 270., 90.) painter.drawPath(p) def _errorBarsCurveFilled(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Fill area around error region.""" if None not in (xmin, xmax, ymin, ymax): for xp, yp, xmn, ymn, xmx, ymx in czip( xplotter, yplotter, xmin, ymin, xmax, ymax): if not s.FillAbove.hideerror: p = qt4.QPainterPath() p.moveTo(xp + (xmx-xp), yp) p.arcTo(qt4.QRectF(xp - (xmx-xp), yp - (yp-ymx), (xmx-xp)*2, (yp-ymx)*2), 0., 90.) p.arcTo(qt4.QRectF(xp - (xp-xmn), yp - (yp-ymx), (xp-xmn)*2, (yp-ymx)*2), 90., 90.) utils.brushExtFillPath(painter, s.FillAbove, p, ignorehide=True) if not s.FillBelow.hideerror: p = qt4.QPainterPath() p.moveTo(xp + (xp-xmn), yp) p.arcTo(qt4.QRectF(xp - (xp-xmn), yp - (ymn-yp), (xp-xmn)*2, (ymn-yp)*2), 180., 90.) p.arcTo(qt4.QRectF(xp - (xmx-xp), yp - (ymn-yp), (xmx-xp)*2, (ymn-yp)*2), 270., 90.) utils.brushExtFillPath(painter, s.FillBelow, p, ignorehide=True) def _errorBarsFilled(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, clip): """Draw filled region as error region.""" ptsabove = qt4.QPolygonF() ptsbelow = qt4.QPolygonF() hidevert = True # keep track of what's shown hidehorz = True if ( 'vert' in style and (ymin is not None and ymax is not None) and not s.ErrorBarLine.hideVert ): hidevert = False # lines above/below points utils.addNumpyToPolygonF(ptsbelow, xplotter, ymin) utils.addNumpyToPolygonF(ptsabove, xplotter, ymax) elif ( 'horz' in style and (xmin is not None and xmax is not None) and not s.ErrorBarLine.hideHorz ): hidehorz = False # lines left/right points utils.addNumpyToPolygonF(ptsbelow, xmin, yplotter) utils.addNumpyToPolygonF(ptsabove, xmax, yplotter) # draw filled regions above/left and below/right if 'fill' in style and not (hidehorz and hidevert): # construct points for error bar regions retnpts = qt4.QPolygonF() utils.addNumpyToPolygonF(retnpts, xplotter[::-1], yplotter[::-1]) # polygons consist of lines joining the points and continuing # back along the plot line (retnpts) if not s.FillBelow.hideerror: utils.brushExtFillPolygon(painter, s.FillBelow, clip, ptsbelow+retnpts, ignorehide=True) if not s.FillAbove.hideerror: utils.brushExtFillPolygon(painter, s.FillAbove, clip, ptsabove+retnpts, ignorehide=True) # draw optional line (on top of fill) utils.plotClippedPolyline(painter, clip, ptsabove) utils.plotClippedPolyline(painter, clip, ptsbelow) # map error bar names to lists of functions (above) _errorBarFunctionMap = { 'none': (), 'bar': (_errorBarsBar,), 'bardiamond': (_errorBarsBar, _errorBarsDiamond,), 'barcurve': (_errorBarsBar, _errorBarsCurve,), 'barbox': (_errorBarsBar, _errorBarsBox,), 'barends': (_errorBarsBar, _errorBarsEnds,), 'box': (_errorBarsBox,), 'boxfill': (_errorBarsBoxFilled, _errorBarsBox,), 'diamond': (_errorBarsDiamond,), 'diamondfill': (_errorBarsDiamond, _errorBarsDiamondFilled), 'curve': (_errorBarsCurve,), 'curvefill': (_errorBarsCurveFilled, _errorBarsCurve,), 'fillhorz': (_errorBarsFilled,), 'fillvert': (_errorBarsFilled,), 'linehorz': (_errorBarsFilled,), 'linevert': (_errorBarsFilled,), 'linehorzbar': (_errorBarsBar, _errorBarsFilled), 'linevertbar': (_errorBarsBar, _errorBarsFilled), } def fillPtsToEdge(painter, pts, posn, cliprect, fillstyle): """Fill points depending on fill mode.""" ft = fillstyle.fillto if ft == 'top': x1, x2 = pts[0].x(), pts[-1].x() y1 = y2 = posn[1] elif ft == 'bottom': x1, x2 = pts[0].x(), pts[-1].x() y1 = y2 = posn[3] elif ft == 'left': y1, y2 = pts[0].y(), pts[-1].y() x1 = x2 = posn[0] elif ft == 'right': y1, y2 = pts[0].y(), pts[-1].y() x1 = x2 = posn[2] else: raise RuntimeError('Invalid fillto mode') polypts = qt4.QPolygonF([qt4.QPointF(x1, y1)]) polypts += pts polypts.append(qt4.QPointF(x2, y2)) utils.brushExtFillPolygon(painter, fillstyle, cliprect, polypts) class MarkerFillBrush(setting.Brush): def __init__(self, name, **args): setting.Brush.__init__(self, name, **args) self.get('color').newDefault( setting.Reference('../color') ) self.add( setting.Colormap( 'colorMap', 'grey', descr = _('If color markers dataset is given, use this colormap ' 'instead of the fill color'), usertext=_('Color map'), formatting=True) ) self.add( setting.Bool( 'colorMapInvert', False, descr = _('Invert color map'), usertext = _('Invert map'), formatting=True) ) class PointPlotter(GenericPlotter): """A class for plotting points and their errors.""" typename='xy' allowusercreation=True description=_('Plot points with lines and errorbars') def __init__(self, parent, name=None): """Initialise XY plotter plotting (xdata, ydata). xdata and ydata are strings specifying the data in the document""" GenericPlotter.__init__(self, parent, name=name) if type(self) == PointPlotter: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" GenericPlotter.addSettings(s) # non-formatting s.add( setting.DatasetExtended( 'yData', 'y', descr=_('Y values, given by dataset, expression or list of values'), usertext=_('Y data')), 0 ) s.add( setting.DatasetExtended( 'xData', 'x', descr=_('X values, given by dataset, expression or list of values'), usertext=_('X data')), 0 ) s.add( setting.DatasetOrStr( 'labels', '', descr=_('Dataset or string to label points'), usertext=_('Labels')), 5 ) s.add( setting.DatasetExtended( 'scalePoints', '', descr = _('Scale size of markers given by dataset, expression' ' or list of values'), usertext=_('Scale markers')), 6 ) # formatting s.add( setting.Int('thinfactor', 1, minval=1, descr=_('Thin number of markers plotted' ' for each datapoint by this factor'), usertext=_('Thin markers'), formatting=True), 0 ) s.add( setting.Color('color', 'black', descr = _('Master color'), usertext = _('Color'), formatting=True), 0 ) s.add( setting.DistancePt('markerSize', '3pt', descr = _('Size of marker to plot'), usertext=_('Marker size'), formatting=True), 0 ) s.add( setting.Marker('marker', 'circle', descr = _('Type of marker to plot'), usertext=_('Marker'), formatting=True), 0 ) s.add( setting.MarkerColor('Color') ) s.add( setting.ErrorStyle('errorStyle', 'bar', descr=_('Style of error bars to plot'), usertext=_('Error style'), formatting=True) ) s.add( setting.XYPlotLine('PlotLine', descr = _('Plot line settings'), usertext = _('Plot line')), pixmap = 'settings_plotline' ) s.add( setting.MarkerLine('MarkerLine', descr = _('Line around the marker settings'), usertext = _('Marker border')), pixmap = 'settings_plotmarkerline' ) s.add( MarkerFillBrush('MarkerFill', descr = _('Marker fill settings'), usertext = _('Marker fill')), pixmap = 'settings_plotmarkerfill' ) s.add( setting.ErrorBarLine('ErrorBarLine', descr = _('Error bar line settings'), usertext = _('Error bar line')), pixmap = 'settings_ploterrorline' ) s.ErrorBarLine.get('color').newDefault( setting.Reference('../color') ) s.add( setting.PointFill('FillBelow', descr = _('Fill mode 1'), usertext = _('Fill 1')), pixmap = 'settings_plotfillbelow' ) s.FillBelow.get('fillto').newDefault('bottom') s.add( setting.PointFill('FillAbove', descr = _('Fill mode 2'), usertext = _('Fill 2')), pixmap = 'settings_plotfillabove' ) s.add( setting.PointLabel('Label', descr = _('Label settings'), usertext=_('Label')), pixmap = 'settings_axislabel' ) @property def userdescription(self): """User-friendly description.""" s = self.settings return "x='%s', y='%s', marker='%s'" % (s.xData, s.yData, s.marker) def _plotErrors(self, posn, painter, xplotter, yplotter, axes, xdata, ydata, cliprect): """Plot error bars (horizontal and vertical). """ s = self.settings style = s.errorStyle if style == 'none': return # default is no error bars xmin = xmax = ymin = ymax = None # draw horizontal error bars if xdata.hasErrors(): xmin, xmax = xdata.getPointRanges() # convert xmin and xmax to graph coordinates xmin = axes[0].dataToPlotterCoords(posn, xmin) xmax = axes[0].dataToPlotterCoords(posn, xmax) # draw vertical error bars if ydata.hasErrors(): ymin, ymax = ydata.getPointRanges() # convert ymin and ymax to graph coordinates ymin = axes[1].dataToPlotterCoords(posn, ymin) ymax = axes[1].dataToPlotterCoords(posn, ymax) # no error bars - break out of processing below if ymin is None and ymax is None and xmin is None and xmax is None: return # iterate to call the error bars functions required to draw style pen = s.ErrorBarLine.makeQPenWHide(painter) pen.setCapStyle(qt4.Qt.FlatCap) painter.setPen(pen) for function in _errorBarFunctionMap[style]: function(style, xmin, xmax, ymin, ymax, xplotter, yplotter, s, painter, cliprect) def affectsAxisRange(self): """This widget provides range information about these axes.""" s = self.settings return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def getRange(self, axis, depname, axrange): """Compute the effect of data on the axis range.""" dataname = {'sx': 'xData', 'sy': 'yData'}[depname] dsetn = self.settings.get(dataname) data = dsetn.getData(self.document) if data: drange = data.getRange() if drange: axrange[0] = min(axrange[0], drange[0]) axrange[1] = max(axrange[1], drange[1]) elif dsetn.isEmpty(): # no valid dataset. # check if there a valid dataset for the other axis. # if there is, treat this as a row number dataname = {'sy': 'xData', 'sx': 'yData'}[depname] data = self.settings.get(dataname).getData(self.document) if data: length = data.data.shape[0] axrange[0] = min(axrange[0], 1) axrange[1] = max(axrange[1], length) def _getLinePoints( self, xvals, yvals, posn, xdata, ydata ): """Get the points corresponding to the line connecting the points.""" pts = qt4.QPolygonF() s = self.settings steps = s.PlotLine.steps # simple continuous line if steps == 'off': utils.addNumpyToPolygonF(pts, xvals, yvals) # stepped line, with points on left elif steps[:4] == 'left': x1 = xvals[:-1] x2 = xvals[1:] y1 = yvals[:-1] y2 = yvals[1:] utils.addNumpyToPolygonF(pts, x1, y1, x2, y1, x2, y2) # stepped line, with points on right elif steps[:5] == 'right': x1 = xvals[:-1] x2 = xvals[1:] y1 = yvals[:-1] y2 = yvals[1:] utils.addNumpyToPolygonF(pts, x1, y1, x1, y2, x2, y2) # stepped line, with points in centre # this is complex as we can't use the mean of the plotter coords, # as the axis could be log elif steps[:6] == 'centre': axes = self.parent.getAxes( (s.xAxis, s.yAxis) ) if xdata.hasErrors(): # Special case if error bars on x points: # here we use the error bars to define the steps xmin, xmax = xdata.getPointRanges() # this is duplicated from drawing error bars: bad # convert xmin and xmax to graph coordinates xmin = axes[0].dataToPlotterCoords(posn, xmin) xmax = axes[0].dataToPlotterCoords(posn, xmax) utils.addNumpyToPolygonF(pts, xmin, yvals, xmax, yvals) else: # we put the bin edges half way between the points # we assume this is the correct thing to do even in log space x1 = xvals[:-1] x2 = xvals[1:] y1 = yvals[:-1] y2 = yvals[1:] xc = 0.5*(x1+x2) utils.addNumpyToPolygonF(pts, x1, y1, xc, y1, xc, y2) if len(xvals) > 0: pts.append( qt4.QPointF(xvals[-1], yvals[-1]) ) elif steps[:7] == 'vcentre': axes = self.parent.getAxes( (s.xAxis, s.yAxis) ) if ydata.hasErrors(): # Special case if error bars on y points: # here we use the error bars to define the steps ymin, ymax = ydata.getPointRanges() # this is duplicated from drawing error bars: bad # convert ymin and ymax to graph coordinates ymin = axes[1].dataToPlotterCoords(posn, ymin) ymax = axes[1].dataToPlotterCoords(posn, ymax) utils.addNumpyToPolygonF(pts, xvals, ymin, xvals, ymax) else: # we put the bin edges half way between the points # we assume this is the correct thing to do even in log space y1 = yvals[:-1] y2 = yvals[1:] x1 = xvals[:-1] x2 = xvals[1:] yc = 0.5*(y1+y2) utils.addNumpyToPolygonF(pts, x1, y1, x1, yc, x2, yc) if len(yvals) > 0: pts.append( qt4.QPointF(xvals[-1], yvals[-1]) ) else: assert False return pts def _getBezierLine(self, poly): """Try to draw a bezier line connecting the points.""" npts = qtloops.bezier_fit_cubic_multi(poly, 0.1, len(poly)+1) i = 0 path = qt4.QPainterPath() lastpt = qt4.QPointF(-999999,-999999) while i < len(npts): if lastpt != npts[i]: path.moveTo(npts[i]) path.cubicTo(npts[i+1], npts[i+2], npts[i+3]) lastpt = npts[i+3] i += 4 return path def _drawBezierLine( self, painter, xvals, yvals, posn, xdata, ydata): """Handle bezier lines and fills.""" pts = self._getLinePoints(xvals, yvals, posn, xdata, ydata) if len(pts) < 2: return path = self._getBezierLine(pts) s = self.settings # do filling for fillstyle in s.FillBelow, s.FillAbove: if not fillstyle.hide: x1, y1, x2, y2 = { 'top': (pts[0].x(), posn[1], pts[-1].x(), posn[1]), 'bottom': (pts[0].x(), posn[3], pts[-1].x(), posn[3]), 'left': (posn[0], pts[0].y(), posn[0], pts[-1].y()), 'right': (posn[2], pts[0].y(), posn[2], pts[-1].y()) }[fillstyle.fillto] temppath = qt4.QPainterPath(path) temppath.lineTo(x2, y2) temppath.lineTo(x1, y1) utils.brushExtFillPath(painter, fillstyle, temppath) if not s.PlotLine.hide: painter.strokePath(path, s.PlotLine.makeQPen(painter)) def _drawPlotLine( self, painter, xvals, yvals, posn, xdata, ydata, cliprect ): """Draw the line connecting the points.""" pts = self._getLinePoints(xvals, yvals, posn, xdata, ydata) if len(pts) < 2: return s = self.settings # do filling for fillstyle in s.FillBelow, s.FillAbove: if not fillstyle.hide: fillPtsToEdge(painter, pts, posn, cliprect, fillstyle) # draw line between points if not s.PlotLine.hide: painter.setPen( s.PlotLine.makeQPen(painter) ) utils.plotClippedPolyline(painter, cliprect, pts) def drawKeySymbol(self, number, painter, x, y, width, height): """Draw the plot symbol and/or line.""" painter.save() cliprect = qt4.QRectF(qt4.QPointF(x,y), qt4.QPointF(x+width,y+height)) painter.setClipRect(cliprect) # draw sample error bar s = self.settings size = s.get('markerSize').convert(painter) style = s.errorStyle # make some fake error bar data to plot xv = s.get('xData').getData(self.document) yv = s.get('yData').getData(self.document) yp = y + height/2 xpts = N.array([x-width, x+width/2, x+2*width]) ypts = N.array([yp, yp, yp]) # size of error bars in key errorsize = height*0.4 # make points for error bars (if any) if xv and xv.hasErrors(): xneg = N.array([x-width, x+width/2-errorsize, x+2*width]) xpos = N.array([x-width, x+width/2+errorsize, x+2*width]) else: xneg = xpos = xpts if yv and yv.hasErrors(): yneg = N.array([yp-errorsize, yp-errorsize, yp-errorsize]) ypos = N.array([yp+errorsize, yp+errorsize, yp+errorsize]) else: yneg = ypos = ypts # plot error bar painter.setPen( s.ErrorBarLine.makeQPenWHide(painter) ) for function in _errorBarFunctionMap[style]: function(style, xneg, xpos, yneg, ypos, xpts, ypts, s, painter, cliprect) # draw line if not s.PlotLine.hide: painter.setPen( s.PlotLine.makeQPen(painter) ) painter.drawLine( qt4.QPointF(x, yp), qt4.QPointF(x+width, yp) ) # draw marker if not s.MarkerLine.hide or not s.MarkerFill.hide: if not s.MarkerFill.hide: painter.setBrush( s.MarkerFill.makeQBrush() ) if not s.MarkerLine.hide: painter.setPen( s.MarkerLine.makeQPen(painter) ) else: painter.setPen( qt4.QPen( qt4.Qt.NoPen ) ) utils.plotMarker(painter, x+width/2, yp, s.marker, size) painter.restore() def drawLabels(self, painter, xplotter, yplotter, textvals, markersize): """Draw labels for the points.""" s = self.settings lab = s.get('Label') # work out offset an alignment deltax = markersize*1.5*{'left':-1, 'centre':0, 'right':1}[lab.posnHorz] deltay = markersize*1.5*{'top':-1, 'centre':0, 'bottom':1}[lab.posnVert] alignhorz = {'left':1, 'centre':0, 'right':-1}[lab.posnHorz] alignvert = {'top':-1, 'centre':0, 'bottom':1}[lab.posnVert] # make font and len textpen = lab.makeQPen() painter.setPen(textpen) font = lab.makeQFont(painter) angle = lab.angle # iterate over each point and plot each label for x, y, t in czip(xplotter+deltax, yplotter+deltay, textvals): utils.Renderer( painter, font, x, y, t, alignhorz, alignvert, angle ).render() def getAxisLabels(self, direction): """Get labels for axis if using a label axis.""" s = self.settings doc = self.document text = s.get('labels').getData(doc, checknull=True) xv = s.get('xData').getData(doc) yv = s.get('yData').getData(doc) # handle missing dataset if yv and not xv and s.get('xData').isEmpty(): length = yv.data.shape[0] xv = document.DatasetRange(length, (1,length)) elif xv and not yv and s.get('yData').isEmpty(): length = xv.data.shape[0] yv = document.DatasetRange(length, (1,length)) if None in (text, xv, yv): return (None, None) if direction == 'horizontal': return (text, xv.data) else: return (text, yv.data) def _pickable(self, bounds): axes = self.fetchAxes() if axes is None: map_fn = None else: map_fn = lambda x, y: ( axes[0].dataToPlotterCoords(bounds, x), axes[1].dataToPlotterCoords(bounds, y) ) return pickable.DiscretePickable(self, 'xData', 'yData', map_fn) def pickPoint(self, x0, y0, bounds, distance = 'radial'): return self._pickable(bounds).pickPoint(x0, y0, bounds, distance) def pickIndex(self, oldindex, direction, bounds): return self._pickable(bounds).pickIndex(oldindex, direction, bounds) def getColorbarParameters(self): """Return parameters for colorbar.""" s = self.settings c = s.Color return (c.min, c.max, c.scaling, s.MarkerFill.colorMap, 0, s.MarkerFill.colorMapInvert) def dataDraw(self, painter, axes, posn, cliprect): """Plot the data on a plotter.""" # get data s = self.settings doc = self.document xv = s.get('xData').getData(doc) yv = s.get('yData').getData(doc) text = s.get('labels').getData(doc, checknull=True) scalepoints = s.get('scalePoints').getData(doc) colorpoints = s.Color.get('points').getData(doc) # if a missing dataset, make a fake dataset for the second one # based on a row number if xv and not yv and s.get('yData').isEmpty(): # use index for y data length = xv.data.shape[0] yv = document.DatasetRange(length, (1,length)) elif yv and not xv and s.get('xData').isEmpty(): # use index for x data length = yv.data.shape[0] xv = document.DatasetRange(length, (1,length)) if not xv or not yv: # no valid dataset, so exit return # if text entered, then multiply up to get same number of values # as datapoints if text: length = min( len(xv.data), len(yv.data) ) text = text*(length // len(text)) + text[:length % len(text)] # loop over chopped up values for xvals, yvals, tvals, ptvals, cvals in ( document.generateValidDatasetParts( xv, yv, text, scalepoints, colorpoints)): #print "Calculating coordinates" # calc plotter coords of x and y points xplotter = axes[0].dataToPlotterCoords(posn, xvals.data) yplotter = axes[1].dataToPlotterCoords(posn, yvals.data) # points are plotted offset in shift-points modes if s.PlotLine.steps != 'off': xpltpoint = N.array(xplotter) if s.PlotLine.steps == 'right-shift-points': xpltpoint[1:] = 0.5*(xplotter[:-1] + xplotter[1:]) elif s.PlotLine.steps == 'left-shift-points': xpltpoint[:-1] = 0.5*(xplotter[:-1] + xplotter[1:]) else: xpltpoint = xplotter ypltpoint = yplotter # plot filled error bars if s.errorStyle in ('fillvert', 'fillhorz'): # filled region errors are painted first self._plotErrors(posn, painter, xpltpoint, ypltpoint, axes, xvals, yvals, cliprect) #print "Painting plot line" # plot data line (and/or filling above or below) if not s.PlotLine.hide or not s.FillAbove.hide or not s.FillBelow.hide: if s.PlotLine.bezierJoin and hasqtloops: self._drawBezierLine( painter, xplotter, yplotter, posn, xvals, yvals ) else: self._drawPlotLine( painter, xplotter, yplotter, posn, xvals, yvals, cliprect ) #print "Painting error bars" # plot normal errors bars if s.errorStyle not in ('fillvert', 'fillhorz'): # normally the error bar is painted after the line self._plotErrors(posn, painter, xpltpoint, ypltpoint, axes, xvals, yvals, cliprect) # plot the points (we do this last so they are on top) markersize = s.get('markerSize').convert(painter) if not s.MarkerLine.hide or not s.MarkerFill.hide: #print "Painting marker fill" if not s.MarkerFill.hide: # filling for markers painter.setBrush( s.MarkerFill.makeQBrush() ) else: # no-filling brush painter.setBrush( qt4.QBrush() ) #print "Painting marker lines" if not s.MarkerLine.hide: # edges of markers painter.setPen( s.MarkerLine.makeQPen(painter) ) else: # invisible pen painter.setPen( qt4.QPen(qt4.Qt.NoPen) ) # thin datapoints as required if s.thinfactor <= 1: xplt, yplt = xpltpoint, ypltpoint else: xplt, yplt = (xpltpoint[::s.thinfactor], ypltpoint[::s.thinfactor]) # whether to scale markers scaling = colorvals = cmap = None if ptvals: scaling = ptvals.data if s.thinfactor > 1: scaling = scaling[::s.thinfactor] # color point individually if cvals and not s.MarkerFill.hide: colorvals = utils.applyScaling( cvals.data, s.Color.scaling, s.Color.min, s.Color.max) if s.thinfactor > 1: colorvals = colorvals[::s.thinfactor] cmap = self.document.getColormap( s.MarkerFill.colorMap, s.MarkerFill.colorMapInvert) # actually plot datapoints utils.plotMarkers(painter, xplt, yplt, s.marker, markersize, scaling=scaling, clip=cliprect, cmap=cmap, colorvals=colorvals, scaleline=s.MarkerLine.scaleLine) # finally plot any labels if tvals and not s.Label.hide: self.drawLabels(painter, xpltpoint, ypltpoint, tvals, markersize) # allow the factory to instantiate an x,y plotter document.thefactory.register( PointPlotter ) veusz-1.21.1/veusz/widgets/colorbar.py0000664000175000017500000002170512346113205016133 0ustar jssjss# Copyright (C) 2007 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """A colorbar widget for the image widget. Should show the scale of the image.""" from __future__ import division from .. import qtall as qt4 import numpy as N from .. import document from .. import setting from .. import utils from . import widget from . import axis def _(text, disambiguation=None, context='ColorBar'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class ColorBar(axis.Axis): """Color bar for showing scale of image. This naturally is descended from an axis """ typename='colorbar' allowusercreation = True description = _('Image color bar') isaxis = False def __init__(self, parent, name=None): """Initialise object and create axes.""" axis.Axis.__init__(self, parent, name=name) if type(self) == ColorBar: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" axis.Axis.addSettings(s) s.add( setting.WidgetChoice('widgetName', '', descr=_('Corresponding widget'), widgettypes=('image', 'xy', 'nonorthpoint'), usertext = _('Widget')), 0 ) s.get('log').readonly = True s.get('datascale').readonly = True s.add( setting.AlignHorzWManual( 'horzPosn', 'right', descr = _('Horizontal position'), usertext=_('Horz posn'), formatting=True) ) s.add( setting.AlignVertWManual( 'vertPosn', 'bottom', descr = _('Vertical position'), usertext=_('Vert posn'), formatting=True) ) s.add( setting.DistanceOrAuto('width', 'Auto', descr = _('Width of colorbar'), usertext=_('Width'), formatting=True) ) s.add( setting.DistanceOrAuto('height', 'Auto', descr = _('Height of colorbar'), usertext=_('Height'), formatting=True) ) s.add( setting.Float( 'horzManual', 0., descr = _('Manual horizontal fractional position'), usertext=_('Horz manual'), formatting=True) ) s.add( setting.Float( 'vertManual', 0., descr = _('Manual vertical fractional position'), usertext=_('Vert manual'), formatting=True) ) s.add( setting.Line('Border', descr = _('Colorbar border line'), usertext=_('Border')), pixmap='settings_border') s.add( setting.SettingBackwardCompat('image', 'widgetName', None) ) @classmethod def allowedParentTypes(klass): from . import graph, grid, nonorthgraph return (graph.Graph, grid.Grid, nonorthgraph.NonOrthGraph) @property def userdescription(self): return _("widget='%s', label='%s'") % ( self.settings.widgetName, self.settings.label) def chooseName(self): """Get name of widget.""" # override axis naming of x and y return widget.Widget.chooseName(self) def _axisDraw(self, posn, parentposn, outerbounds, painter, phelper): """Do actual drawing.""" s = self.settings # get height of label font bounds = self.computeBounds(parentposn, phelper) font = s.get('Label').makeQFont(phelper) painter.setFont(font) fontheight = utils.FontMetrics(font, painter.device()).height() horz = s.direction == 'horizontal' # use above to estimate width and height if necessary w = s.get('width') if w.isAuto(): if horz: totalwidth = bounds[2] - bounds[0] - 2*fontheight else: totalwidth = fontheight else: totalwidth = w.convert(painter) h = s.get('height') if h.isAuto(): if horz: totalheight = fontheight else: totalheight = bounds[3] - bounds[1] - 2*fontheight else: totalheight = h.convert(painter) # work out horizontal position h = s.horzPosn if h == 'left': bounds[0] += fontheight bounds[2] = bounds[0] + totalwidth elif h == 'right': bounds[2] -= fontheight bounds[0] = bounds[2] - totalwidth elif h == 'centre': delta = (bounds[2]-bounds[0]-totalwidth)/2. bounds[0] += delta bounds[2] -= delta elif h == 'manual': bounds[0] += (bounds[2]-bounds[0])*s.horzManual bounds[2] = bounds[0] + totalwidth # work out vertical position v = s.vertPosn if v == 'top': bounds[1] += fontheight bounds[3] = bounds[1] + totalheight elif v == 'bottom': bounds[3] -= fontheight bounds[1] = bounds[3] - totalheight elif v == 'centre': delta = (bounds[3]-bounds[1]-totalheight)/2. bounds[1] += delta bounds[3] -= delta elif v == 'manual': bounds[1] += (bounds[3]-bounds[1])*s.vertManual bounds[3] = bounds[1] + totalheight # FIXME: this is ugly - update bounds in helper state phelper.states[(self,0)].bounds = bounds # do no painting if hidden or no image imgwidget = s.get('widgetName').findWidget() if s.hide: return bounds self.updateAxisLocation(bounds) # update image if necessary with new settings if imgwidget is not None: minval, maxval, axisscale, cmapname, trans, invert = \ imgwidget.getColorbarParameters() cmap = self.document.getColormap(cmapname, invert) img = utils.makeColorbarImage( minval, maxval, axisscale, cmap, trans, direction=s.direction) else: # couldn't find widget minval, maxval, axisscale = 0., 1., 'linear' img = None s.get('log').setSilent(axisscale == 'log') self.setAutoRange([minval, maxval]) self.computePlottedRange(force=True) # now draw image on axis... minpix, maxpix = self.graphToPlotterCoords( bounds, N.array([minval, maxval]) ) routside = qt4.QRectF( bounds[0], bounds[1], bounds[2]-bounds[0], bounds[3]-bounds[1] ) # really draw the img if img is not None: # coordinates to draw image and to clip rectangle if s.direction == 'horizontal': c = [ minpix, bounds[1], maxpix, bounds[3] ] cl = [ self.coordParr1, bounds[1], self.coordParr2, bounds[3] ] else: c = [ bounds[0], maxpix, bounds[2], minpix ] cl = [ bounds[0], self.coordParr1, bounds[2], self.coordParr2 ] r = qt4.QRectF(c[0], c[1], c[2]-c[0], c[3]-c[1]) rclip = qt4.QRectF(cl[0], cl[1], cl[2]-cl[0], cl[3]-cl[1]) painter.save() painter.setClipRect(rclip & routside) painter.drawImage(r, img) painter.restore() # if there's a border if not s.Border.hide: painter.setPen( s.get('Border').makeQPen(painter) ) painter.setBrush( qt4.QBrush() ) painter.drawRect( routside ) # actually draw axis axis.Axis._axisDraw(self, bounds, parentposn, None, painter, phelper) # allow the factory to instantiate a colorbar document.thefactory.register( ColorBar ) veusz-1.21.1/veusz/widgets/fit.py0000644000175000017500000003431112327177747015131 0ustar jssjss# fit.py # fitting plotter # Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division, absolute_import, print_function import re import sys import numpy as N from ..compat import czip, cstr from .. import document from .. import setting from .. import utils from .. import qtall as qt4 from .function import FunctionPlotter from . import widget try: import minuit except ImportError: minuit = None def _(text, disambiguation=None, context='Fit'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) def minuitFit(evalfunc, params, names, values, xvals, yvals, yserr): """Do fitting with minuit (if installed).""" def chi2(params): """generate a lambda function to impedance-match between PyMinuit's use of multiple parameters versus our use of a single numpy vector.""" c = ((evalfunc(params, xvals) - yvals)**2 / yserr**2).sum() if chi2.runningFit: chi2.iters += 1 p = [chi2.iters, c] + params.tolist() str = ("%5i " + "%8g " * (len(params)+1)) % tuple(p) print(str) return c namestr = ', '.join(names) fnstr = 'lambda %s: chi2(N.array([%s]))' % (namestr, namestr) # this is safe because the only user-controlled variable is len(names) fn = eval(fnstr, {'chi2' : chi2, 'N' : N}) print(_('Fitting via Minuit:')) m = minuit.Minuit(fn, fix_x=True, **values) # run the fit chi2.runningFit = True chi2.iters = 0 m.migrad() # do some error analysis have_symerr, have_err = False, False try: chi2.runningFit = False m.hesse() have_symerr = True m.minos() have_err = True except minuit.MinuitError as e: print(e) if str(e).startswith('Discovered a new minimum'): # the initial fit really failed raise # print the results retchi2 = m.fval dof = len(yvals) - len(params) redchi2 = retchi2 / dof if have_err: print(_('Fit results:\n') + "\n".join([ u" %s = %g \u00b1 %g (+%g / %g)" % (n, m.values[n], m.errors[n], m.merrors[(n, 1.0)], m.merrors[(n, -1.0)]) for n in names])) elif have_symerr: print(_('Fit results:\n') + "\n".join([ u" %s = %g \u00b1 %g" % (n, m.values[n], m.errors[n]) for n in names])) print(_('MINOS error estimate not available.')) else: print(_('Fit results:\n') + "\n".join([ ' %s = %g' % (n, m.values[n]) for n in names])) print(_('No error analysis available: fit quality uncertain')) print("chi^2 = %g, dof = %i, reduced-chi^2 = %g" % (retchi2, dof, redchi2)) vals = m.values return vals, retchi2, dof class Fit(FunctionPlotter): """A plotter to fit a function to data.""" typename='fit' allowusercreation=True description=_('Fit a function to data') def __init__(self, parent, name=None): FunctionPlotter.__init__(self, parent, name=name) if type(self) == Fit: self.readDefaults() self.addAction( widget.Action('fit', self.actionFit, descr = _('Fit function'), usertext = _('Fit function')) ) @classmethod def addSettings(klass, s): """Construct list of settings.""" FunctionPlotter.addSettings(s) s.add( setting.FloatDict( 'values', {'a': 0.0, 'b': 1.0}, descr = _('Variables and fit values'), usertext=_('Parameters')), 1 ) s.add( setting.DatasetExtended( 'xData', 'x', descr = _('X data to fit (dataset name, list of values ' 'or expression)'), usertext=_('X data')), 2 ) s.add( setting.DatasetExtended( 'yData', 'y', descr = _('Y data to fit (dataset name, list of values ' 'or expression)'), usertext=_('Y data')), 3 ) s.add( setting.Bool( 'fitRange', False, descr = _('Fit only the data between the ' 'minimum and maximum of the axis for ' 'the function variable'), usertext=_('Fit only range')), 4 ) s.add( setting.WidgetChoice( 'outLabel', '', descr=_('Write best fit parameters to this text label ' 'after fitting'), widgettypes=('label',), usertext=_('Output label')), 5 ) s.add( setting.Str('outExpr', '', descr = _('Output best fitting expression'), usertext=_('Output expression')), 6, readonly=True ) s.add( setting.Float('chi2', -1, descr = 'Output chi^2 from fitting', usertext=_('Fit χ2')), 7, readonly=True ) s.add( setting.Int('dof', -1, descr = _('Output degrees of freedom from fitting'), usertext=_('Fit d.o.f.')), 8, readonly=True ) s.add( setting.Float('redchi2', -1, descr = _('Output reduced-chi-squared from fitting'), usertext=_('Fit reduced χ2')), 9, readonly=True ) f = s.get('function') f.newDefault('a + b*x') f.descr = _('Function to fit') # modify description s.get('min').usertext=_('Min. fit range') s.get('max').usertext=_('Max. fit range') def affectsAxisRange(self): """This widget provides range information about these axes.""" s = self.settings return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def getRange(self, axis, depname, axrange): """Update range with range of data.""" dataname = {'sx': 'xData', 'sy': 'yData'}[depname] data = self.settings.get(dataname).getData(self.document) if data: drange = data.getRange() if drange: axrange[0] = min(axrange[0], drange[0]) axrange[1] = max(axrange[1], drange[1]) def initEnviron(self): """Copy data into environment.""" env = self.document.eval_context.copy() env.update( self.settings.values ) return env def updateOutputLabel(self, ops, vals, chi2, dof): """Use best fit parameters to update text label.""" s = self.settings labelwidget = s.get('outLabel').findWidget() if labelwidget is not None: # build up a set of X=Y values loc = self.document.locale txt = [] for l, v in sorted(vals.items()): val = utils.formatNumber(v, '%.4Vg', locale=loc) txt.append( '%s = %s' % (l, val) ) # add chi2 output txt.append( r'\chi^{2}_{\nu} = %s/%i = %s' % ( utils.formatNumber(chi2, '%.4Vg', locale=loc), dof, utils.formatNumber(chi2/dof, '%.4Vg', locale=loc) )) # update label with text text = r'\\'.join(txt) ops.append( document.OperationSettingSet( labelwidget.settings.get('label') , text ) ) def actionFit(self): """Fit the data.""" s = self.settings # check and get compiled for of function compiled = self.document.compileCheckedExpression(s.function) if compiled is None: return # populate the input parameters paramnames = sorted(s.values) params = N.array( [s.values[p] for p in paramnames] ) # FIXME: loads of error handling!! d = self.document # choose dataset depending on fit variable if s.variable == 'x': xvals = s.get('xData').getData(d).data ydata = s.get('yData').getData(d) else: xvals = s.get('yData').getData(d).data ydata = s.get('xData').getData(d) yvals = ydata.data yserr = ydata.serr # if there are no errors on data if yserr is None: if ydata.perr is not None and ydata.nerr is not None: print("Warning: Symmeterising positive and negative errors") yserr = N.sqrt( 0.5*(ydata.perr**2 + ydata.nerr**2) ) else: print("Warning: No errors on y values. Assuming 5% errors.") yserr = yvals*0.05 yserr[yserr < 1e-8] = 1e-8 # if the fitRange parameter is on, we chop out data outside the # range of the axis if s.fitRange: # get ranges for axes if s.variable == 'x': drange = self.parent.getAxes((s.xAxis,))[0].getPlottedRange() mask = N.logical_and(xvals >= drange[0], xvals <= drange[1]) else: drange = self.parent.getAxes((s.yAxis,))[0].getPlottedRange() mask = N.logical_and(yvals >= drange[0], yvals <= drange[1]) xvals, yvals, yserr = xvals[mask], yvals[mask], yserr[mask] print("Fitting %s from %g to %g" % (s.variable, drange[0], drange[1])) evalenv = self.initEnviron() def evalfunc(params, xvals): # update environment with variable and parameters evalenv[self.settings.variable] = xvals evalenv.update( czip(paramnames, params) ) try: return eval(compiled, evalenv) + xvals*0. except Exception as e: self.document.log(cstr(e)) return N.nan # minimum set for fitting if s.min != 'Auto': if s.variable == 'x': mask = xvals >= s.min else: mask = yvals >= s.min xvals, yvals, yserr = xvals[mask], yvals[mask], yserr[mask] # maximum set for fitting if s.max != 'Auto': if s.variable == 'x': mask = xvals <= s.max else: mask = yvals <= s.max xvals, yvals, yserr = xvals[mask], yvals[mask], yserr[mask] if s.min != 'Auto' or s.max != 'Auto': print("Fitting %s between %s and %s" % (s.variable, s.min, s.max)) # various error checks if len(xvals) == 0: sys.stderr.write(_('No data values. Not fitting.\n')) return if len(xvals) != len(yvals) or len(xvals) != len(yserr): sys.stderr.write(_('Fit data not equal in length. Not fitting.\n')) return if len(params) > len(xvals): sys.stderr.write(_('No degrees of freedom for fit. Not fitting\n')) return # actually do the fit, either via Minuit or our own LM fitter chi2 = 1 dof = 1 if minuit is not None: vals, chi2, dof = minuitFit(evalfunc, params, paramnames, s.values, xvals, yvals, yserr) else: print(_('Minuit not available, falling back to simple L-M fitting:')) retn, chi2, dof = utils.fitLM(evalfunc, params, xvals, yvals, yserr) vals = {} for i, v in czip(paramnames, retn): vals[i] = float(v) # list of operations do we can undo the changes operations = [] # populate the return parameters operations.append( document.OperationSettingSet(s.get('values'), vals) ) # populate the read-only fit quality params operations.append( document.OperationSettingSet(s.get('chi2'), float(chi2)) ) operations.append( document.OperationSettingSet(s.get('dof'), int(dof)) ) if dof <= 0: print(_('No degrees of freedom in fit.\n')) redchi2 = -1. else: redchi2 = float(chi2/dof) operations.append( document.OperationSettingSet(s.get('redchi2'), redchi2) ) # expression for fit expr = self.generateOutputExpr(vals) operations.append( document.OperationSettingSet(s.get('outExpr'), expr) ) self.updateOutputLabel(operations, vals, chi2, dof) # actually change all the settings d.applyOperation( document.OperationMultiple(operations, descr=_('fit')) ) def generateOutputExpr(self, vals): """Try to generate text form of output expression. vals is a dict of variable: value pairs returns the expression """ paramvals = vals.copy() s = self.settings # also substitute in data name for variable if s.variable == 'x': paramvals['x'] = s.xData else: paramvals['y'] = s.yData # split expression up into parts of text and nums, separated # by non-text/nums parts = re.split('([^A-Za-z0-9.])', s.function) # replace part by things in paramvals, if they exist for i, p in enumerate(parts): if p in paramvals: parts[i] = str( paramvals[p] ) return ''.join(parts) # allow the factory to instantiate an x,y plotter document.thefactory.register( Fit ) veusz-1.21.1/veusz/widgets/graph.py0000644000175000017500000002643012327177747015453 0ustar jssjss# graph widget for containing other sorts of widget # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import textwrap from .. import qtall as qt4 from .. import setting from .. import utils from .. import document from . import widget from . import controlgraph def _(text, disambiguation=None, context='Graph'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Graph(widget.Widget): """Graph for containing other sorts of widgets""" typename='graph' allowusercreation = True description = _('Base graph') def __init__(self, parent, name=None): """Initialise object and create axes.""" widget.Widget.__init__(self, parent, name=name) self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" widget.Widget.addSettings(s) s.add( setting.Distance( 'leftMargin', '1.7cm', descr=_('Distance from left of graph to edge'), usertext=_('Left margin'), formatting=True) ) s.add( setting.Distance( 'rightMargin', '0.2cm', descr=_('Distance from right of graph to edge'), usertext=_('Right margin'), formatting=True) ) s.add( setting.Distance( 'topMargin', '0.2cm', descr=_('Distance from top of graph to edge'), usertext=_('Top margin'), formatting=True) ) s.add( setting.Distance( 'bottomMargin', '1.7cm', descr=_('Distance from bottom of graph to edge'), usertext=_('Bottom margin'), formatting=True) ) s.add( setting.FloatOrAuto('aspect', 'Auto', descr=_('Fix aspect ratio of graph to this value'), usertext=_('Aspect ratio'), minval = 0.01, maxval = 100., formatting=True) ) s.add( setting.Notes( 'notes', '', descr=_('User-defined notes'), usertext=_('Notes') ) ) s.add( setting.GraphBrush( 'Background', descr = _('Background plot fill'), usertext=_('Background')), pixmap='settings_bgfill' ) s.add( setting.Line('Border', descr = _('Graph border line'), usertext=_('Border')), pixmap='settings_border') @classmethod def allowedParentTypes(klass): from . import page, grid return (page.Page, grid.Grid) @property def userdescription(self): """Return user-friendly description.""" return textwrap.fill(self.settings.notes, 60) def addDefaultSubWidgets(self): """Add axes automatically.""" from . import axis if self.parent.getChild('x') is None: axis.Axis(self, name='x') if self.parent.getChild('y') is None: axis.Axis(self, name='y') def getAxesDict(self, axesnames, ignoremissing=False): """Get the axes for widgets to plot against. axesnames is a list/set of names to find. Returns a dict of objects """ axes = {} # recursively go back up the tree to find axes w = self while w is not None and len(axes) < len(axesnames): for c in w.children: name = c.name if ( name in axesnames and name not in axes and c.isaxis ): axes[name] = c w = w.parent # didn't find everything... if w is None and not ignoremissing: for name in axesnames: if name not in axes: axes[name] = None # return list of found axes return axes def getAxes(self, axesnames): """Return a list of axes widgets given a list of names.""" ad = self.getAxesDict(axesnames) return [ad[n] for n in axesnames] def adjustBoundsForAspect(self, bounds): s = self.settings if s.aspect != 'Auto': saspect = s.aspect width = bounds[2]-bounds[0] height = bounds[3]-bounds[1] gaspect = width/height bounds = list(bounds) if saspect > gaspect: # want a graph which is wider than the current size # => add space to top/bottom newheight = width / saspect delta = (height-newheight) / 2 bounds[1] += delta bounds[3] -= delta else: # want a graph which is narrower than the current size # => add space to left/right newwidth = height * saspect delta = (width-newwidth) / 2 bounds[0] += delta bounds[2] -= delta return bounds def getMargins(self, painthelper): """Use settings to compute margins.""" s = self.settings return ( s.get('leftMargin').convert(painthelper), s.get('topMargin').convert(painthelper), s.get('rightMargin').convert(painthelper), s.get('bottomMargin').convert(painthelper) ) def draw(self, parentposn, painthelper, outerbounds = None): '''Update the margins before drawing.''' # yuck, avoid circular imports from . import axisbroken s = self.settings bounds = self.computeBounds(parentposn, painthelper) maxbounds = self.computeBounds(parentposn, painthelper, withmargin=False) # do no painting if hidden if s.hide: return bounds # controls for adjusting graph margins painter = painthelper.painter(self, bounds) painthelper.setControlGraph(self, [ controlgraph.ControlMarginBox(self, bounds, maxbounds, painthelper) ]) bounds = self.adjustBoundsForAspect(bounds) with painter: # set graph rectangle attributes path = qt4.QPainterPath() path.addRect( qt4.QRectF(qt4.QPointF(bounds[0], bounds[1]), qt4.QPointF(bounds[2], bounds[3])) ) utils.brushExtFillPath(painter, s.Background, path, stroke=s.Border.makeQPenWHide(painter)) # debugging positions (uncomment) # painter.drawRect( qt4.QRectF( # qt4.QPointF(parentposn[0], parentposn[1]), # qt4.QPointF(parentposn[2], parentposn[3]) )) # if outerbounds: # painter.drawRect( qt4.QRectF( # qt4.QPointF(outerbounds[0], outerbounds[1]), # qt4.QPointF(outerbounds[2], outerbounds[3]) )) # child drawing algorithm is a bit complex due to axes # being shared between graphs and broken axes # this is a map of axis names to plot to axis widgets axestodraw = {} # axes widgets for each plotter (precalculated by Page) axesofwidget = painthelper.plotteraxismap for c in self.children: try: for a in axesofwidget[c]: axestodraw[a.name] = a except (KeyError, AttributeError): if c.isaxis: axestodraw[c.name] = c # grid lines are normally plotted before other child widgets axisdrawlist = sorted(axestodraw.items(), reverse=True) for aname, awidget in axisdrawlist: awidget.updateAxisLocation(bounds) awidget.computePlottedRange() awidget.drawGrid(bounds, painthelper, outerbounds=outerbounds, ontop=False) # broken axis handling brokenaxes = set() for axis in axestodraw.values(): if isinstance(axis, axisbroken.AxisBroken): brokenaxes.add(axis) # don't duplicate drawing axes axesdrawn = set() # do normal drawing of children # iterate over children in reverse order for c in reversed(self.children): if c.isaxis: axesdrawn.add(c) axes = axesofwidget.get(c, None) if axes is not None and any((a in brokenaxes for a in axes)): # handle broken axes childbrokenaxes = sorted([ (a.name, a) for a in axes if a in brokenaxes ]) childbrokenaxes.sort() def iteratebrokenaxes(b): """Recursively iterate over each broken axis and redraw child for each. We might have more than one broken axis per child, so hence this rather strange iteration. """ ax = b[0][1] for i in range(ax.breakvnum): ax.switchBreak(i, bounds) if len(b) == 1: c.draw(bounds, painthelper, outerbounds=outerbounds) else: iteratebrokenaxes(b[1:]) ax.switchBreak(None, bounds) iteratebrokenaxes(childbrokenaxes) else: # standard non broken axis drawing c.draw(bounds, painthelper, outerbounds=outerbounds) # then for grid lines on top for aname, awidget in axisdrawlist: awidget.drawGrid(bounds, painthelper, outerbounds=outerbounds, ontop=True) # draw remaining axes for aname, awidget in axisdrawlist: if awidget not in axesdrawn: awidget.draw(bounds, painthelper, outerbounds=outerbounds) return bounds def updateControlItem(self, cgi): """Graph resized or moved - call helper routine to move self.""" cgi.setWidgetMargins() # allow users to make Graph objects document.thefactory.register( Graph ) veusz-1.21.1/veusz/widgets/boxplot.py0000644000175000017500000004353312327177747016044 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """For making box plots.""" from __future__ import division import math import numpy as N from ..compat import crange, czip from .. import qtall as qt4 from .. import setting from .. import document from .. import utils from .plotters import GenericPlotter def _(text, disambiguation=None, context='BoxPlot'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) def percentile(sortedds, perc): """Given a sorted dataset, get the percentile perc. Interpolates between data points.""" index = perc * 0.01 * (sortedds.shape[0]-1) # interpolate between indices frac, index = math.modf(index) index = int(index) indexplus1 = min(index+1, sortedds.shape[0]-1) interpol = (1-frac)*sortedds[index] + frac*sortedds[indexplus1] return interpol def swapline(painter, x1, y1, x2, y2, swap): """Draw line, swapping x and y coordinates if swap is True.""" if swap: painter.drawLine( qt4.QPointF(y1, x1), qt4.QPointF(y2, x2) ) else: painter.drawLine( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) def swapbox(painter, x1, y1, x2, y2, swap): """Return box, swapping x and y coordinates if swap is True.""" if swap: return qt4.QRectF(qt4.QPointF(y1, x1), qt4.QPointF(y2, x2)) else: return qt4.QRectF(qt4.QPointF(x1, y1), qt4.QPointF(x2, y2)) class _Stats(object): """Store statistics about box.""" def calculate(self, data, whiskermode): """Calculate statistics for data.""" cleaned = data[ N.isfinite(data) ] cleaned.sort() if len(cleaned) == 0: self.median = self.botquart = self.topquart = self.mean = \ self.botwhisker = self.topwhisker = N.nan return self.median = percentile(cleaned, 50) self.botquart = percentile(cleaned, 25) self.topquart = percentile(cleaned, 75) self.mean = N.mean(cleaned) if whiskermode == 'min/max': self.botwhisker = cleaned.min() self.topwhisker = cleaned.max() elif whiskermode == '1.5IQR': iqr = self.topquart - self.botquart eltop = N.searchsorted(cleaned, self.topquart+1.5*iqr)-1 self.topwhisker = cleaned[eltop] elbot = max(N.searchsorted(cleaned, self.botquart-1.5*iqr)-1, 0) self.botwhisker = cleaned[elbot] elif whiskermode == '1 stddev': stddev = N.std(cleaned) self.topwhisker = self.mean+stddev self.botwhisker = self.mean-stddev elif whiskermode == '9/91 percentile': self.topwhisker = percentile(cleaned, 91) self.botwhisker = percentile(cleaned, 9) elif whiskermode == '2/98 percentile': self.topwhisker = percentile(cleaned, 98) self.botwhisker = percentile(cleaned, 2) else: raise RuntimeError("Invalid whisker mode") self.outliers = cleaned[ (cleaned < self.botwhisker) | (cleaned > self.topwhisker) ] class BoxPlot(GenericPlotter): """Plot bar charts.""" typename='boxplot' allowusercreation=True description=_('Plot box plots') def __init__(self, parent, name=None): """Initialise box plot.""" GenericPlotter.__init__(self, parent, name=name) if type(self) == BoxPlot: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" GenericPlotter.addSettings(s) s.remove('key') s.add( setting.Choice('whiskermode', ('min/max', '1.5IQR', '1 stddev', '9/91 percentile', '2/98 percentile'), '1.5IQR', descr = _('Whisker mode'), usertext=_('Whisker mode')), 0 ) s.add( setting.Choice('direction', ('horizontal', 'vertical'), 'vertical', descr = _('Horizontal or vertical boxes'), usertext=_('Direction')), 0 ) s.add( setting.DatasetOrStr('labels', '', descr=_('Dataset or string to label bars'), usertext=_('Labels')), 0 ) s.add( setting.DatasetExtended( 'posn', '', descr = _('Dataset or list of values giving ' 'positions of boxes (optional)'), usertext=_('Positions')), 0 ) # calculate statistics from these datasets s.add( setting.Datasets('values', ('data',), descr = _('Datasets containing values to ' 'calculate statistics for'), usertext=_('Datasets')), 0 ) # alternate mode where data are provided for boxes s.add( setting.DatasetExtended( 'whiskermax', '', descr=_('Dataset with whisker maxima or list of values'), usertext=_('Whisker max')), 0 ) s.add( setting.DatasetExtended( 'whiskermin', '', descr=_('Dataset with whisker minima or list of values'), usertext=_('Whisker min')), 0 ) s.add( setting.DatasetExtended( 'boxmax', '', descr=_('Dataset with box maxima or list of values'), usertext=_('Box max')), 0 ) s.add( setting.DatasetExtended( 'boxmin', '', descr=_('Dataset with box minima or list of values'), usertext=_('Box min')), 0 ) s.add( setting.DatasetExtended( 'median', '', descr=_('Dataset with medians or list of values'), usertext=_('Median')), 0 ) s.add( setting.DatasetExtended( 'mean', '', descr=_('Dataset with means or list of values'), usertext=_('Mean')), 0 ) # switch between different modes s.add( setting.BoolSwitch('calculate', True, descr = _('Calculate statistics from datasets' ' rather than given manually'), usertext = _('Calculate'), settingstrue=('whiskermode', 'values'), settingsfalse=('boxmin', 'whiskermin', 'boxmax', 'whiskermax', 'mean', 'median')), 0 ) # formatting options s.add( setting.Float('fillfraction', 0.75, descr = _('Fill fraction of boxes'), usertext=_('Fill fraction'), formatting=True) ) s.add( setting.Marker('outliersmarker', 'circle', descr = _('Marker for outliers'), usertext=_('Outliers'), formatting=True) ) s.add( setting.Marker('meanmarker', 'linecross', descr = _('Marker for mean'), usertext=_('Mean'), formatting=True) ) s.add( setting.DistancePt('markerSize', '3pt', descr = _('Size of markers to plot'), usertext=_('Markers size'), formatting=True) ) s.add( setting.GraphBrush( 'Fill', descr = _('Box fill'), usertext=_('Box fill')), pixmap='settings_bgfill' ) s.add( setting.Line('Border', descr = _('Box border line'), usertext=_('Box border')), pixmap='settings_border') s.add( setting.Line('Whisker', descr = _('Whisker line'), usertext=_('Whisker line')), pixmap='settings_whisker') s.add( setting.Line('MarkersLine', descr = _('Line around markers'), usertext = _('Markers border')), pixmap = 'settings_plotmarkerline' ) s.add( setting.BoxPlotMarkerFillBrush('MarkersFill', descr = _('Markers fill'), usertext = _('Markers fill')), pixmap = 'settings_plotmarkerfill' ) @property def userdescription(self): """Friendly description for user.""" s = self.settings return "values='%s', position='%s'" % ( ', '.join(s.values), s.posn) def affectsAxisRange(self): """This widget provides range information about these axes.""" s = self.settings return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def rangeManual(self): """For updating range in manual mode.""" s = self.settings ds = [] for name in ('whiskermin', 'whiskermax', 'boxmin', 'boxmax', 'mean', 'median'): ds.append( s.get(name).getData(self.document) ) r = [N.inf, -N.inf] if None not in ds: concat = N.concatenate([d.data for d in ds]) r[0] = N.nanmin(concat) r[1] = N.nanmax(concat) return r def getPosns(self): """Get values of positions of bars.""" s = self.settings doc = self.document posns = s.get('posn').getData(doc) if posns is not None: # manual positions return posns.data else: if s.calculate: # number of datasets vals = s.get('values').getData(doc) else: # length of mean array vals = s.get('mean').getData(doc) if vals: vals = vals.data if vals is None: return N.array([]) else: return N.arange(1, len(vals)+1, dtype=N.float64) def getRange(self, axis, depname, axrange): """Update axis range from data.""" s = self.settings doc = self.document if ( (depname == 'sx' and s.direction == 'horizontal') or (depname == 'sy' and s.direction == 'vertical') ): # update axis in direction of data if s.calculate: # update from values values = s.get('values').getData(doc) if values: for v in values: if len(v.data) > 0: axrange[0] = min(axrange[0], N.nanmin(v.data)) axrange[1] = max(axrange[1], N.nanmax(v.data)) else: # update from manual entries drange = self.rangeManual() axrange[0] = min(axrange[0], drange[0]) axrange[1] = max(axrange[1], drange[1]) else: # update axis in direction of datasets posns = self.getPosns() if len(posns) > 0: axrange[0] = min(axrange[0], N.nanmin(posns)-0.5) axrange[1] = max(axrange[1], N.nanmax(posns)+0.5) def getAxisLabels(self, direction): """Get labels for axis if using a label axis.""" s = self.settings doc = self.document text = s.get('labels').getData(doc, checknull=True) values = s.get('values').getData(doc) if text is None or values is None: return (None, None) positions = self.getPosns() return (text, positions) def plotBox(self, painter, axes, boxposn, posn, width, clip, stats): """Draw box for dataset.""" if not N.isfinite(stats.median): # skip bad datapoints return s = self.settings horz = (s.direction == 'horizontal') # convert quartiles, top and bottom whiskers to plotter medplt, botplt, topplt, botwhisplt, topwhisplt = tuple( axes[not horz].dataToPlotterCoords( posn, N.array([ stats.median, stats.botquart, stats.topquart, stats.botwhisker, stats.topwhisker ])) ) # draw whisker top to bottom p = s.Whisker.makeQPenWHide(painter) p.setCapStyle(qt4.Qt.FlatCap) painter.setPen(p) swapline(painter, boxposn, topwhisplt, boxposn, botwhisplt, horz) # draw ends of whiskers endsize = width/2 swapline(painter, boxposn-endsize/2, topwhisplt, boxposn+endsize/2, topwhisplt, horz) swapline(painter, boxposn-endsize/2, botwhisplt, boxposn+endsize/2, botwhisplt, horz) # draw box fill boxpath = qt4.QPainterPath() boxpath.addRect( swapbox(painter, boxposn-width/2, botplt, boxposn+width/2, topplt, horz) ) utils.brushExtFillPath(painter, s.Fill, boxpath) # draw line across box p = s.Whisker.makeQPenWHide(painter) p.setCapStyle(qt4.Qt.FlatCap) painter.setPen(p) swapline(painter, boxposn-width/2, medplt, boxposn+width/2, medplt, horz) # draw box painter.strokePath(boxpath, s.Border.makeQPenWHide(painter) ) # draw outliers painter.setPen( s.MarkersLine.makeQPenWHide(painter) ) painter.setBrush( s.MarkersFill.makeQBrushWHide() ) markersize = s.get('markerSize').convert(painter) if stats.outliers.shape[0] != 0: pltvals = axes[not horz].dataToPlotterCoords(posn, stats.outliers) otherpos = N.zeros(pltvals.shape) + boxposn if horz: x, y = pltvals, otherpos else: x, y = otherpos, pltvals utils.plotMarkers( painter, x, y, s.outliersmarker, markersize, clip=clip ) # draw mean meanplt = axes[not horz].dataToPlotterCoords( posn, N.array([stats.mean]))[0] if horz: x, y = meanplt, boxposn else: x, y = boxposn, meanplt utils.plotMarker( painter, x, y, s.meanmarker, markersize ) def dataDraw(self, painter, axes, widgetposn, clip): """Plot the data on a plotter.""" s = self.settings # get data doc = self.document positions = self.getPosns() if s.calculate: # calculate from data values = s.get('values').getData(doc) if values is None: return else: # use manual datasets datasets = [ s.get(x).getData(doc) for x in ('whiskermin', 'whiskermax', 'boxmin', 'boxmax', 'mean', 'median') ] if None in datasets: return # get axes widgets axes = self.parent.getAxes( (s.xAxis, s.yAxis) ) # return if there are no proper axes if ( None in axes or axes[0].settings.direction != 'horizontal' or axes[1].settings.direction != 'vertical' ): return # get boxes visible along direction of boxes to work out width horz = (s.direction == 'horizontal') plotposns = axes[horz].dataToPlotterCoords(widgetposn, positions) if horz: inplot = (plotposns > widgetposn[1]) & (plotposns < widgetposn[3]) else: inplot = (plotposns > widgetposn[0]) & (plotposns < widgetposn[2]) inplotposn = plotposns[inplot] if inplotposn.shape[0] < 2: if horz: width = (widgetposn[3]-widgetposn[1])*0.5 else: width = (widgetposn[2]-widgetposn[0])*0.5 else: # use minimum different between points to get width inplotposn.sort() width = N.nanmin(inplotposn[1:] - inplotposn[:-1]) # adjust width width = width * s.fillfraction if s.calculate: # calculated boxes for vals, plotpos in czip(values, plotposns): stats = _Stats() stats.calculate(vals.data, s.whiskermode) self.plotBox(painter, axes, plotpos, widgetposn, width, clip, stats) else: # manually given boxes vals = [d.data for d in datasets] + [plotposns] lens = [len(d) for d in vals] for i in crange(min(lens)): stats = _Stats() stats.topwhisker = vals[0][i] stats.botwhisker = vals[1][i] stats.botquart = vals[2][i] stats.topquart = vals[3][i] stats.mean = vals[4][i] stats.median = vals[5][i] stats.outliers = N.array([]) self.plotBox(painter, axes, vals[6][i], widgetposn, width, clip, stats) # allow the factory to instantiate a boxplot document.thefactory.register( BoxPlot ) veusz-1.21.1/veusz/widgets/textlabel.py0000664000175000017500000002062512245431266016324 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """For plotting one or more text labels on a graph.""" from __future__ import division import itertools from ..compat import czip from .. import document from .. import setting from .. import utils from .. import qtall as qt4 from . import plotters from . import controlgraph def _(text, disambiguation=None, context='TextLabel'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class BorderLine(setting.Line): '''Plot line around text.''' def __init__(self, name, **args): setting.Line.__init__(self, name, **args) self.get('hide').newDefault(True) class TextLabel(plotters.FreePlotter): """Add a text label to a graph.""" typename = 'label' description = _('Text label') allowusercreation = True def __init__(self, parent, name=None): plotters.FreePlotter.__init__(self, parent, name=name) if type(self) == TextLabel: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" plotters.FreePlotter.addSettings(s) s.add( setting.DatasetOrStr('label', '', descr=_('Text to show or text dataset'), usertext=_('Label')), 0 ) s.add( setting.AlignHorz('alignHorz', 'left', descr=_('Horizontal alignment of label'), usertext=_('Horz alignment'), formatting=True), 7) s.add( setting.AlignVert('alignVert', 'bottom', descr=_('Vertical alignment of label'), usertext=_('Vert alignment'), formatting=True), 8) s.add( setting.Float('angle', 0., descr=_('Angle of the label in degrees'), usertext=_('Angle'), formatting=True), 9 ) s.add( setting.DistancePt( 'margin', '4pt', descr = _('Margin of fill/border'), usertext=_('Margin'), formatting=True), 10 ) s.add( setting.Bool('clip', False, descr=_('Clip text to its container'), usertext=_('Clip'), formatting=True), 11 ) s.add( setting.Text('Text', descr = _('Text settings'), usertext=_('Text')), pixmap = 'settings_axislabel' ) s.add( setting.ShapeFill( 'Background', descr=_('Fill behind text'), usertext=_('Background')), pixmap = 'settings_bgfill' ) s.add( BorderLine( 'Border', descr=_('Border around text'), usertext=_('Border')), pixmap = 'settings_border' ) # convert text to alignments used by Renderer cnvtalignhorz = { 'left': -1, 'centre': 0, 'right': 1 } cnvtalignvert = { 'top': 1, 'centre': 0, 'bottom': -1 } @property def userdescription(self): """User friendly description.""" s = self.settings return _("text='%s'") % s.label def draw(self, posn, phelper, outerbounds = None): """Draw the text label.""" s = self.settings d = self.document # exit if hidden if s.hide or s.Text.hide: return text = s.get('label').getData(d) xp, yp = self._getPlotterCoords(posn) if xp is None or yp is None: # we can't calculate coordinates return clip = None if s.clip: clip = qt4.QRectF( qt4.QPointF(posn[0], posn[1]), qt4.QPointF(posn[2], posn[3]) ) borderorfill = not s.Border.hide or not s.Background.hide painter = phelper.painter(self, posn, clip=clip) with painter: textpen = s.get('Text').makeQPen() painter.setPen(textpen) font = s.get('Text').makeQFont(painter) margin = s.get('margin').convert(painter) # we should only be able to move non-dataset labels isnotdataset = ( not s.get('xPos').isDataset(d) and not s.get('yPos').isDataset(d) ) controlgraphitems = [] for index, (x, y, t) in enumerate(czip( xp, yp, itertools.cycle(text))): # render the text dx = dy = 0 if borderorfill: dx = -TextLabel.cnvtalignhorz[s.alignHorz]*margin dy = TextLabel.cnvtalignvert[s.alignVert]*margin r = utils.Renderer( painter, font, x+dx, y+dy, t, TextLabel.cnvtalignhorz[s.alignHorz], TextLabel.cnvtalignvert[s.alignVert], s.angle ) tbounds = r.getBounds() if borderorfill: tbounds = [ tbounds[0]-margin, tbounds[1]-margin, tbounds[2]+margin, tbounds[3]+margin ] rect = qt4.QRectF( qt4.QPointF(tbounds[0], tbounds[1]), qt4.QPointF(tbounds[2], tbounds[3])) path = qt4.QPainterPath() path.addRect(rect) pen = s.get('Border').makeQPenWHide(painter) utils.brushExtFillPath(painter, s.Background, path, stroke=pen) r.render() # add cgi for adjustable positions if isnotdataset: cgi = controlgraph.ControlMovableBox(self, tbounds, phelper, crosspos = (x, y)) cgi.labelpt = (x, y) cgi.widgetposn = posn cgi.index = index controlgraphitems.append(cgi) phelper.setControlGraph(self, controlgraphitems) def updateControlItem(self, cgi): """Update position of point given new name and vals.""" s = self.settings pointsX = list(s.xPos) # make a copy here so original is not modifed pointsY = list(s.yPos) ind = cgi.index # calculate new position coordinate for item xpos, ypos = self._getGraphCoords(cgi.widgetposn, cgi.deltacrosspos[0]+cgi.posn[0], cgi.deltacrosspos[1]+cgi.posn[1]) # this is a small distance away to get delta xposd, yposd = self._getGraphCoords(cgi.widgetposn, cgi.deltacrosspos[0]+cgi.posn[0]+1, cgi.deltacrosspos[1]+cgi.posn[1]+1) if xpos is None or ypos is None: return roundx = utils.round2delt(xpos, xposd) roundy = utils.round2delt(ypos, yposd) pointsX[ind], pointsY[ind] = roundx, roundy operations = ( document.OperationSettingSet(s.get('xPos'), pointsX), document.OperationSettingSet(s.get('yPos'), pointsY) ) self.document.applyOperation( document.OperationMultiple(operations, descr=_('move label')) ) # allow the factory to instantiate a text label document.thefactory.register( TextLabel ) veusz-1.21.1/veusz/widgets/pickable.py0000664000175000017500000002134312237406466016115 0ustar jssjss# pickable.py # stuff related to the Picker (aka Read Data) tool # Copyright (C) 2011 Benjamin K. Stuhl # Email: Benjamin K. Stuhl # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division import numpy as N from ..compat import CBool from .. import document class PickInfo(CBool): """Encapsulates the results of a Pick operation. screenpos and coords are numeric (x,y) tuples, labels are the textual labels for the x and y datasets, and index is some object that the picker can use to figure out what the 'next' and 'previous' points are. index must implement __str__(); return '' if it has no user-visible meaning.""" def __init__(self, widget=None, screenpos=None, labels=None, coords=None, index=None): self.widget = widget self.screenpos = screenpos self.labels = labels self.coords = coords self.index = index self.distance = float('inf') self.displaytype = ('numeric', 'numeric') def cbool(self): return bool(self.widget and self.screenpos and self.labels and self.coords) class Index: """A class containing all the state a GenericPickable needs to find the next or previous point""" def __init__(self, ivar, index, sign): self.ivar = ivar self.index = index self.sign = sign # default to not trusting the actual index to be meaningful self.useindex = False def __str__(self): if not self.useindex: return '' else: # 1-based index return str(self.index+1) def _chooseOrderingSign(m, c, p): """Figures out whether p or m is visually right of c""" assert c is not None if p is not None and m is not None: if p[0] > m[0] or (p[0] == m[0] and p[1] < m[1]): # p is visually to the right of or above m return 1 else: return -1 elif p is not None: if p[0] > c[0]: # p is visually right of c return 1 else: return -1 elif m is not None: if m[0] < c[0]: # m is visually left of c return 1 else: return -1 else: assert m is not None or p is not None class GenericPickable: """Utility class which abstracts the math of picking the closest point out of a list of points""" def __init__(self, widget, labels, vals, screenvals): self.widget = widget self.labels = labels self.xvals, self.yvals = vals self.xscreen, self.yscreen = screenvals def _pickSign(self, i): if len(self.xscreen) <= 1: # we only have one element, so it doesn't matter anyways return 1 if i == 0: m = None else: m = self.xscreen[i-1], self.yscreen[i-1] c = self.xscreen[i], self.yscreen[i] if i+1 == len(self.xscreen): p = None else: p = self.xscreen[i+1], self.yscreen[i+1] return _chooseOrderingSign(m, c, p) def pickPoint(self, x0, y0, bounds, distance_direction): info = PickInfo(self.widget, labels=self.labels) if self.widget.settings.hide: return info if None in (self.xvals, self.yvals): return info if len(self.xscreen) == 0 or len(self.yscreen) == 0: return info # calculate distances if distance_direction == 'vertical': # measure distance along y dist = N.abs(self.yscreen - y0) elif distance_direction == 'horizontal': # measure distance along x dist = N.abs(self.xscreen - x0) elif distance_direction == 'radial': # measure radial distance dist = N.sqrt((self.xscreen - x0)**2 + (self.yscreen - y0)**2) else: # programming error assert (distance_direction == 'radial' or distance_direction == 'vertical' or distance_direction == 'horizontal') # ignore points which are offscreen outofbounds = ( (self.xscreen < bounds[0]) | (self.xscreen > bounds[2]) | (self.yscreen < bounds[1]) | (self.yscreen > bounds[3]) ) dist[outofbounds] = float('inf') m = N.min(dist) # if there are multiple equidistant points, arbitrarily take # the first one i = N.nonzero(dist == m)[0][0] info.screenpos = self.xscreen[i], self.yscreen[i] info.coords = self.xvals[i], self.yvals[i] info.distance = m info.index = Index(self.xvals[i], i, self._pickSign(i)) return info def pickIndex(self, oldindex, direction, bounds): info = PickInfo(self.widget, labels=self.labels) if self.widget.settings.hide: return info if None in (self.xvals, self.yvals): return info if oldindex.index is None: # no explicit index, so find the closest location to the previous # independent variable value i = N.logical_not( N.logical_or( self.xvals < oldindex.ivar, self.xvals > oldindex.ivar) ) # and pick the next if oldindex.sign == 1: i = max(N.nonzero(i)[0]) else: i = min(N.nonzero(i)[0]) else: i = oldindex.index if direction == 'right': incr = oldindex.sign elif direction == 'left': incr = -oldindex.sign else: assert direction == 'right' or direction == 'left' i += incr # skip points that are outside of the bounds while ( i >= 0 and i < len(self.xscreen) and (self.xscreen[i] < bounds[0] or self.xscreen[i] > bounds[2] or self.yscreen[i] < bounds[1] or self.yscreen[i] > bounds[3]) ): i += incr if i < 0 or i >= len(self.xscreen): return info info.screenpos = self.xscreen[i], self.yscreen[i] info.coords = self.xvals[i], self.yvals[i] info.index = Index(self.xvals[i], i, oldindex.sign) return info class DiscretePickable(GenericPickable): """A specialization of GenericPickable that knows how to deal with widgets with axes and data sets""" def __init__(self, widget, xdata_propname, ydata_propname, mapdata_fn): s = widget.settings doc = widget.document self.xdata = xdata = s.get(xdata_propname).getData(doc) self.ydata = ydata = s.get(ydata_propname).getData(doc) labels = s.__getattr__(xdata_propname), s.__getattr__(ydata_propname) if not xdata or not ydata or not mapdata_fn: GenericPickable.__init__( self, widget, labels, (None, None), (None, None) ) return # map all the valid data x, y = N.array([]), N.array([]) xs, ys = N.array([]), N.array([]) for xvals, yvals in document.generateValidDatasetParts(xdata, ydata): chunklen = min(len(xvals.data), len(yvals.data)) x = N.append(x, xvals.data[:chunklen]) y = N.append(y, yvals.data[:chunklen]) xs, ys = mapdata_fn(x, y) # and set us up with the mapped data GenericPickable.__init__( self, widget, labels, (x, y), (xs, ys) ) def pickPoint(self, x0, y0, bounds, distance_direction): info = GenericPickable.pickPoint(self, x0, y0, bounds, distance_direction) info.displaytype = (self.xdata.displaytype, self.ydata.displaytype) if not info: return info # indicies are persistent info.index.useindex = True return info def pickIndex(self, oldindex, direction, bounds): info = GenericPickable.pickIndex(self, oldindex, direction, bounds) info.displaytype = (self.xdata.displaytype, self.ydata.displaytype) if not info: return info # indicies are persistent info.index.useindex = True return info veusz-1.21.1/veusz/widgets/vectorfield.py0000644000175000017500000002231112327177747016652 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division import numpy as N from ..compat import czip from .. import setting from .. import document from .. import utils from .. import qtall as qt4 from . import plotters def _(text, disambiguation=None, context='VectorField'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class VectorField(plotters.GenericPlotter): '''A plotter for plotting a vector field.''' typename = 'vectorfield' allowusercreation = True description = _('Plot a vector field') def __init__(self, parent, name=None): """Initialse vector field plotter.""" plotters.GenericPlotter.__init__(self, parent, name=name) if type(self) == VectorField: self.readDefaults() @classmethod def addSettings(klass, s): '''Construct list of settings.''' plotters.GenericPlotter.addSettings(s) # datasets s.add( setting.DatasetExtended( 'data1', '', dimensions = 2, descr = _('X coordinate length or vector magnitude'), usertext = _('dx or r')), 0 ) s.add( setting.DatasetExtended( 'data2', '', dimensions = 2, descr = _('Y coordinate length or vector angle'), usertext = _('dy or theta')), 1 ) s.add( setting.Choice('mode', ['cartesian', 'polar'], 'cartesian', descr = _('Cartesian (dx,dy) or polar (r,theta)'), usertext = _('Mode')), 2 ) s.add( setting.FloatChoice( 'rotate', [0., 45., 90., 135., 180., -135., -90., -45.], 0., descr = _('Rotate vector clockwise by this angle in degrees'), usertext = _('Rotate')), 3 ) s.add( setting.Bool( 'reflectx', False, descr = _('Reflect vector in X direction'), usertext = _('Reflect X')), 4 ) s.add( setting.Bool( 'reflecty', False, descr = _('Reflect vector in Y direction'), usertext = _('Reflect Y')), 5 ) # formatting s.add( setting.DistancePt('baselength', '10pt', descr = _('Base length of unit vector'), usertext = _('Base length'), formatting=True), 0 ) s.add( setting.DistancePt('arrowsize', '2pt', descr = _('Size of any arrows'), usertext = _('Arrow size'), formatting=True), 1 ) s.add( setting.Bool('scalearrow', True, descr = _('Scale arrow head by length'), usertext = _('Scale arrow'), formatting=True), 2 ) s.add( setting.Arrow('arrowfront', 'none', descr = _('Arrow in front direction'), usertext=_('Arrow front'), formatting=True), 3) s.add( setting.Arrow('arrowback', 'none', descr = _('Arrow in back direction'), usertext=_('Arrow back'), formatting=True), 4) s.add( setting.Line('Line', descr = _('Line style'), usertext = _('Line')), pixmap = 'settings_plotline' ) s.add( setting.ArrowFill('Fill', descr = _('Arrow fill settings'), usertext = _('Arrow fill')), pixmap = 'settings_plotmarkerfill' ) def affectsAxisRange(self): """Range information provided by widget.""" s = self.settings return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def getRange(self, axis, depname, axrange): """Automatically determine the ranges of variable on the axes.""" for name in ('data1', 'data2'): data = self.settings.get(name).getData(self.document) if data is None: continue if data.dimensions == 2: xr, yr = data.getDataRanges() if depname == 'sx': dxrange = xr axrange[0] = min( axrange[0], dxrange[0] ) axrange[1] = max( axrange[1], dxrange[1] ) elif depname == 'sy': dyrange = yr axrange[0] = min( axrange[0], dyrange[0] ) axrange[1] = max( axrange[1], dyrange[1] ) def drawKeySymbol(self, number, painter, x, y, width, height): """Draw the plot symbol and/or line.""" painter.save() s = self.settings painter.setPen( s.Line.makeQPenWHide(painter) ) painter.setBrush( s.get('Fill').makeQBrushWHide() ) utils.plotLineArrow(painter, x+width, y+height*0.5, width, 180, height*0.25, arrowleft=s.arrowfront, arrowright=s.arrowback) painter.restore() def dataDraw(self, painter, axes, posn, cliprect): """Draw the widget.""" s = self.settings d = self.document # ignore non existing datasets data1 = s.get('data1').getData(d) data2 = s.get('data2').getData(d) if data1 is None or data2 is None: return # require 2d datasets if data1.dimensions != 2 or data2.dimensions != 2: return # get base length (ensure > 0) baselength = max(s.get('baselength').convert(painter), 1e-6) # try to be nice if the datasets don't match data1st, data2nd = data1.data, data2.data xw = min(data1st.shape[1], data2nd.shape[1]) yw = min(data1st.shape[0], data2nd.shape[0]) # get pixel coordinates xc, yc = data1.getPixelCentres() xc, yc = xc[:xw], yc[:yw] xdsvals = N.reshape(N.tile(xc, yw), xw*yw) ydsvals = N.reshape(N.tile(yc[:, N.newaxis], xw), xw*yw) # convert using axes to plotter values xplotter = axes[0].dataToPlotterCoords(posn, xdsvals) yplotter = axes[1].dataToPlotterCoords(posn, ydsvals) pen = s.Line.makeQPenWHide(painter) painter.setPen(pen) if s.mode == 'cartesian': dx = (data1st[:yw, :xw] * baselength).ravel() dy = (data2nd[:yw, :xw] * baselength).ravel() elif s.mode == 'polar': r = data1st[:yw, :xw].ravel() * baselength theta = data2nd[:yw, :xw].ravel() dx = r * N.cos(theta) dy = r * N.sin(theta) if s.rotate != 0.: angle = -s.rotate / 180 * N.pi rotx = dx*N.cos(angle) - dy*N.sin(angle) roty = dx*N.sin(angle) + dy*N.cos(angle) dx, dy = rotx, roty if s.reflectx: dx = -dx if s.reflecty: dy = -dy x1, x2 = xplotter-dx, xplotter+dx y1, y2 = yplotter+dy, yplotter-dy if s.arrowfront == 'none' and s.arrowback == 'none': utils.plotLinesToPainter(painter, x1, y1, x2, y2, cliprect) else: arrowsize = s.get('arrowsize').convert(painter) painter.setBrush( s.get('Fill').makeQBrushWHide() ) # this is backward - have to convert from dx, dy to angle, length angles = 180 - N.arctan2(dy, dx) * (180./N.pi) lengths = N.sqrt(dx**2+dy**2) * 2 # scale arrow heads by arrow length if requested if s.scalearrow: arrowsizes = (arrowsize/baselength/2) * lengths else: arrowsizes = N.zeros(lengths.shape) + arrowsize for x, y, l, a, asize in czip(x2, y2, lengths, angles, arrowsizes): if l != 0.: utils.plotLineArrow(painter, x, y, l, a, asize, arrowleft=s.arrowfront, arrowright=s.arrowback) # allow the factory to instantiate a vector field document.thefactory.register( VectorField ) veusz-1.21.1/veusz/widgets/polar.py0000644000175000017500000003276412273225057015462 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Polar plot widget.""" from __future__ import division import numpy as N from .nonorthgraph import NonOrthGraph from .axisticks import AxisTicks from . import axis from ..compat import crange from .. import qtall as qt4 from .. import document from .. import setting from .. import utils def _(text, disambiguation=None, context='Polar'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Tick(setting.Line): '''Polar tick settings.''' def __init__(self, name, **args): setting.Line.__init__(self, name, **args) self.add( setting.DistancePt( 'length', '6pt', descr = _('Length of major ticks'), usertext=_('Length') ) ) self.add( setting.Int( 'number', 6, descr = _('Number of major ticks to aim for'), usertext=_('Number')) ) self.add( setting.Bool('hidespokes', False, descr = _('Hide radial spokes'), usertext = _('Hide spokes')) ) self.add( setting.Bool('hideannuli', False, descr = _('Hide annuli'), usertext = _('Hide annuli') ) ) self.get('color').newDefault('grey') def getLength(self, painter): '''Return tick length in painter coordinates''' return self.get('length').convert(painter) class TickLabel(axis.TickLabel): """For tick label.""" def __init__(self, *args, **argsv): axis.TickLabel.__init__(self, *args, **argsv) self.remove('offset') self.remove('rotate') self.remove('hide') self.add( setting.Bool('hideradial', False, descr = _('Hide radial labels'), usertext=_('Hide radial') ) ) self.add( setting.Bool('hidetangential', False, descr = _('Hide tangential labels'), usertext=_('Hide tangent') ) ) class Polar(NonOrthGraph): '''Polar plotter.''' typename='polar' allowusercreation = True description = _('Polar graph') def __init__(self, parent, name=None): '''Initialise polar plot.''' NonOrthGraph.__init__(self, parent, name=name) if type(self) == NonOrthGraph: self.readDefaults() @classmethod def addSettings(klass, s): '''Construct list of settings.''' NonOrthGraph.addSettings(s) s.add( setting.FloatOrAuto('minradius', 'Auto', descr=_('Minimum value of radius'), usertext=_('Min radius')) ) s.add( setting.FloatOrAuto('maxradius', 'Auto', descr=_('Maximum value of radius'), usertext=_('Max radius')) ) s.add( setting.Choice('units', ('degrees', 'radians'), 'degrees', descr = _('Angular units'), usertext=_('Units')) ) s.add( setting.Choice('direction', ('clockwise', 'anticlockwise'), 'anticlockwise', descr = _('Angle direction'), usertext = _('Direction')) ) s.add( setting.Choice('position0', ('right', 'top', 'left', 'bottom'), 'right', descr = _('Direction of 0 angle'), usertext = _(u'Position of 0°')) ) s.add( setting.Bool('log', False, descr = _('Logarithmic radial axis'), usertext = _('Log')) ) s.add( TickLabel('TickLabels', descr = _('Tick labels'), usertext=_('Tick labels')), pixmap='settings_axisticklabels' ) s.add( Tick('Tick', descr = _('Tick line'), usertext=_('Tick')), pixmap='settings_axismajorticks' ) s.get('leftMargin').newDefault('1cm') s.get('rightMargin').newDefault('1cm') s.get('topMargin').newDefault('1cm') s.get('bottomMargin').newDefault('1cm') @property def userdescription(self): s = self.settings return _("'units=%s, direction=%s, log=%s") % ( s.units, s.direction, str(s.log)) def coordRanges(self): '''Get ranges of coordinates.''' angularrange = [[0., 2.*N.pi], [0., 360]][ self.settings.units == 'degrees' ] return [ [self._minradius, self._maxradius], angularrange ] def toPlotAngle(self, angles): """Convert one or more angles to angle on plot.""" s = self.settings # unit conversion if s.units == 'degrees': angles = angles * (N.pi/180.) # change direction if self.settings.direction == 'anticlockwise': angles = -angles # add offset angles -= {'right': 0, 'top': 0.5*N.pi, 'left': N.pi, 'bottom': 1.5*N.pi}[self.settings.position0] return angles def toPlotRadius(self, radii): """Convert radii to a plot radii.""" if self.settings.log: logmin = N.log(self._minradius) logmax = N.log(self._maxradius) r = ( N.log(N.clip(radii, 1e-99, 1e99)) - logmin ) / ( logmax - logmin) else: r = (radii - self._minradius) / ( self._maxradius - self._minradius) return N.where(r > 0., r, 0.) def graphToPlotCoords(self, coorda, coordb): '''Convert coordinates in r, theta to x, y.''' ca = self.toPlotRadius(coorda) cb = self.toPlotAngle(coordb) x = self._xc + ca * N.cos(cb) * self._xscale y = self._yc + ca * N.sin(cb) * self._yscale return x, y def drawFillPts(self, painter, extfill, cliprect, ptsx, ptsy): '''Draw points for plotting a fill.''' pts = qt4.QPolygonF() utils.addNumpyToPolygonF(pts, ptsx, ptsy) filltype = extfill.filltype if filltype == 'center': pts.append( qt4.QPointF(self._xc, self._yc) ) utils.brushExtFillPolygon(painter, extfill, cliprect, pts) elif filltype == 'outside': pp = qt4.QPainterPath() pp.moveTo(self._xc, self._yc) pp.arcTo(cliprect, 0, 360) pp.addPolygon(pts) utils.brushExtFillPath(painter, extfill, pp) elif filltype == 'polygon': utils.brushExtFillPolygon(painter, extfill, cliprect, pts) def drawGraph(self, painter, bounds, datarange, outerbounds=None): '''Plot graph area and axes.''' s = self.settings if datarange is None: datarange = [0., 1., 0., 1.] if s.maxradius == 'Auto': self._maxradius = datarange[1] else: self._maxradius = s.maxradius if s.minradius == 'Auto': if s.log: if datarange[0] > 0.: self._minradius = datarange[0] else: self._minradius = self._maxradius / 100. else: if datarange[0] >= 0: self._minradius = 0. else: self._minradius = datarange[0] else: self._minradius = s.minradius # stop negative values if s.log: self._minradius = N.clip(self._minradius, 1e-99, 1e99) self._maxradius = N.clip(self._maxradius, 1e-99, 1e99) if self._minradius == self._maxradius: self._maxradius = self._minradius + 1 self._xscale = (bounds[2]-bounds[0])*0.5 self._yscale = (bounds[3]-bounds[1])*0.5 self._xc = 0.5*(bounds[0]+bounds[2]) self._yc = 0.5*(bounds[3]+bounds[1]) path = qt4.QPainterPath() path.addEllipse( qt4.QRectF( qt4.QPointF(bounds[0], bounds[1]), qt4.QPointF(bounds[2], bounds[3]) ) ) utils.brushExtFillPath(painter, s.Background, path, stroke=s.Border.makeQPenWHide(painter)) def setClip(self, painter, bounds): '''Set clipping for graph.''' p = qt4.QPainterPath() p.addEllipse( qt4.QRectF( qt4.QPointF(bounds[0], bounds[1]), qt4.QPointF(bounds[2], bounds[3]) ) ) painter.setClipPath(p) def drawAxes(self, painter, bounds, datarange, outerbounds=None): '''Plot axes.''' s = self.settings t = s.Tick # handle reversed axes using min and max below r = [self._minradius, self._maxradius] atick = AxisTicks(min(r), max(r), t.number, t.number*4, extendmin=False, extendmax=False, logaxis=s.log) atick.getTicks() majtick = atick.tickvals # drop 0 at origin if self._minradius == 0. and not s.log: majtick = majtick[1:] # draw ticks as circles if not t.hideannuli: painter.setPen( s.Tick.makeQPenWHide(painter) ) painter.setBrush( qt4.QBrush() ) for tick in majtick: radius = self.toPlotRadius(tick) if radius > 0: rect = qt4.QRectF( qt4.QPointF( self._xc - radius*self._xscale, self._yc - radius*self._yscale ), qt4.QPointF( self._xc + radius*self._xscale, self._yc + radius*self._yscale ) ) painter.drawEllipse(rect) # setup axes plot tl = s.TickLabels scale, format = tl.scale, tl.format if format == 'Auto': format = atick.autoformat painter.setPen( tl.makeQPen() ) font = tl.makeQFont(painter) # draw radial axis if not s.TickLabels.hideradial: for tick in majtick: num = utils.formatNumber(tick*scale, format, locale=self.document.locale) x = self.toPlotRadius(tick) * self._xscale + self._xc r = utils.Renderer(painter, font, x, self._yc, num, alignhorz=-1, alignvert=-1, usefullheight=True) r.render() if s.units == 'degrees': angles = [ u'0°', u'30°', u'60°', u'90°', u'120°', u'150°', u'180°', u'210°', u'240°', u'270°', u'300°', u'330°' ] else: angles = [ '0', u'π/6', u'π/3', u'π/2', u'2π/3', u'5π/6', u'π', u'7π/6', u'4π/3', u'3π/2', u'5π/3', u'11π/6' ] align = [ (-1, 1), (-1, 1), (-1, 1), (0, 1), (1, 1), (1, 1), (1, 0), (1, -1), (1, -1), (0, -1), (-1, -1), (-1, -1) ] if s.direction == 'anticlockwise': angles = angles[0:1] + angles[1:][::-1] # rotate labels if zero not at right if s.position0 == 'top': angles = angles[3:] + angles[:4] elif s.position0 == 'left': angles = angles[6:] + angles[:7] elif s.position0 == 'bottom': angles = angles[9:] + angles[:10] # draw labels around plot if not s.TickLabels.hidetangential: for i in crange(12): angle = 2 * N.pi / 12 x = self._xc + N.cos(angle*i) * self._xscale y = self._yc + N.sin(angle*i) * self._yscale r = utils.Renderer(painter, font, x, y, angles[i], alignhorz=align[i][0], alignvert=align[i][1], usefullheight=True) r.render() # draw spokes if not t.hidespokes: painter.setPen( s.Tick.makeQPenWHide(painter) ) painter.setBrush( qt4.QBrush() ) angle = 2 * N.pi / 12 lines = [] for i in crange(12): x = self._xc + N.cos(angle*i) * self._xscale y = self._yc + N.sin(angle*i) * self._yscale lines.append( qt4.QLineF(qt4.QPointF(self._xc, self._yc), qt4.QPointF(x, y)) ) painter.drawLines(lines) document.thefactory.register(Polar) veusz-1.21.1/veusz/widgets/function.py0000644000175000017500000003427712327177747016207 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """For plotting numerical functions.""" from __future__ import division import numpy as N from ..compat import czip, cstr from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from . import pickable from .plotters import GenericPlotter def _(text, disambiguation=None, context='Function'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class FunctionPlotter(GenericPlotter): """Function plotting class.""" typename='function' allowusercreation=True description=_('Plot a function') def __init__(self, parent, name=None): """Initialise plotter.""" GenericPlotter.__init__(self, parent, name=name) if type(self) == FunctionPlotter: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" GenericPlotter.addSettings(s) s.add( setting.Int('steps', 50, minval = 3, descr = _('Number of steps to evaluate the function' ' over'), usertext=_('Steps'), formatting=True), 0 ) s.add( setting.Choice('variable', ['x', 'y'], 'x', descr=_('Variable the function is a function of'), usertext=_('Variable')), 0 ) s.add( setting.Str('function', 'x', descr=_('Function expression'), usertext=_('Function')), 0 ) s.add(setting.FloatOrAuto('min', 'Auto', descr=_('Minimum value at which to plot function'), usertext=_('Min'))) s.add(setting.FloatOrAuto('max', 'Auto', descr=_('Maximum value at which to plot function'), usertext=_('Max'))) s.add( setting.Line('Line', descr = _('Function line settings'), usertext = _('Plot line')), pixmap = 'settings_plotline' ) s.add( setting.PlotterFill('FillBelow', descr = _('Fill below/left function'), usertext = _('Fill below')), pixmap = 'settings_plotfillbelow' ) s.add( setting.PlotterFill('FillAbove', descr = _('Fill mode above/right function'), usertext = _('Fill above')), pixmap = 'settings_plotfillabove' ) @property def userdescription(self): """User-friendly description.""" return "%(variable)s = %(function)s" % self.settings def logEvalError(self, ex): """Write error message to document log for exception ex.""" self.document.log( "Error evaluating expression in function widget '%s': '%s'" % ( self.name, cstr(ex))) def affectsAxisRange(self): s = self.settings if s.variable == 'x': return ((s.yAxis, 'both'),) else: return ((s.xAxis, 'both'),) def requiresAxisRange(self): s = self.settings if s.variable == 'x': return (('both', s.xAxis),) else: return (('both', s.yAxis),) def getRange(self, axis, depname, axrange): """Adjust the range of the axis depending on the values plotted.""" s = self.settings # ignore empty function if s.function.strip() == '': return # ignore if function isn't sensible compiled = self.document.compileCheckedExpression(s.function) if compiled is None: return # find axis to find variable range over axis = self.lookupAxis( {'x': s.xAxis, 'y': s.yAxis}[s.variable] ) if not axis: return # get range of that axis varaxrange = list(axis.getPlottedRange()) if varaxrange[0] == varaxrange[1]: return # trim to range if s.min != 'Auto': varaxrange[0] = max(s.min, varaxrange[0]) if s.max != 'Auto': varaxrange[1] = min(s.max, varaxrange[1]) # work out function in steps try: if axis.settings.log: # log spaced steps l1, l2 = N.log(varaxrange[1]), N.log(varaxrange[0]) delta = (l2-l1)/20. points = N.exp(N.arange(l1, l2+delta, delta)) else: # linear spaced steps delta = (varaxrange[1] - varaxrange[0])/20. points = N.arange(varaxrange[0], varaxrange[1]+delta, delta) except ZeroDivisionError: # delta is zero return env = self.initEnviron() env[s.variable] = points try: vals = eval(compiled, env) + points*0. except: # something wrong in the evaluation return # get values which are finite: excluding nan and inf finitevals = vals[N.isfinite(vals)] # update the automatic range if len(finitevals) > 0: axrange[0] = min(N.min(finitevals), axrange[0]) axrange[1] = max(N.max(finitevals), axrange[1]) def _plotLine(self, painter, xpts, ypts, bounds, clip): """ Plot the points in xpts, ypts.""" x1, y1, x2, y2 = bounds maxdeltax = (x2-x1)*3/4 maxdeltay = (y2-y1)*3/4 # idea is to collect points until we go out of the bounds # or reach the end, then plot them pts = qt4.QPolygonF() lastx = lasty = -65536 for x, y in czip(xpts, ypts): # ignore point if it outside sensible bounds if x < -32767 or y < -32767 or x > 32767 or y > 32767: if len(pts) >= 2: utils.plotClippedPolyline(painter, clip, pts) pts.clear() else: # if the jump wasn't too large, add the point to the points if abs(x-lastx) < maxdeltax and abs(y-lasty) < maxdeltay: pts.append( qt4.QPointF(x, y) ) else: # draw what we have until now, and start a new line if len(pts) >= 2: utils.plotClippedPolyline(painter, clip, pts) pts.clear() pts.append( qt4.QPointF(x, y) ) lastx = x lasty = y # draw remaining points if len(pts) >= 2: utils.plotClippedPolyline(painter, clip, pts) def _fillRegion(self, painter, pxpts, pypts, bounds, belowleft, clip, brush): """Fill the region above/below or left/right of the points. belowleft fills below if the variable is 'x', or left if 'y' otherwise it fills above/right.""" # find starting and ending points for the filled region x1, y1, x2, y2 = bounds # trimming can lead to too few points if len(pxpts) < 2 or len(pypts) < 2: return pts = qt4.QPolygonF() if self.settings.variable == 'x': if belowleft: pts.append(qt4.QPointF(pxpts[0], y2)) endpt = qt4.QPointF(pxpts[-1], y2) else: pts.append(qt4.QPointF(pxpts[0], y1)) endpt = qt4.QPointF(pxpts[-1], y1) else: if belowleft: pts.append(qt4.QPointF(x1, pypts[0])) endpt = qt4.QPointF(x1, pypts[-1]) else: pts.append(qt4.QPointF(x2, pypts[0])) endpt = qt4.QPointF(x2, pypts[-1]) # add the points between utils.addNumpyToPolygonF(pts, pxpts, pypts) # stick on the ending point pts.append(endpt) # draw the clipped polygon clipped = qt4.QPolygonF() utils.polygonClip(pts, clip, clipped) path = qt4.QPainterPath() path.addPolygon(clipped) utils.brushExtFillPath(painter, brush, path) def drawKeySymbol(self, number, painter, x, y, width, height): """Draw the plot symbol and/or line.""" s = self.settings yp = y + height/2 # draw line if not s.Line.hide: painter.setBrush( qt4.QBrush() ) painter.setPen( s.Line.makeQPen(painter) ) painter.drawLine( qt4.QPointF(x, yp), qt4.QPointF(x+width, yp) ) def initEnviron(self): """Set up function environment.""" return self.document.eval_context.copy() def getIndependentPoints(self, axes, posn): """Calculate the real and screen points to plot for the independent axis""" s = self.settings if ( None in axes or axes[0].settings.direction != 'horizontal' or axes[1].settings.direction != 'vertical' ): return None, None # get axes function is plotted along and on and # plot coordinates along axis function plotted along if s.variable == 'x': axis1, axis2 = axes[0], axes[1] minval, maxval = posn[0], posn[2] else: axis1, axis2 = axes[1], axes[0] minval, maxval = posn[1], posn[3] # get equally spaced coordinates along axis in plotter coords plotpts = N.arange(s.steps) * ((maxval-minval) / (s.steps-1)) + minval # convert to axis coordinates axispts = axis1.plotterToDataCoords(posn, plotpts) # trim according to min and max. have to convert back to plotter too. if s.min != 'Auto': axispts = axispts[ axispts >= s.min ] plotpts = axis1.dataToPlotterCoords(posn, axispts) if s.max != 'Auto': axispts = axispts[ axispts <= s.max ] plotpts = axis1.dataToPlotterCoords(posn, axispts) return axispts, plotpts def calcDependentPoints(self, axispts, axes, posn): """Calculate the real and screen points to plot for the dependent axis""" s = self.settings if ( None in axes or axes[0].settings.direction != 'horizontal' or axes[1].settings.direction != 'vertical' ): return None, None if axispts is None: return None, None compiled = self.document.compileCheckedExpression(s.function) if not compiled: return None, None axis2 = axes[1] if s.variable == 'x' else axes[0] # evaluate function env = self.initEnviron() env[s.variable] = axispts try: results = eval(compiled, env) + N.zeros(axispts.shape) resultpts = axis2.dataToPlotterCoords(posn, results) except Exception as e: self.logEvalError(e) results = None resultpts = None return results, resultpts def calcFunctionPoints(self, axes, posn): ipts, pipts = self.getIndependentPoints(axes, posn) dpts, pdpts = self.calcDependentPoints(ipts, axes, posn) if self.settings.variable == 'x': return (ipts, dpts), (pipts, pdpts) else: return (dpts, ipts), (pdpts, pipts) def _pickable(self, posn): s = self.settings axisnames = [s.xAxis, s.yAxis] axes = self.parent.getAxes(axisnames) if s.variable == 'x': axisnames[1] = axisnames[1] + '(' + axisnames[0] + ')' else: axisnames[0] = axisnames[0] + '(' + axisnames[1] + ')' (xpts, ypts), (pxpts, pypts) = self.calcFunctionPoints(axes, posn) return pickable.GenericPickable( self, axisnames, (xpts, ypts), (pxpts, pypts) ) def pickPoint(self, x0, y0, bounds, distance='radial'): return self._pickable(bounds).pickPoint(x0, y0, bounds, distance) def pickIndex(self, oldindex, direction, bounds): return self._pickable(bounds).pickIndex(oldindex, direction, bounds) def dataDraw(self, painter, axes, posn, cliprect): """Draw the function.""" s = self.settings # exit if hidden or function blank if s.function.strip() == '': return # get the points to plot by evaluating the function (xpts, ypts), (pxpts, pypts) = self.calcFunctionPoints(axes, posn) # draw the function line if pxpts is None or pypts is None: # not sure how to deal with errors here painter.setPen( setting.settingdb.color('error') ) f = qt4.QFont() f.setPointSize(20) painter.setFont(f) painter.drawText( cliprect, qt4.Qt.AlignCenter, "Cannot evaluate '%s'" % s.function ) else: if not s.FillBelow.hide: self._fillRegion(painter, pxpts, pypts, posn, True, cliprect, s.FillBelow) if not s.FillAbove.hide: self._fillRegion(painter, pxpts, pypts, posn, False, cliprect, s.FillAbove) if not s.Line.hide: painter.setBrush( qt4.QBrush() ) painter.setPen( s.Line.makeQPen(painter) ) self._plotLine(painter, pxpts, pypts, posn, cliprect) # allow the factory to instantiate an function plotter document.thefactory.register( FunctionPlotter ) veusz-1.21.1/veusz/widgets/axisbroken.py0000664000175000017500000003523112237406466016511 0ustar jssjss# Copyright (C) 2013 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## '''An axis which can be broken in places.''' from __future__ import division import bisect import numpy as N from ..compat import crange, czip from .. import qtall as qt4 from .. import setting from .. import document from .. import utils from . import axis from . import controlgraph def _(text, disambiguation=None, context='BrokenAxis'): '''Translate text.''' return qt4.QCoreApplication.translate(context, text, disambiguation) class AxisBroken(axis.Axis): '''An axis widget which can have gaps in it.''' typename = 'axis-broken' description = 'Axis with breaks in it' def __init__(self, parent, name=None): """Initialise axis.""" axis.Axis.__init__(self, parent, name=name) self.rangeswitch = None self.breakchangeset = -1 if type(self) == AxisBroken: self.readDefaults() @classmethod def addSettings(klass, s): '''Construct list of settings.''' axis.Axis.addSettings(s) s.add( setting.FloatList( 'breakPoints', [], descr = _('Pairs of values to start and stop breaks'), usertext = _('Break pairs'), ), 4 ) s.add( setting.FloatList( 'breakPosns', [], descr = _('Positions (fractions) along axis where to break'), usertext = _('Break positions'), formatting=True, ) ) def switchBreak(self, num, posn, otherposition=None): """Switch to break given (or None to disable).""" self.rangeswitch = num if num is None: self.plottedrange = self.orig_plottedrange else: self.plottedrange = [self.breakvstarts[num], self.breakvstops[num]] self.updateAxisLocation(posn, otherposition=otherposition) def plotterToGraphCoords(self, bounds, vals): """Convert values in plotter coordinates to data values. This needs to know about whether we've not switched between the breaks. Note that this implementation is very slow! Hopefully it won't be called often. """ if self.rangeswitch is not None: return axis.Axis.plotterToGraphCoords(self, bounds, vals) # scaled to be fractional coordinates in bounds if self.settings.direction == 'horizontal': svals = (vals - bounds[0]) / (bounds[2] - bounds[0]) else: svals = (vals - bounds[3]) / (bounds[1] - bounds[3]) # first work out which break region the values are in out = [] for sval, val in czip(svals, vals): # find index for appropriated scaled starting value breaki = bisect.bisect_left(self.posstarts, sval) - 1 if ( breaki >= 0 and breaki < self.breakvnum and sval <= self.posstops[breaki] ): self.switchBreak(breaki, bounds) coord = axis.Axis.plotterToGraphCoords( self, bounds, N.array([val]))[0] else: coord = N.nan out.append(coord) self.switchBreak(None, bounds) return N.array(out) def _graphToPlotter(self, vals): """Convert graph values to plotter coords. This could be slow if no range selected """ if self.rangeswitch is not None: return axis.Axis._graphToPlotter(self, vals) out = [] for val in vals: breaki = bisect.bisect_left(self.breakvstarts, val) - 1 if breaki >= 0 and breaki < self.breakvnum: if val > self.breakvstops[breaki] and breaki < self.breakvnum-1: # in gap, so use half-value coord = 0.5*(self.posstops[breaki]+self.posstarts[breaki+1]) b = self.currentbounds if self.settings.direction == 'horizontal': coord = coord*(b[2] - b[0]) + b[0] else: coord = coord*(b[3] - b[1]) + b[1] else: # lookup value self.switchBreak(breaki, self.currentbounds) coord = axis.Axis._graphToPlotter(self, N.array([val])) else: coord = N.nan out.append(coord) self.switchBreak(None, self.currentbounds) return N.array(out) def updateAxisLocation(self, bounds, otherposition=None): """Recalculate broken axis positions.""" s = self.settings if self.document.changeset != self.breakchangeset: self.breakchangeset = self.document.changeset # actually start and stop values on axis num = len(s.breakPoints) // 2 posns = list(s.breakPosns) posns.sort() # add on more break positions if not specified if len(posns) < num: start = 0. if len(posns) != 0: start = posns[-1] posns = posns + list( N.arange(1,num-len(posns)+1) * ( (1.-start) / (num-len(posns)+1) + start )) # fractional difference between starts and stops breakgap = 0.05 # collate fractional positions for starting and stopping starts = [0.] stops = [] for pos in posns: stops.append( pos - breakgap/2. ) starts.append( pos + breakgap/2. ) stops.append(1.) # scale according to allowable range d = s.upperPosition - s.lowerPosition self.posstarts = N.array(starts)*d + s.lowerPosition self.posstops = N.array(stops)*d + s.lowerPosition # pass lower and upper ranges if a particular range is chosen if self.rangeswitch is None: lowerupper = None else: lowerupper = ( self.posstarts[self.rangeswitch], self.posstops[self.rangeswitch] ) axis.Axis.updateAxisLocation(self, bounds, otherposition=otherposition, lowerupperposition=lowerupper) def computePlottedRange(self): """Given range of data, recompute stops and start values of breaks.""" axis.Axis.computePlottedRange(self) r = self.orig_plottedrange = self.plottedrange points = list(self.settings.breakPoints) points.sort() if r[1] < r[0]: points.reverse() # filter to range newpoints = [] for i in crange(0, len(points)//2 * 2, 2): if points[i] >= min(r) and points[i+1] <= max(r): newpoints += [points[i], points[i+1]] self.breakvnum = num = len(newpoints)//2 + 1 self.breakvlist = [self.plottedrange[0]] + newpoints + [ self.plottedrange[1]] # axis values for starting and stopping self.breakvstarts = [ self.breakvlist[i*2] for i in crange(num) ] self.breakvstops = [ self.breakvlist[i*2+1] for i in crange(num) ] # compute ticks for each range self.minorticklist = [] self.majorticklist = [] for i in crange(self.breakvnum): self.plottedrange = [self.breakvstarts[i], self.breakvstops[i]] reverse = self.plottedrange[0] > self.plottedrange[1] if reverse: self.plottedrange.reverse() self.computeTicks(allowauto=False) if reverse: self.plottedrange.reverse() self.minorticklist.append(self.minortickscalc) self.majorticklist.append(self.majortickscalc) self.plottedrange = self.orig_plottedrange def _autoMirrorDraw(self, posn, painter): """Mirror axis to opposite side of graph if there isn't an axis there already.""" # swap axis to other side s = self.settings if s.otherPosition < 0.5: otheredge = 1. else: otheredge = 0. # temporarily change position of axis to other side for drawing self.updateAxisLocation(posn, otherposition=otheredge) if not s.Line.hide: self._drawAxisLine(painter) for i in crange(self.breakvnum): self.switchBreak(i, posn, otherposition=otheredge) # plot coordinates of ticks coordticks = self._graphToPlotter(self.majorticklist[i]) coordminorticks = self._graphToPlotter(self.minorticklist[i]) if not s.MinorTicks.hide: self._drawMinorTicks(painter, coordminorticks) if not s.MajorTicks.hide: self._drawMajorTicks(painter, coordticks) self.switchBreak(None, posn) def _drawAxisLine(self, painter): """Draw the line of the axis, indicating broken positions. We currently use a triangle to mark the broken position """ # these are x and y, or y and x coordinates p1 = [self.posstarts[0]] p2 = [0.] # mirror shape using this setting markdirn = -1 if self.coordReflected: markdirn = -markdirn # add shape for each break for start, stop in czip( self.posstarts[1:], self.posstops[:-1] ): p1 += [stop, (start+stop)*0.5, start] p2 += [0, markdirn*(start-stop)*0.5, 0] # end point p1.append(self.posstops[-1]) p2.append(0.) # scale points by length of axis and add correct origin scale = self.coordParr2 - self.coordParr1 p1 = N.array(p1) * scale + self.coordParr1 p2 = N.array(p2) * scale + self.coordPerp if self.settings.direction == 'vertical': p1, p2 = p2, p1 # convert to polygon and draw poly = qt4.QPolygonF() utils.addNumpyToPolygonF(poly, p1, p2) pen = self.settings.get('Line').makeQPen(painter) pen.setCapStyle(qt4.Qt.FlatCap) painter.setPen(pen) painter.drawPolyline(poly) def drawGrid(self, parentposn, phelper, outerbounds=None, ontop=False): """Code to draw gridlines. This is separate from the main draw routine because the grid should be behind/infront the data points. """ s = self.settings if ( s.hide or (s.MinorGridLines.hide and s.GridLines.hide) or s.GridLines.onTop != bool(ontop) ): return # draw grid on a different layer, depending on whether on top or not layer = (-2, -1)[bool(ontop)] painter = phelper.painter(self, parentposn, layer=layer) self.updateAxisLocation(parentposn) with painter: painter.save() painter.setClipRect( qt4.QRectF( qt4.QPointF(parentposn[0], parentposn[1]), qt4.QPointF(parentposn[2], parentposn[3]) ) ) for i in crange(self.breakvnum): self.switchBreak(i, parentposn) if not s.MinorGridLines.hide: coordminorticks = self._graphToPlotter(self.minorticklist[i]) self._drawGridLines('MinorGridLines', painter, coordminorticks, parentposn) if not s.GridLines.hide: coordticks = self._graphToPlotter(self.majorticklist[i]) self._drawGridLines('GridLines', painter, coordticks, parentposn) self.switchBreak(None, parentposn) painter.restore() def _axisDraw(self, posn, parentposn, outerbounds, painter, phelper): """Main drawing routine of axis.""" s = self.settings # multiplication factor if reflection on the axis is requested sign = 1 if s.direction == 'vertical': sign *= -1 if self.coordReflected: sign *= -1 # keep track of distance from axis # text to output texttorender = [] # plot the line along the axis if not s.Line.hide: self._drawAxisLine(painter) max_delta = 0 for i in crange(self.breakvnum): self.switchBreak(i, posn) # plot coordinates of ticks coordticks = self._graphToPlotter(self.majorticklist[i]) coordminorticks = self._graphToPlotter(self.minorticklist[i]) self._delta_axis = 0 # plot minor ticks if not s.MinorTicks.hide: self._drawMinorTicks(painter, coordminorticks) # plot major ticks if not s.MajorTicks.hide: self._drawMajorTicks(painter, coordticks) # plot tick labels suppresstext = self._suppressText(painter, parentposn, outerbounds) if not s.TickLabels.hide and not suppresstext: self._drawTickLabels(phelper, painter, coordticks, sign, outerbounds, self.majorticklist[i], texttorender) # this is the maximum delta of any of the breaks max_delta = max(max_delta, self._delta_axis) self.switchBreak(None, posn) self._delta_axis = max_delta # draw an axis label if not s.Label.hide and not suppresstext: self._drawAxisLabel(painter, sign, outerbounds, texttorender) # mirror axis at other side of plot if s.autoMirror and self._shouldAutoMirror(): self._autoMirrorDraw(posn, painter) self._drawTextWithoutOverlap(painter, texttorender) # make control item for axis phelper.setControlGraph(self, [ controlgraph.ControlAxisLine( self, self.settings.direction, self.coordParr1, self.coordParr2, self.coordPerp, posn) ]) # allow the factory to instantiate the widget document.thefactory.register( AxisBroken ) veusz-1.21.1/veusz/widgets/root.py0000664000175000017500000001340712237406466015330 0ustar jssjss# root.py # Represents the root widget for plotting the document # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import textwrap from .. import qtall as qt4 from .. import document from .. import setting from . import widget from . import controlgraph def _(text, disambiguation=None, context='Root'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Root(widget.Widget): """Root widget class for plotting the document.""" typename='document' allowusercreation = False def __init__(self, parent, name=None, document=None): """Initialise object.""" widget.Widget.__init__(self, parent, name=name) s = self.settings self.document = document # don't want user to be able to hide entire document stylesheet = setting.StyleSheet(descr=_('Master settings for document'), usertext=_('Style sheet')) s.add(stylesheet) self.fillStylesheet(stylesheet) if type(self) == Root: self.readDefaults() s.get('englishlocale').setOnModified(self.changeLocale) @classmethod def addSettings(klass, s): widget.Widget.addSettings(s) s.remove('hide') s.add( setting.DistancePhysical( 'width', '15cm', descr=_('Width of the pages'), usertext=_('Page width'), formatting=True) ) s.add( setting.DistancePhysical( 'height', '15cm', descr=_('Height of the pages'), usertext=_('Page height'), formatting=True) ) s.add( setting.Bool( 'englishlocale', False, descr=_('Use US/English number formatting for ' 'document'), usertext=_('English locale'), formatting=True) ) s.add( setting.Notes( 'notes', '', descr=_('User-defined notes'), usertext=_('Notes') ) ) @classmethod def allowedParentTypes(klass): return (None,) @property def userdescription(self): """Return user-friendly description.""" return textwrap.fill(self.settings.notes, 60) def changeLocale(self): """Update locale of document if changed by user.""" if self.settings.englishlocale: self.document.locale = qt4.QLocale.c() else: self.document.locale = qt4.QLocale() self.document.locale.setNumberOptions(qt4.QLocale.OmitGroupSeparator) def getPage(self, pagenum): """Get page widget.""" try: return self.children[pagenum] except IndexError: return None def draw(self, painthelper, pagenum): """Draw the page requested on the painter.""" xw, yw = painthelper.pagesize posn = [0, 0, xw, yw] painter = painthelper.painter(self, posn) with painter: page = self.children[pagenum] page.draw( posn, painthelper ) # w and h are non integer w = self.settings.get('width').convert(painter) h = self.settings.get('height').convert(painter) painthelper.setControlGraph(self, [ controlgraph.ControlMarginBox(self, [0, 0, w, h], [-10000, -10000, 10000, 10000], painthelper, ismovable = False) ] ) def updateControlItem(self, cgi): """Call helper to set page size.""" cgi.setPageSize() def fillStylesheet(self, stylesheet): """Register widgets with stylesheet.""" for widgetname in document.thefactory.listWidgets(): klass = document.thefactory.getWidgetClass(widgetname) if klass.allowusercreation or klass == Root: newsett = setting.Settings(name=klass.typename, usertext = klass.typename, pixmap="button_%s" % klass.typename) classset = setting.Settings('temp') klass.addSettings(classset) # copy formatting settings to stylesheet for name in classset.setnames: # might become recursive if name == 'StyleSheet': continue sett = classset.setdict[name] # skip non formatting settings #if hasattr(sett, 'formatting') and not sett.formatting: # continue newsett.add( sett.copy() ) stylesheet.add(newsett) # allow the factory to instantiate this document.thefactory.register( Root ) veusz-1.21.1/veusz/widgets/axisticks.py0000644000175000017500000005023712327177747016356 0ustar jssjss# axisticks.py # algorithm to work out what tick-marks to put on an axis # Copyright (C) 2003 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import math import numpy as N from ..compat import crange from .. import utils """Algorithms for working with axis ticks. These algorithms were designed by me (Jeremy Sanders), so there may well be bugs. Please report them. The idea is to try to achieve a set number of major and minor ticks by looking though a list of allowable interval values (after taking account of what power of 10 the coordinates are in). """ class AxisTicksBase(object): """Base class of axis ticks classes.""" def __init__( self, minval, maxval, numticks, numminorticks, logaxis = False, prefermore = True, extendmin = False, extendmax = False, forceinterval = None ): """Initialise the class. minval and maxval are the range of the data to be plotted numticks number of major ticks to aim for logaxis: axis logarithmic? prefermore: prefer more ticks rather than fewer extendbounds: extend minval and maxval to nearest tick if okay forceinterval: force interval to one given (if allowed). interval is tuple as returned in self.interval after calling getTicks() """ self.minval = minval self.maxval = maxval self.numticks = numticks self.numminorticks = numminorticks self.logaxis = logaxis self.prefermore = prefermore self.extendmin = extendmin self.extendmax = extendmax self.forceinterval = forceinterval def getTicks( self ): """Calculate and return the position of the major ticks. Results are returned as attributes of this object in interval, minval, maxval, tickvals, minorticks, autoformat """ class AxisTicks(AxisTicksBase): """Class to work out at what values axis major ticks should appear.""" # the allowed values we allow ticks to increase by # first values are the major tick intervals, followed by a list # of allowed minors allowed_minorintervals_linear = { 1.: (0.1, 0.2, 0.5), 2.: (0.2, 0.5, 1.), 5.: (0.5, 1., 2.5), 2.5: (0.5,) } # just get the allowable majors allowed_intervals_linear = sorted(allowed_minorintervals_linear) # the allowed values we can increase by in log space # by default we increase by 10^3 # if the first value is chosen we can use the "special" log minor ticks allowed_intervals_log = (1., 3., 6., 9., 12., 15., 19.) # positions we're allowed to put minor intervals allowed_minorintervals_log = (1., 3., 6., 9., 12., 15., 19.) # how much we should allow axes to extend to a tick max_extend_factor = 0.15 def _calcTickValues( self, minval, maxval, delta ): """Compute the tick values, given minval, maxval and delta.""" startmult = int( math.ceil( minval / delta ) ) stopmult = int( math.floor( maxval / delta ) ) return N.arange(startmult, stopmult+1) * delta def _tickNums(self, minval, maxval, delta): """Calculate number of ticks between minval and maxval with delta.""" startmult = int( math.ceil( minval / delta ) ) stopmult = int( math.floor( maxval / delta ) ) return (stopmult-startmult)+1 def _calcNoTicks( self, interval, logdelta ): """Return the number of ticks with spacing interval*10^logdelta. Returns a tuple (noticks, minval, maxval). """ # store these for modification (if we extend bounds) minval = self.minval maxval = self.maxval # calculate tick spacing and maximum extension factor delta = interval * (10**logdelta) maxextend = (maxval - minval) * AxisTicks.max_extend_factor # should we try to extend to nearest interval*10^logdelta? if self.extendmin: # extend minval if possible if math.fabs( math.modf( minval / delta )[0] ) > 1e-8: d = minval - ( math.floor( minval / delta ) * delta ) if d <= maxextend: minval -= d if self.extendmax: # extend maxval if possible if math.fabs( math.modf( maxval / delta)[0] ) > 1e-8: d = ( (math.floor(maxval / delta)+1.) * delta) - maxval if d <= maxextend: maxval += d numticks = self._tickNums(minval, maxval, delta) return (numticks, minval, maxval) def _calcLinearMinorTickValues(self, minval, maxval, interval, logstep, allowedintervals): """Get the best values for minor ticks on a linear axis Algorithm tries to look for best match to nominorticks Pass routine major ticks from minval to maxval with steps of interval*(10**logstep) """ # iterate over allowed minor intervals best = -1 best_numticks = -1 best_delta = 1000000 mult = 10.**logstep # iterate over allowed minor intervals for minint in allowedintervals: numticks = self._tickNums(minval, maxval, minint*mult) d = abs( self.numminorticks - numticks ) # if this is a better match to the number of ticks # we want, choose this if ((d < best_delta ) or (d == best_delta and (self.prefermore and numticks > best_numticks) or (not self.prefermore and numticks < best_numticks)) ): best = minint best_delta = d best_numticks = numticks # use best value to return tick values return self._calcTickValues(minval, maxval, best*mult) def _calcLogMinorTickValues( self, minval, maxval ): """Calculate minor tick values with a log scale.""" # this is a scale going e.g. 1,2,3,...8,9,10,20,30...90,100,200... # round down to nearest power of 10 for each alpha = int( math.floor( N.log10(minval) ) ) beta = int( math.floor( N.log10(maxval) ) ) ticks = [] # iterate over range in log space for i in crange(alpha, beta+1): power = 10.**i # add ticks for values in correct range for j in crange(2, 10): v = power*j # blah log conversions mean we have to use 'fuzzy logic' if ( math.fabs(v - minval)/v < 1e-6 or v > minval ) and \ ( math.fabs(v - maxval)/v < 1e-6 or v < maxval ) : ticks.append(v) return N.array( ticks ) def _selectBestTickFromSelection(self, selection): """Choose best tick from selection given.""" # we now try to find the best matching value minabsdelta = 1e99 mindelta = 1e99 bestsel = () # find the best set of tick labels for s in selection: # difference between what we want and what we have delta = s[0] - self.numticks absdelta = abs(delta) # if it matches better choose this if absdelta < minabsdelta: minabsdelta = absdelta mindelta = delta bestsel = s # if we find two closest matching label sets, we # test whether we prefer too few to too many labels if absdelta == minabsdelta: if (self.prefermore and (delta > mindelta)) or \ (not self.prefermore and (delta < mindelta)): minabsdelta = absdelta mindelta = delta bestsel = s return bestsel def _getBestTickSelection(self, allowed_intervals): """Go through allowed tick intervals and find one best matching requested parameters.""" # work out range and log range therange = self.maxval - self.minval intlogrange = int( N.log10( therange ) ) # we step variable to move through log space to find best ticks logstep = intlogrange + 1 # we iterate down in log spacing, until we have more than twice # the number of ticks requested. # Maybe a better algorithm is required selection = [] # keep track of largest number of ticks calculated largestno = 0 while True: for interval in allowed_intervals: no, minval, maxval = self._calcNoTicks( interval, logstep ) selection.append( (no, interval, logstep, minval, maxval ) ) largestno = max(largestno, no) if largestno > self.numticks*2: break logstep -= 1 # necessary as we don't want 10**x on axis if |x|<1 # :-( if logstep < 0 and self.logaxis: break return selection def _tickSelector(self, allowed_intervals): """With minval and maxval find best tick positions.""" if self.forceinterval is None: # get selection of closely matching ticks selection = self._getBestTickSelection(allowed_intervals) # now we have the best, we work out the ticks and return bestsel = self._selectBestTickFromSelection(selection) dummy, interval, loginterval, minval, maxval = bestsel else: # forced specific interval requested interval, loginterval = self.forceinterval no, minval, maxval = self._calcNoTicks(interval, loginterval) # calculate the positions of the ticks from parameters tickdelta = interval * 10.**loginterval ticks = self._calcTickValues( minval, maxval, tickdelta ) return (minval, maxval, ticks, interval, loginterval) def getTicks(self): """Calculate and return the position of the major ticks. """ if self.logaxis: # which intervals we'll accept for major ticks intervals = AxisTicks.allowed_intervals_log # transform range into log space self.minval = N.log10( self.minval ) self.maxval = N.log10( self.maxval ) else: # which linear intervals we'll allow intervals = AxisTicks.allowed_intervals_linear # avoid breakage if range is zero if abs(self.minval - self.maxval) < 1e-99: self.maxval = self.minval + 1. minval, maxval, tickvals, interval, loginterval = self._tickSelector( intervals ) # work out the most appropriate minor tick intervals if not self.logaxis: # just plain minor ticks # try to achieve no of minors close to value requested minorticks = self._calcLinearMinorTickValues( minval, maxval, interval, loginterval, AxisTicks.allowed_minorintervals_linear[interval] ) else: # log axis if interval == 1.: # calculate minor ticks # here we use 'conventional' minor log tick spacing # e.g. 0.9, 1, 2, .., 8, 9, 10, 20, 30 ... minorticks = self._calcLogMinorTickValues( 10.**minval, 10.**maxval) # Here we test whether more log major tick values are needed... # often we might only have one tick value, and so we add 2, then 5 # this is a bit of a hack: better ideas please!! if len(tickvals) < 2: # get lower power of 10 low10 = int( math.floor(minval) ) # could use numpy here for i in (2., 5., 20., 50.): n = low10 + math.log10(i) if n >= minval and n <= maxval: tickvals = N.concatenate( (tickvals, N.array([n]) )) else: # if we increase by more than one power of 10 on the # axis, we can't do the above, so we do linear ticks # in log space # aim is to choose powers of 3 for majors and minors # to make it easy to read the axis. comments? minorticks = self._calcLinearMinorTickValues( minval, maxval, interval, loginterval, AxisTicks.allowed_minorintervals_log) minorticks = 10.**minorticks # transform normal ticks back to real space minval = 10.**minval maxval = 10.**maxval tickvals = 10.**tickvals self.interval = (interval, loginterval) self.minorticks = minorticks self.minval = minval self.maxval = maxval self.tickvals = tickvals self.autoformat = '%Vg' class DateTicks(AxisTicksBase): """For formatting dates. We want something that chooses appropriate intervals So we want to choose most apropriate interval depending on number of ticks requested """ # possible intervals for a time/date axis # tuples of ((y, m, d, h, m, s, msec), autoformat) intervals = ( ((200, 0, 0, 0, 0, 0, 0), '%VDY'), ((100, 0, 0, 0, 0, 0, 0), '%VDY'), ((50, 0, 0, 0, 0, 0, 0), '%VDY'), ((20, 0, 0, 0, 0, 0, 0), '%VDY'), ((10, 0, 0, 0, 0, 0, 0), '%VDY'), ((5, 0, 0, 0, 0, 0, 0), '%VDY'), ((2, 0, 0, 0, 0, 0, 0), '%VDY'), ((1, 0, 0, 0, 0, 0, 0), '%VDY'), ((0, 6, 0, 0, 0, 0, 0), '%VDY-%VDm'), ((0, 4, 0, 0, 0, 0, 0), '%VDY-%VDm'), ((0, 3, 0, 0, 0, 0, 0), '%VDY-%VDm'), ((0, 2, 0, 0, 0, 0, 0), '%VDY-%VDm'), ((0, 1, 0, 0, 0, 0, 0), '%VDY-%VDm'), ((0, 0, 28, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), ((0, 0, 14, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), ((0, 0, 7, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), ((0, 0, 2, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), ((0, 0, 1, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), ((0, 0, 0, 12, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), ((0, 0, 0, 6, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), ((0, 0, 0, 4, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), ((0, 0, 0, 3, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), ((0, 0, 0, 2, 0, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 1, 0, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 30, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 15, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 10, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 5, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 2, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 1, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 0, 30, 0), '%VDH:%VDM:%VDS'), ((0, 0, 0, 0, 0, 15, 0), '%VDH:%VDM:%VDS'), ((0, 0, 0, 0, 0, 10, 0), '%VDH:%VDM:%VDS'), ((0, 0, 0, 0, 0, 5, 0), '%VDH:%VDM:%VDS'), ((0, 0, 0, 0, 0, 2, 0), '%VDH:%VDM:%VDS'), ((0, 0, 0, 0, 0, 1, 0), '%VDH:%VDM:%VDS'), ((0, 0, 0, 0, 0, 0, 500000), '%VDH:%VDM:%VDVS'), ((0, 0, 0, 0, 0, 0, 200000), '%VDVS'), ((0, 0, 0, 0, 0, 0, 100000), '%VDVS'), ((0, 0, 0, 0, 0, 0, 50000), '%VDVS'), ((0, 0, 0, 0, 0, 0, 10000), '%VDVS'), ) intervals_sec = N.array([(ms*1e-6+s+mi*60+hr*60*60+dy*24*60*60+ mn*(365/12.)*24*60*60+ yr*365*24*60*60) for (yr, mn, dy, hr, mi, s, ms), fmt in intervals]) def bestTickFinder(self, minval, maxval, numticks, extendmin, extendmax, intervals, intervals_sec): """Try to find best choice of numticks ticks between minval and maxval intervals is an array similar to self.intervals intervals_sec is an array similar to self.intervals_sec Returns a tuple (minval, maxval, estimatedsize, ticks, textformat)""" delta = maxval - minval # iterate over different intervals and find one closest to what we want estimated = delta / intervals_sec tick1 = max(estimated.searchsorted(numticks)-1, 0) tick2 = min(tick1+1, len(estimated)-1) del1 = abs(estimated[tick1] - numticks) del2 = abs(estimated[tick2] - numticks) if del1 < del2: best = tick1 else: best = tick2 besttt, format = intervals[best] mindate = utils.floatToDateTime(minval) maxdate = utils.floatToDateTime(maxval) # round min and max to nearest minround = utils.tupleToDateTime(utils.roundDownToTimeTuple(mindate, besttt)) maxround = utils.tupleToDateTime(utils.roundDownToTimeTuple(maxdate, besttt)) if minround == mindate: mintick = minround else: # rounded down, so move on to next tick mintick = utils.addTimeTupleToDateTime(minround, besttt) maxtick = maxround # extend bounds if requested deltamin = utils.datetimeToFloat(mindate)-utils.datetimeToFloat(mintick) if extendmin and (deltamin != 0. and deltamin < delta*0.15): mindate = utils.addTimeTupleToDateTime(minround, [-x for x in besttt]) mintick = mindate deltamax = utils.datetimeToFloat(maxdate)-utils.datetimeToFloat(maxtick) if extendmax and (deltamax != 0. and deltamax < delta*0.15): maxdate = utils.addTimeTupleToDateTime(maxtick, besttt) maxtick = maxdate # make ticks ticks = [] dt = mintick while dt <= maxtick: ticks.append( utils.datetimeToFloat(dt)) dt = utils.addTimeTupleToDateTime(dt, besttt) return ( utils.datetimeToFloat(mindate), utils.datetimeToFloat(maxdate), intervals_sec[best], N.array(ticks), format ) def filterIntervals(self, estint): """Filter intervals and intervals_sec to be multiples of estint seconds.""" intervals = [] intervals_sec = [] for i, inter in enumerate(self.intervals_sec): ratio = estint / inter if abs(ratio-int(ratio)) < ratio*.01: intervals.append(self.intervals[i]) intervals_sec.append(inter) return intervals, N.array(intervals_sec) def getTicks(self): """Calculate and return the position of the major ticks. """ # find minor ticks mindate, maxdate, est, ticks, format = self.bestTickFinder( self.minval, self.maxval, self.numticks, self.extendmin, self.extendmax, self.intervals, self.intervals_sec) # try to make minor ticks divide evenly into major ticks intervals, intervals_sec = self.filterIntervals(est) # get minor ticks ig, ig, ig, minorticks, ig = self.bestTickFinder( mindate, maxdate, self.numminorticks, False, False, intervals, intervals_sec) self.interval = (intervals, intervals_sec) self.minval = mindate self.maxval = maxdate self.minorticks = minorticks self.tickvals = ticks self.autoformat = format veusz-1.21.1/veusz/widgets/plotters.py0000664000175000017500000002153512237406466016222 0ustar jssjss# plotters.py # plotting classes # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """A generic plotter widget which is inherited by function and point.""" from __future__ import division from .. import qtall as qt4 import numpy as N from .. import setting from . import widget def _(text, disambiguation=None, context='Plotters'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class GenericPlotter(widget.Widget): """Generic plotter.""" typename='genericplotter' isplotter = True def __init__(self, parent, name=None): """Initialise object, setting axes.""" widget.Widget.__init__(self, parent, name=name) @classmethod def allowedParentTypes(klass): from . import graph return (graph.Graph,) @classmethod def addSettings(klass, s): """Construct list of settings.""" widget.Widget.addSettings(s) s.add( setting.Str('key', '', descr = _('Description of the plotted data to appear in key'), usertext=_('Key text')) ) s.add( setting.Axis('xAxis', 'x', 'horizontal', descr = _('Name of X-axis to use'), usertext=_('X axis')) ) s.add( setting.Axis('yAxis', 'y', 'vertical', descr = _('Name of Y-axis to use'), usertext=_('Y axis')) ) def getAxesNames(self): """Returns names of axes used.""" s = self.settings return (s.xAxis, s.yAxis) def getNumberKeys(self): """Return number of key entries.""" if self.settings.key: return 1 else: return 0 def getKeyText(self, number): """Get key entry.""" return self.settings.key def drawKeySymbol(self, number, painter, x, y, width, height): """Draw the plot symbol and/or line at (x,y) in a box width*height. This is used to plot a key """ pass def clipAxesBounds(self, axes, bounds): """Returns clipping rectange for start and stop values of axis.""" # get range x1, x2 = axes[0].coordParr1, axes[0].coordParr2 if x1 > x2: x1, x2 = x2, x1 y1, y2 = axes[1].coordParr2, axes[1].coordParr1 if y1 > y2: y1, y2 = y2, y1 # actually clip the data cliprect = qt4.QRectF(qt4.QPointF(x1, y1), qt4.QPointF(x2, y2)) return cliprect def getAxisLabels(self, direction): """Get labels for datapoints and coordinates, or None if none. direction is 'horizontal' or 'vertical' return (labels, coordinates) """ return (None, None) def fetchAxes(self): """Returns the axes for this widget""" axes = self.parent.getAxes( (self.settings.xAxis, self.settings.yAxis) ) # fail if we don't have good axes if ( None in axes or axes[0].settings.direction != 'horizontal' or axes[1].settings.direction != 'vertical' ): return None return axes def lookupAxis(self, axisname): """Find widget associated with axisname.""" w = self.parent while w: for c in w.children: if c.name == axisname and c.isaxis: return c w = w.parent return None def affectsAxisRange(self): """Returns information on the following axes. format is ( ('x', 'sx'), ('y', 'sy') ) where key is the axis and value is a provided bound """ return () def requiresAxisRange(self): """Requires information about the axis given before providing information. Format (('sx', 'x'), ('sy', 'y')) """ return () def getRange(self, axis, depname, therange): """Update range variable for axis with dependency name given.""" pass def draw(self, parentposn, painthelper, outerbounds = None): """Draw for generic plotters.""" posn = self.computeBounds(parentposn, painthelper) # exit if hidden or function blank if self.settings.hide: return # get axes widgets axes = self.fetchAxes() if not axes: return # clip data within bounds of plotter cliprect = self.clipAxesBounds(axes, posn) painter = painthelper.painter(self, posn, clip=cliprect) with painter: self.dataDraw(painter, axes, posn, cliprect) return posn def dataDraw(self, painter, axes, posn, cliprect): """Actually plot the data.""" pass class FreePlotter(widget.Widget): """A plotter which can be plotted on the page or in a graph.""" def __init__(self, parent, name=None): """Initialise object, setting axes.""" widget.Widget.__init__(self, parent, name=name) @classmethod def allowedParentTypes(klass): from . import page, graph return (graph.Graph, page.Page) @classmethod def addSettings(klass, s): """Construct list of settings.""" widget.Widget.addSettings(s) s.add( setting.DatasetExtended( 'xPos', [0.5], descr=_('List of fractional X coordinates or dataset'), usertext=_('X positions'), formatting=False) ) s.add( setting.DatasetExtended( 'yPos', [0.5], descr=_('List of fractional Y coordinates or dataset'), usertext=_('Y positions'), formatting=False) ) s.add( setting.Choice('positioning', ['axes', 'relative'], 'relative', descr=_('Use axes or fractional ' 'position to place label'), usertext=_('Position mode'), formatting=False) ) s.add( setting.Axis('xAxis', 'x', 'horizontal', descr = _('Name of X-axis to use'), usertext=_('X axis')) ) s.add( setting.Axis('yAxis', 'y', 'vertical', descr = _('Name of Y-axis to use'), usertext=_('Y axis')) ) def _getPlotterCoords(self, posn, xsetting='xPos', ysetting='yPos'): """Calculate coordinates from relative or axis positioning. xsetting and ysetting are the settings to get data from """ s = self.settings xpos = s.get(xsetting).getFloatArray(self.document) ypos = s.get(ysetting).getFloatArray(self.document) if xpos is None or ypos is None: return None, None if s.positioning == 'axes': if hasattr(self.parent, 'getAxes'): axes = self.parent.getAxes( (s.xAxis, s.yAxis) ) else: return None, None if None in axes: return None, None xpos = axes[0].dataToPlotterCoords(posn, xpos) ypos = axes[1].dataToPlotterCoords(posn, ypos) else: xpos = posn[0] + (posn[2]-posn[0])*xpos ypos = posn[3] - (posn[3]-posn[1])*ypos return xpos, ypos def _getGraphCoords(self, posn, xplt, yplt): """Calculate graph coodinates given plot coordinates xplt, yplt.""" s = self.settings xplt = N.array(xplt) yplt = N.array(yplt) if s.positioning == 'axes': if hasattr(self.parent, 'getAxes'): axes = self.parent.getAxes( (s.xAxis, s.yAxis) ) else: return None, None if None in axes: return None, None xpos = axes[0].plotterToDataCoords(posn, xplt) ypos = axes[1].plotterToDataCoords(posn, yplt) else: xpos = (xplt - posn[0]) / (posn[2]-posn[0]) ypos = (yplt - posn[3]) / (posn[1]-posn[3]) return xpos, ypos veusz-1.21.1/veusz/widgets/contour.py0000644000175000017500000005444012327177747016045 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Contour plotting from 2d datasets. Contour plotting requires that the veusz_helpers package is installed, as a C routine (taken from matplotlib) is used to trace the contours. """ from __future__ import division, print_function import sys import math from ..compat import czip, crange from .. import qtall as qt4 import numpy as N from .. import setting from .. import document from .. import utils from . import plotters try: from ..helpers._nc_cntr import Cntr from ..helpers.qtloops import LineLabeller except ImportError: Cntr = None LineLabeller = object # allow class definition below def _(text, disambiguation=None, context='Contour'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) def finitePoly(poly): """Remove non-finite coordinates from numpy arrays of coordinates.""" out = [] for line in poly: finite = N.isfinite(line) validrows = N.logical_and(finite[:,0], finite[:,1]) out.append( line[validrows] ) return out class ContourLineLabeller(LineLabeller): def __init__(self, clip, rot, painter, font): LineLabeller.__init__(self, clip, rot) self.clippath = qt4.QPainterPath() self.clippath.addRect(clip) self.labels = [] self.painter = painter self.font = font def drawAt(self, idx, rect): """Called to draw the label with the index given.""" text = self.labels[idx] if not text: return angle = rect.angle*180/math.pi if angle < -90 or angle > 90: angle += 180 rend = utils.Renderer( self.painter, self.font, rect.cx, rect.cy, text, alignhorz=0, alignvert=0, angle=angle) rend.render() if rect.xw > 0: p = qt4.QPainterPath() p.addPolygon(rect.makePolygon()) self.clippath -= p class ContourFills(setting.Settings): """Settings for contour fills.""" def __init__(self, name, **args): setting.Settings.__init__(self, name, **args) self.add( setting.FillSet( 'fills', [], descr = _('Fill styles to plot between contours'), usertext=_('Fill styles'), formatting=True) ) self.add( setting.Bool('hide', False, descr = _('Hide fills'), usertext = _('Hide'), formatting = True) ) class ContourLines(setting.Settings): """Settings for contour lines.""" def __init__(self, name, **args): setting.Settings.__init__(self, name, **args) self.add( setting.LineSet( 'lines', [('solid', '1pt', 'black', False)], descr = _('Line styles to plot the contours ' 'using'), usertext=_('Line styles'), formatting=True) ) self.add( setting.Bool('hide', False, descr = _('Hide lines'), usertext = _('Hide'), formatting = True) ) class SubContourLines(setting.Settings): """Sub-dividing contour line settings.""" def __init__(self, name, **args): setting.Settings.__init__(self, name, **args) self.add( setting.LineSet( 'lines', [('dot1', '1pt', 'black', False)], descr = _('Line styles used for sub-contours'), usertext=_('Line styles'), formatting=True) ) self.add( setting.Int('numLevels', 5, minval=2, descr=_('Number of sub-levels to plot between ' 'each contour'), usertext='Levels') ) self.add( setting.Bool('hide', True, descr=_('Hide lines'), usertext=_('Hide'), formatting=True) ) class ContourLabel(setting.Text): """For tick labels on axes.""" def __init__(self, name, **args): setting.Text.__init__(self, name, **args) self.add( setting.Str( 'format', '%.3Vg', descr = _('Format of the tick labels'), usertext=_('Format')) ) self.add( setting.Float('scale', 1., descr=_('A scale factor to apply to the values ' 'of the tick labels'), usertext=_('Scale')) ) self.add( setting.Bool('rotate', True, descr=_('Rotate labels to follow lines'), usertext=_('Rotate')) ) self.get('hide').newDefault(True) class Contour(plotters.GenericPlotter): """A class which plots contours on a graph with a specified coordinate system.""" typename='contour' allowusercreation=True description=_('Plot a 2d dataset as contours') def __init__(self, parent, name=None): """Initialise plotter with axes.""" plotters.GenericPlotter.__init__(self, parent, name=name) if Cntr is None: print(('WARNING: Veusz cannot import contour module\n' 'Please run python setup.py build\n' 'Contour support is disabled'), file=sys.stderr) # keep track of settings so we recalculate when necessary self.lastdataset = None self.contsettings = None # cached traced contours self._cachedcontours = None self._cachedpolygons = None self._cachedsubcontours = None if type(self) == Contour: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" plotters.GenericPlotter.addSettings(s) s.add( setting.Dataset('data', '', dimensions = 2, descr = _('Dataset to plot'), usertext=_('Dataset')), 0 ) s.add( setting.FloatOrAuto('min', 'Auto', descr = _('Minimum value of contour scale'), usertext=_('Min. value')), 1 ) s.add( setting.FloatOrAuto('max', 'Auto', descr = _('Maximum value of contour scale'), usertext=_('Max. value')), 2 ) s.add( setting.Int('numLevels', 5, minval = 1, descr = _('Number of contour levels to plot'), usertext=_('Number levels')), 3 ) s.add( setting.Choice('scaling', ['linear', 'sqrt', 'log', 'squared', 'manual'], 'linear', descr = _('Scaling between contour levels'), usertext=_('Scaling')), 4 ) s.add( setting.FloatList('manualLevels', [], descr = _('Levels to use for manual scaling'), usertext=_('Manual levels')), 5 ) s.add( setting.Bool('keyLevels', False, descr=_('Show levels in key'), usertext=_('Levels in key')), 6 ) s.add( setting.FloatList('levelsOut', [], descr = _('Levels used in the plot'), usertext=_('Output levels')), 7, readonly=True ) s.add( ContourLabel('ContourLabels', descr = _('Contour label settings'), usertext = _('Contour labels')), pixmap = 'settings_axisticklabels' ) s.add( ContourLines('Lines', descr=_('Contour lines'), usertext=_('Contour lines')), pixmap = 'settings_contourline' ) s.add( ContourFills('Fills', descr=_('Fill within contours'), usertext=_('Contour fills')), pixmap = 'settings_contourfill' ) s.add( SubContourLines('SubLines', descr=_('Sub-contour lines'), usertext=_('Sub-contour lines')), pixmap = 'settings_subcontourline' ) s.add( setting.SettingBackwardCompat('lines', 'Lines/lines', None) ) s.add( setting.SettingBackwardCompat('fills', 'Fills/fills', None) ) s.remove('key') @property def userdescription(self): """User friendly description.""" s = self.settings out = [] if s.data: out.append( s.data ) if s.scaling == 'manual': out.append('manual levels (%s)' % ( ', '.join([str(i) for i in s.manualLevels]))) else: out.append('%(numLevels)i %(scaling)s levels (%(min)s to %(max)s)' % s) return ', '.join(out) def calculateLevels(self): """Calculate contour levels from data and settings. Returns levels as 1d numpy """ # get dataset s = self.settings d = self.document minval, maxval = 0., 1. if s.data in d.data: # scan data data = d.data[s.data].data minval, maxval = N.nanmin(data), N.nanmax(data) if not N.isfinite(minval): minval = 0. if not N.isfinite(maxval): maxval = 1. # override if not auto if s.min != 'Auto': minval = s.min if s.max != 'Auto': maxval = s.max numlevels = s.numLevels scaling = s.scaling if numlevels == 1 and scaling != 'manual': # calculations below assume numlevels > 1 levels = N.array([minval,]) else: # trap out silly cases if minval == maxval: minval = 0. maxval = 1. # calculate levels for each scaling if scaling == 'linear': delta = (maxval - minval) / (numlevels-1) levels = minval + N.arange(numlevels)*delta elif scaling == 'sqrt': delta = N.sqrt(maxval - minval) / (numlevels-1) levels = minval + (N.arange(numlevels)*delta)**2 elif scaling == 'log': if minval == 0.: minval = 1. if minval == maxval: maxval = minval + 1 delta = N.log(maxval/minval) / (numlevels-1) levels = N.exp(N.arange(numlevels)*delta)*minval elif scaling == 'squared': delta = (maxval - minval)**2 / (numlevels-1) levels = minval + N.sqrt(N.arange(numlevels)*delta) else: # manual levels = N.array(s.manualLevels) # for the user later # we do this to convert array to list of floats s.levelsOut = [float(i) for i in levels] return minval, maxval, levels def calculateSubLevels(self, minval, maxval, levels): """Calculate sublevels between contours.""" s = self.settings num = s.SubLines.numLevels if s.SubLines.hide or len(s.SubLines.lines) == 0 or len(levels) <= 1: return N.array([]) # indices where contour levels should be placed numcont = (len(levels)-1) * num indices = N.arange(numcont) indices = indices[indices % num != 0] scaling = s.scaling if scaling == 'linear': delta = (maxval-minval) / numcont slev = indices*delta + minval elif scaling == 'log': delta = N.log( maxval/minval ) / numcont slev = N.exp(indices*delta) * minval elif scaling == 'sqrt': delta = N.sqrt( maxval-minval ) / numcont slev = minval + (indices*delta)**2 elif scaling == 'squared': delta = (maxval-minval)**2 / numcont slev = minval + N.sqrt(indices*delta) elif scaling == 'manual': drange = N.arange(1, num) out = [[]] for conmin, conmax in czip(levels[:-1], levels[1:]): delta = (conmax-conmin) / num out.append( conmin+drange*delta ) slev = N.hstack(out) return slev def affectsAxisRange(self): """Range information provided by widget.""" s = self.settings return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def getRange(self, axis, depname, axrange): """Automatically determine the ranges of variable on the axes.""" # this is copied from Image, probably should combine s = self.settings d = self.document # return if no data or if the dataset isn't two dimensional data = d.data.get(s.data, None) if data is None or data.dimensions != 2: return xr, yr = data.getDataRanges() if depname == 'sx': axrange[0] = min( axrange[0], xr[0] ) axrange[1] = max( axrange[1], xr[1] ) elif depname == 'sy': axrange[0] = min( axrange[0], yr[0] ) axrange[1] = max( axrange[1], yr[1] ) def getNumberKeys(self): """How many keys to show.""" self.checkContoursUpToDate() if self.settings.keyLevels: return len( self.settings.levelsOut ) else: return 0 def getKeyText(self, number): """Get key entry.""" s = self.settings if s.keyLevels: cl = s.get('ContourLabels') return utils.formatNumber( s.levelsOut[number] * cl.scale, cl.format, locale=self.document.locale ) else: return '' def drawKeySymbol(self, number, painter, x, y, width, height): """Draw key for contour level.""" painter.setPen( self.settings.Lines.get('lines').makePen(painter, number)) painter.drawLine(x, y+height/2, x+width, y+height/2) def checkContoursUpToDate(self): """Update contours if necessary. Returns True if okay to plot contours, False if error """ s = self.settings d = self.document # return if no data or if the dataset isn't two dimensional data = d.data.get(s.data, None) if data is None or data.dimensions != 2 or data.data.size == 0: self.contsettings = self.lastdataset = None s.levelsOut = [] return False contsettings = ( s.min, s.max, s.numLevels, s.scaling, s.SubLines.numLevels, len(s.Fills.fills) == 0 or s.Fills.hide, len(s.SubLines.lines) == 0 or s.SubLines.hide, tuple(s.manualLevels) ) if data is not self.lastdataset or contsettings != self.contsettings: self.updateContours() self.lastdataset = data self.contsettings = contsettings return True def dataDraw(self, painter, axes, posn, cliprect): """Draw the contours.""" # update contours if necessary if not self.checkContoursUpToDate(): return self.plotContourFills(painter, posn, axes, cliprect) self.plotContours(painter, posn, axes, cliprect) self.plotSubContours(painter, posn, axes, cliprect) def updateContours(self): """Update calculated contours.""" s = self.settings d = self.document minval, maxval, levels = self.calculateLevels() sublevels = self.calculateSubLevels(minval, maxval, levels) # find coordinates of image coordinate bounds data = d.data[s.data] rangex, rangey = data.getDataRanges() yw, xw = data.data.shape if xw == 0 or yw == 0: return xc, yc = data.getPixelCentres() xpts = N.reshape( N.tile(xc, yw), (yw, xw) ) ypts = N.tile(yc[:, N.newaxis], xw) # only keep finite data points mask = N.logical_not(N.isfinite(data.data)) # iterate over the levels and trace the contours self._cachedcontours = None self._cachedpolygons = None self._cachedsubcontours = None if Cntr is not None: c = Cntr(xpts, ypts, data.data, mask) # trace the contour levels if len(s.Lines.lines) != 0: self._cachedcontours = [] for level in levels: linelist = c.trace(level) self._cachedcontours.append( finitePoly(linelist) ) # trace the polygons between the contours if len(s.Fills.fills) != 0 and len(levels) > 1 and not s.Fills.hide: self._cachedpolygons = [] for level1, level2 in czip(levels[:-1], levels[1:]): linelist = c.trace(level1, level2) self._cachedpolygons.append( finitePoly(linelist) ) # trace sub-levels if len(sublevels) > 0: self._cachedsubcontours = [] for level in sublevels: linelist = c.trace(level) self._cachedsubcontours.append( finitePoly(linelist) ) def _plotContours(self, painter, posn, axes, linestyles, contours, showlabels, hidelines, clip): """Plot a set of contours. """ s = self.settings # no lines cached as no line styles if contours is None: return cl = s.get('ContourLabels') font = cl.makeQFont(painter) descent = qt4.QFontMetricsF(font).descent() # linelabeller does clipping and labelling of contours linelabeller = ContourLineLabeller(clip, cl.rotate, painter, font) levels = [] # iterate over each level, and list of lines for num, linelist in enumerate(contours): if showlabels: number = s.levelsOut[num] text = utils.formatNumber(number * cl.scale, cl.format, locale=self.document.locale) rend = utils.Renderer(painter, font, 0, 0, text, alignhorz=0, alignvert=0, angle=0) textdims = qt4.QSizeF(*rend.getDimensions()) textdims += qt4.QSizeF(descent*2, descent*2) else: textdims = qt4.QSizeF(0, 0) # iterate over each complete line of the contour for curve in linelist: # convert coordinates from graph to plotter xplt = axes[0].dataToPlotterCoords(posn, curve[:,0]) yplt = axes[1].dataToPlotterCoords(posn, curve[:,1]) pts = qt4.QPolygonF() utils.addNumpyToPolygonF(pts, xplt, yplt) linelabeller.addLine(pts, textdims) if showlabels: linelabeller.labels.append(text) else: linelabeller.labels.append(None) levels.append(num) painter.save() linelabeller.process() painter.setClipPath(linelabeller.clippath) for i in crange(linelabeller.getNumPolySets()): polyset = linelabeller.getPolySet(i) painter.setPen(linestyles.makePen(painter, levels[i])) for poly in polyset: painter.drawPolyline(poly) painter.restore() def plotContours(self, painter, posn, axes, clip): """Plot the traced contours on the painter.""" s = self.settings self._plotContours(painter, posn, axes, s.Lines.get('lines'), self._cachedcontours, not s.ContourLabels.hide, s.Lines.hide, clip) def plotSubContours(self, painter, posn, axes, clip): """Plot sub contours on painter.""" s = self.settings self._plotContours(painter, posn, axes, s.SubLines.get('lines'), self._cachedsubcontours, False, s.SubLines.hide, clip) def plotContourFills(self, painter, posn, axes, clip): """Plot the traced contours on the painter.""" s = self.settings # don't draw if there are no cached polygons if self._cachedpolygons is None or s.Fills.hide: return # iterate over each level, and list of lines for num, polylist in enumerate(self._cachedpolygons): # iterate over each complete line of the contour path = qt4.QPainterPath() for poly in polylist: # convert coordinates from graph to plotter xplt = axes[0].dataToPlotterCoords(posn, poly[:,0]) yplt = axes[1].dataToPlotterCoords(posn, poly[:,1]) pts = qt4.QPolygonF() utils.addNumpyToPolygonF(pts, xplt, yplt) clippedpoly = qt4.QPolygonF() utils.polygonClip(pts, clip, clippedpoly) path.addPolygon(clippedpoly) # fill polygons brush = s.Fills.get('fills').returnBrushExtended(num) utils.brushExtFillPath(painter, brush, path) # allow the factory to instantiate a contour document.thefactory.register( Contour ) veusz-1.21.1/veusz/widgets/shape.py0000664000175000017500000003340412237406466015444 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """For plotting shapes.""" from __future__ import division, print_function import itertools import os from ..compat import czip, cbytes from .. import qtall as qt4 from .. import setting from .. import document from .. import utils from . import widget from . import controlgraph from . import plotters def _(text, disambiguation=None, context='Shape'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Shape(plotters.FreePlotter): """A shape on a page/graph.""" def __init__(self, parent, name=None): plotters.FreePlotter.__init__(self, parent, name=name) @classmethod def addSettings(klass, s): """Construct list of settings.""" plotters.FreePlotter.addSettings(s) s.add( setting.ShapeFill('Fill', descr = _('Shape fill'), usertext=_('Fill')), pixmap = 'settings_bgfill' ) s.add( setting.Line('Border', descr = _('Shape border'), usertext=_('Border')), pixmap = 'settings_border' ) s.add( setting.Bool('clip', False, descr=_('Clip shape to its container'), usertext=_('Clip'), formatting=True) ) class BoxShape(Shape): """For drawing box-like shapes.""" def __init__(self, parent, name=None): Shape.__init__(self, parent, name=name) @classmethod def addSettings(klass, s): """Construct list of settings.""" Shape.addSettings(s) s.add( setting.DatasetExtended( 'width', [0.1], descr=_('List of fractional widths, dataset or expression'), usertext=_('Widths'), formatting=False), 3 ) s.add( setting.DatasetExtended( 'height', [0.1], descr=_('List of fractional heights, dataset or expression'), usertext=_('Heights'), formatting=False), 4 ) s.add( setting.DatasetExtended( 'rotate', [0.], descr=_('Rotation angles of shape, dataset or expression'), usertext=_('Rotate'), formatting=False), 5 ) def drawShape(self, painter, rect): pass def draw(self, posn, phelper, outerbounds = None): """Plot the key on a plotter.""" s = self.settings d = self.document if s.hide: return # get positions of shapes width = s.get('width').getFloatArray(d) height = s.get('height').getFloatArray(d) rotate = s.get('rotate').getFloatArray(d) if width is None or height is None or rotate is None: return # translate coordinates from axes or relative values xpos, ypos = self._getPlotterCoords(posn) if xpos is None or ypos is None: # we can't calculate coordinates return # if a dataset is used, we can't use control items isnotdataset = ( not s.get('xPos').isDataset(d) and not s.get('yPos').isDataset(d) and not s.get('width').isDataset(d) and not s.get('height').isDataset(d) and not s.get('rotate').isDataset(d) ) controlgraphitems = [] clip = None if s.clip: clip = qt4.QRectF( qt4.QPointF(posn[0], posn[1]), qt4.QPointF(posn[2], posn[3]) ) painter = phelper.painter(self, posn, clip=clip) with painter: # drawing settings for shape if not s.Border.hide: painter.setPen( s.get('Border').makeQPen(painter) ) else: painter.setPen( qt4.QPen(qt4.Qt.NoPen) ) # iterate over positions index = 0 dx, dy = posn[2]-posn[0], posn[3]-posn[1] for x, y, w, h, r in czip(xpos, ypos, itertools.cycle(width), itertools.cycle(height), itertools.cycle(rotate)): wp, hp = dx*w, dy*h painter.save() painter.translate(x, y) if r != 0: painter.rotate(r) self.drawShape(painter, qt4.QRectF(-wp*0.5, -hp*0.5, wp, hp)) painter.restore() if isnotdataset: cgi = controlgraph.ControlResizableBox( self, [x, y], [wp, hp], r, allowrotate=True) cgi.index = index cgi.widgetposn = posn index += 1 controlgraphitems.append(cgi) phelper.setControlGraph(self, controlgraphitems) def updateControlItem(self, cgi): """If control item is moved or resized, this is called.""" s = self.settings # calculate new position coordinate for item xpos, ypos = self._getGraphCoords(cgi.widgetposn, cgi.posn[0], cgi.posn[1]) if xpos is None or ypos is None: return xw = abs(cgi.dims[0] / (cgi.widgetposn[2]-cgi.widgetposn[0])) yw = abs(cgi.dims[1] / (cgi.widgetposn[1]-cgi.widgetposn[3])) # actually do the adjustment on the document xp, yp = list(s.xPos), list(s.yPos) w, h, r = list(s.width), list(s.height), list(s.rotate) xp[cgi.index] = xpos yp[cgi.index] = ypos w[min(cgi.index, len(w)-1)] = xw h[min(cgi.index, len(h)-1)] = yw r[min(cgi.index, len(r)-1)] = cgi.angle operations = ( document.OperationSettingSet(s.get('xPos'), xp), document.OperationSettingSet(s.get('yPos'), yp), document.OperationSettingSet(s.get('width'), w), document.OperationSettingSet(s.get('height'), h), document.OperationSettingSet(s.get('rotate'), r) ) self.document.applyOperation( document.OperationMultiple(operations, descr=_('adjust shape')) ) class Rectangle(BoxShape): """Draw a rectangle, or rounded rectangle.""" typename = 'rect' description = _('Rectangle') allowusercreation = True def __init__(self, parent, name=None): BoxShape.__init__(self, parent, name=name) if type(self) == Rectangle: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" BoxShape.addSettings(s) s.add( setting.Int('rounding', 0, minval=0, maxval=100, descr=_('Round corners with this percentage'), usertext=_('Rounding corners'), formatting=True) ) def drawShape(self, painter, rect): s = self.settings path = qt4.QPainterPath() if s.rounding == 0: path.addRect(rect) else: path.addRoundedRect(rect, s.rounding, s.rounding) utils.brushExtFillPath(painter, s.Fill, path, stroke=painter.pen()) class Ellipse(BoxShape): """Draw an ellipse.""" typename = 'ellipse' description = _('Ellipse') allowusercreation = True def __init__(self, parent, name=None): BoxShape.__init__(self, parent, name=name) if type(self) == Ellipse: self.readDefaults() def drawShape(self, painter, rect): s = self.settings path = qt4.QPainterPath() path.addEllipse(rect) utils.brushExtFillPath(painter, s.Fill, path, stroke=painter.pen()) class ImageFile(BoxShape): """Draw an image.""" typename = 'imagefile' description = _('Image file') allowusercreation = True def __init__(self, parent, name=None): BoxShape.__init__(self, parent, name=name) if type(self) == ImageFile: self.readDefaults() self.cacheimage = None self.cachefilename = None self.cachestat = None self.cacheembeddata = None self.addAction( widget.Action('embed', self.actionEmbed, descr = _('Embed image in Veusz document ' 'to remove dependency on external file'), usertext = _('Embed image')) ) @classmethod def addSettings(klass, s): """Construct list of settings.""" BoxShape.addSettings(s) s.add( setting.ImageFilename('filename', '', descr=_('Image filename'), usertext=_('Filename'), formatting=False), posn=0 ) s.add( setting.Str('embeddedImageData', '', descr=_('Embedded base 64-encoded image data, ' 'used if filename set to {embedded}'), usertext=_('Embedded data'), hidden=True) ) s.add( setting.Bool('aspect', True, descr=_('Preserve aspect ratio'), usertext=_('Preserve aspect'), formatting=True), posn=0 ) s.Border.get('hide').newDefault(True) def actionEmbed(self): """Embed external image into veusz document.""" s = self.settings if s.filename == '{embedded}': print("Data already embedded") return # get data from external file try: f = open(s.filename, 'rb') data = f.read() f.close() except EnvironmentError: print("Could not find file. Not embedding.") return # convert to base 64 to make it nicer in the saved file encoded = cbytes(qt4.QByteArray(data).toBase64()).decode('ascii') # now put embedded data in hidden setting ops = [ document.OperationSettingSet(s.get('filename'), '{embedded}'), document.OperationSettingSet(s.get('embeddedImageData'), encoded) ] self.document.applyOperation( document.OperationMultiple(ops, descr=_('embed image')) ) def updateCachedImage(self): """Update cache.""" s = self.settings self.cachestat = os.stat(s.filename) self.cacheimage = qt4.QImage(s.filename) self.cachefilename = s.filename def updateCachedEmbedded(self): """Update cached image from embedded data.""" s = self.settings self.cacheimage = qt4.QImage() # convert the embedded data from base64 and load into the image decoded = qt4.QByteArray.fromBase64(s.embeddedImageData) self.cacheimage.loadFromData(decoded) # we cache the data we have decoded self.cacheembeddata = s.embeddedImageData def drawShape(self, painter, rect): """Draw image.""" s = self.settings # draw border and fill painter.drawRect(rect) # check to see whether image needs reloading image = None if s.filename != '' and os.path.isfile(s.filename): if (self.cachefilename != s.filename or os.stat(s.filename) != self.cachestat): # update the image cache self.updateCachedImage() # clear any embedded image data self.settings.get('embeddedImageData').set('') image = self.cacheimage # or needs recreating from embedded data if s.filename == '{embedded}': if s.embeddedImageData is not self.cacheembeddata: self.updateCachedEmbedded() image = self.cacheimage # if no image, then use default image if ( not image or image.isNull() or image.width() == 0 or image.height() == 0 ): # load replacement image fname = os.path.join(utils.imagedir, 'button_imagefile.svg') r = qt4.QSvgRenderer(fname) r.render(painter, rect) else: # image rectangle irect = qt4.QRectF(image.rect()) # preserve aspect ratio if s.aspect: xr = rect.width() / irect.width() yr = rect.height() / irect.height() if xr > yr: rect = qt4.QRectF( rect.left()+(rect.width()-irect.width()*yr)*0.5, rect.top(), irect.width()*yr, rect.height()) else: rect = qt4.QRectF( rect.left(), rect.top()+(rect.height()-irect.height()*xr)*0.5, rect.width(), irect.height()*xr) # finally draw image painter.drawImage(rect, image, irect) document.thefactory.register( Ellipse ) document.thefactory.register( Rectangle ) document.thefactory.register( ImageFile ) veusz-1.21.1/veusz/widgets/bar.py0000664000175000017500000005211512241672475015110 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """For plotting bar graphs.""" from __future__ import division import numpy as N from ..compat import crange, czip from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from .plotters import GenericPlotter def _(text, disambiguation=None, context='BarPlotter'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class BarFill(setting.Settings): '''Filling of bars.''' def __init__(self, name, **args): setting.Settings.__init__(self, name, **args) self.add( setting.FillSet('fills', [('solid', 'grey', False)], descr = _('Fill styles for dataset bars'), usertext=_('Fill styles')) ) class BarLine(setting.Settings): '''Edges of bars.''' def __init__(self, name, **args): setting.Settings.__init__(self, name, **args) self.add( setting.LineSet('lines', [('solid', '0.5pt', 'black', False)], descr = _('Line styles for dataset bars'), usertext=_('Line styles')) ) def extend1DArray(array, length, missing=0.): """Return array with length given (original if appropriate. Values are extended with value given.""" if len(array) == length: return array retn = N.resize(array, length) retn[len(array):] = missing return retn class BarPlotter(GenericPlotter): """Plot bar charts.""" typename='bar' allowusercreation=True description=_('Plot bar charts') def __init__(self, parent, name=None): """Initialise bar chart.""" GenericPlotter.__init__(self, parent, name=name) if type(self) == BarPlotter: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" GenericPlotter.addSettings(s) # get rid of default key setting s.remove('key') s.add( setting.Strings('keys', ('',), descr=_('Key text for each dataset'), usertext=_('Key text')), 0) s.add( setting.DatasetOrStr('labels', '', descr=_('Dataset or string to label bars'), usertext=_('Labels')), 5 ) s.add( setting.Choice('mode', ('grouped', 'stacked', 'stacked-area'), 'grouped', descr=_('Show datasets grouped ' 'together or as a single bar'), usertext=_('Mode')), 0) s.add( setting.Choice('direction', ('horizontal', 'vertical'), 'vertical', descr = _('Horizontal or vertical bar chart'), usertext=_('Direction')), 0 ) s.add( setting.DatasetExtended('posn', '', descr = _('Position of bars, dataset ' ' or expression (optional)'), usertext=_('Positions')), 0 ) s.add( setting.Datasets('lengths', ('y',), descr = _('Datasets containing lengths of bars'), usertext=_('Lengths')), 0 ) s.add( setting.Float('barfill', 0.75, minval = 0., maxval = 1., descr = _('Filling fraction of bars' ' (between 0 and 1)'), usertext=_('Bar fill'), formatting=True) ) s.add( setting.Float('groupfill', 0.9, minval = 0., maxval = 1., descr = _('Filling fraction of groups of bars' ' (between 0 and 1)'), usertext=_('Group fill'), formatting=True) ) s.add( setting.Choice('errorstyle', ('none', 'bar', 'barends'), 'bar', descr=_('Error bar style to show'), usertext=_('Error style'), formatting=True) ) s.add(BarFill('BarFill', descr=_('Bar fill'), usertext=_('Fill')), pixmap = 'settings_bgfill') s.add(BarLine('BarLine', descr=_('Bar line'), usertext=_('Line')), pixmap = 'settings_border') s.add( setting.ErrorBarLine('ErrorBarLine', descr = _('Error bar line settings'), usertext = _('Error bar line')), pixmap = 'settings_ploterrorline' ) @property def userdescription(self): """User-friendly description.""" s = self.settings return _("lengths='%s', position='%s'") % (', '.join(s.lengths), s.posn) def affectsAxisRange(self): """This widget provides range information about these axes.""" s = self.settings return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def getAxisLabels(self, direction): """Get labels for bar for appropriate axis.""" s = self.settings if s.direction != direction: # if horizontal bars, want labels on vertical axis and vice versa doc = self.document labels = s.get('labels').getData(doc, checknull=True) positions = s.get('posn').getData(doc) if positions is None: lengths = s.get('lengths').getData(doc) if not lengths: return (None, None) p = N.arange( max([len(d.data) for d in lengths]) )+1. else: p = positions.data return (labels, p) else: return (None, None) def singleBarDataRange(self, datasets): """For single bars where multiple datasets are added, compute maximum range.""" minv, maxv = 0., 0. for data in czip(*[ds.data for ds in datasets]): totpos = sum( [d for d in data if d > 0] ) totneg = sum( [d for d in data if d < 0] ) minv = min(minv, totneg) maxv = max(maxv, totpos) return minv, maxv def getRange(self, axis, depname, axrange): """Update axis range from data.""" s = self.settings if ((s.direction == 'horizontal' and depname == 'sx') or (s.direction == 'vertical' and depname == 'sy')): # update from lengths data = s.get('lengths').getData(self.document) if s.mode == 'grouped': # update range from individual datasets for d in data: drange = d.getRange() if drange is not None: axrange[0] = min(axrange[0], drange[0]) axrange[1] = max(axrange[1], drange[1]) else: # update range from sum of datasets minv, maxv = self.singleBarDataRange(data) axrange[0] = min(axrange[0], minv) axrange[1] = max(axrange[1], maxv) else: if s.posn: # use given positions data = s.get('posn').getData(self.document) if data: drange = data.getRange() if drange is not None: axrange[0] = min(axrange[0], drange[0]) axrange[1] = max(axrange[1], drange[1]) else: # count bars data = s.get('lengths').getData(self.document) if data: maxlen = max([len(d) for d in data]) axrange[0] = min(1-0.5, axrange[0]) axrange[1] = max(maxlen+0.5, axrange[1]) def findBarPositions(self, lengths, positions, axes, posn): """Work out centres of bar / bar groups and maximum width.""" ishorz = self.settings.direction == 'horizontal' if positions is None: p = N.arange( max([len(d.data) for d in lengths]) )+1. else: p = positions.data # work out positions of bars # get vertical axis if horz, and vice-versa axis = axes[ishorz] posns = axis.dataToPlotterCoords(posn, p) if len(posns) <= 1: if ishorz: maxwidth = posn[2]-posn[0] else: maxwidth = posn[3]-posn[1] else: maxwidth = N.nanmin(N.abs(posns[1:]-posns[:-1])) return posns, maxwidth def calculateErrorBars(self, dataset, vals): """Get values for error bars.""" minval = None maxval = None if 'serr' in dataset: s = N.nan_to_num(dataset['serr']) minval = vals - s maxval = vals + s else: if 'nerr' in dataset: minval = vals + N.nan_to_num(dataset['nerr']) if 'perr' in dataset: maxval = vals + N.nan_to_num(dataset['perr']) return minval, maxval def drawErrorBars(self, painter, posns, barwidth, yvals, dataset, axes, widgetposn): """Draw (optional) error bars on bars.""" s = self.settings if s.errorstyle == 'none': return minval, maxval = self.calculateErrorBars(dataset, yvals) if minval is None and maxval is None: return # handle one sided errors if minval is None: minval = yvals if maxval is None: maxval = yvals # convert errors to coordinates ishorz = s.direction == 'horizontal' mincoord = axes[not ishorz].dataToPlotterCoords(widgetposn, minval) mincoord = N.clip(mincoord, -32767, 32767) maxcoord = axes[not ishorz].dataToPlotterCoords(widgetposn, maxval) maxcoord = N.clip(maxcoord, -32767, 32767) # draw error bars painter.setPen( self.settings.ErrorBarLine.makeQPenWHide(painter) ) w = barwidth*0.25 if ishorz: utils.plotLinesToPainter(painter, mincoord, posns, maxcoord, posns) if s.errorstyle == 'barends': utils.plotLinesToPainter(painter, mincoord, posns-w, mincoord, posns+w) utils.plotLinesToPainter(painter, maxcoord, posns-w, maxcoord, posns+w) else: utils.plotLinesToPainter(painter, posns, mincoord, posns, maxcoord) if s.errorstyle == 'barends': utils.plotLinesToPainter(painter, posns-w, mincoord, posns+w, mincoord) utils.plotLinesToPainter(painter, posns-w, maxcoord, posns+w, maxcoord) def plotBars(self, painter, s, dsnum, clip, corners): """Plot a set of boxes.""" # get style brush = s.BarFill.get('fills').returnBrushExtended(dsnum) pen = s.BarLine.get('lines').makePen(painter, dsnum) lw = pen.widthF() * 2 # make clip box bigger to avoid lines showing extclip = qt4.QRectF(qt4.QPointF(clip.left()-lw, clip.top()-lw), qt4.QPointF(clip.right()+lw, clip.bottom()+lw)) # plot bars path = qt4.QPainterPath() utils.addNumpyPolygonToPath( path, extclip, corners[0], corners[1], corners[2], corners[1], corners[2], corners[3], corners[0], corners[3]) utils.brushExtFillPath(painter, brush, path, stroke=pen) def barDrawGroup(self, painter, posns, maxwidth, dsvals, axes, widgetposn, clip): """Draw groups of bars.""" s = self.settings # calculate bar and group widths numgroups = len(dsvals) groupwidth = maxwidth usablewidth = groupwidth * s.groupfill bardelta = usablewidth / numgroups barwidth = bardelta * s.barfill ishorz = s.direction == 'horizontal' # bar extends from these coordinates zeropt = axes[not ishorz].dataToPlotterCoords(widgetposn, N.array([0.])) for dsnum, dataset in enumerate(dsvals): # convert bar length to plotter coords lengthcoord = axes[not ishorz].dataToPlotterCoords( widgetposn, dataset['data']) # these are the coordinates perpendicular to the bar posns1 = posns + (-usablewidth*0.5 + bardelta*dsnum + (bardelta-barwidth)*0.5) posns2 = posns1 + barwidth if ishorz: p = (zeropt + N.zeros(posns1.shape), posns1, lengthcoord, posns2) else: p = (posns1, zeropt + N.zeros(posns2.shape), posns2, lengthcoord) self.plotBars(painter, s, dsnum, clip, p) # draw error bars self.drawErrorBars(painter, posns2-barwidth*0.5, barwidth, dataset['data'], dataset, axes, widgetposn) def calcStackedPoints(self, dsvals, axis, widgetposn): """Calculate stacked dataset coordinates for plotting.""" # keep track of last most negative or most positive values in bars poslen = len(dsvals[0]['data']) lastneg = N.zeros(poslen) lastpos = N.zeros(poslen) # returned stacked values and coordinates stackedvals = [] stackedcoords = [] for dsnum, data in enumerate(dsvals): # add on value to last value in correct direction data = data['data'] new = N.where(data < 0., lastneg+data, lastpos+data) # work out maximum extents for next time lastneg = N.min( N.vstack((lastneg, new)), axis=0 ) lastpos = N.max( N.vstack((lastpos, new)), axis=0 ) # convert values to plotter coordinates newplt = axis.dataToPlotterCoords(widgetposn, new) stackedvals.append(new) stackedcoords.append(newplt) return stackedvals, stackedcoords def barDrawStacked(self, painter, posns, maxwidth, dsvals, axes, widgetposn, clip): """Draw each dataset in a single bar.""" s = self.settings # get positions of groups of bars barwidth = maxwidth * s.barfill # get axis which values are plotted along ishorz = s.direction == 'horizontal' vaxis = axes[not ishorz] # compute stacked coordinates stackedvals, stackedcoords = self.calcStackedPoints( dsvals, vaxis, widgetposn) # coordinates of origin zerocoords = vaxis.dataToPlotterCoords(widgetposn, N.zeros(posns.shape)) # positions of bar perpendicular to bar direction posns1 = posns - barwidth*0.5 posns2 = posns1 + barwidth # draw bars (reverse order, so edges are plotted correctly) for dsnum, coords in czip( crange(len(stackedcoords)-1, -1, -1), stackedcoords[::-1]): # we iterate over each of these coordinates if ishorz: p = (zerocoords, posns1, coords, posns2) else: p = (posns1, zerocoords, posns2, coords) self.plotBars(painter, s, dsnum, clip, p) # draw error bars for barval, dsval in czip(stackedvals, dsvals): self.drawErrorBars(painter, posns, barwidth, barval, dsval, axes, widgetposn) def areaDrawStacked(self, painter, posns, maxwidth, dsvals, axes, widgetposn, clip): """Draw a stacked area plot""" s = self.settings # get axis which values are plotted along ishorz = s.direction == 'horizontal' vaxis = axes[not ishorz] # compute stacked coordinates stackedvals, stackedcoords = self.calcStackedPoints( dsvals, vaxis, widgetposn) # coordinates of origin zerocoords = vaxis.dataToPlotterCoords(widgetposn, N.zeros(posns.shape)) # bail out if problem if len(zerocoords) == 0 or len(posns) == 0: return # draw areas (reverse order, so edges are plotted correctly) for dsnum, coords in czip( crange(len(stackedcoords)-1, -1, -1), stackedcoords[::-1]): # add points at end to make polygon p1 = N.hstack( [ [zerocoords[0]], coords, [zerocoords[-1]] ] ) p2 = N.hstack( [ [posns[0]], posns, [posns[-1]] ] ) # construct polygon on path, clipped poly = qt4.QPolygonF() if ishorz: utils.addNumpyToPolygonF(poly, p1, p2) else: utils.addNumpyToPolygonF(poly, p2, p1) clippoly = qt4.QPolygonF() utils.polygonClip(poly, clip, clippoly) path = qt4.QPainterPath() path.addPolygon(clippoly) path.closeSubpath() # actually draw polygon brush = s.BarFill.get('fills').returnBrushExtended(dsnum) utils.brushExtFillPath(painter, brush, path) # now draw lines poly = qt4.QPolygonF() if ishorz: utils.addNumpyToPolygonF(poly, coords, posns) else: utils.addNumpyToPolygonF(poly, posns, coords) pen = s.BarLine.get('lines').makePen(painter, dsnum) painter.setPen(pen) utils.plotClippedPolyline(painter, clip, poly) # draw error bars barwidth = maxwidth * s.barfill for barval, dsval in czip(stackedvals, dsvals): self.drawErrorBars(painter, posns, barwidth, barval, dsval, axes, widgetposn) def getNumberKeys(self): """Return maximum number of keys.""" lengths = self.settings.get('lengths').getData(self.document) if not lengths: return 0 return min( len([k for k in self.settings.keys if k]), len(lengths) ) def getKeyText(self, number): """Get key entry.""" return [k for k in self.settings.keys if k][number] def drawKeySymbol(self, number, painter, x, y, width, height): """Draw a fill rectangle for key entry.""" self.plotBars(painter, self.settings, number, qt4.QRectF(0,0,32767,32767), ([x], [y+height*0.1], [x+width], [y+height*0.8])) def dataDraw(self, painter, axes, widgetposn, clip): """Plot the data on a plotter.""" s = self.settings # get data doc = self.document positions = s.get('posn').getData(doc) lengths = s.get('lengths').getData(doc) if not lengths: return # where the bars are to be placed horizontally barposns, maxwidth = self.findBarPositions(lengths, positions, axes, widgetposn) # only use finite positions origposnlen = len(barposns) validposn = N.isfinite(barposns) barposns = barposns[validposn] # this is a bit rubbish - we take the datasets and # make sure they have the same lengths as posns and remove NaNs # Datasets are stored as dicts dsvals = [] for dataset in lengths: vals = {} for key in ('data', 'serr', 'nerr', 'perr'): v = getattr(dataset, key) if v is not None: vals[key] = extend1DArray(N.nan_to_num(v), origposnlen)[validposn] dsvals.append(vals) # actually do the drawing fn = {'stacked': self.barDrawStacked, 'stacked-area': self.areaDrawStacked, 'grouped': self.barDrawGroup}[s.mode] fn(painter, barposns, maxwidth, dsvals, axes, widgetposn, clip) # allow the factory to instantiate a bar plotter document.thefactory.register( BarPlotter ) veusz-1.21.1/veusz/widgets/nonorthpoint.py0000644000175000017500000002512212327177747017110 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Non orthogonal point plotting.""" from __future__ import division import numpy as N from ..compat import czip from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from . import pickable from .nonorthgraph import NonOrthGraph, FillBrush from .widget import Widget from .point import MarkerFillBrush def _(text, disambiguation=None, context='NonOrthPoint'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class NonOrthPoint(Widget): '''Widget for plotting points in a non-orthogonal plot.''' typename = 'nonorthpoint' allowusercreation = True description = _('Plot points on a graph with non-orthogonal axes') def __init__(self, parent, name=None): """Initialise plotter.""" Widget.__init__(self, parent, name=name) if type(self) == NonOrthPoint: self.readDefaults() @classmethod def addSettings(klass, s): '''Settings for widget.''' Widget.addSettings(s) s.add( setting.DatasetExtended( 'data1', 'x', descr=_('Dataset containing 1st dataset, list of values ' 'or expression'), usertext=_('Dataset 1')) ) s.add( setting.DatasetExtended( 'data2', 'y', descr=_('Dataset containing 2nd dataset, list of values ' 'or expression'), usertext=_('Dataset 2')) ) s.add( setting.DatasetOrStr( 'labels', '', descr=_('Dataset or string to label points'), usertext=_('Labels')) ) s.add( setting.DatasetExtended( 'scalePoints', '', descr = _('Scale size of plotted markers by this dataset, ' ' list of values or expression'), usertext=_('Scale markers')) ) s.add( setting.MarkerColor('Color') ) s.add( setting.Color('color', 'black', descr = _('Master color'), usertext = _('Color'), formatting=True), 0 ) s.add( setting.DistancePt('markerSize', '3pt', descr = _('Size of marker to plot'), usertext=_('Marker size'), formatting=True), 0 ) s.add( setting.Marker('marker', 'circle', descr = _('Type of marker to plot'), usertext=_('Marker'), formatting=True), 0 ) s.add( setting.Line('PlotLine', descr = _('Plot line settings'), usertext = _('Plot line')), pixmap = 'settings_plotline' ) s.PlotLine.get('color').newDefault( setting.Reference('../color') ) s.add( setting.MarkerLine('MarkerLine', descr = _('Line around the marker settings'), usertext = _('Marker border')), pixmap = 'settings_plotmarkerline' ) s.add( MarkerFillBrush('MarkerFill', descr = _('Marker fill settings'), usertext = _('Marker fill')), pixmap = 'settings_plotmarkerfill' ) s.add( FillBrush('Fill1', descr = _('Fill settings (1)'), usertext = _('Area fill 1')), pixmap = 'settings_plotfillbelow' ) s.add( FillBrush('Fill2', descr = _('Fill settings (2)'), usertext = _('Area fill 2')), pixmap = 'settings_plotfillbelow' ) s.add( setting.PointLabel('Label', descr = _('Label settings'), usertext=_('Label')), pixmap = 'settings_axislabel' ) @classmethod def allowedParentTypes(klass): return (NonOrthGraph,) @property def userdescription(self): return _("data1='%s', data2='%s'") % ( self.settings.data1, self.settings.data2) def updateDataRanges(self, inrange): '''Extend inrange to range of data.''' d1 = self.settings.get('data1').getData(self.document) if d1: inrange[0] = min( N.nanmin(d1.data), inrange[0] ) inrange[1] = max( N.nanmax(d1.data), inrange[1] ) d2 = self.settings.get('data2').getData(self.document) if d2: inrange[2] = min( N.nanmin(d2.data), inrange[2] ) inrange[3] = max( N.nanmax(d2.data), inrange[3] ) def pickPoint(self, x0, y0, bounds, distance = 'radial'): p = pickable.DiscretePickable(self, 'data1', 'data2', lambda v1, v2: self.parent.graphToPlotCoords(v1, v2)) return p.pickPoint(x0, y0, bounds, distance) def pickIndex(self, oldindex, direction, bounds): p = pickable.DiscretePickable(self, 'data1', 'data2', lambda v1, v2: self.parent.graphToPlotCoords(v1, v2)) return p.pickIndex(oldindex, direction, bounds) def drawLabels(self, painter, xplotter, yplotter, textvals, markersize): """Draw labels for the points. This is copied from the xy (point) widget class, so it probably should be somehow be shared. FIXME: sane automatic placement of labels """ s = self.settings lab = s.get('Label') # work out offset an alignment deltax = markersize*1.5*{'left':-1, 'centre':0, 'right':1}[lab.posnHorz] deltay = markersize*1.5*{'top':-1, 'centre':0, 'bottom':1}[lab.posnVert] alignhorz = {'left':1, 'centre':0, 'right':-1}[lab.posnHorz] alignvert = {'top':-1, 'centre':0, 'bottom':1}[lab.posnVert] # make font and len textpen = lab.makeQPen() painter.setPen(textpen) font = lab.makeQFont(painter) angle = lab.angle # iterate over each point and plot each label for x, y, t in czip(xplotter+deltax, yplotter+deltay, textvals): utils.Renderer( painter, font, x, y, t, alignhorz, alignvert, angle ).render() def getColorbarParameters(self): """Return parameters for colorbar.""" s = self.settings c = s.Color return (c.min, c.max, c.scaling, s.MarkerFill.colorMap, 0, s.MarkerFill.colorMapInvert) def draw(self, parentposn, phelper, outerbounds=None): '''Plot the data on a plotter.''' posn = self.computeBounds(parentposn, phelper) s = self.settings d = self.document # exit if hidden if s.hide: return d1 = s.get('data1').getData(d) d2 = s.get('data2').getData(d) dscale = s.get('scalePoints').getData(d) colorpoints = s.Color.get('points').getData(d) text = s.get('labels').getData(d, checknull=True) if not d1 or not d2: return x1, y1, x2, y2 = posn cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) painter = phelper.painter(self, posn) with painter: self.parent.setClip(painter, posn) # split parts separated by NaNs for v1, v2, scalings, cvals, textitems in document.generateValidDatasetParts( d1, d2, dscale, colorpoints, text): # convert data (chopping down length) v1d, v2d = v1.data, v2.data minlen = min(v1d.shape[0], v2d.shape[0]) v1d, v2d = v1d[:minlen], v2d[:minlen] px, py = self.parent.graphToPlotCoords(v1d, v2d) # do fill1 (if any) if not s.Fill1.hide: self.parent.drawFillPts(painter, s.Fill1, cliprect, px, py) # do fill2 if not s.Fill2.hide: self.parent.drawFillPts(painter, s.Fill2, cliprect, px, py) # plot line if not s.PlotLine.hide: painter.setBrush( qt4.QBrush() ) painter.setPen(s.PlotLine.makeQPen(painter)) pts = qt4.QPolygonF() utils.addNumpyToPolygonF(pts, px, py) utils.plotClippedPolyline(painter, cliprect, pts) # plot markers markersize = s.get('markerSize').convert(painter) if not s.MarkerLine.hide or not s.MarkerFill.hide: pscale = colorvals = cmap = None if scalings: pscale = scalings.data # color point individually if cvals and not s.MarkerFill.hide: colorvals = utils.applyScaling( cvals.data, s.Color.scaling, s.Color.min, s.Color.max) cmap = self.document.getColormap( s.MarkerFill.colorMap, s.MarkerFill.colorMapInvert) painter.setBrush(s.MarkerFill.makeQBrushWHide()) painter.setPen(s.MarkerLine.makeQPenWHide(painter)) utils.plotMarkers(painter, px, py, s.marker, markersize, scaling=pscale, clip=cliprect, cmap=cmap, colorvals=colorvals, scaleline=s.MarkerLine.scaleLine) # finally plot any labels if textitems and not s.Label.hide: self.drawLabels(painter, px, py, textitems, markersize) # allow the factory to instantiate plotter document.thefactory.register( NonOrthPoint ) veusz-1.21.1/veusz/widgets/page.py0000664000175000017500000003217312237406466015262 0ustar jssjss# Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Widget that represents a page in the document.""" from __future__ import division import collections import textwrap import numpy as N from ..compat import crange, citems from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from . import widget from . import controlgraph def _(text, disambiguation=None, context='Page'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) defaultrange = [1e99, -1e99] def _resolveLinkedAxis(axis): """Follow a chain of axis function dependencies.""" loopcheck = set() while axis is not None and axis.isLinked(): loopcheck.add(axis) axis = axis.getLinkedAxis() if axis in loopcheck: # fail if loop return None return axis class AxisDependHelper(object): """A class to work out the dependency of widgets on axes and vice versa, in terms of ranges of the axes. Note: Here a widget is really (widget, depname), as each widget can have a different dependency (e.g. sx and sy dependencies for plotters). It then works out the ranges for each of the axes from the plotters. connection types: plotter->axis : axis needs to know data range axis->plotter : plotter needs to know axis range axis<->axis : axes are mutually dependent aim: calculate ranges of axes given plotters problem: cycles in the graph f1<-x: function f1 depends on axis x f2<-y: function f2 depends on axis y y<-f1: axis y depends on function f1 x<-f2: axis x depends on function f2 solution: break dependency cycle: choose somewhere - probably better to choose where widget depends on axis however, axis<->axis cycle can't be broken additional solution: convert all dependencies on axis1 or axis2 to axiscomb x <-> axis1 <-> axis2 For linked axes (e.g. AxisFunction): * Don't keep track of range separately -> propagate to real axis * For dependency order resolution, use real axis * In self.deps, use axisfunction axis so we know which axis to use """ def __init__(self): # map widgets to widgets it depends on self.deps = collections.defaultdict(list) # list of axes self.axes = [] # list of plotters associated with each axis self.axis_plotter_map = collections.defaultdict(list) # ranges for each axis self.ranges = {} # pairs of dependent widgets self.pairs = [] # track axes which map from one axis to another self.axis_to_axislinked = {} self.axislinked_to_axis = {} def recursivePlotterSearch(self, widget): """Find a list of plotters below widget. Builds up a dict of "nodes" representing each widget: plotter/axis Each node is a list of tuples saying which widgets need evaling first The tuples are (widget, depname), where depname is a name for the part of the plotter, e.g. "sx" or "sy" for x or y. """ if widget.isplotter: # keep track of which widgets depend on which axes widgetaxes = {} for axname in widget.getAxesNames(): axis = widget.lookupAxis(axname) widgetaxes[axname] = axis self.axis_plotter_map[axis].append(widget) # if the widget is a plotter, find which axes the widget # can provide range information about for axname, depname in widget.affectsAxisRange(): origaxis = widgetaxes[axname] resolvedaxis = _resolveLinkedAxis(origaxis) if resolvedaxis is not None and resolvedaxis.usesAutoRange(): # only add dependency if axis has an automatic range self.deps[(origaxis, None)].append((widget, depname)) self.pairs.append( ((widget, depname), (resolvedaxis, None)) ) # find which axes the plotter needs information from for depname, axname in widget.requiresAxisRange(): origaxis = widgetaxes[axname] resolvedaxis = _resolveLinkedAxis(origaxis) if resolvedaxis is not None and resolvedaxis.usesAutoRange(): self.deps[(widget, depname)].append((origaxis, None)) self.pairs.append( ((resolvedaxis, None), (widget, depname)) ) elif widget.isaxis: if widget.isaxis and widget.isLinked(): # function of another axis linked = widget.getLinkedAxis() if linked is not None: self.axis_to_axislinked[linked] = widget self.axislinked_to_axis[widget] = linked else: # make a range for a normal axis self.axes.append(widget) self.ranges[widget] = list(defaultrange) for c in widget.children: self.recursivePlotterSearch(c) def breakCycles(self, origcyclic): """Remove cycles if possible.""" numcyclic = len(origcyclic) best = -1 for i in crange(len(self.pairs)): if not self.pairs[i][0][0].isaxis: p = self.pairs[:i] + self.pairs[i+1:] ordered, cyclic = utils.topological_sort(p) if len(cyclic) <= numcyclic: numcyclic = len(cyclic) best = i # delete best, or last one if none better found p = self.pairs[best] del self.pairs[best] try: idx = self.deps[p[1]].index(p[0]) del self.deps[p[1]][idx] except ValueError: pass def _updateAxisAutoRange(self, axis): """Update auto range for axis.""" # set actual range on axis, as axis no longer has a # dependency axrange = self.ranges[axis] if axrange == defaultrange: axrange = None # print "Updating", axis.name, axrange axis.setAutoRange(axrange) del self.ranges[axis] def _updateRangeFromPlotter(self, axis, plotter, plotterdep): """Update the range for axis from the plotter.""" if axis.isLinked(): # take range and map back to real axis therange = list(defaultrange) plotter.getRange(axis, plotterdep, therange) if therange != defaultrange: # follow up chain loopcheck = set() while axis.isLinked(): loopcheck.add(axis) therange = axis.invertFunctionVals(therange) axis = axis.getLinkedAxis() if axis in loopcheck: axis = None if axis is not None and therange is not None: self.ranges[axis] = [ N.nanmin((self.ranges[axis][0], therange[0])), N.nanmax((self.ranges[axis][1], therange[1])) ] else: plotter.getRange(axis, plotterdep, self.ranges[axis]) def processWidgetDeps(self, dep): """Process dependencies for a single widget.""" widget, widget_dep = dep # iterate over dependent widgets for widgetd, widgetd_dep in self.deps[dep]: # print "Dep: ", widget.name, widgetd.name if ( widgetd.isplotter and (not widgetd.settings.isSetting('hide') or not widgetd.settings.hide) ): self._updateRangeFromPlotter(widget, widgetd, widgetd_dep) elif widgetd.isaxis: axis = _resolveLinkedAxis(widgetd) if axis in self.ranges: self._updateAxisAutoRange(axis) def processDepends(self): """Go through dependencies of widget. If the dependency has no dependency itself, then update the axis with the widget or vice versa Algorithm: Iterate over dependencies for widget. If the widget has a dependency on a widget which doesn't have a dependency itself, update range from that widget. Then delete that depency from the dependency list. """ # get ordered list, breaking cycles while True: ordered, cyclic = utils.topological_sort(self.pairs) if not cyclic: break self.breakCycles(cyclic) # iterate over widgets in order for dep in ordered: self.processWidgetDeps(dep) # process deps for any axis functions while dep[0] in self.axis_to_axislinked: dep = (self.axis_to_axislinked[dep[0]], None) self.processWidgetDeps(dep) def findAxisRanges(self): """Find the ranges from the plotters and set the axis ranges. Follows the dependencies calculated above. """ self.processDepends() # set any remaining ranges for axis in list(self.ranges.keys()): self._updateAxisAutoRange(axis) class Page(widget.Widget): """A class for representing a page of plotting.""" typename='page' allowusercreation = True description=_('Blank page') def __init__(self, parent, name=None): """Initialise object.""" widget.Widget.__init__(self, parent, name=name) if type(self) == Page: self.readDefaults() @classmethod def addSettings(klass, s): widget.Widget.addSettings(s) # page sizes are initially linked to the document page size s.add( setting.DistancePhysical( 'width', setting.Reference('/width'), descr=_('Width of page'), usertext=_('Page width'), formatting=True) ) s.add( setting.DistancePhysical( 'height', setting.Reference('/height'), descr=_('Height of page'), usertext=_('Page height'), formatting=True) ) s.add( setting.Notes( 'notes', '', descr=_('User-defined notes'), usertext=_('Notes') ) ) @classmethod def allowedParentTypes(klass): from . import root return (root.Root,) @property def userdescription(self): """Return user-friendly description.""" return textwrap.fill(self.settings.notes, 60) def draw(self, parentposn, painthelper, outerbounds=None): """Draw the plotter. Clip graph inside bounds.""" # document should pass us the page bounds x1, y1, x2, y2 = parentposn # find ranges of axes axisdependhelper = AxisDependHelper() axisdependhelper.recursivePlotterSearch(self) axisdependhelper.findAxisRanges() # store axis->plotter mappings in painthelper painthelper.axisplottermap.update(axisdependhelper.axis_plotter_map) # reverse mapping pamap = collections.defaultdict(list) for axis, plotters in citems(painthelper.axisplottermap): for plot in plotters: pamap[plot].append(axis) painthelper.plotteraxismap.update(pamap) if self.settings.hide: bounds = self.computeBounds(parentposn, painthelper) return bounds # clip to page painter = painthelper.painter(self, parentposn) with painter: # w and h are non integer w = self.settings.get('width').convert(painter) h = self.settings.get('height').convert(painter) painthelper.setControlGraph(self, [ controlgraph.ControlMarginBox(self, [0, 0, w, h], [-10000, -10000, 10000, 10000], painthelper, ismovable = False) ] ) bounds = widget.Widget.draw(self, parentposn, painthelper, parentposn) return bounds def updateControlItem(self, cgi): """Call helper to set page size.""" cgi.setPageSize() # allow the factory to instantiate this document.thefactory.register( Page ) veusz-1.21.1/veusz/widgets/grid.py0000664000175000017500000003532312237406466015273 0ustar jssjss# Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """The grid class allows graphs to be arranged in a regular grid. The graphs may share axes if they are stored in the grid widget. """ from __future__ import division from ..compat import crange from .. import document from .. import setting from .. import qtall as qt4 from . import widget from . import graph from . import controlgraph def _(text, disambiguation=None, context='Grid'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class _gridengine: """Internal class to build up grid of widgets.""" def __init__(self, columns, rows): """Initialise allocater, with N columns or rows (other set to None).""" # we don't allocate space until it is used self.alloced = [] self.rows = rows self.columns = columns # starting position self.row = 0 self.col = 0 def isAlloced(self, c, r): """Returns whether element (c,r) allocated.""" if r >= len(self.alloced): return False row = self.alloced[r] if c >= len( row ): return False else: return row[c] def isAllocedBlock(self, c, r, w, h): """Is the block (c,r) -> (c+w,r+h) allocated?""" for y in crange(h): for x in crange(w): if self.isAlloced(c+x, y+r): return True return False def setAlloced(self, c, r): """Set element (c,r) as allocated.""" while r >= len(self.alloced): self.alloced.append( [] ) row = self.alloced[r] while c >= len(row): row.append( False ) row[c] = True def setAllocedBlock(self, c, r, w, h): """Set block (c,r)->(c+w,r+h) as allocated.""" for y in crange(h): for x in crange(w): self.setAlloced(x+c, y+r) def add(self, width, height): """Add a block of width x height, returning position as tuple.""" if self.columns is not None: # wrap around if item too wide # (providing we didn't request more columns than we have - # in that case we ignore the request) if ((self.col + width) > self.columns) and (width <= self.columns): self.col = 0 self.row += 1 # increase column until we can allocate the block # if we run out of columns, move to the next row while self.isAllocedBlock(self.col, self.row, width, height): self.col += 1 if (self.col + width > self.columns) and \ (width <= self.columns): self.col = 0 self.row += 1 # save position c = self.col r = self.row self.col += width else: # work in row based layout now if ((self.row + height) > self.rows) and (height <= self.rows): self.row = 0 self.col += 1 # increase row until we can allocate the next block # if we run out of rows, move to the next column while self.isAllocedBlock(self.col, self.row, width, height): self.row += 1 if (self.row + height > self.rows) and (height <= self.rows): self.row = 0 self.col += 1 # save position c = self.col r = self.row self.row += height # allocate and return block position self.setAllocedBlock(c, r, width, height) return (c, r) def getAllocedDimensions(self): """Return the columns x rows allocated.""" # assumes blocks don't get unset h = len(self.alloced) w = 0 for l in self.alloced: w = max(w, len(l)) return (w, h) class Grid(widget.Widget): """Class to hold plots in a grid arrangement. The idea is we either specify how many rows or columns to use. If we specify no of rows, then we fill vertically until we exceed rows, then we add another column. The same is true if cols is specified. """ typename='grid' allowusercreation=True description=_('Arrange graphs in a grid') def __init__(self, parent, name=None): """Initialise the grid. """ widget.Widget.__init__(self, parent, name=name) # we're not descended from if type(self) == Grid: self.readDefaults() self.addAction( widget.Action( 'zeroMargins', self.actionZeroMargins, descr = _('Zero margins of graphs in grid'), usertext = _('Zero margins')) ) # calculated positions for children self.childpositions = {} # watch for changes to these variables to decide whether to # recalculate positions self.lastdimensions = None self.lastscalings = None self.lastchildren = None @classmethod def addSettings(klass, s): """Construct list of settings.""" widget.Widget.addSettings(s) s.add(setting.Int('rows', 2, descr = _('Number of rows in grid'), usertext=_('Number of rows')) ) s.add(setting.Int('columns', 2, descr = _('Number of columns in grid'), usertext=_('Number of columns')) ) s.add( setting.FloatList( 'scaleRows', [], descr = _('Row scaling factors. A sequence' ' of values\nby which to scale rows ' 'relative to each other.'), usertext=_('Row scalings')) ) s.add( setting.FloatList( 'scaleCols', [], descr = _('Column scaling factors. A sequence' ' of values\nby which to scale columns' ' relative to each other.'), usertext=_('Column scalings')) ) s.add( setting.Distance( 'leftMargin', '1.7cm', descr=_('Distance from left of grid to edge of page'), usertext=_('Left margin'), formatting=True) ) s.add( setting.Distance( 'rightMargin', '0.2cm', descr=_('Distance from right of grid to edge of page'), usertext=_('Right margin'), formatting=True) ) s.add( setting.Distance( 'topMargin', '0.2cm', descr=_('Distance from top of grid to edge of page'), usertext=_('Top margin'), formatting=True) ) s.add( setting.Distance( 'bottomMargin', '1.7cm', descr=_('Distance from bottom of grid to edge of page'), usertext=_('Bottom margin'), formatting=True) ) s.add( setting.Distance( 'internalMargin', '0cm', descr=_('Gap between grid members'), usertext=_('Internal margin'), formatting=True) ) @classmethod def allowedParentTypes(klass): from . import page return (page.Page, Grid) @property def userdescription(self): """User friendly description.""" s = self.settings return "%(rows)i rows, %(columns)i columns" % s def _recalcPositions(self): """(internal) recalculate the positions of the children.""" # class to handle management ge = _gridengine(self.settings.columns, self.settings.rows) # copy children, and remove any which are axes children = [ c for c in self.children if not c.isaxis ] child_dimensions = {} child_posns = {} for c in children: dims = (1, 1) child_dimensions[c] = dims child_posns[c] = ge.add(*dims) nocols, norows = ge.getAllocedDimensions() self.dims = (nocols, norows) # exit if there aren't any children if nocols == 0 or norows == 0: return # get total scaling factors for cols scalecols = list(self.settings.scaleCols[:nocols]) scalecols += [1.]*(nocols-len(scalecols)) totscalecols = sum(scalecols) if totscalecols == 0.: totscalecols = 1. # fractional starting positions of columns last = 0. startcols = [last] for scale in scalecols: last += scale/totscalecols startcols.append(last) # similarly get total scaling factors for rows scalerows = list(self.settings.scaleRows[:norows]) scalerows += [1.]*(norows-len(scalerows)) totscalerows = sum(scalerows) if totscalerows == 0.: totscalerows = 1. # fractional starting positions of rows last = 0. startrows = [last] for scale in scalerows: last += scale/totscalerows startrows.append(last) # iterate over children, and modify positions self.childpositions.clear() for child in children: dims = child_dimensions[child] pos = child_posns[child] self.childpositions[child] = ( ( pos[0], pos[1] ), ( startcols[pos[0]], startrows[pos[1]], startcols[pos[0]+dims[0]], startrows[pos[1]+dims[1]] ), ) def actionZeroMargins(self): """Zero margins of plots inside this grid.""" operations = [] for c in self.children: if isinstance(c, graph.Graph): s = c.settings for v in ('leftMargin', 'topMargin', 'rightMargin', 'bottomMargin'): operations.append( document.OperationSettingSet(s.get(v), '0cm') ) self.document.applyOperation( document.OperationMultiple(operations, descr='zero margins') ) def _drawChild(self, phelper, child, bounds, parentposn): """Draw child at correct position, with correct bounds.""" # default positioning coutbound = newbounds = parentposn if child in self.childpositions: intmargin = self.settings.get('internalMargin').convert(phelper) cidx, cpos = self.childpositions[child] # calculate size after margins dx = bounds[2]-bounds[0] dy = bounds[3]-bounds[1] marx = intmargin*max(0, self.dims[0]-1) mary = intmargin*max(0, self.dims[1]-1) if dx > marx and dy > mary: dx -= marx dy -= mary else: # margins too big intmargin = 0 # bounds for child newbounds = [ bounds[0]+dx*cpos[0]+intmargin*cidx[0], bounds[1]+dy*cpos[1]+intmargin*cidx[1], bounds[0]+dx*cpos[2]+intmargin*cidx[0], bounds[1]+dy*cpos[3]+intmargin*cidx[1] ] # bounds the axes can spread into coutbound = list(newbounds) # adjust outer bounds to half the internal margin space if cidx[0] > 0: coutbound[0] -= intmargin/2. if cidx[1] > 0: coutbound[1] -= intmargin/2. if cidx[0] < self.dims[0]-1: coutbound[2] += intmargin/2. if cidx[1] < self.dims[1]-1: coutbound[3] += intmargin/2. # work out bounds for graph in box # this is the space available for axes, etc # FIXME: should consider case if no graphs to side if cidx[0] == 0: coutbound[0] = parentposn[0] if cidx[1] == 0: coutbound[1] = parentposn[1] if cidx[0] == self.dims[0]-1: coutbound[2] = parentposn[2] if cidx[1] == self.dims[1]-1: coutbound[3] = parentposn[3] # draw widget child.draw(newbounds, phelper, outerbounds=coutbound) def getMargins(self, painthelper): """Use settings to compute margins.""" s = self.settings return ( s.get('leftMargin').convert(painthelper), s.get('topMargin').convert(painthelper), s.get('rightMargin').convert(painthelper), s.get('bottomMargin').convert(painthelper) ) def draw(self, parentposn, phelper, outerbounds=None): """Draws the widget's children.""" s = self.settings # if the contents have been modified, recalculate the positions dimensions = (s.columns, s.rows) scalings = (s.scaleRows, s.scaleCols) if ( self.children != self.lastchildren or self.lastdimensions != dimensions or self.lastscalings != scalings ): self._recalcPositions() self.lastchildren = list(self.children) self.lastdimensions = dimensions self.lastscalings = scalings bounds = self.computeBounds(parentposn, phelper) maxbounds = self.computeBounds(parentposn, phelper, withmargin=False) painter = phelper.painter(self, bounds) # controls for adjusting grid margins phelper.setControlGraph(self,[ controlgraph.ControlMarginBox(self, bounds, maxbounds, phelper)]) with painter: for child in self.children: if not child.isaxis: self._drawChild(phelper, child, bounds, parentposn) # do not call widget.Widget.draw, do not collect 200 pounds pass def updateControlItem(self, cgi): """Grid resized or moved - call helper routine to move self.""" cgi.setWidgetMargins() # allow the factory to instantiate a grid document.thefactory.register( Grid ) veusz-1.21.1/veusz/widgets/polygon.py0000664000175000017500000000674712237406466016045 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division from .. import document from .. import setting from .. import qtall as qt4 from .. import utils from . import plotters def _(text, disambiguation=None, context='Polygon'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Polygon(plotters.FreePlotter): """For plotting polygons.""" typename = 'polygon' allowusercreeation = True description = _('Plot a polygon') def __init__(self, parent, name=None): """Initialise object, setting axes.""" plotters.FreePlotter.__init__(self, parent, name=name) @classmethod def addSettings(klass, s): """Construct list of settings.""" plotters.FreePlotter.addSettings(s) s.add( setting.Line('Line', descr = _('Line around polygon'), usertext = _('Line')), pixmap = 'settings_plotline' ) s.add( setting.BrushExtended('Fill', descr = _('Fill within polygon'), usertext = _('Fill')), pixmap = 'settings_plotfillbelow' ) def draw(self, posn, phelper, outerbounds=None): """Plot the data on a plotter.""" s = self.settings # exit if hidden if s.hide: return # get points in plotter coordinates xp, yp = self._getPlotterCoords(posn) if xp is None or yp is None: # we can't calculate coordinates return x1, y1, x2, y2 = posn cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) painter = phelper.painter(self, posn, clip=cliprect) with painter: pen = s.Line.makeQPenWHide(painter) pw = pen.widthF()*2 lineclip = qt4.QRectF( qt4.QPointF(x1-pw, y1-pw), qt4.QPointF(x2+pw, y2+pw) ) # this is a hack as we generate temporary fake datasets path = qt4.QPainterPath() for xvals, yvals in document.generateValidDatasetParts( document.Dataset(xp), document.Dataset(yp)): poly = qt4.QPolygonF() utils.addNumpyToPolygonF(poly, xvals.data, yvals.data) clippedpoly = qt4.QPolygonF() utils.polygonClip(poly, lineclip, clippedpoly) path.addPolygon(clippedpoly) path.closeSubpath() utils.brushExtFillPath(painter, s.Fill, path, stroke=pen) # allow the factory to instantiate this document.thefactory.register( Polygon ) veusz-1.21.1/veusz/widgets/axisfunction.py0000644000175000017500000004545712327177747017076 0ustar jssjss# Copyright (C) 2013 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## '''An axis based on a function of another axis.''' from __future__ import division import numpy as N from ..compat import crange, cstr from .. import qtall as qt4 from .. import setting from .. import document from . import axis def _(text, disambiguation=None, context='FunctionAxis'): '''Translate text.''' return qt4.QCoreApplication.translate(context, text, disambiguation) class AxisError(RuntimeError): pass class FunctionError(AxisError): pass def solveFunction(function, vals, mint=None, maxt=None): '''Solve a function for a list of values (vals), if we don't know where the solution lies. function is a function to call. This tries a range of possible input values, and uses binary search to refine the solution. mint and maxt are the bounds to use when solving ''' xvals = N.array( ( -1e90, -1e70, -1e50, -1e40, -1e30, -1e20, -1e10, -1e8, -1e6, -1e5, -1e4, -1e3, -1e2, -1e1, -4e0, -2e0, -1e0, -1e-1, -1e-2, -1e-3, -1e-4, -1e-6, -1e-8, -1e-10, -1e-12, -1e-14, -1e-18, -1e-22, -1e-26, -1e-30, -1e-34, -1e-40, -1e-50, -1e-70, -1e-90, 0, 1e-90, 1e-70, 1e-50, 1e-40, 1e-34, 1e-30, 1e-26, 1e-22, 1e-18, 1e-14, 1e-12, 1e-10, 1e-8, 1e-6, 1e-4, 1e-3, 1e-2, 1e-1, 1e0, 2e0, 4e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e8, 1e10, 1e20, 1e30, 1e40, 1e50, 1e70, 1e90 )) if mint is not None: xvals = N.hstack(( mint, xvals[xvals > mint] )) if maxt is not None: xvals = N.hstack(( xvals[xvals < maxt], maxt )) # yvalue in correct shape yvals = function(xvals) + N.zeros(len(xvals)) anynan = N.any( N.isnan(yvals) ) if anynan: raise FunctionError(_('Invalid regions in function ' '(try setting minimum or maximum t)')) # remove any infinite regions f = N.isfinite(yvals) xfilt = xvals[f] yfilt = yvals[f] if len(yfilt) < 2: raise FunctionError(_('Solutions to equation cannot be found')) # check for monotonicity delta = yfilt[1:] - yfilt[:-1] pos, neg = N.all(delta >= 0), N.all(delta <= 0) if not (pos or neg): raise FunctionError(_('Not a monotonic function ' '(try setting minimum or maximum t)')) if pos and neg: raise FunctionError(_('Constant function')) # easier if the values are increasing only if neg: yfilt = yfilt[::-1] xfilt = xfilt[::-1] # do binary search for each input value out = [] for thisval in vals: # renorm to zero ydelta = yfilt - thisval # solution is between this and the next idx = N.searchsorted(ydelta, 0.) if idx == 0: if ydelta[0] == 0.: # work around value being at start of array idx = 1 else: raise AxisError(_('No solution found')) elif idx == len(ydelta): raise AxisError(_('No solution found')) x1, x2 = xfilt[idx-1], xfilt[idx] y1, y2 = ydelta[idx-1], ydelta[idx] # binary search tol = abs(1e-6 * thisval) for i in crange(30): # print x1, y1, "->", x2, y2 if abs(y1) <= tol and abs(y1) < abs(y2): x2, y2 = x1, y1 break # found solution if abs(y2) <= tol: x1, y1 = x2, y2 break # found solution if y1 == y2 or ((y1<0) and (y2<0)) or ((y1>0) and (y2>0)): raise AxisError(_('No solution found')) ### This is a bit faster, but bisection is simpler # xv = N.linspace(x1, x2, num=100) # yv = function(xv) + xv*0. - thisval # idx = N.searchsorted(yv, 0.) # if idx == 0: # idx = 1 # x1 = xv[idx-1] # y1 = yv[idx-1] # x2 = xv[idx] # y2 = yv[idx] x3 = 0.5*(x1+x2) y3 = function(x3) - thisval if not N.isfinite(y3): raise AxisError(_('Non-finite value encountered')) if y3 < 0: x1 = x3 y1 = y3 else: x2 = x3 y2 = y3 out.append(0.5*(x1+x2)) return out class AxisFunction(axis.Axis): '''An axis using an function of another axis.''' typename = 'axis-function' description = 'An axis based on a function of the values of another axis' def __init__(self, *args, **argsv): axis.Axis.__init__(self, *args, **argsv) self.cachedfuncobj = None self.cachedbounds = None self.funcchangeset = -1 self.boundschangeset = -1 if type(self) == AxisFunction: self.readDefaults() @classmethod def addSettings(klass, s): '''Construct list of settings.''' axis.Axis.addSettings(s) s.add( setting.BoolSwitch( 'linked', False, settingsfalse=('min', 'max'), settingstrue=('linkedaxis',), descr=_('Link axis to another axis'), usertext=_('Linked') ), 0 ) s.add( setting.Str('function', 't', descr=_('Monotonic function (use t as variable)'), usertext=_('Function')), 1 ) s.add( setting.Axis('linkedaxis', '', 'both', descr = _('Axis which this axis is based on'), usertext=_('Linked axis')), 6 ) s.add( setting.FloatOrAuto('mint', 'Auto', descr=_('Minimum value of t or Auto'), usertext=('Min t')), 7 ) s.add( setting.FloatOrAuto('maxt', 'Auto', descr=_('Maximum value of t or Auto'), usertext=('Max t')), 8 ) s.get('autoRange').hidden = True s.get('autoRange').newDefault('exact') @property def userdescription(self): """User friendly description.""" s = self.settings return _("axis='%s', function='%s'") % (s.linkedaxis, s.function) def logError(self, ex): '''Write error message to document log for exception ex.''' self.document.log( _("Error in axis-function (%s): '%s'") % ( self.settings.function, cstr(ex))) def getMinMaxT(self): '''Get minimum and maximum t.''' mint = self.settings.mint if mint == 'Auto': mint = None maxt = self.settings.maxt if maxt == 'Auto': maxt = None return mint, maxt def getFunction(self): '''Check whether function needs to be compiled.''' if self.funcchangeset == self.document.changeset: return self.cachedfuncobj self.funcchangeset = self.document.changeset compiled = self.document.compileCheckedExpression( self.settings.function.strip()) if compiled is None: self.cachedfuncobj = None else: # a python function for doing the evaluation and handling # errors env = self.document.eval_context.copy() def function(t): env['t'] = t try: return eval(compiled, env) except Exception as e: self.logError(e) return N.nan + t self.cachedfuncobj = function mint, maxt = self.getMinMaxT() try: solveFunction(function, [0.], mint=mint, maxt=maxt) except FunctionError as e: self.logError(e) self.cachedfuncobj = None except AxisError: pass return self.cachedfuncobj def invertFunctionVals(self, vals): '''Convert values which are a function of fn and compute t.''' fn = self.getFunction() if fn is None: return None mint, maxt = self.getMinMaxT() try: return solveFunction(fn, vals, mint=mint, maxt=maxt) except Exception as e: self.logError(e) return None def lookupAxis(self, axisname): '''Find widget associated with axisname.''' w = self.parent while w: for c in w.children: if ( c.name == axisname and c.isaxis and c is not self ): return c w = w.parent return None def isLinked(self): '''Is this axis linked to another?''' return self.settings.linked def getLinkedAxis(self): '''Get the widget for the linked axis.''' if not self.settings.linked: return None linked = self.lookupAxis(self.settings.linkedaxis) if linked is self: return None return linked def computePlottedRange(self, force=False): '''Use other axis to compute range.''' if self.docchangeset == self.document.changeset and not force: return therange = None linked = self.getLinkedAxis() fn = self.getFunction() if linked is not None and fn is not None: # compute our range from the linked axis linked.computePlottedRange() try: therange = fn(N.array(linked.plottedrange)) * N.ones(2) except Exception as e: self.logError(e) if not N.all( N.isfinite(therange) ): therange = None axis.Axis.computePlottedRange(self, force=force, overriderange=therange) def _orderCoordinates(self): '''Put coordinates in correct order for linear interpolation.''' if len(self.graphcoords) == 0: self.graphcoords = None return if self.graphcoords[0] > self.graphcoords[-1]: # order must be increasing (for forward conversion) self.graphcoords = self.graphcoords[::-1] self.pixcoords = self.pixcoords[::-1] if self.pixcoords_inv[0] > self.pixcoords_inv[-1]: # likewise increasing order for inverse self.pixcoords_inv = self.pixcoords_inv[::-1] self.graphcoords_inv = self.graphcoords_inv[::-1] def _updateLinkedAxis(self, bounds, fraccoords): '''Calculate coordinate conversion for linked axes.''' link = self.getLinkedAxis() if link is None: return # To do the inverse calculation, we define a grid of pixel # values. We need some sensitivity outside the axis range to # get angles of lines correct. We start with fractional graph # coordinates to translate to the other axis coordinates. # coordinate values on the other axis try: linkwidth = link.coordParr2-link.coordParr1 linkorigin = link.coordParr1 linkbounds = link.currentbounds except AttributeError: # if hasn't been initialised return # lookup what pixels are on linked axis in values linkpixcoords = fraccoords*linkwidth + linkorigin linkgraphcoords = link.plotterToGraphCoords(linkbounds, linkpixcoords) # flip round if coordinates reversed if linkgraphcoords[0] > linkgraphcoords[-1]: linkgraphcoords = linkgraphcoords[::-1] linkpixcoords = linkpixcoords[::-1] fraccoords = fraccoords[::-1] # Chop to range. This is rather messy as there are several # sets of coordinates to extend and chop: graph coordinates, # pixel coordinates and fractional coordinates. mint, maxt = self.getMinMaxT() if mint is not None and mint > linkgraphcoords[0]: mintpix = link.graphToPlotterCoords(linkbounds, N.array([mint])) sel = linkgraphcoords > mint linkgraphcoords = N.hstack((mint, linkgraphcoords[sel])) linkpixcoords = N.hstack((mintpix, linkpixcoords[sel])) frac = (mintpix - linkorigin) / linkwidth fraccoords = N.hstack((frac, fraccoords[sel])) if maxt is not None and maxt < linkgraphcoords[-1]: maxtpix = link.graphToPlotterCoords(linkbounds, N.array([maxt])) sel = linkgraphcoords < maxt linkgraphcoords = N.hstack((linkgraphcoords[sel], maxt)) linkpixcoords = N.hstack((linkpixcoords[sel], maxtpix)) frac = (maxtpix - linkorigin) / linkwidth fraccoords = N.hstack((fraccoords[sel], frac)) try: ourgraphcoords = self.getFunction()(linkgraphcoords) except: return deltas = ourgraphcoords[1:] - ourgraphcoords[:-1] pos = N.all(deltas >= 0.) neg = N.all(deltas <= 0.) if (not pos and not neg) or (pos and neg): self.logError(_('Not a monotonic function')) return # Select only finite vals. We store _inv coords separately # as linear interpolation requires increasing values. f = N.isfinite(linkgraphcoords + ourgraphcoords) self.graphcoords = self.graphcoords_inv = ourgraphcoords[f] # This is true if the axis is plotting on the same graph in # the same direction. If this is the case, use our coordinates # directly. if ( link.settings.direction == self.settings.direction and link.currentbounds == bounds ): self.pixcoords = self.pixcoords_inv = linkpixcoords[f] else: # convert fractions to our coordinates self.pixcoords = self.pixcoords_inv = ( fraccoords[f]*(self.coordParr2-self.coordParr1)+self.coordParr1) # put output coordinates in correct order self._orderCoordinates() def _updateFreeAxis(self, bounds, fraccoords): '''Calculate coordinates for a free axis.''' self.computePlottedRange() trange = self.invertFunctionVals(N.array(self.plottedrange)) if trange is None: return tvals = fraccoords*(trange[1]-trange[0]) + trange[0] if tvals[0] > tvals[-1]: # simplifies below if t is in order tvals = tvals[::-1] fraccoords = fraccoords[::-1] # limit t to the range if given mint, maxt = self.getMinMaxT() if mint is not None and mint > tvals[0]: sel = tvals > mint minfrac = (mint - trange[0]) / (trange[1]-trange[0]) fraccoords = N.hstack( (minfrac, fraccoords[sel]) ) tvals = N.hstack( (mint, tvals[sel]) ) if maxt is not None and maxt < tvals[-1]: sel = tvals < maxt maxfrac = (maxt - trange[0]) / (trange[1]-trange[0]) fraccoords = N.hstack( (fraccoords[sel], maxfrac) ) tvals = N.hstack( (tvals[sel], maxt) ) try: ourgraphcoords = self.getFunction()(tvals) except Exception: return deltas = ourgraphcoords[1:] - ourgraphcoords[:-1] pos = N.all(deltas >= 0.) neg = N.all(deltas <= 0.) if (not pos and not neg) or (pos and neg): self.logError(_('Not a monotonic function')) return # Select only finite vals. We store _inv coords separately # as linear interpolation requires increasing values. f = N.isfinite(ourgraphcoords) self.graphcoords = self.graphcoords_inv = ourgraphcoords[f] self.pixcoords = self.pixcoords_inv = ( fraccoords[f]*(self.coordParr2-self.coordParr1) + self.coordParr1 ) # put output coordinates in correct order self._orderCoordinates() def updateAxisLocation(self, bounds, otherposition=None, lowerupperposition=None): '''Calculate conversion from pixels to axis values.''' axis.Axis.updateAxisLocation(self, bounds, otherposition=otherposition, lowerupperposition=lowerupperposition) if ( self.boundschangeset == self.document.changeset and bounds == self.cachedbounds ): # don't recalculate unless document updated or bounds changes return self.cachedbounds = list(bounds) self.boundschangeset = self.document.changeset self.graphcoords = None # fractional coordinate grid to evaluate functions fraccoords = N.hstack(( N.linspace(-10., -2.0, 5, endpoint=False), N.linspace(-2.0, -0.2, 25, endpoint=False), N.linspace(-0.2, +1.2, 250, endpoint=False), N.linspace(+1.2, +3.0, 25, endpoint=False), N.linspace(+3.0, +11., 5, endpoint=False) )) if self.isLinked(): self._updateLinkedAxis(bounds, fraccoords) else: self._updateFreeAxis(bounds, fraccoords) def _linearInterpolWarning(self, vals, xcoords, ycoords): '''Linear interpolation, giving out of bounds warning.''' if any(vals < xcoords[0]) or any(vals > xcoords[-1]): self.document.log( _('Warning: values exceed bounds in axis-function')) return N.interp(vals, xcoords, ycoords) def _graphToPlotter(self, vals): '''Override normal axis graph->plotter coords to do lookup.''' if self.graphcoords is None: return axis.Axis._graphToPlotter(self, vals) else: return self._linearInterpolWarning( vals, self.graphcoords, self.pixcoords) def plotterToGraphCoords(self, bounds, vals): '''Override normal axis plotter->graph coords to do lookup.''' if self.graphcoords is None: return axis.Axis.plotterToGraphCoords(self, bounds, vals) else: self.updateAxisLocation(bounds) return self._linearInterpolWarning( vals, self.pixcoords_inv, self.graphcoords_inv) # allow the factory to instantiate the widget document.thefactory.register( AxisFunction ) veusz-1.21.1/veusz/widgets/nonorthfunction.py0000664000175000017500000001604412237406466017602 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## '''Non orthogonal function plotting.''' from __future__ import division import numpy as N from ..compat import cstr from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from . import pickable from .nonorthgraph import NonOrthGraph, FillBrush from .widget import Widget def _(text, disambiguation=None, context='NonOrthFunction'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class NonOrthFunction(Widget): '''Widget for plotting a function on a non-orthogonal plot.''' typename = 'nonorthfunc' allowusercreation = True description = _('Plot a function on graphs with non-orthogonal axes') def __init__(self, parent, name=None): '''Initialise plotter.''' Widget.__init__(self, parent, name=name) if type(self) == NonOrthFunction: self.readDefaults() @classmethod def addSettings(klass, s): '''Settings for widget.''' Widget.addSettings(s) s.add( setting.Str('function', 'a', descr=_('Function expression'), usertext=_('Function')) ) s.add( setting.Choice('variable', ['a', 'b'], 'a', descr=_('Variable the function is a function of'), usertext=_('Variable')) ) s.add(setting.FloatOrAuto('min', 'Auto', descr=_('Minimum value at which to plot function'), usertext=_('Min'))) s.add(setting.FloatOrAuto('max', 'Auto', descr=_('Maximum value at which to plot function'), usertext=_('Max'))) s.add( setting.Line('PlotLine', descr = _('Plot line settings'), usertext = _('Plot line')), pixmap = 'settings_plotline' ) s.add( FillBrush('Fill1', descr = _('Fill settings (1)'), usertext = _('Area fill 1')), pixmap = 'settings_plotfillbelow' ) s.add( FillBrush('Fill2', descr = _('Fill settings (2)'), usertext = _('Area fill 2')), pixmap = 'settings_plotfillbelow' ) s.add( setting.Int('steps', 50, descr = _('Number of steps to evaluate the function' ' over'), usertext=_('Steps'), formatting=True), 0 ) @classmethod def allowedParentTypes(klass): return (NonOrthGraph,) @property def userdescription(self): return _("function='%s'") % self.settings.function def initEnviron(self): '''Set up function environment.''' return self.document.eval_context.copy() def logEvalError(self, ex): '''Write error message to document log for exception ex.''' self.document.log( "Error evaluating expression in function widget '%s': '%s'" % ( self.name, cstr(ex))) def getFunctionPoints(self): '''Get points for plotting function. Return (apts, bpts) ''' # get range of variable in expression s = self.settings crange = self.parent.coordRanges()[ {'a': 0, 'b': 1}[s.variable] ] if s.min != 'Auto': crange[0] = s.min if s.max != 'Auto': crange[1] = s.max steps = max(2, s.steps) # input values for function invals = ( N.arange(steps)*(1./(steps-1))*(crange[1]-crange[0]) + crange[0] ) # do evaluation env = self.initEnviron() env[s.variable] = invals comp = self.document.compileCheckedExpression(s.function) if comp is None: return N.array([]), N.array([]) try: vals = eval(comp, env) + invals*0. except Exception as e: self.logEvalError(e) vals = invals = N.array([]) # return points if s.variable == 'a': return invals, vals else: return vals, invals def updateDataRanges(self, inrange): '''Update ranges of data given function.''' def _pickable(self): apts, bpts = self.getFunctionPoints() px, py = self.parent.graphToPlotCoords(apts, bpts) if self.settings.variable == 'a': labels = ('a', 'b(a)') else: labels = ('a(b)', 'b') return pickable.GenericPickable( self, labels, (apts, bpts), (px, py) ) def pickPoint(self, x0, y0, bounds, distance='radial'): return self._pickable().pickPoint(x0, y0, bounds, distance) def pickIndex(self, oldindex, direction, bounds): return self._pickable().pickIndex(oldindex, direction, bounds) def draw(self, parentposn, phelper, outerbounds=None): '''Plot the function on a plotter.''' posn = self.computeBounds(parentposn, phelper) s = self.settings # exit if hidden if s.hide: return apts, bpts = self.getFunctionPoints() px, py = self.parent.graphToPlotCoords(apts, bpts) x1, y1, x2, y2 = posn cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) painter = phelper.painter(self, posn) with painter: self.parent.setClip(painter, posn) # plot line painter.setBrush(qt4.QBrush()) painter.setPen( s.PlotLine.makeQPenWHide(painter) ) for x, y in utils.validLinePoints(px, py): if not s.Fill1.hide: self.parent.drawFillPts(painter, s.Fill1, cliprect, x, y) if not s.Fill2.hide: self.parent.drawFillPts(painter, s.Fill2, cliprect, x, y) if not s.PlotLine.hide: p = qt4.QPolygonF() utils.addNumpyToPolygonF(p, x, y) painter.setBrush(qt4.QBrush()) painter.setPen( s.PlotLine.makeQPen(painter) ) utils.plotClippedPolyline(painter, cliprect, p) document.thefactory.register( NonOrthFunction ) veusz-1.21.1/veusz/widgets/key.py0000664000175000017500000004433012346112416015122 0ustar jssjss# key symbol plotting # Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division import math from ..compat import crange, citems from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from . import widget from . import controlgraph def _(text, disambiguation=None, context='Key'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) ############################################################################# # classes for controlling key position interactively class ControlKey(object): """Control the position of a key on a plot.""" def __init__( self, widget, parentposn, boxposn, boxdims, textheight ): """widget is widget to adjust parentposn: posn of parent on plot xpos, ypos: position of key width. height: size of key textheight: """ self.widget = widget self.parentposn = tuple(parentposn) self.posn = tuple(boxposn) self.dims = tuple(boxdims) self.textheight = textheight def createGraphicsItem(self): return _GraphControlKey(self) class _GraphControlKey(qt4.QGraphicsRectItem): """The graphical rectangle which is dragged around to reposition the key.""" def __init__(self, params): qt4.QGraphicsRectItem.__init__(self, params.posn[0], params.posn[1], params.dims[0], params.dims[1]) self.params = params self.setCursor(qt4.Qt.SizeAllCursor) self.setZValue(1.) self.setFlag(qt4.QGraphicsItem.ItemIsMovable) self.highlightpen = qt4.QPen(qt4.Qt.red, 2, qt4.Qt.DotLine) pposn, dims = params.parentposn, params.dims th = params.textheight # special places on the plot xposn = { 'left': pposn[0] + th, 'centre': pposn[0] + 0.5*(pposn[2]-pposn[0]-dims[0]), 'right': pposn[2] - th - dims[0] } yposn = { 'top': pposn[1] + th, 'centre': pposn[1] + 0.5*(pposn[3]-pposn[1]-dims[1]), 'bottom': pposn[3] - th - dims[1] } # these are special places where the key is aligned self.highlightpoints = {} for xname, xval in citems(xposn): for yname, yval in citems(yposn): self.highlightpoints[(xname, yname)] = qt4.QPointF(xval, yval) self.updatePen() def checkHighlight(self): """Check to see whether box is over hightlight area. Returns (x, y) name or None if not.""" rect = self.rect() rect.translate(self.pos()) highlight = None highlightrect = qt4.QRectF(rect.left()-10, rect.top()-10, 20, 20) for name, point in citems(self.highlightpoints): if highlightrect.contains(point): highlight = name break return highlight def updatePen(self): """Update color of rectangle if it is over a hightlight area.""" if self.checkHighlight(): self.setPen(self.highlightpen) else: self.setPen(controlgraph.controlLinePen()) def mouseMoveEvent(self, event): """Set correct pen for box.""" qt4.QGraphicsRectItem.mouseMoveEvent(self, event) self.updatePen() def mouseReleaseEvent(self, event): """Update widget with position.""" qt4.QGraphicsRectItem.mouseReleaseEvent(self, event) highlight = self.checkHighlight() if highlight: # in a highlight zone so use highlight zone name to set position hp, vp = highlight hm, vm = 0., 0. else: # calculate the position of the box to work out Manual fractions rect = self.rect() rect.translate(self.pos()) pposn = self.params.parentposn hp, vp = 'manual', 'manual' hm = (rect.left() - pposn[0]) / (pposn[2] - pposn[0]) vm = (pposn[3] - rect.bottom()) / (pposn[3] - pposn[1]) # update widget with positions s = self.params.widget.settings operations = ( document.OperationSettingSet(s.get('horzPosn'), hp), document.OperationSettingSet(s.get('vertPosn'), vp), document.OperationSettingSet(s.get('horzManual'), hm), document.OperationSettingSet(s.get('vertManual'), vm), ) self.params.widget.document.applyOperation( document.OperationMultiple(operations, descr=_('move key'))) ############################################################################ class Key(widget.Widget): """Key on graph.""" typename = 'key' description = _('Plot key') allowusercreation = True def __init__(self, parent, name=None): widget.Widget.__init__(self, parent, name=name) if type(self) == Key: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" widget.Widget.addSettings(s) s.add( setting.Text('Text', descr = _('Text settings'), usertext=_('Text')), pixmap = 'settings_axislabel' ) s.add( setting.KeyBrush('Background', descr = _('Key background fill'), usertext=_('Background')), pixmap = 'settings_bgfill' ) s.add( setting.Line('Border', descr = _('Key border line'), usertext=_('Border')), pixmap = 'settings_border' ) s.add( setting.Str('title', '', descr=_('Key title text'), usertext=_('Title')) ) s.add( setting.AlignHorzWManual( 'horzPosn', 'right', descr = _('Horizontal key position'), usertext=_('Horz posn'), formatting=True) ) s.add( setting.AlignVertWManual( 'vertPosn', 'bottom', descr = _('Vertical key position'), usertext=_('Vert posn'), formatting=True) ) s.add( setting.Distance('keyLength', '1cm', descr = _('Length of line to show in sample'), usertext=_('Key length'), formatting=True) ) s.add( setting.AlignVert( 'keyAlign', 'top', descr = _('Alignment of key symbols relative to text'), usertext = _('Key alignment'), formatting = True) ) s.add( setting.Float( 'horzManual', 0., descr = _('Manual horizontal fractional position'), usertext=_('Horz manual'), formatting=True) ) s.add( setting.Float( 'vertManual', 0., descr = _('Manual vertical fractional position'), usertext=_('Vert manual'), formatting=True) ) s.add( setting.Float( 'marginSize', 1., minval = 0., descr = _('Width of margin in characters'), usertext=_('Margin size'), formatting=True) ) s.add( setting.Int( 'columns', 1, descr = _('Number of columns in key'), usertext = _('Columns'), minval = 1, maxval = 100, formatting = True) ) s.add( setting.Bool( 'symbolswap', False, descr=_('Put key symbol on right and text on left'), usertext=_('Swap symbol'), formatting=True) ) @classmethod def allowedParentTypes(klass): from . import graph return (graph.Graph,) @staticmethod def _layoutChunk(entries, start, dims): """Layout the entries into the given box, starting at start""" row, col = start numrows, numcols = dims colstats = [0] * numcols layout = [] for (plotter, num, lines) in entries: if row+lines > numrows: # this item doesn't fit in this column, so move to the next col += 1 row = 0 if col >= numcols: # this layout failed, suggest expanding the box by 1 row return ([], [], numrows+1) if lines > numrows: # this layout failed, suggest expanding the box to |lines| return ([], [], lines) # col -> yp, row -> xp layout.append( (plotter, num, col, row, lines) ) row += lines colstats[col] += 1 return (layout, colstats, numrows) def _layout(self, entries, totallines): """Layout the items, trying to keep the box as small as possible while still filling the columns""" maxcols = self.settings.columns numcols = min(maxcols, max(len(entries), 1)) if not entries: return (list(), (0, 0)) # start with evenly-sized rows and expand to fit numrows = totallines // numcols layout = [] while not layout: # try to do a first cut of the layout, and expand the box until # everything fits (layout, colstats, newrows) = self._layoutChunk(entries, (0, 0), (numrows, numcols)) if not layout: numrows = newrows # ok, we've got a layout where everything fits, now pull items right # to fill the remaining columns, if need be while colstats[-1] == 0: # shift 1 item to the right, up to the first column that has # excess items meanoccupation = max(1, sum(colstats)/numcols) # loop until we find a victim item which can be safely moved victimcol = numcols while True: # find the right-most column with excess occupation number for i in reversed(crange(victimcol)): if colstats[i] > meanoccupation: victimcol = i break # find the last item in the victim column victim = 0 for i in reversed(crange(len(layout))): if layout[i][2] == victimcol: victim = i break # try to relayout with the victim item shoved to the next column (newlayout, newcolstats, newrows) = self._layoutChunk(entries[victim:], (0, victimcol+1), (numrows, numcols)) if newlayout: # the relayout worked, so accept it layout = layout[0:victim] + newlayout colstats[victimcol] -= 1 del colstats[victimcol+1:] colstats += newcolstats[victimcol+1:] break # if we've run out of potential victims, just return what we have if victimcol == 0: return (layout, (numrows, numcols)) return (layout, (numrows, numcols)) def draw(self, parentposn, phelper, outerbounds = None): """Plot the key on a plotter.""" s = self.settings if s.hide: return painter = phelper.painter(self, parentposn) with painter: self._doDrawing(painter, phelper, parentposn) def _doDrawing(self, painter, phelper, parentposn): """Do the actual drawing.""" s = self.settings font = s.get('Text').makeQFont(painter) painter.setFont(font) height = utils.FontMetrics(font, painter.device()).height() margin = s.marginSize * height showtext = not s.Text.hide # total number of layout lines required totallines = 0 # reserve space for the title titlewidth, titleheight = 0, 0 if s.title != '': titlefont = qt4.QFont(font) titlefont.setPointSize(max(font.pointSize() * 1.2, font.pointSize() + 2)) titlewidth, titleheight = utils.Renderer(painter, titlefont, 0, 0, s.title).getDimensions() titleheight += 0.5*margin # maximum width of text required maxwidth = 1 entries = [] # iterate over children and find widgets which are suitable for c in self.parent.children: try: num = c.getNumberKeys() except AttributeError: continue if not c.settings.hide: # add an entry for each key entry for each widget for i in crange(num): lines = 1 if showtext: w, h = utils.Renderer(painter, font, 0, 0, c.getKeyText(i)).getDimensions() maxwidth = max(maxwidth, w) lines = max(1, math.ceil(h/height)) totallines += lines entries.append( (c, i, lines) ) # layout the box layout, (numrows, numcols) = self._layout(entries, totallines) # width of key part of key symbolwidth = s.get('keyLength').convert(painter) keyswidth = ( (maxwidth + height + symbolwidth)*numcols + height*(numcols-1) ) # total width of box totalwidth = max(keyswidth, titlewidth) totalheight = numrows * height + titleheight if not s.Border.hide: totalwidth += 2*margin totalheight += margin # work out horizontal position h = s.horzPosn if h == 'left': x = parentposn[0] + height elif h == 'right': x = parentposn[2] - height - totalwidth elif h == 'centre': x = ( parentposn[0] + 0.5*(parentposn[2] - parentposn[0] - totalwidth) ) elif h == 'manual': x = parentposn[0] + (parentposn[2]-parentposn[0])*s.horzManual # work out vertical position v = s.vertPosn if v == 'top': y = parentposn[1] + height elif v == 'bottom': y = parentposn[3] - totalheight - height elif v == 'centre': y = ( parentposn[1] + 0.5*(parentposn[3] - parentposn[1] - totalheight) ) elif v == 'manual': y = ( parentposn[3] - (parentposn[3]-parentposn[1])*s.vertManual - totalheight ) # for controlgraph boxposn = (x, y) boxdims = (totalwidth, totalheight) # draw surrounding box boxpath = qt4.QPainterPath() boxpath.addRect(qt4.QRectF(x, y, totalwidth, totalheight)) if not s.Background.hide: utils.brushExtFillPath(painter, s.Background, boxpath) if not s.Border.hide: painter.strokePath(boxpath, s.get('Border').makeQPen(painter) ) y += margin*0.5 # center and draw the title if s.title: xpos = x + (totalwidth-titlewidth)/2 utils.Renderer(painter, titlefont, xpos, y, s.title, alignvert=1).render() y += titleheight # centres key below title x += (totalwidth-keyswidth)/2 textpen = s.get('Text').makeQPen() swap = s.symbolswap # plot dataset entries for (plotter, num, xp, yp, lines) in layout: xpos = x + xp*(maxwidth+2*height+symbolwidth) ypos = y + yp*height # plot key symbol painter.save() keyoffset = 0 if s.keyAlign == 'centre': keyoffset = (lines-1)*height/2.0 elif s.keyAlign == 'bottom': keyoffset = (lines-1)*height sx = xpos if swap: sx += maxwidth + height plotter.drawKeySymbol(num, painter, sx, ypos+keyoffset, symbolwidth, height) painter.restore() # write key text if showtext: painter.setPen(textpen) if swap: lx = xpos + maxwidth alignx = 1 else: lx = xpos + height + symbolwidth alignx = -1 utils.Renderer(painter, font, lx, ypos, plotter.getKeyText(num), alignx, 1).render() phelper.setControlGraph( self, [ControlKey(self, parentposn, boxposn, boxdims, height)] ) document.thefactory.register( Key ) veusz-1.21.1/veusz/widgets/__init__.py0000664000175000017500000000346512237406466016107 0ustar jssjss# Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Widgets are defined in this module.""" from .widget import Widget, Action from .axis import Axis from .axisbroken import AxisBroken from .axisfunction import AxisFunction from .graph import Graph from .grid import Grid from .plotters import GenericPlotter, FreePlotter from .pickable import PickInfo from .point import PointPlotter from .function import FunctionPlotter from .textlabel import TextLabel from .page import Page from .root import Root from .key import Key from .fit import Fit from .image import Image from .contour import Contour from .colorbar import ColorBar from .shape import Shape, BoxShape, Rectangle, Ellipse, ImageFile from .line import Line from .bar import BarPlotter from .polygon import Polygon from .vectorfield import VectorField from .boxplot import BoxPlot from .polar import Polar from .ternary import Ternary from .nonorthpoint import NonOrthPoint from .nonorthfunction import NonOrthFunction veusz-1.21.1/veusz/widgets/line.py0000664000175000017500000003003112237406466015264 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Plotting a line with arrowheads or labels.""" from __future__ import division import math import itertools import numpy as N from ..compat import czip from .. import qtall as qt4 from .. import setting from .. import document from .. import utils from . import controlgraph from . import plotters def _(text, disambiguation=None, context='Line'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Line(plotters.FreePlotter): """A line on the plot/graph.""" typename='line' description=_('Line or arrow') allowusercreation = True def __init__(self, parent, name=None): """Construct plotter.""" plotters.FreePlotter.__init__(self, parent, name=name) if type(self) == Line: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" plotters.FreePlotter.addSettings(s) s.add( setting.ChoiceSwitch( 'mode', ('length-angle', 'point-to-point'), 'length-angle', descr=_('Provide line position and length,angle or ' 'first and second points'), usertext=_('Mode'), formatting=False, showfn = lambda val: val == 'length-angle', settingstrue = ('length', 'angle'), settingsfalse = ('xPos2', 'yPos2'), ), 0) s.add( setting.DatasetExtended( 'length', [0.2], descr=_('List of fractional lengths, dataset or expression'), usertext=_('Lengths'), formatting=False), 4 ) s.add( setting.DatasetExtended( 'angle', [0.], descr=_('List of angle of lines, dataset or expression ' '(degrees)'), usertext=_('Angles'), formatting=False), 5 ) s.add( setting.DatasetExtended( 'xPos2', [1.], descr=_('List of fractional X coordinates, dataset or ' 'expression for point 2'), usertext=_('X positions 2'), formatting=False), 6 ) s.add( setting.DatasetExtended( 'yPos2', [1.], descr=_('List of fractional Y coordinates, dataset or ' 'expression for point 2'), usertext=_('Y positions 2'), formatting=False), 7 ) s.add( setting.Bool('clip', False, descr=_('Clip line to its container'), usertext=_('Clip'), formatting=True), 0 ) s.add( setting.Line('Line', descr = _('Line style'), usertext = _('Line')), pixmap = 'settings_plotline' ) s.add( setting.ArrowFill('Fill', descr = _('Arrow fill settings'), usertext = _('Arrow fill')), pixmap = 'settings_plotmarkerfill' ) s.add( setting.DistancePt('arrowSize', '5pt', descr = _('Size of arrow to plot'), usertext=_('Arrow size'), formatting=True), 0) s.add( setting.Arrow('arrowright', 'none', descr = _('Arrow to plot on right side'), usertext=_('Arrow right'), formatting=True), 0) s.add( setting.Arrow('arrowleft', 'none', descr = _('Arrow to plot on left side'), usertext=_('Arrow left'), formatting=True), 0) def _computeLinesLengthAngle(self, posn, lengthscaling): """Return set of lines to plot for length-angle.""" s = self.settings d = self.document # translate coordinates from axes or relative values xpos, ypos = self._getPlotterCoords(posn) # get lengths and angles of lines length = s.get('length').getFloatArray(d) angle = s.get('angle').getFloatArray(d) if None in (xpos, ypos, length, angle): return None length *= lengthscaling maxlen = max( len(xpos), len(ypos), len(length), len(angle) ) if maxlen > 1: if len(xpos) == 1: xpos = itertools.cycle(xpos) if len(ypos) == 1: ypos = itertools.cycle(ypos) if len(length) == 1: length = itertools.cycle(length) if len(angle) == 1: angle = itertools.cycle(angle) out = [] for v in czip(xpos, ypos, length, angle): # skip lines which have nans if N.all( N.isfinite(v) ): out.append(v) return out def _computeLinesPointToPoint(self, posn): """Return set of lines for point to point.""" # translate coordinates from axes or relative values xpos, ypos = self._getPlotterCoords(posn) xpos2, ypos2 = self._getPlotterCoords(posn, xsetting='xPos2', ysetting='yPos2') if None in (xpos, ypos, xpos2, ypos2): return None maxlen = max( len(xpos), len(ypos), len(xpos2), len(ypos2) ) if maxlen > 1: if len(xpos) == 1: xpos = itertools.cycle(xpos) if len(ypos) == 1: ypos = itertools.cycle(ypos) if len(xpos2) == 1: xpos2 = itertools.cycle(xpos2) if len(ypos2) == 1: ypos2 = itertools.cycle(ypos2) out = [] for v in czip(xpos, ypos, xpos2, ypos2): # skip nans again if N.all( N.isfinite(v) ): length = math.sqrt( (v[0]-v[2])**2 + (v[1]-v[3])**2 ) angle = math.atan2( v[3]-v[1], v[2]-v[0] ) / math.pi * 180. out.append( (v[0], v[1], length, angle) ) return out def draw(self, posn, phelper, outerbounds = None): """Plot the key on a plotter.""" s = self.settings d = self.document if s.hide: return # if a dataset is used, we can't use control items isnotdataset = ( not s.get('xPos').isDataset(d) and not s.get('yPos').isDataset(d) ) if s.mode == 'length-angle': isnotdataset = ( isnotdataset and not s.get('length').isDataset(d) and not s.get('angle').isDataset(d) ) else: isnotdataset = ( isnotdataset and not s.get('xPos2').isDataset(d) and not s.get('yPos2').isDataset(d) ) # now do the drawing clip = None if s.clip: clip = qt4.QRectF( qt4.QPointF(posn[0], posn[1]), qt4.QPointF(posn[2], posn[3]) ) painter = phelper.painter(self, posn, clip=clip) with painter: # adjustable positions for the lines arrowsize = s.get('arrowSize').convert(painter) # drawing settings for line if not s.Line.hide: painter.setPen( s.get('Line').makeQPen(painter) ) else: painter.setPen( qt4.QPen(qt4.Qt.NoPen) ) # settings for fill if not s.Fill.hide: painter.setBrush( s.get('Fill').makeQBrush() ) else: painter.setBrush( qt4.QBrush() ) # iterate over positions scaling = posn[2]-posn[0] if s.mode == 'length-angle': lines = self._computeLinesLengthAngle(posn, scaling) else: lines = self._computeLinesPointToPoint(posn) if lines is None: return controlgraphitems = [] for index, (x, y, l, a) in enumerate(lines): utils.plotLineArrow(painter, x, y, l, a, arrowsize=arrowsize, arrowleft=s.arrowleft, arrowright=s.arrowright) if isnotdataset: cgi = controlgraph.ControlLine( self, x, y, x + l*math.cos(a/180.*math.pi), y + l*math.sin(a/180.*math.pi)) cgi.index = index cgi.widgetposn = posn controlgraphitems.append(cgi) phelper.setControlGraph(self, controlgraphitems) def updateControlItem(self, cgi, pt1, pt2): """If control items are moved, update line.""" s = self.settings # calculate new position coordinate for item xpos, ypos = self._getGraphCoords( cgi.widgetposn, [pt1[0], pt1[0]+1], [pt1[1], pt1[1]+1]) if xpos is None or ypos is None: return x, y = list(s.xPos), list(s.yPos) idx = min(cgi.index, len(x)-1) if not N.allclose(x[idx], xpos[0]): x[idx] = utils.round2delt(xpos[0], xpos[1]) idx = min(cgi.index, len(y)-1) if not N.allclose(y[idx], ypos[0]): y[idx] = utils.round2delt(ypos[0], ypos[1]) operations = [ document.OperationSettingSet(s.get('xPos'), x), document.OperationSettingSet(s.get('yPos'), y), ] if s.mode == 'length-angle': # convert 2nd point to length, angle def la(ptx, pty): length = ( math.sqrt( (ptx-pt1[0])**2 + (pty-pt1[1])**2 ) / (cgi.widgetposn[2]-cgi.widgetposn[0]) ) angle = ( (math.atan2( pty-pt1[1], ptx-pt1[0] ) * 180. / math.pi) % 360. ) return length, angle length, angle = la(pt2[0], pt2[1]) # calculate length angle for neighbouring point, to get delta ldelt, adelt = la(pt2[0]+1, pt2[1]+1) # update values l, a = list(s.length), list(s.angle) idx = min(cgi.index, len(l)-1) if abs(l[idx]-length) > 1e-8: l[idx] = utils.round2delt(length, ldelt) idx = min(cgi.index, len(a)-1) if abs(a[idx]-angle) > 1e-8: a[idx] = utils.round2delt(angle, adelt) operations += [ document.OperationSettingSet(s.get('length'), l), document.OperationSettingSet(s.get('angle'), a), ] else: xpos2, ypos2 = self._getGraphCoords( cgi.widgetposn, [pt2[0], pt2[0]+1], [pt2[1], pt2[1]+1]) if xpos2 is not None and ypos2 is not None: x2, y2 = list(s.xPos2), list(s.yPos2) idx = min(cgi.index, len(x2)-1) if not N.allclose(x2[idx], xpos2[0]): x2[idx] = utils.round2delt(xpos2[0], xpos2[1]) idx = min(cgi.index, len(y2)-1) if not N.allclose(y2[idx], ypos2[0]): y2[idx] = utils.round2delt(ypos2[0], ypos2[1]) operations += [ document.OperationSettingSet(s.get('xPos2'), x2), document.OperationSettingSet(s.get('yPos2'), y2) ] self.document.applyOperation( document.OperationMultiple(operations, descr=_('adjust lines')) ) document.thefactory.register( Line ) veusz-1.21.1/veusz/widgets/nonorthgraph.py0000664000175000017500000001443712237406466017062 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Non orthogonal graph root.""" from __future__ import division from . import controlgraph from .widget import Widget from .. import qtall as qt4 from .. import setting filloptions = ('center', 'outside', 'top', 'bottom', 'left', 'right', 'polygon') def _(text, disambiguation=None, context='NonOrthGraph'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class FillBrush(setting.BrushExtended): '''Brush for filling point region.''' def __init__(self, *args, **argsv): setting.BrushExtended.__init__(self, *args, **argsv) self.add( setting.Choice('filltype', filloptions, 'center', descr=_('Fill to this edge/position'), usertext=_('Fill type')) ) self.get('hide').newDefault(True) class NonOrthGraph(Widget): '''Non-orthogonal graph base widget.''' @classmethod def addSettings(klass, s): '''Construct list of settings.''' Widget.addSettings(s) s.add( setting.Distance( 'leftMargin', '1.7cm', descr=_('Distance from left of graph to edge'), usertext=_('Left margin'), formatting=True) ) s.add( setting.Distance( 'rightMargin', '0.2cm', descr=_('Distance from right of graph to edge'), usertext=_('Right margin'), formatting=True) ) s.add( setting.Distance( 'topMargin', '0.2cm', descr=_('Distance from top of graph to edge'), usertext=_('Top margin'), formatting=True) ) s.add( setting.Distance( 'bottomMargin', '1.7cm', descr=_('Distance from bottom of graph to edge'), usertext=_('Bottom margin'), formatting=True) ) s.add( setting.GraphBrush( 'Background', descr = _('Background plot fill'), usertext=_('Background')), pixmap='settings_bgfill' ) s.add( setting.Line('Border', descr = _('Graph border line'), usertext=_('Border')), pixmap='settings_border') @classmethod def allowedParentTypes(klass): from . import page, grid return (page.Page, grid.Grid) def graphToPlotCoords(self, coorda, coordb): '''Convert graph to plotting coordinates. Returns (plta, pltb) coordinates ''' def coordRanges(self): '''Return coordinate ranges of plot. This is in the form [[mina, maxa], [minb, maxb]].''' def drawFillPts(self, painter, extfill, bounds, ptsx, ptsy): '''Draw set of points for filling. extfill: extended fill brush bounds: usual tuple (minx, miny, maxx, maxy) ptsx, ptsy: translated plotter coordinates ''' def drawGraph(self, painter, bounds, datarange, outerbounds=None): '''Plot graph area. datarange is [mina, maxa, minb, maxb] or None ''' def drawAxes(self, painter, bounds, datarange, outerbounds=None): '''Plot axes. datarange is [mina, maxa, minb, maxb] or None ''' def setClip(self, painter, bounds): '''Set clipping for graph.''' def getDataRange(self): """Get automatic data range. Return None if no data.""" drange = [1e199, -1e199, 1e199, -1e199] for c in self.children: if hasattr(c, 'updateDataRanges'): c.updateDataRanges(drange) # no data if drange[0] > drange[1] or drange[2] > drange[3]: drange = None return drange def getMargins(self, painthelper): """Use settings to compute margins.""" s = self.settings return ( s.get('leftMargin').convert(painthelper), s.get('topMargin').convert(painthelper), s.get('rightMargin').convert(painthelper), s.get('bottomMargin').convert(painthelper) ) def draw(self, parentposn, phelper, outerbounds=None): '''Update the margins before drawing.''' s = self.settings bounds = self.computeBounds(parentposn, phelper) maxbounds = self.computeBounds(parentposn, phelper, withmargin=False) # do no painting if hidden if s.hide: return bounds painter = phelper.painter(self, bounds) with painter: # plot graph datarange = self.getDataRange() self.drawGraph(painter, bounds, datarange, outerbounds=outerbounds) self.drawAxes(painter, bounds, datarange, outerbounds=outerbounds) # paint children for c in reversed(self.children): c.draw(bounds, phelper, outerbounds=outerbounds) # controls for adjusting margins phelper.setControlGraph(self, [ controlgraph.ControlMarginBox(self, bounds, maxbounds, phelper)]) return bounds def updateControlItem(self, cgi): """Graph resized or moved - call helper routine to move self.""" cgi.setWidgetMargins() veusz-1.21.1/veusz/widgets/widget.py0000664000175000017500000003056612237406466015635 0ustar jssjss# widget.py # fundamental graph plotting widget # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import itertools from ..compat import czip, crepr from .. import document from .. import setting from .. import qtall as qt4 def _(text, disambiguation=None, context='Widget'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Action(object): """A class to wrap functions operating on widgets. Attributes: name: name of action function: function to call with no arguments descr: description of action usertext: name of action to display to user """ def __init__(self, name, function, descr='', usertext=''): """Initialise Action Name of action is name Calls function function() on invocation Action has description descr Usertext is short form of name to display to user.""" self.name = name self.function = function self.descr = descr self.usertext = usertext class Widget(object): """ Fundamental plotting widget interface.""" # differentiate widgets, settings and setting nodetype = 'widget' typename = 'generic' allowusercreation = False isaxis = False isplotter = False def __init__(self, parent, name=None): """Initialise a blank widget.""" # save parent widget for later self.parent = parent self.document = None if not self.isAllowedParent(parent): raise RuntimeError("Widget parent is of incorrect type") if name is None: name = self.chooseName() self.name = name # propagate document if parent is not None: self.document = parent.document parent.addChild(self) # store child widgets self.children = [] # settings for widget self.settings = setting.Settings( 'Widget_' + self.typename, setnsmode='widgetsettings' ) self.settings.parent = self self.addSettings(self.settings) # actions for widget self.actions = [] @classmethod def allowedParentTypes(klass): """Get types of widgets this can be a child of.""" return () @classmethod def addSettings(klass, s): """Add items to settings s.""" s.add( setting.Bool('hide', False, descr = _('Hide object'), usertext = _('Hide'), formatting = True) ) def isWidget(self): """Is this object a widget?""" return True def getDocument(self): """Return document. Unfortunately we need this as document is shadowed in StyleSheet, sigh.""" return self.document def rename(self, name): """Change name of self.""" if self.parent is None: raise ValueError('Cannot rename root widget') if name.find('/') != -1: raise ValueError('Names cannot contain "/"') # check whether name already exists in siblings for i in self.parent.children: if i != self and i.name == name: raise ValueError('New name "%s" already exists' % name) self.name = name def addDefaultSubWidgets(self): '''Add default sub widgets to widget, if any''' pass def addAction(self, action): """Assign name to operation. action is action class above """ self.actions.append( action ) def getAction(self, name): """Get action associated with name.""" for a in self.actions: if a.name == name: return a return None def isAllowedParent(self, parent): """Is the parent a suitable type?""" return parent is None or any( ( isinstance(parent, t) for t in self.allowedParentTypes() ) ) def willAllowParent(cls, parent): """Is the parent of an allowed type to have this type as a child?""" # allow base widget to have no parent ap = cls.allowedParentTypes() if parent is None and len(ap) > 0 and ap[0] is None: return True for p in ap: if isinstance(parent, p): return True return False willAllowParent = classmethod(willAllowParent) def addChild(self, child, index=9999999): """Add child to list. index is a position to place the new child """ self.children.insert(index, child) def createUniqueName(self, prefix): """Create a name using the prefix which hasn't been used before.""" names = self.childnames i = 1 while "%s%i" % (prefix, i) in names: i += 1 return "%s%i" % (prefix, i) def chooseName(self): """Make a name for widget if not specified.""" if self.parent is None: return '/' else: return self.parent.createUniqueName(self.typename) @property def userdescription(self): """Return a user-friendly description of what this is (e.g. function).""" return '' def prefLookup(self, name): """Get the value of a preference in the form foo/bar/baz""" if len(name) > 0 and name[0] == '/': obj = self.document.basewidget name = name[1:] else: obj = self parts = name.split('/') noparts = len(parts) # this could be recursive, but why bother # loop while we iterate through the family i = 0 while i < noparts and obj.hasChild( parts[i] ): obj = obj.getChild( parts[i] ) i += 1 if i == noparts: raise ValueError("Specified a widget, not a setting") else: return obj.settings.getFromPath( parts[i:] ) def getChild(self, name): """Return a child with a name.""" for i in self.children: if i.name == name: return i return None def hasChild(self, name): """Return whether there is a child with a name.""" return self.getChild(name) is not None @property def childnames(self): """Return the child names.""" return [i.name for i in self.children] def removeChild(self, name): """Remove a child.""" i = 0 nc = len(self.children) while i < nc and self.children[i].name != name: i += 1 if i < nc: self.children.pop(i) else: raise ValueError("Cannot remove graph '%s' - does not exist" % name) def widgetSiblingIndex(self): """Get index of widget in its siblings.""" if self.parent is None: return 0 else: return self.parent.children.index(self) @property def path(self): """Returns a path for the object, e.g. /plot1/x.""" obj = self build = '' while obj.parent is not None: build = '/' + obj.name + build obj = obj.parent if len(build) == 0: build = '/' return build def getMargins(self, painthelper): """Return margins of widget.""" return (0., 0., 0., 0.) def computeBounds(self, parentposn, painthelper, withmargin=True): """Compute a bounds array, giving the bounding box for the widget.""" if withmargin: x1, y1, x2, y2 = parentposn dx1, dy1, dx2, dy2 = self.getMargins(painthelper) return [ x1+dx1, y1+dy1, x2-dx2, y2-dy2 ] else: return parentposn def draw(self, parentposn, painthelper, outerbounds = None): """Draw the widget and its children in posn (a tuple with x1,y1,x2,y2). painter is the widget.Painter to draw on outerbounds contains "ultimate" bounds we don't go outside """ bounds = self.computeBounds(parentposn, painthelper) if not self.settings.hide: # iterate over children in reverse order for c in reversed(self.children): c.draw(bounds, painthelper, outerbounds=outerbounds) # return our final bounds return bounds def getSaveText(self, saveall = False): """Return text to restore object If saveall is true, save everything, including defaults.""" # set everything first text = self.settings.saveText(saveall) # now go throught the subwidgets for c in self.children: text += ( "Add('%s', name=%s, autoadd=False)\n" % (c.typename, crepr(c.name)) ) # if we need to go to the child, go there ctext = c.getSaveText(saveall) if ctext != '': text += ("To(%s)\n" "%s" "To('..')\n") % (crepr(c.name), ctext) return text def readDefaults(self): """Read the default settings. Also set settings to stylesheet """ self.settings.readDefaults('', self.name) self.settings.linkToStylesheet() def buildFlatWidgetList(self, thelist): """Return a built up list of the widgets in the tree.""" thelist.append(self) for child in self.children: child.buildFlatWidgetList(thelist) def _recursiveBuildSlots(self, slots): """Build up a flat representation of the places where widgets can be placed The list consists of (parent, index) tuples """ slots.append( (self, 0) ) for child, index in czip(self.children, itertools.count(1)): child._recursiveBuildSlots(slots) slots.append( (self, index) ) def moveChild(self, w, direction): """Move the child widget w up in the hierarchy in the direction. direction is -1 for 'up' or +1 for 'down' Returns True if succeeded """ # find position of child in self c = self.children oldindex = c.index(w) # remove the widget from its current location c.pop(oldindex) # build a list of places widgets can be placed (slots) slots = [] self.document.basewidget._recursiveBuildSlots(slots) # find self list - must be a better way to do this - # probably doesn't matter too much, however ourslot = (self, oldindex) ourindex = 0 while ourindex < len(slots) and slots[ourindex] != ourslot: ourindex += 1 # should never happen assert ourindex < len(slots) # move up or down the list until we find a suitable parent ourindex += direction while ( ourindex >= 0 and ourindex < len(slots) and not w.isAllowedParent(slots[ourindex][0]) ): ourindex += direction # we failed to find a new parent if ourindex < 0 or ourindex >= len(slots): c.insert(oldindex, w) return False else: newparent, newindex = slots[ourindex] existingname = w.name in newparent.childnames newparent.children.insert(newindex, w) w.parent = newparent # require a new name because of a clash if existingname: w.name = w.chooseName() return True def updateControlItem(self, controlitem, pos): """Update the widget's control point. controlitem is the control item in question.""" pass # allow the factory to instantiate a generic widget document.thefactory.register( Widget ) veusz-1.21.1/veusz/widgets/ternary.py0000664000175000017500000005023612244156023016017 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2011 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Ternary plot widget.""" from __future__ import division import numpy as N import math from ..compat import czip from .nonorthgraph import NonOrthGraph from .axisticks import AxisTicks from .axis import MajorTick, MinorTick, GridLine, MinorGridLine, AxisLabel, \ TickLabel from .. import qtall as qt4 from .. import document from .. import setting from .. import utils def _(text, disambiguation=None, context='Ternary'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) def rotatePts(x, y, theta): '''Rotate points by theta degrees.''' s = math.sin(theta*math.pi/180.) c = math.cos(theta*math.pi/180.) return x*c-y*s, x*s+y*c # translate coordinates a,b,c from user to plot # user can select different coordinate systems coord_lookup = { 'bottom-left': (0, 1, 2), 'bottom-right': (0, 2, 1), 'left-bottom': (1, 0, 2), 'left-right': (2, 0, 1), 'right-bottom': (1, 2, 0), 'right-left': (2, 1, 0) } # useful trigonometric identities sin30 = 0.5 sin60 = cos30 = 0.86602540378 tan30 = 0.5773502691 class Ternary(NonOrthGraph): '''Ternary plotter.''' typename='ternary' allowusercreation = True description = _('Ternary graph') def __init__(self, parent, name=None): '''Initialise ternary plot.''' NonOrthGraph.__init__(self, parent, name=name) if type(self) == NonOrthGraph: self.readDefaults() @classmethod def addSettings(klass, s): '''Construct list of settings.''' NonOrthGraph.addSettings(s) s.add( setting.Choice('mode', ('percentage', 'fraction'), 'percentage', descr=_('Show percentages or fractions'), usertext=_('Mode')) ) s.add( setting.Choice('coords', ('bottom-left', 'bottom-right', 'left-bottom', 'left-right', 'right-left', 'right-bottom'), 'bottom-left', descr=_('Axes to use for plotting coordinates'), usertext=_('Coord system')) ) s.add( setting.Str('labelbottom', '', descr=_('Bottom axis label text'), usertext=_('Label bottom')) ) s.add( setting.Str('labelleft', '', descr=_('Left axis label text'), usertext=_('Label left')) ) s.add( setting.Str('labelright', '', descr=_('Right axis label text'), usertext=_('Label right')) ) s.add( setting.Float('originleft', 0., descr=_('Fractional origin of left axis at its top'), usertext=_('Left origin')) ) s.add( setting.Float('originbottom', 0., descr=_('Fractional origin of bottom axis at its left'), usertext=_('Bottom origin')) ) s.add( setting.Float('fracsize', 1., descr=_('Fractional size of plot'), usertext=_('Size')) ) s.add( setting.Bool('reverse', False, descr=_('Reverse axes'), usertext=_('Reverse')) ) s.add( AxisLabel('Label', descr = _('Axis label settings'), usertext = _('Axis label')), pixmap='settings_axislabel' ) s.add( TickLabel('TickLabels', descr = _('Tick label settings'), usertext = _('Tick labels')), pixmap='settings_axisticklabels' ) s.add( MajorTick('MajorTicks', descr = _('Major tick line settings'), usertext = _('Major ticks')), pixmap='settings_axismajorticks' ) s.add( MinorTick('MinorTicks', descr = _('Minor tick line settings'), usertext = _('Minor ticks')), pixmap='settings_axisminorticks' ) s.add( GridLine('GridLines', descr = _('Grid line settings'), usertext = _('Grid lines')), pixmap='settings_axisgridlines' ) s.add( MinorGridLine('MinorGridLines', descr = _('Minor grid line settings'), usertext = _('Grid lines for minor ticks')), pixmap='settings_axisminorgridlines' ) s.get('leftMargin').newDefault('1cm') s.get('rightMargin').newDefault('1cm') s.get('topMargin').newDefault('1cm') s.get('bottomMargin').newDefault('1cm') s.MajorTicks.get('number').newDefault(10) s.MinorTicks.get('number').newDefault(50) s.GridLines.get('hide').newDefault(False) s.TickLabels.remove('rotate') @property def userdescription(self): return _("mode='%s'") % self.settings.mode def _maxVal(self): '''Get maximum value on axis.''' if self.settings.mode == 'percentage': return 100. else: return 1. def coordRanges(self): '''Get ranges of coordinates.''' mv = self._maxVal() # ranges for each coordinate ra = [self._orgbot*mv, (self._orgbot+self._size)*mv] rb = [self._orgleft*mv, (self._orgleft+self._size)*mv] rc = [self._orgright*mv, (self._orgright+self._size)*mv] ranges = [ra, rb, rc] lookup = coord_lookup[self.settings.coords] return ranges[lookup.index(0)], ranges[lookup.index(1)] def graphToPlotCoords(self, coorda, coordb): '''Convert coordinates in r, theta to x, y.''' s = self.settings # normalize coordinates maxval = self._maxVal() coordan = coorda / maxval coordbn = coordb / maxval # the three coordinates on the plot clist = [coordan, coordbn, 1.-coordan-coordbn] # select the right coordinates for a, b and c given the system # requested by the user # normalise by origins and plot size lookup = coord_lookup[s.coords] cbot = ( clist[ lookup[0] ] - self._orgbot ) / self._size cleft = ( clist[ lookup[1] ] - self._orgleft ) / self._size cright = ( clist[ lookup[2] ] - self._orgright ) / self._size # from Ingram, 1984, Area, 16, 175 # remember that y goes in the opposite direction here if s.reverse: cleft, cright = cright, cleft x = (1-(0.5*cright + cbot))*self._width + self._box[0] else: x = (0.5*cright + cbot)*self._width + self._box[0] y = self._box[3] - cright * sin60 * self._width return x, y def drawFillPts(self, painter, brushext, cliprect, ptsx, ptsy): '''Draw points for plotting a fill.''' pts = qt4.QPolygonF() utils.addNumpyToPolygonF(pts, ptsx, ptsy) filltype = brushext.filltype # this is broken: FIXME if filltype == 'left': dyend = ptsy[-1]-self._box[1] pts.append( qt4.QPointF(ptsx[-1]-dyend*tan30, self._box[1]) ) dystart = ptsy[0]-self._box[1] pts.append( qt4.QPointF(ptsx[0]-dystart*tan30, self._box[1]) ) elif filltype == 'right': pts.append( qt4.QPointF(self._box[2], ptsy[-1]) ) pts.append( qt4.QPointF(self._box[2], ptsy[0]) ) elif filltype == 'bottom': dyend = self._box[3]-ptsy[-1] pts.append( qt4.QPointF(ptsx[-1]-dyend*tan30, self._box[3]) ) dystart = self._box[3]-ptsy[0] pts.append( qt4.QPointF(ptsx[0]-dystart*tan30, self._box[3]) ) elif filltype == 'polygon': pass else: pts = None if pts is not None: utils.brushExtFillPolygon(painter, brushext, cliprect, pts) def drawGraph(self, painter, bounds, datarange, outerbounds=None): '''Plot graph area and axes.''' s = self.settings xw, yw = bounds[2]-bounds[0], bounds[3]-bounds[1] d60 = 60./180.*math.pi ang = math.atan2(yw, xw/2.) if ang > d60: # taller than wider widthh = xw/2 height = math.tan(d60) * widthh else: # wider than taller height = yw widthh = height / math.tan(d60) # box for equilateral triangle self._box = ( (bounds[2]+bounds[0])/2 - widthh, (bounds[1]+bounds[3])/2 - height/2, (bounds[2]+bounds[0])/2 + widthh, (bounds[1]+bounds[3])/2 + height/2 ) self._width = widthh*2 self._height = height # triangle shaped polygon for graph self._tripoly = p = qt4.QPolygonF() p.append( qt4.QPointF(self._box[0], self._box[3]) ) p.append( qt4.QPointF(self._box[0]+widthh, self._box[1]) ) p.append( qt4.QPointF(self._box[2], self._box[3]) ) path = qt4.QPainterPath() path.addPolygon(p) path.closeSubpath() utils.brushExtFillPath(painter, s.Background, path, stroke=s.Border.makeQPenWHide(painter)) # work out origins and size self._size = max(min(s.fracsize, 1.), 0.) # make sure we don't go past the ends of the allowed range # value of origin of left axis at top self._orgleft = min(s.originleft, 1.-self._size) # value of origin of bottom axis at left self._orgbot = min(s.originbottom, 1.-self._size) # origin of right axis at bottom self._orgright = 1. - self._orgleft - (self._orgbot + self._size) def _computeTickVals(self): """Compute tick values.""" s = self.settings # this is a hack as we lose ends off the axis otherwise d = 1e-6 # get ticks along left axis atickleft = AxisTicks(self._orgleft-d, self._orgleft+self._size+d, s.MajorTicks.number, s.MinorTicks.number, extendmin=False, extendmax=False) atickleft.getTicks() # use the interval from above to calculate ticks for right atickright = AxisTicks(self._orgright-d, self._orgright+self._size+d, s.MajorTicks.number, s.MinorTicks.number, extendmin=False, extendmax=False, forceinterval = atickleft.interval) atickright.getTicks() # then calculate for bottom atickbot = AxisTicks(self._orgbot-d, self._orgbot+self._size+d, s.MajorTicks.number, s.MinorTicks.number, extendmin=False, extendmax=False, forceinterval = atickleft.interval) atickbot.getTicks() return atickbot, atickleft, atickright def setClip(self, painter, bounds): '''Set clipping for graph.''' p = qt4.QPainterPath() p.addPolygon( self._tripoly ) painter.setClipPath(p) def _getLabels(self, ticks, autoformat): """Return tick labels.""" labels = [] tl = self.settings.TickLabels format = tl.format scale = tl.scale if format.lower() == 'auto': format = autoformat for v in ticks: l = utils.formatNumber(v*scale, format, locale=self.document.locale) labels.append(l) if self.settings.reverse: labels = labels[::-1] return labels def _drawTickSet(self, painter, tickSetn, gridSetn, tickbot, tickleft, tickright, tickLabelSetn=None, labelSetn=None): '''Draw a set of ticks (major or minor). tickSetn: tick setting to get line details gridSetn: setting for grid line (if any) tickXXX: tick arrays for each axis tickLabelSetn: setting used to label ticks, or None if minor ticks labelSetn: setting for labels, if any ''' # this is mostly a lot of annoying trigonometry # compute line ends for ticks and grid lines tl = tickSetn.get('length').convert(painter) mv = self._maxVal() reverse = bool(self.settings.reverse) revsign = [1, -1][reverse] # bottom ticks x1 = (tickbot - self._orgbot)/self._size*self._width + self._box[0] y1 = self._box[3] + N.zeros(x1.shape) x2 = x1 - revsign * tl * sin30 y2 = y1 + tl * cos30 tickbotline = (x1, y1, x2, y2) # bottom grid (removing lines at edge of plot) scaletick = 1 - (tickbot-self._orgbot)/self._size gx = x1 + scaletick*self._width*sin30 gy = y1 - scaletick*self._width*cos30 ne = (scaletick > 1e-6) & (scaletick < (1-1e-6)) gridbotline = (x1[ne], y1[ne], gx[ne], gy[ne]) # left ticks x1 = -(tickleft - self._orgleft)/self._size*self._width*sin30 + ( self._box[0] + self._box[2])*0.5 y1 = (tickleft - self._orgleft)/self._size*self._width*cos30 + self._box[1] if reverse: x2 = x1 - tl y2 = y1 else: x2 = x1 - tl * sin30 y2 = y1 - tl * cos30 tickleftline = (x1, y1, x2, y2) # left grid scaletick = 1 - (tickleft-self._orgleft)/self._size gx = x1 + scaletick*self._width*sin30 gy = self._box[3] + N.zeros(y1.shape) ne = (scaletick > 1e-6) & (scaletick < (1-1e-6)) gridleftline = (x1[ne], y1[ne], gx[ne], gy[ne]) # right ticks x1 = -(tickright - self._orgright)/self._size*self._width*sin30+self._box[2] y1 = -(tickright - self._orgright)/self._size*self._width*cos30+self._box[3] if reverse: x2 = x1 + tl * sin30 y2 = y1 - tl * cos30 else: x2 = x1 + tl y2 = y1 tickrightline = (x1, y1, x2, y2) # right grid scaletick = 1 - (tickright-self._orgright)/self._size gx = x1 - scaletick*self._width gy = y1 gridrightline = (x1[ne], y1[ne], gx[ne], gy[ne]) if not gridSetn.hide: # draw the grid pen = gridSetn.makeQPen(painter) painter.setPen(pen) utils.plotLinesToPainter(painter, *gridbotline) utils.plotLinesToPainter(painter, *gridleftline) utils.plotLinesToPainter(painter, *gridrightline) # calculate deltas for ticks bdelta = ldelta = rdelta = 0 if not tickSetn.hide: # draw ticks themselves pen = tickSetn.makeQPen(painter) pen.setCapStyle(qt4.Qt.FlatCap) painter.setPen(pen) utils.plotLinesToPainter(painter, *tickbotline) utils.plotLinesToPainter(painter, *tickleftline) utils.plotLinesToPainter(painter, *tickrightline) ldelta += tl*sin30 bdelta += tl*cos30 rdelta += tl if tickLabelSetn is not None and not tickLabelSetn.hide: # compute the labels for the ticks tleftlabels = self._getLabels(tickleft*mv, '%Vg') trightlabels = self._getLabels(tickright*mv, '%Vg') tbotlabels = self._getLabels(tickbot*mv, '%Vg') painter.setPen( tickLabelSetn.makeQPen() ) font = tickLabelSetn.makeQFont(painter) painter.setFont(font) fm = utils.FontMetrics(font, painter.device()) sp = fm.leading() + fm.descent() off = tickLabelSetn.get('offset').convert(painter) # draw tick labels in each direction hlabbot = wlableft = wlabright = 0 for l, x, y in czip(tbotlabels, tickbotline[2], tickbotline[3]+off): r = utils.Renderer(painter, font, x, y, l, 0, 1, 0) bounds = r.render() hlabbot = max(hlabbot, bounds[3]-bounds[1]) for l, x, y in czip(tleftlabels, tickleftline[2]-off-sp, tickleftline[3]): r = utils.Renderer(painter, font, x, y, l, 1, 0, 0) bounds = r.render() wlableft = max(wlableft, bounds[2]-bounds[0]) for l, x, y in czip(trightlabels,tickrightline[2]+off+sp, tickrightline[3]): r = utils.Renderer(painter, font, x, y, l, -1, 0, 0) bounds = r.render() wlabright = max(wlabright, bounds[2]-bounds[0]) bdelta += hlabbot+off+sp ldelta += wlableft+off+sp rdelta += wlabright+off+sp if labelSetn is not None and not labelSetn.hide: # draw label on edges (if requested) painter.setPen( labelSetn.makeQPen() ) font = labelSetn.makeQFont(painter) painter.setFont(font) fm = utils.FontMetrics(font, painter.device()) sp = fm.leading() + fm.descent() off = labelSetn.get('offset').convert(painter) # alignment align = {'at-minimum': -1, 'centre': 0, 'at-maximum': 1}[ labelSetn.position] aoffset = {'at-minimum': -self._width/2, 'centre': 0, 'at-maximum': self._width/2}[labelSetn.position] # bottom label r = utils.Renderer(painter, font, 0, 0, self.settings.labelbottom, align, 1) painter.save() painter.translate(self._box[0]+self._width/2, self._box[3] + bdelta + off) painter.translate(aoffset, 0) r.render() painter.restore() # left label - rotate frame before drawing so we can get # the bounds correct r = utils.Renderer(painter, font, 0, -sp, self.settings.labelleft, -align, -1) painter.save() painter.translate(self._box[0]+self._width*0.25, 0.5*(self._box[1]+self._box[3])) painter.rotate(-60) painter.translate(-aoffset, -ldelta - off) r.render() painter.restore() # right label r = utils.Renderer(painter, font, 0, -sp, self.settings.labelright, -align, -1) painter.save() painter.translate(self._box[0]+self._width*0.75 , 0.5*(self._box[1]+self._box[3]) ) painter.rotate(60) painter.translate(-aoffset, -ldelta - off) r.render() painter.restore() def drawAxes(self, painter, bounds, datarange, outerbounds=None): '''Draw plot axes.''' s = self.settings # compute tick values for later when plotting axes tbot, tleft, tright = self._computeTickVals() # draw the major ticks self._drawTickSet(painter, s.MajorTicks, s.GridLines, tbot.tickvals, tleft.tickvals, tright.tickvals, tickLabelSetn=s.TickLabels, labelSetn=s.Label) # now draw the minor ones self._drawTickSet(painter, s.MinorTicks, s.MinorGridLines, tbot.minorticks, tleft.minorticks, tright.minorticks) document.thefactory.register(Ternary) veusz-1.21.1/veusz/widgets/axis.py0000644000175000017500000013037412346113260015276 0ustar jssjss# Copyright (C) 2003 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Widget to plot axes, and to handle conversion of coordinates to plot positions.""" from __future__ import division import numpy as N from ..compat import czip from .. import qtall as qt4 from .. import document from .. import setting from .. import utils from . import widget from . import axisticks from . import controlgraph ############################################################################### def _(text, disambiguation=None, context='Axis'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class MajorTick(setting.Line): '''Major tick settings.''' def __init__(self, name, **args): setting.Line.__init__(self, name, **args) self.add( setting.DistancePt( 'length', '6pt', descr = _('Length of major ticks'), usertext= _('Length') ) ) self.add( setting.Int( 'number', 6, descr = _('Number of major ticks to aim for'), usertext= _('Number') ) ) self.add( setting.FloatList('manualTicks', [], descr = _('List of tick values' ' overriding defaults'), usertext= _('Manual ticks') ) ) def getLength(self, painter): '''Return tick length in painter coordinates''' return self.get('length').convert(painter) class MinorTick(setting.Line): '''Minor tick settings.''' def __init__(self, name, **args): setting.Line.__init__(self, name, **args) self.add( setting.DistancePt( 'length', '3pt', descr = _('Length of minor ticks'), usertext= _('Length')) ) self.add( setting.Int( 'number', 20, descr = _('Number of minor ticks to aim for'), usertext= _('Number') ) ) def getLength(self, painter): '''Return tick length in painter coordinates''' return self.get('length').convert(painter) class GridLine(setting.Line): '''Grid line settings.''' def __init__(self, name, **args): setting.Line.__init__(self, name, **args) self.get('color').newDefault( 'grey' ) self.get('hide').newDefault( True ) self.get('style').newDefault( 'dotted' ) self.add( setting.Bool( 'onTop', False, descr = _('Put grid lines on top of graph'), usertext = _('On top') ) ) class MinorGridLine(setting.Line): '''Minor tick grid line settings.''' def __init__(self, name, **args): setting.Line.__init__(self, name, **args) self.get('color').newDefault( 'lightgrey' ) self.get('hide').newDefault( True ) self.get('style').newDefault( 'dotted' ) class AxisLabel(setting.Text): """For axis labels.""" def __init__(self, name, **args): setting.Text.__init__(self, name, **args) self.add( setting.Bool( 'atEdge', False, descr = _('Place axis label close to edge' ' of graph'), usertext= _('At edge') ) ) self.add( setting.RotateInterval( 'rotate', '0', descr = 'Angle by which to rotate label by', usertext='Rotate') ) self.add( setting.DistancePt( 'offset', '0pt', descr = _('Additional offset of axis label' ' from axis tick labels'), usertext= _('Label offset') ) ) self.add( setting.Choice( 'position', ('at-minimum', 'centre', 'at-maximum'), 'centre', descr = _('Position of axis label'), usertext = _('Position') ) ) class TickLabel(setting.Text): """For tick labels on axes.""" formatchoices = ('Auto', '%Vg', '%Ve', '%VE', '%g', '%e', '%.2f') descriptions = ( _('Automatic'), _('General numerical format'), _('Scientific notation'), _('Engineering suffix notation'), _('C-style general format'), _('C-style scientific notation'), _('2 decimal places always shown') ) def __init__(self, name, **args): setting.Text.__init__(self, name, **args) self.add( setting.RotateInterval( 'rotate', '0', descr = _('Angle by which to rotate label by'), usertext= _('Rotate') ) ) self.add( setting.ChoiceOrMore( 'format', TickLabel.formatchoices, 'Auto', descr = _('Format of the tick labels'), descriptions=TickLabel.descriptions, usertext= _('Format') ) ) self.add( setting.Float('scale', 1., descr=_('A scale factor to apply to the values ' 'of the tick labels'), usertext=_('Scale') ) ) self.add( setting.DistancePt( 'offset', '0pt', descr = _('Additional offset of axis tick ' 'labels from axis'), usertext= _('Tick offset') ) ) ############################################################################### class Axis(widget.Widget): """Manages and draws an axis.""" typename = 'axis' allowusercreation = True description = 'Axis to a plot or shared in a grid' isaxis = True def __init__(self, parent, name=None): """Initialise axis.""" widget.Widget.__init__(self, parent, name=name) s = self.settings if type(self) == Axis: self.readDefaults() if self.name == 'y' and s.direction != 'vertical': s.direction = 'vertical' elif self.name == 'x' and s.direction != 'horizontal': s.direction = 'horizontal' # automatic range self.setAutoRange(None) # document updates change set variable when things need recalculating self.docchangeset = -1 self.currentbounds = [0,0,1,1] @classmethod def addSettings(klass, s): """Construct list of settings.""" widget.Widget.addSettings(s) s.add( setting.Str('label', '', descr=_('Axis label text'), usertext=_('Label')) ) s.add( setting.AxisBound('min', 'Auto', descr=_('Minimum value of axis'), usertext=_('Min')) ) s.add( setting.AxisBound('max', 'Auto', descr=_('Maximum value of axis'), usertext=_('Max')) ) s.add( setting.Bool('log', False, descr = _('Whether axis is logarithmic'), usertext=_('Log')) ) s.add( setting.Choice( 'autoRange', ('exact', 'next-tick', '+2%', '+5%', '+10%', '+15%'), 'next-tick', descr = _('If axis range not specified, use range of ' 'data and this setting'), descriptions = (_('Use exact data range'), _('Use data range, rounding up to tick marks'), _('Use data range, adding 2% of range'), _('Use data range, adding 5% of range'), _('Use data range, adding 10% of range'), _('Use data range, adding 15% of range'), ), formatting = True, usertext = _('Auto range') ) ) s.add( setting.Choice('mode', ('numeric', 'datetime', 'labels'), 'numeric', descr = _('Type of ticks to show on on axis'), usertext=_('Mode')) ) s.add( setting.SettingBackwardCompat( 'autoExtend', 'autoRange', True, translatefn = lambda x: ('exact', 'next-tick')[x], formatting=True ) ) # this setting no longer used s.add( setting.Bool('autoExtendZero', True, descr = _('Extend axis to zero if close (UNUSED)'), usertext=_('Zero extend'), hidden=True, formatting=True) ) s.add( setting.Bool('autoMirror', True, descr = _('Place axis on opposite side of graph ' 'if none'), usertext=_('Auto mirror'), formatting=True) ) s.add( setting.Bool('reflect', False, descr = _('Place axis text and ticks on other side' ' of axis'), usertext=_('Reflect'), formatting=True) ) s.add( setting.Bool('outerticks', False, descr = _('Place ticks on outside of graph'), usertext=_('Outer ticks'), formatting=True) ) s.add( setting.Float('datascale', 1., descr=_('Scale data plotted by this factor'), usertext=_('Scale')) ) s.add( setting.Choice('direction', ['horizontal', 'vertical'], 'horizontal', descr = _('Direction of axis'), usertext=_('Direction')) ) s.add( setting.Float('lowerPosition', 0., descr=_('Fractional position of lower end of ' 'axis on graph'), usertext=_('Min position')) ) s.add( setting.Float('upperPosition', 1., descr=_('Fractional position of upper end of ' 'axis on graph'), usertext=_('Max position')) ) s.add( setting.Float('otherPosition', 0., descr=_('Fractional position of axis ' 'in its perpendicular direction'), usertext=_('Axis position')) ) s.add( setting.WidgetPath('match', '', descr = _('Match the scale of this axis to the ' 'axis specified'), usertext=_('Match'), allowedwidgets = [Axis] )) s.add( setting.Line('Line', descr = _('Axis line settings'), usertext = _('Axis line')), pixmap='settings_axisline' ) s.add( AxisLabel('Label', descr = _('Axis label settings'), usertext = _('Axis label')), pixmap='settings_axislabel' ) s.add( TickLabel('TickLabels', descr = _('Tick label settings'), usertext = _('Tick labels')), pixmap='settings_axisticklabels' ) s.add( MajorTick('MajorTicks', descr = _('Major tick line settings'), usertext = _('Major ticks')), pixmap='settings_axismajorticks' ) s.add( MinorTick('MinorTicks', descr = _('Minor tick line settings'), usertext = _('Minor ticks')), pixmap='settings_axisminorticks' ) s.add( GridLine('GridLines', descr = _('Grid line settings'), usertext = _('Grid lines')), pixmap='settings_axisgridlines' ) s.add( MinorGridLine('MinorGridLines', descr = _('Minor grid line settings'), usertext = _('Grid lines for minor ticks')), pixmap='settings_axisminorgridlines' ) @classmethod def allowedParentTypes(klass): from . import graph, grid return (graph.Graph, grid.Grid) @property def userdescription(self): """User friendly description.""" s = self.settings return "range %s to %s%s" % ( str(s.min), str(s.max), ['',' (log)'][s.log]) def isLinked(self): """Whether is an axis linked to another.""" return False def getLinkedAxis(self): """Return axis linked to this one (or None).""" return None def setAutoRange(self, autorange): """Set the automatic range of this axis (called from page helper).""" if autorange: scale = self.settings.datascale self.autorange = ar = [x*scale for x in autorange] if self.settings.log: ar[0] = max(1e-99, ar[0]) else: if self.settings.log: self.autorange = [1e-2, 1.] else: self.autorange = [0., 1.] def usesAutoRange(self): """Return whether any of the bounds are automatically determined.""" return self.settings.min == 'Auto' or self.settings.max == 'Auto' def computePlottedRange(self, force=False, overriderange=None): """Convert the range requested into a plotted range.""" if self.docchangeset == self.document.changeset and not force: return s = self.settings if overriderange is None: self.plottedrange = [s.min, s.max] else: self.plottedrange = overriderange # match the scale of this axis to another matched = False if s.match != '': # locate widget we're matching # this is ensured to be an Axis try: widget = s.get('match').getReferredWidget() except utils.InvalidType: widget = None # this looks valid + sanity checks if (widget is not None and widget != self and widget.settings.match == ''): # update if out of date if widget.docchangeset != self.document.changeset: widget.computePlottedRange() # copy the range self.plottedrange = list(widget.plottedrange) matched = True # automatic lookup of minimum if not matched and overriderange is None: if s.min == 'Auto': self.plottedrange[0] = self.autorange[0] if s.max == 'Auto': self.plottedrange[1] = self.autorange[1] # yuck, but sometimes it's true # tweak range to make sure things don't blow up further down the # line if ( abs(self.plottedrange[0] - self.plottedrange[1]) < ( abs(self.plottedrange[0]) + abs(self.plottedrange[1]) )*1e-12 ): self.plottedrange[1] = ( self.plottedrange[0] + max(1., self.plottedrange[0]*0.1) ) # handle axis values round the wrong way invertaxis = self.plottedrange[0] > self.plottedrange[1] if invertaxis: self.plottedrange = self.plottedrange[::-1] # make sure log axes don't blow up if s.log: if self.plottedrange[0] < 1e-99: self.plottedrange[0] = 1e-99 if self.plottedrange[1] < 1e-99: self.plottedrange[1] = 1e-99 if self.plottedrange[0] == self.plottedrange[1]: self.plottedrange[1] = self.plottedrange[0]*2 r = s.autoRange if r == 'exact': pass elif r == 'next-tick': pass else: val = {'+2%': 0.02, '+5%': 0.05, '+10%': 0.1, '+15%': 0.15}[r] if s.log: # logarithmic logrng = abs( N.log(self.plottedrange[1]) - N.log(self.plottedrange[0]) ) if s.min == 'Auto': self.plottedrange[0] /= N.exp(logrng * val) if s.max == 'Auto': self.plottedrange[1] *= N.exp(logrng * val) else: # linear rng = self.plottedrange[1] - self.plottedrange[0] if s.min == 'Auto': self.plottedrange[0] -= rng*val if s.max == 'Auto': self.plottedrange[1] += rng*val self.computeTicks() # invert bounds if axis was inverted if invertaxis: self.plottedrange = self.plottedrange[::-1] self.docchangeset = self.document.changeset def computeTicks(self, allowauto=True): """Update ticks given plotted range. if allowauto is False, then do not allow ticks to be updated """ s = self.settings if s.mode in ('numeric', 'labels'): tickclass = axisticks.AxisTicks else: tickclass = axisticks.DateTicks nexttick = s.autoRange == 'next-tick' extendmin = nexttick and s.min == 'Auto' and allowauto extendmax = nexttick and s.max == 'Auto' and allowauto # create object to compute ticks axs = tickclass(self.plottedrange[0], self.plottedrange[1], s.MajorTicks.number, s.MinorTicks.number, extendmin = extendmin, extendmax = extendmax, logaxis = s.log ) axs.getTicks() self.plottedrange[0] = axs.minval self.plottedrange[1] = axs.maxval self.majortickscalc = axs.tickvals self.minortickscalc = axs.minorticks self.autoformat = axs.autoformat # override values if requested if len(s.MajorTicks.manualTicks) > 0: ticks = [] for i in s.MajorTicks.manualTicks: if i >= self.plottedrange[0] and i <= self.plottedrange[1]: ticks.append(i) self.majortickscalc = N.array(ticks) def getPlottedRange(self): """Return the range plotted by the axes.""" self.computePlottedRange() return (self.plottedrange[0], self.plottedrange[1]) def updateAxisLocation(self, bounds, otherposition=None, lowerupperposition=None): """Recalculate coordinates on plotter of axis. otherposition: override otherPosition setting lowerupperposition: set to tuple (lower, upper) to override lowerPosition and upperPosition settings """ s = self.settings if lowerupperposition is None: p1, p2 = s.lowerPosition, s.upperPosition else: p1, p2 = lowerupperposition if otherposition is None: otherposition = s.otherPosition x1, y1, x2, y2 = self.currentbounds = bounds dx = x2 - x1 dy = y2 - y1 if s.direction == 'horizontal': # horizontal self.coordParr1 = x1 + dx*p1 self.coordParr2 = x1 + dx*p2 # other axis coordinates self.coordPerp = y2 - dy*otherposition self.coordPerp1 = y1 self.coordPerp2 = y2 else: # vertical self.coordParr1 = y2 - dy*p1 self.coordParr2 = y2 - dy*p2 # other axis coordinates self.coordPerp = x1 + dx*otherposition self.coordPerp1 = x1 self.coordPerp2 = x2 # is this axis reflected if otherposition > 0.5: self.coordReflected = not s.reflect else: self.coordReflected = s.reflect def graphToPlotterCoords(self, bounds, vals): """Convert graph coordinates to plotter coordinates on this axis. bounds specifies the plot bounds vals is numpy of coordinates Returns positions as a numpy """ # if the doc was modified, recompute the range self.updateAxisLocation(bounds) return self._graphToPlotter(vals) def _graphToPlotter(self, vals): """Convert the coordinates assuming the machinery is in place.""" # work out fractional posistions, then convert to pixels if self.settings.log: fracposns = self.logConvertToPlotter(vals) else: fracposns = self.linearConvertToPlotter(vals) return self.coordParr1 + fracposns*(self.coordParr2-self.coordParr1) def dataToPlotterCoords(self, posn, data): """Convert data values to plotter coordinates, scaling if necessary.""" self.updateAxisLocation(posn) return self._graphToPlotter(data*self.settings.datascale) def plotterToGraphCoords(self, bounds, vals): """Convert plotter coordinates on this axis to graph coordinates. bounds specifies the plot bounds vals is a numpy of coordinates returns a numpy of floats """ self.updateAxisLocation( bounds ) # work out fractional positions of the plotter coords frac = ( (vals.astype(N.float64) - self.coordParr1) / (self.coordParr2 - self.coordParr1) ) # convert from fractional to graph if self.settings.log: return self.logConvertFromPlotter(frac) else: return self.linearConvertFromPlotter(frac) def plotterToDataCoords(self, bounds, vals): """Convert plotter coordinates to data, removing scaling.""" try: scale = 1./self.settings.datascale except ZeroDivisionError: scale = 0. return scale * self.plotterToGraphCoords(bounds, vals) def linearConvertToPlotter(self, v): """Convert graph coordinates to fractional plotter units for linear scale. """ return ( (v - self.plottedrange[0]) / (self.plottedrange[1] - self.plottedrange[0]) ) def linearConvertFromPlotter(self, v): """Convert from (fractional) plotter coords to graph coords. """ return ( self.plottedrange[0] + v * (self.plottedrange[1]-self.plottedrange[0]) ) def logConvertToPlotter(self, v): """Convert graph coordinates to fractional plotter units for log10 scale. """ log1 = N.log(self.plottedrange[0]) log2 = N.log(self.plottedrange[1]) return ( N.log( N.clip(v, 1e-99, 1e99) ) - log1 )/(log2 - log1) def logConvertFromPlotter(self, v): """Convert from fraction plotter coords to graph coords with log scale. """ return ( self.plottedrange[0] * ( self.plottedrange[1]/self.plottedrange[0] )**v ) def againstWhichEdge(self): """Returns edge this axis is against, if any. Returns 0-3,None for (left, top, right, bottom, None) """ s = self.settings op = abs(s.otherPosition) if op > 1e-3 and op < 0.999: return None else: if s.direction == 'vertical': if op <= 1e-3: return 0 else: return 2 else: if op <= 1e-3: return 3 else: return 1 def swapline(self, painter, a1, b1, a2, b2): """Draw line, but swap x & y coordinates if vertical axis.""" if self.settings.direction == 'horizontal': painter.drawLine(qt4.QPointF(a1, b1), qt4.QPointF(a2, b2)) else: painter.drawLine(qt4.QPointF(b1, a1), qt4.QPointF(b2, a2)) def swaplines(self, painter, a1, b1, a2, b2): """Multiline version of swapline where a1, b1, a2, b2 are arrays.""" if self.settings.direction == 'horizontal': a = (a1, b1, a2, b2) else: a = (b1, a1, b2, a2) utils.plotLinesToPainter(painter, a[0], a[1], a[2], a[3]) def _drawGridLines(self, subset, painter, coordticks, parentposn): """Draw grid lines on the plot.""" painter.setPen( self.settings.get(subset).makeQPen(painter) ) # drop points which overlap with graph box (if used) if self.parent.typename == 'graph': if not self.parent.settings.Border.hide: if self.settings.direction == 'horizontal': ok = ( (N.abs(coordticks-parentposn[0]) > 1e-3) & (N.abs(coordticks-parentposn[2]) > 1e-3) ) else: ok = ( (N.abs(coordticks-parentposn[1]) > 1e-3) & (N.abs(coordticks-parentposn[3]) > 1e-3) ) coordticks = coordticks[ok] self.swaplines(painter, coordticks, coordticks*0.+self.coordPerp1, coordticks, coordticks*0.+self.coordPerp2) def _drawAxisLine(self, painter): """Draw the line of the axis.""" pen = self.settings.get('Line').makeQPen(painter) pen.setCapStyle(qt4.Qt.FlatCap) painter.setPen(pen) self.swapline( painter, self.coordParr1, self.coordPerp, self.coordParr2, self.coordPerp ) def _drawMinorTicks(self, painter, coordminorticks): """Draw minor ticks on plot.""" s = self.settings mt = s.get('MinorTicks') pen = mt.makeQPen(painter) pen.setCapStyle(qt4.Qt.FlatCap) painter.setPen(pen) delta = mt.getLength(painter) if s.direction == 'vertical': delta *= -1 if self.coordReflected: delta *= -1 if s.outerticks: delta *= -1 y = coordminorticks*0.+self.coordPerp self.swaplines( painter, coordminorticks, y, coordminorticks, y-delta ) def _drawMajorTicks(self, painter, tickcoords): """Draw major ticks on the plot.""" s = self.settings mt = s.get('MajorTicks') pen = mt.makeQPen(painter) pen.setCapStyle(qt4.Qt.FlatCap) painter.setPen(pen) startdelta = mt.getLength(painter) delta = startdelta if s.direction == 'vertical': delta *= -1 if self.coordReflected: delta *= -1 if s.outerticks: delta *= -1 y = tickcoords*0.+self.coordPerp self.swaplines( painter, tickcoords, y, tickcoords, y-delta ) # account for ticks if they are in the direction of the label if s.outerticks and not self.coordReflected: self._delta_axis += abs(delta) def generateLabelLabels(self, phelper): """Generate list of positions and labels from widgets using this axis.""" try: plotters = phelper.axisplottermap[self] except (AttributeError, KeyError): return dir = self.settings.direction minval, maxval = self.plottedrange for plotter in plotters: # get label and label coordinates from plotter (if any) labels, coords = plotter.getAxisLabels(dir) if None not in (labels, coords): # convert coordinates to plotter coordinates pcoords = self._graphToPlotter(coords) for coord, pcoord, lab in czip(coords, pcoords, labels): # return labels that are within the plotted range # of coordinates if N.isfinite(coord) and (minval <= coord <= maxval): yield pcoord, lab def _drawTickLabels(self, phelper, painter, coordticks, sign, outerbounds, tickvals, texttorender): """Draw tick labels on the plot. texttorender is a list which contains text for the axis to render after checking for collisions """ s = self.settings vertical = s.direction == 'vertical' font = s.get('TickLabels').makeQFont(painter) painter.setFont(font) fm = utils.FontMetrics(font, painter.device()) tl_spacing = fm.leading() + fm.descent() # work out font alignment angle = int(s.TickLabels.rotate) if not self.coordReflected and angle != 0: angle = 360-angle if vertical: # limit tick labels to be directly below/besides axis ax, ay = 1, 0 else: ax, ay = 0, 1 if self.coordReflected: ax, ay = -ax, -ay # get information about text scales tl = s.get('TickLabels') scale = tl.scale pen = tl.makeQPen() # an extra offset if required self._delta_axis += tl.get('offset').convert(painter) def generateTickLabels(): """Return plotter position of labels and label text.""" # get format for labels format = s.TickLabels.format if format.lower() == 'auto': format = self.autoformat # generate positions and labels for posn, tickval in czip(coordticks, tickvals): text = utils.formatNumber(tickval*scale, format, locale=self.document.locale) yield posn, text # position of label perpendicular to axis perpposn = self.coordPerp + sign*(self._delta_axis+tl_spacing) # use generator function to get labels and positions if s.mode == 'labels': ticklabels = self.generateLabelLabels(phelper) else: ticklabels = generateTickLabels() # iterate over each label maxdim = 0 for parlposn, text in ticklabels: # x and y round other way if vertical if vertical: x, y = perpposn, parlposn else: x, y = parlposn, perpposn r = utils.Renderer(painter, font, x, y, text, alignhorz=ax, alignvert=ay, angle=angle) if outerbounds is not None: # make sure ticks are within plot if vertical: r.ensureInBox(miny=outerbounds[1], maxy=outerbounds[3], extraspace=True) else: r.ensureInBox(minx=outerbounds[0], maxx=outerbounds[2], extraspace=True) bnd = r.getBounds() texttorender.append( (r, pen) ) # keep track of maximum extent of label perpendicular to axis if vertical: maxdim = max(maxdim, bnd[2] - bnd[0]) else: maxdim = max(maxdim, bnd[3] - bnd[1]) # keep track of where we are self._delta_axis += 2*tl_spacing + maxdim def _drawAxisLabel(self, painter, sign, outerbounds, texttorender): """Draw an axis label on the plot. texttorender is a list which contains text for the axis to render after checking for collisions """ s = self.settings sl = s.Label label = s.get('Label') font = label.makeQFont(painter) painter.setFont(font) fm = utils.FontMetrics(font, painter.device()) al_spacing = fm.leading() + fm.descent() # an extra offset if required self._delta_axis += label.get('offset').convert(painter) text = s.label # avoid adding blank text to plot if not text: return horz = s.direction == 'horizontal' align1 = 1 align2 = {'centre': 0, 'at-minimum': -1, 'at-maximum': 1}[sl.position] if horz: ax, ay = align2, align1 else: ax, ay = align1, align2 reflected = self.coordReflected if reflected: if horz: ay = -ay else: ax = -ax # angle of text (logic is slightly complex) angle = int(sl.rotate) if horz: if not reflected: angle = 360-angle else: angle = angle+270 if reflected: angle = 360-angle angle = angle % 360 if sl.position == 'centre': x = 0.5*(self.coordParr1 + self.coordParr2) elif sl.position == 'at-minimum': x = self.coordParr1 else: x = self.coordParr2 y = self.coordPerp + sign*(self._delta_axis+al_spacing) if not horz: x, y = y, x # make axis label flush with edge of plot if # it's appropriate if outerbounds is not None and sl.atEdge: if abs(s.otherPosition) < 1e-4 and not reflected: if horz: y = outerbounds[3] ay = -ay else: x = outerbounds[0] ax = -ax elif abs(s.otherPosition-1.) < 1e-4 and reflected: if horz: y = outerbounds[1] ay = -ay else: x = outerbounds[2] ax = -ax r = utils.Renderer(painter, font, x, y, text, ax, ay, angle, usefullheight = True) # make sure text is in plot rectangle if outerbounds is not None: r.ensureInBox( minx=outerbounds[0], maxx=outerbounds[2], miny=outerbounds[1], maxy=outerbounds[3] ) texttorender.insert(0, (r, s.get('Label').makeQPen()) ) def _shouldAutoMirror(self): """Work out whether to do mirroring.""" # FIXME: This is a nasty hack: must think of a better way to do this s = self.settings countaxis = 0 for c in self.parent.children: try: # don't allow descendents of axis to look like an axis # to this function (e.g. colorbar) if c.isaxis and s.direction == c.settings.direction: countaxis += 1 except AttributeError: # if it's not an axis we get here pass return countaxis <= 1 def _autoMirrorDraw(self, posn, painter, coordticks, coordminorticks): """Mirror axis to opposite side of graph if there isn't an axis there already.""" # swap axis to other side s = self.settings if s.otherPosition < 0.5: otheredge = 1. else: otheredge = 0. # temporarily change position of axis to other side for drawing self.updateAxisLocation(posn, otherposition=otheredge) if not s.Line.hide: self._drawAxisLine(painter) if not s.MinorTicks.hide: self._drawMinorTicks(painter, coordminorticks) if not s.MajorTicks.hide: self._drawMajorTicks(painter, coordticks) def chooseName(self): """Get default name for axis. Make x and y axes, then axisN.""" try: widgets = set(self.parent.childnames) except AttributeError: widgets = set() for name in ('x', 'y'): if name not in widgets: return name return widget.Widget.chooseName(self) def _suppressText(self, painter, parentposn, outerbounds): """Whether to suppress drawing text on this axis because it is too close to the edge of its parent bounding box. If the edge of the plot is within textheight then suppress text """ if outerbounds is None: return False s = self.settings height = utils.FontMetrics( s.get('Label').makeQFont(painter), painter.device()).height() otherposition = s.otherPosition if s.direction == 'vertical': if ( ( otherposition < 0.01 and abs(parentposn[0]-outerbounds[0]) < height) or ( otherposition > 0.99 and abs(parentposn[2]-outerbounds[2]) < height) ): return True else: if ( ( otherposition < 0.01 and abs(parentposn[3]-outerbounds[3]) < height) or ( otherposition > 0.99 and abs(parentposn[1]-outerbounds[1]) < height) ): return True return False def drawGrid(self, parentposn, phelper, outerbounds=None, ontop=False): """Code to draw gridlines. This is separate from the main draw routine because the grid should be behind/infront the data points. """ s = self.settings if ( s.hide or (s.MinorGridLines.hide and s.GridLines.hide) or s.GridLines.onTop != bool(ontop) ): return # draw grid on a different layer, depending on whether on top or not layer = (-2, -1)[bool(ontop)] painter = phelper.painter(self, parentposn, layer=layer) self.updateAxisLocation(parentposn) with painter: painter.save() painter.setClipRect( qt4.QRectF( qt4.QPointF(parentposn[0], parentposn[1]), qt4.QPointF(parentposn[2], parentposn[3]) ) ) if not s.MinorGridLines.hide: coordminorticks = self._graphToPlotter(self.minortickscalc) self._drawGridLines('MinorGridLines', painter, coordminorticks, parentposn) if not s.GridLines.hide: coordticks = self._graphToPlotter(self.majortickscalc) self._drawGridLines('GridLines', painter, coordticks, parentposn) painter.restore() def draw(self, parentposn, phelper, outerbounds=None): """Plot the axis on the painter. """ posn = self.computeBounds(parentposn, phelper) self.updateAxisLocation(posn) # exit if axis is hidden if self.settings.hide: return self.computePlottedRange() painter = phelper.painter(self, posn) with painter: self._axisDraw(posn, parentposn, outerbounds, painter, phelper) def _drawTextWithoutOverlap(self, painter, texttorender): """Aall the text is drawn at the end so that we can check it doesn't overlap. texttorender is a list of (Renderer, QPen) tuples. """ overlaps = utils.RectangleOverlapTester() for r, pen in texttorender: rect = r.getTightBounds() if not overlaps.willOverlap(rect): painter.setPen(pen) r.render() overlaps.addRect(rect) # debug # poly = rect.makePolygon() # painter.drawPolygon(poly) def _axisDraw(self, posn, parentposn, outerbounds, painter, phelper): """Internal drawing routine.""" s = self.settings # make control item for axis phelper.setControlGraph(self, [ controlgraph.ControlAxisLine( self, s.direction, self.coordParr1, self.coordParr2, self.coordPerp, posn) ]) # get tick vals coordticks = self._graphToPlotter(self.majortickscalc) coordminorticks = self._graphToPlotter(self.minortickscalc) texttorender = [] # multiplication factor if reflection on the axis is requested sign = 1 if s.direction == 'vertical': sign *= -1 if self.coordReflected: sign *= -1 # plot the line along the axis if not s.Line.hide: self._drawAxisLine(painter) # plot minor ticks if not s.MinorTicks.hide: self._drawMinorTicks(painter, coordminorticks) # keep track of distance from axis self._delta_axis = 0 # plot major ticks if not s.MajorTicks.hide: self._drawMajorTicks(painter, coordticks) # plot tick labels suppresstext = self._suppressText(painter, parentposn, outerbounds) if not s.TickLabels.hide and not suppresstext: self._drawTickLabels(phelper, painter, coordticks, sign, outerbounds, self.majortickscalc, texttorender) # draw an axis label if not s.Label.hide and not suppresstext: self._drawAxisLabel(painter, sign, outerbounds, texttorender) # mirror axis at other side of plot if s.autoMirror and self._shouldAutoMirror(): self._autoMirrorDraw(posn, painter, coordticks, coordminorticks) self._drawTextWithoutOverlap(painter, texttorender) def updateControlItem(self, cgi): """Update axis position from control item.""" s = self.settings p = cgi.maxposn if cgi.zoomed(): # zoom axis scale # we convert a neighbouring pixel to see how we should # round the text c1, c2, c1delt, c2delt = self.plotterToGraphCoords( cgi.maxposn, N.array([cgi.minzoom, cgi.maxzoom, cgi.minzoom+1, cgi.maxzoom-1])) if c1 > c2: c1, c2 = c2, c1 c1delt, c2delt = c2delt, c1delt round1 = utils.round2delt(c1, c1delt) round2 = utils.round2delt(c2, c2delt) ops = [] if ( (s.min == 'Auto' or not N.allclose(c1, s.min, rtol=1e-8)) and N.isfinite(round1) ): ops.append( document.OperationSettingSet( s.get('min'), round1) ) if ( (s.max == 'Auto' or not N.allclose(c2, s.max, rtol=1e-8)) and N.isfinite(round2) ): ops.append( document.OperationSettingSet( s.get('max'), round2) ) self.document.applyOperation( document.OperationMultiple(ops, descr=_('zoom axis'))) elif cgi.moved(): # move axis # convert positions to fractions pt1, pt2, ppt1, ppt2 = ( (3, 1, 0, 2), (0, 2, 3, 1) ) [s.direction == 'horizontal'] minfrac = abs((cgi.minpos - p[pt1]) / (p[pt2] - p[pt1])) maxfrac = abs((cgi.maxpos - p[pt1]) / (p[pt2] - p[pt1])) axisfrac = abs((cgi.axispos - p[ppt1]) / (p[ppt2] - p[ppt1])) # swap if wrong way around if minfrac > maxfrac: minfrac, maxfrac = maxfrac, minfrac # update doc ops = [] if s.lowerPosition != minfrac: ops.append( document.OperationSettingSet( s.get('lowerPosition'), round(minfrac, 3)) ) if s.upperPosition != maxfrac: ops.append( document.OperationSettingSet( s.get('upperPosition'), round(maxfrac, 3)) ) if s.otherPosition != axisfrac: ops.append( document.OperationSettingSet( s.get('otherPosition'), round(axisfrac, 3)) ) self.document.applyOperation( document.OperationMultiple(ops, descr=_('adjust axis'))) # allow the factory to instantiate an axis document.thefactory.register( Axis ) veusz-1.21.1/veusz/widgets/image.py0000644000175000017500000002673712327177747015446 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Image plotting from 2d datasets.""" from __future__ import division from .. import qtall as qt4 import numpy as N from .. import setting from .. import document from .. import utils from . import plotters def _(text, disambiguation=None, context='Image'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) def cropLinearImageToBox(image, pltx, plty, posn): """Given a plotting range pltx[0]->pltx[1], plty[0]->plty[1] and plotting bounds posn, return an image which is cropped to posn. Returns: - updated pltx range - updated plty range - cropped image """ x1, y1, x2, y2 = posn pltx1, pltx2 = pltx pltw = pltx2-pltx1 plty2, plty1 = plty plth = plty2-plty1 imw = image.width() imh = image.height() pixw = pltw / imw pixh = plth / imh cutr = [0, 0, imw-1, imh-1] # work out where image intercepts posn, and make sure image # fills at least that area # need to chop left if pltx1 < x1: d = int((x1-pltx1) / pixw) cutr[0] += d pltx[0] += d*pixw # need to chop right if pltx2 > x2: d = max(0, int((pltx2-x2) / pixw) - 1) cutr[2] -= d pltx[1] -= d*pixw # chop top if plty1 < y1: d = int((y1-plty1) / pixh) cutr[1] += d plty[1] += d*pixh # chop bottom if plty2 > y2: d = max(0, int((plty2-y2) / pixh) - 1) cutr[3] -= d plty[0] -= d*pixh # create chopped-down image newimage = image.copy( cutr[0], cutr[1], cutr[2]-cutr[0]+1, cutr[3]-cutr[1]+1) # return new image coordinates and image return pltx, plty, newimage def cropGridImageToBox(image, gridx, gridy, posn): """Given an image, pixel coordinates and box, crop image to box.""" def trimGrid(grid, p1, p2): """Trim grid to bounds given, returning index range.""" if grid[0] < grid[-1]: # fwd order i1 = max(N.searchsorted(grid, p1, side='right')-1, 0) i2 = min(N.searchsorted(grid, p2, side='left'), len(grid)) + 1 else: # reverse order of grid gridr = grid[::-1] i1 = max( len(grid) - N.searchsorted(gridr, p2, side='left')-1, 0) i2 = min( len(grid) - N.searchsorted(gridr, p1, side='right'), len(grid) ) + 1 return i1, i2 def trimEdge(grid, minval, maxval): """Trim outer gridpoints to minval and maxval.""" if grid[0] < grid[-1]: grid[0] = max(grid[0], minval) grid[-1] = min(grid[-1], maxval) else: grid[0] = min(grid[0], maxval) grid[-1] = max(grid[-1], minval) # see whether cropping necessary x1, x2 = trimGrid(gridx, posn[0], posn[2]) y1, y2 = trimGrid(gridy, posn[1], posn[3]) if x1 > 0 or y1 > 0 or x2 < len(gridx)-1 or y2 < len(gridy)-1: # do cropping image = image.copy(x1, len(gridy)-y2, x2-x1-1, y2-y1-1) gridx = N.array(gridx[x1:x2]) gridy = N.array(gridy[y1:y2]) # trim outer grid point to viewable range trimEdge(gridx, posn[0], posn[2]) trimEdge(gridy, posn[1], posn[3]) return gridx, gridy, image class Image(plotters.GenericPlotter): """A class which plots an image on a graph with a specified coordinate system.""" typename='image' allowusercreation=True description=_('Plot a 2d dataset as an image') def __init__(self, parent, name=None): """Initialise plotter with axes.""" plotters.GenericPlotter.__init__(self, parent, name=name) if type(self) == Image: self.readDefaults() @classmethod def addSettings(klass, s): """Construct list of settings.""" plotters.GenericPlotter.addSettings(s) s.add( setting.DatasetExtended( 'data', '', dimensions = 2, descr = _('Dataset to plot'), usertext=_('Dataset')), 0 ) s.add( setting.FloatOrAuto( 'min', 'Auto', descr = _('Minimum value of image scale'), usertext=_('Min. value')), 1 ) s.add( setting.FloatOrAuto( 'max', 'Auto', descr = _('Maximum value of image scale'), usertext=_('Max. value')), 2 ) s.add( setting.Choice( 'colorScaling', ['linear', 'sqrt', 'log', 'squared'], 'linear', descr = _('Scaling to transform numbers to color'), usertext=_('Scaling')), 3 ) s.add( setting.DatasetExtended( 'transparencyData', '', dimensions = 2, descr = _('Dataset to use for transparency (0 to 1)'), usertext=_('Trans. data')), 4 ) s.add( setting.Colormap( 'colorMap', 'grey', descr = _('Set of colors to plot data with'), usertext=_('Colormap'), formatting=True), 5 ) s.add( setting.Bool( 'colorInvert', False, descr = _('Invert color map'), usertext=_('Invert colormap'), formatting=True), 6 ) s.add( setting.Int( 'transparency', 0, descr = _('Transparency percentage'), usertext = _('Transparency'), minval = 0, maxval = 100, formatting=True), 7 ) s.add( setting.Bool( 'smooth', False, descr = _('Smooth image to display resolution'), usertext = _('Smooth'), formatting = True ) ) @property def userdescription(self): """User friendly description.""" s = self.settings out = [] if s.data: out.append(s.data) out += [s.colorScaling, s.colorMap] return ', '.join(out) def getDataValueRange(self, data): """Update data range from data.""" s = self.settings minval = s.min if minval == 'Auto' and data is not None: minval = N.nanmin(data.data) maxval = s.max if maxval == 'Auto' and data is not None: maxval = N.nanmax(data.data) # this is used currently by colorbar objects return (minval, maxval) def affectsAxisRange(self): """Range information provided by widget.""" s = self.settings return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def getRange(self, axis, depname, axrange): """Automatically determine the ranges of variable on the axes.""" # this is copied from Image, probably should combine s = self.settings d = self.document # return if no data data = s.get('data').getData(d) if data is None or data.dimensions != 2: return xr, yr = data.getDataRanges() if depname == 'sx': axrange[0] = min( axrange[0], xr[0] ) axrange[1] = max( axrange[1], xr[1] ) elif depname == 'sy': axrange[0] = min( axrange[0], yr[0] ) axrange[1] = max( axrange[1], yr[1] ) def getColorbarParameters(self): """Return parameters for colorbar.""" s = self.settings d = self.document data = s.get('data').getData(d) minval, maxval = self.getDataValueRange(data) return (minval, maxval, s.colorScaling, s.colorMap, s.transparency, s.colorInvert) def dataDraw(self, painter, axes, posn, clip): """Draw image.""" s = self.settings d = self.document data = s.get('data').getData(d) if s.hide or data is None or data.dimensions != 2: return transimg = s.get('transparencyData').getData(d) if transimg is not None: transimg = transimg.data rangex, rangey = data.getDataRanges() pltrangex = axes[0].dataToPlotterCoords(posn, N.array(rangex)) pltrangey = axes[1].dataToPlotterCoords(posn, N.array(rangey)) # make QImage from data cmap = d.getColormap(s.colorMap, s.colorInvert) datavaluerange = self.getDataValueRange(data) image = utils.applyColorMap( cmap, s.colorScaling, data.data, datavaluerange[0], datavaluerange[1], s.transparency, transimg=transimg) if data.isLinearImage(): # linearly spaced grid if ( pltrangex[0] < posn[0] or pltrangex[1] > posn[2] or pltrangey[0] < posn[1] or pltrangey[1] > posn[3] ): # need to crop image pltrangex, pltrangey, image = cropLinearImageToBox( image, pltrangex, pltrangey, posn) else: # get pixel edges, converted to plotter coordinates xedgep, yedgep = data.getPixelEdges( scalefnx=lambda v: axes[0].dataToPlotterCoords(posn, v), scalefny=lambda v: axes[1].dataToPlotterCoords(posn, v)) # crop any pixels completely outside posn xedgep, yedgep, image = cropGridImageToBox( image, xedgep, yedgep, posn) # make image on linear grid image = utils.resampleLinearImage(image, xedgep, yedgep) pltrangex = xedgep[0], xedgep[-1] pltrangey = yedgep[0], yedgep[-1] # optionally smooth images before displaying if s.smooth: image = image.scaled( pltrangex[1]-pltrangex[0], pltrangey[0]-pltrangey[1], qt4.Qt.IgnoreAspectRatio, qt4.Qt.SmoothTransformation) # get position and size of output image xp, yp = pltrangex[0], pltrangey[1] xw = pltrangex[1]-pltrangex[0] yw = pltrangey[0]-pltrangey[1] # invert output drawing if axes go from positive->negative # we only translate the coordinate system if this is the case xscale = 1 if xw > 0 else -1 yscale = 1 if yw > 0 else -1 if xscale != 1 or yscale != 1: painter.save() painter.translate(xp, yp) xp = yp = 0 painter.scale(xscale, yscale) # draw image #image = image.copy(qt4.QRect(qt4.QPoint(0, 0), qt4.QPoint(20, 20))) painter.drawImage(qt4.QRectF(xp, yp, abs(xw), abs(yw)), image) # restore painter if image was inverted if xscale != 1 or yscale != 1: painter.restore() # allow the factory to instantiate an image document.thefactory.register(Image) veusz-1.21.1/veusz/widgets/controlgraph.py0000664000175000017500000006012412237406466017045 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """ Classes for moving widgets around Control items have a createGraphicsItem method which returns a graphics item to control the object """ from __future__ import division import math from ..compat import crange, czip from .. import qtall as qt4 from .. import document from .. import setting def _(text, disambiguation=None, context='controlgraph'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) ############################################################################## class _ShapeCorner(qt4.QGraphicsRectItem): """Representing the corners of the rectangle.""" def __init__(self, parent, rotator=False): qt4.QGraphicsRectItem.__init__(self, parent) if rotator: self.setBrush( qt4.QBrush(setting.settingdb.color('cntrlline')) ) self.setRect(-3, -3, 6, 6) else: self.setBrush(qt4.QBrush(setting.settingdb.color('cntrlcorner')) ) self.setRect(-5, -5, 10, 10) self.setPen(qt4.QPen(qt4.Qt.NoPen)) self.setFlag(qt4.QGraphicsItem.ItemIsMovable) self.setZValue(3.) def mouseMoveEvent(self, event): """Notify parent on move.""" qt4.QGraphicsRectItem.mouseMoveEvent(self, event) self.parentItem().updateFromCorner(self, event) def mouseReleaseEvent(self, event): """Notify parent on unclicking.""" qt4.QGraphicsRectItem.mouseReleaseEvent(self, event) self.parentItem().updateWidget() ############################################################################## def controlLinePen(): """Get pen for lines around shapes.""" return qt4.QPen(setting.settingdb.color('cntrlline'), 2, qt4.Qt.DotLine) class _EdgeLine(qt4.QGraphicsLineItem): """Line used for edges of resizing box.""" def __init__(self, parent, ismovable = True): qt4.QGraphicsLineItem.__init__(self, parent) self.setPen(controlLinePen()) self.setZValue(2.) if ismovable: self.setFlag(qt4.QGraphicsItem.ItemIsMovable) self.setCursor(qt4.Qt.SizeAllCursor) def mouseMoveEvent(self, event): """Notify parent on move.""" qt4.QGraphicsLineItem.mouseMoveEvent(self, event) self.parentItem().updateFromLine(self, self.pos()) def mouseReleaseEvent(self, event): """Notify parent on unclicking.""" qt4.QGraphicsLineItem.mouseReleaseEvent(self, event) self.parentItem().updateWidget() ############################################################################## class ControlMarginBox(object): def __init__(self, widget, posn, maxposn, painthelper, ismovable = True, isresizable = True): """Create control box item. widget: widget this is controllng posn: coordinates of box [x1, y1, x2, y2] maxposn: coordinates of biggest possibe box painthelper: painterhelper to get scaling from ismovable: box can be moved isresizable: box can be resized """ # save values self.posn = posn self.maxposn = maxposn self.widget = widget self.ismovable = ismovable self.isresizable = isresizable # we need these later to convert back to original units self.pagesize = painthelper.pagesize self.scaling = painthelper.scaling self.dpi = painthelper.dpi def createGraphicsItem(self): return _GraphMarginBox(self) def setWidgetMargins(self): """A helpful routine for setting widget margins after moving or resizing. This is called by the widget after receiving updateControlItem """ s = self.widget.settings # get margins in pixels left = self.posn[0] - self.maxposn[0] right = self.maxposn[2] - self.posn[2] top = self.posn[1] - self.maxposn[1] bottom = self.maxposn[3] - self.posn[3] # set up fake painthelper containing veusz scalings helper = document.PaintHelper(self.pagesize, scaling=self.scaling, dpi=self.dpi) # convert to physical units left = s.get('leftMargin').convertInverse(left, helper) right = s.get('rightMargin').convertInverse(right, helper) top = s.get('topMargin').convertInverse(top, helper) bottom = s.get('bottomMargin').convertInverse(bottom, helper) # modify widget margins operations = ( document.OperationSettingSet(s.get('leftMargin'), left), document.OperationSettingSet(s.get('rightMargin'), right), document.OperationSettingSet(s.get('topMargin'), top), document.OperationSettingSet(s.get('bottomMargin'), bottom) ) self.widget.document.applyOperation( document.OperationMultiple(operations, descr=_('resize margins'))) def setPageSize(self): """Helper for setting document/page widget size. This is called by the widget after receiving updateControlItem """ s = self.widget.settings # get margins in pixels width = self.posn[2] - self.posn[0] height = self.posn[3] - self.posn[1] # set up fake painter containing veusz scalings helper = document.PaintHelper(self.pagesize, scaling=self.scaling, dpi=self.dpi) # convert to physical units width = s.get('width').convertInverse(width, helper) height = s.get('height').convertInverse(height, helper) # modify widget margins operations = ( document.OperationSettingSet(s.get('width'), width), document.OperationSettingSet(s.get('height'), height), ) self.widget.document.applyOperation( document.OperationMultiple(operations, descr=_('change page size'))) class _GraphMarginBox(qt4.QGraphicsItem): """A box which can be moved or resized. Can automatically set margins or widget """ # posn coords of each corner mapcornertoposn = ( (0, 1), (2, 1), (0, 3), (2, 3) ) def __init__(self, params): """Create control box item.""" qt4.QGraphicsItem.__init__(self) self.params = params self.setZValue(2.) # create corners of box self.corners = [_ShapeCorner(self) for i in crange(4)] # lines connecting corners self.lines = [_EdgeLine(self, ismovable=params.ismovable) for i in crange(4)] # hide corners if box is not resizable if not params.isresizable: for c in self.corners: c.hide() self.updateCornerPosns() def updateCornerPosns(self): """Update all corners from updated box.""" par = self.params pos = par.posn # update cursors self.corners[0].setCursor(qt4.Qt.SizeFDiagCursor) self.corners[1].setCursor(qt4.Qt.SizeBDiagCursor) self.corners[2].setCursor(qt4.Qt.SizeBDiagCursor) self.corners[3].setCursor(qt4.Qt.SizeFDiagCursor) # trim box to maximum size pos[0] = max(pos[0], par.maxposn[0]) pos[1] = max(pos[1], par.maxposn[1]) pos[2] = min(pos[2], par.maxposn[2]) pos[3] = min(pos[3], par.maxposn[3]) # move corners for corner, (xindex, yindex) in czip(self.corners, self.mapcornertoposn): corner.setPos( qt4.QPointF( pos[xindex], pos[yindex] ) ) # move lines w, h = pos[2]-pos[0], pos[3]-pos[1] self.lines[0].setPos(pos[0], pos[1]) self.lines[0].setLine(0, 0, w, 0) self.lines[1].setPos(pos[2], pos[1]) self.lines[1].setLine(0, 0, 0, h) self.lines[2].setPos(pos[2], pos[3]) self.lines[2].setLine(0, 0, -w, 0) self.lines[3].setPos(pos[0], pos[3]) self.lines[3].setLine(0, 0, 0, -h) def updateFromLine(self, line, thispos): """Edge line of box was moved - update bounding box.""" par = self.params # need old coordinate to work out how far line has moved try: li = self.lines.index(line) except ValueError: return ox = par.posn[ (0, 2, 2, 0)[li] ] oy = par.posn[ (1, 1, 3, 3)[li] ] # add on deltas to box coordinates dx, dy = thispos.x()-ox, thispos.y()-oy # make sure box can't be moved outside the allowed region if dx > 0: dx = min(dx, par.maxposn[2]-par.posn[2]) else: dx = -min(abs(dx), abs(par.maxposn[0]-par.posn[0])) if dy > 0: dy = min(dy, par.maxposn[3]-par.posn[3]) else: dy = -min(abs(dy), abs(par.maxposn[1]-par.posn[1])) # move the box par.posn[0] += dx par.posn[1] += dy par.posn[2] += dx par.posn[3] += dy # update corner coords and other line coordinates self.updateCornerPosns() def updateFromCorner(self, corner, event): """Move corner of box to new position.""" try: index = self.corners.index(corner) except ValueError: return pos = self.params.posn pos[ self.mapcornertoposn[index][0] ] = corner.x() pos[ self.mapcornertoposn[index][1] ] = corner.y() # this is needed if the corners move past each other if pos[0] > pos[2]: # swap x pos[0], pos[2] = pos[2], pos[0] self.corners[0], self.corners[1] = self.corners[1], self.corners[0] self.corners[2], self.corners[3] = self.corners[3], self.corners[2] if pos[1] > pos[3]: # swap y pos[1], pos[3] = pos[3], pos[1] self.corners[0], self.corners[2] = self.corners[2], self.corners[0] self.corners[1], self.corners[3] = self.corners[3], self.corners[1] self.updateCornerPosns() def boundingRect(self): return qt4.QRectF(0, 0, 0, 0) def paint(self, painter, option, widget): """Intentionally empty painter.""" def updateWidget(self): """Update widget margins.""" self.params.widget.updateControlItem(self.params) ############################################################################## class ControlResizableBox(object): """Control a resizable box. Item resizes centred around a position """ def __init__(self, widget, posn, dims, angle, allowrotate=False): """Initialise with widget and boxbounds shape. Rotation is allowed if allowrotate is set """ self.widget = widget self.posn = posn self.dims = dims self.angle = angle self.allowrotate = allowrotate def createGraphicsItem(self): return _GraphResizableBox(self) class _GraphResizableBox(qt4.QGraphicsRectItem): """Control a resizable box. Item resizes centred around a position """ def __init__(self, params): """Initialise with widget and boxbounds shape. Rotation is allowed if allowrotate is set """ qt4.QGraphicsRectItem.__init__(self, params.posn[0], params.posn[1], params.dims[0], params.dims[1]) self.params = params self.rotate(params.angle) # initial setup self.setCursor(qt4.Qt.SizeAllCursor) self.setZValue(1.) self.setFlag(qt4.QGraphicsItem.ItemIsMovable) self.setPen(controlLinePen()) self.setBrush( qt4.QBrush() ) # create child graphicsitem for each corner self.corners = [_ShapeCorner(self) for i in crange(4)] self.corners[0].setCursor(qt4.Qt.SizeFDiagCursor) self.corners[1].setCursor(qt4.Qt.SizeBDiagCursor) self.corners[2].setCursor(qt4.Qt.SizeBDiagCursor) self.corners[3].setCursor(qt4.Qt.SizeFDiagCursor) # whether box is allowed to be rotated self.rotator = None if params.allowrotate: self.rotator = _ShapeCorner(self, rotator=True) self.rotator.setCursor(qt4.Qt.CrossCursor) self.updateCorners() self.rotator.setPos( 0, -abs(params.dims[1]*0.5) ) def updateFromCorner(self, corner, event): """Take position and update corners.""" par = self.params if corner in self.corners: # compute size from corner position par.dims[0] = abs(corner.pos().x()*2) par.dims[1] = abs(corner.pos().y()*2) elif corner == self.rotator: # work out angle relative to centre of widget delta = event.scenePos() - self.scenePos() angle = math.atan2( delta.y(), delta.x() ) # change to degrees from correct direction par.angle = (angle*(180/math.pi) + 90.) % 360 # apply rotation selfpt = self.pos() self.resetTransform() self.setPos(selfpt) self.rotate(par.angle) self.updateCorners() def updateCorners(self): """Update corners on size.""" par = self.params # update position and size self.setPos( par.posn[0], par.posn[1] ) self.setRect( -par.dims[0]*0.5, -par.dims[1]*0.5, par.dims[0], par.dims[1] ) # update corners self.corners[0].setPos(-par.dims[0]*0.5, -par.dims[1]*0.5) self.corners[1].setPos( par.dims[0]*0.5, -par.dims[1]*0.5) self.corners[2].setPos(-par.dims[0]*0.5, par.dims[1]*0.5) self.corners[3].setPos( par.dims[0]*0.5, par.dims[1]*0.5) if self.rotator: # set rotator position (constant distance) self.rotator.setPos( 0, -abs(par.dims[1]*0.5) ) def mouseReleaseEvent(self, event): """If the item has been moved, do and update.""" qt4.QGraphicsRectItem.mouseReleaseEvent(self, event) self.updateWidget() def mouseMoveEvent(self, event): """Keep track of movement.""" qt4.QGraphicsRectItem.mouseMoveEvent(self, event) self.params.posn = [self.pos().x(), self.pos().y()] def updateWidget(self): """Tell the user the graphicsitem has been moved or resized.""" self.params.widget.updateControlItem(self.params) ############################################################################## class ControlMovableBox(ControlMarginBox): """Item for user display for controlling widget. This is a dotted movable box with an optional "cross" where the real position of the widget is """ def __init__(self, widget, posn, painthelper, crosspos=None): ControlMarginBox.__init__(self, widget, posn, [-10000, -10000, 10000, 10000], painthelper, isresizable=False) self.deltacrosspos = (crosspos[0] - self.posn[0], crosspos[1] - self.posn[1]) def createGraphicsItem(self): return _GraphMovableBox(self) class _GraphMovableBox(_GraphMarginBox): def __init__(self, params): _GraphMarginBox.__init__(self, params) self.cross = _ShapeCorner(self) self.cross.setCursor(qt4.Qt.SizeAllCursor) self.updateCornerPosns() def updateCornerPosns(self): _GraphMarginBox.updateCornerPosns(self) par = self.params if hasattr(self, 'cross'): # this fails if called before self.cross is initialised! self.cross.setPos( par.deltacrosspos[0] + par.posn[0], par.deltacrosspos[1] + par.posn[1] ) def updateFromCorner(self, corner, event): if corner == self.cross: # if cross moves, move whole box par = self.params cx, cy = self.cross.pos().x(), self.cross.pos().y() dx = cx - (par.deltacrosspos[0] + par.posn[0]) dy = cy - (par.deltacrosspos[1] + par.posn[1]) par.posn[0] += dx par.posn[1] += dy par.posn[2] += dx par.posn[3] += dy self.updateCornerPosns() else: _GraphMarginBox.updateFromCorner(self, corner, event) ############################################################################## class ControlLine(object): """For controlling the position and ends of a line.""" def __init__(self, widget, x1, y1, x2, y2): self.widget = widget self.line = x1, y1, x2, y2 def createGraphicsItem(self): return _GraphLine(self) class _GraphLine(qt4.QGraphicsLineItem): """Represents the line as a graphics item.""" def __init__(self, params): qt4.QGraphicsLineItem.__init__(self, *params.line) self.params = params self.setCursor(qt4.Qt.SizeAllCursor) self.setFlag(qt4.QGraphicsItem.ItemIsMovable) self.setZValue(1.) self.setPen(controlLinePen()) self.pts = [_ShapeCorner(self, rotator=True), _ShapeCorner(self, rotator=True)] self.pts[0].setPos(params.line[0], params.line[1]) self.pts[1].setPos(params.line[2], params.line[3]) self.pts[0].setCursor(qt4.Qt.CrossCursor) self.pts[1].setCursor(qt4.Qt.CrossCursor) def updateFromCorner(self, corner, event): """Take position and update ends of line.""" line = (self.pts[0].x(), self.pts[0].y(), self.pts[1].x(), self.pts[1].y()) self.setLine(*line) def mouseReleaseEvent(self, event): """If widget has moved, tell it.""" qt4.QGraphicsItem.mouseReleaseEvent(self, event) self.updateWidget() def updateWidget(self): """Update caller with position and line positions.""" pt1 = ( self.pts[0].x() + self.pos().x(), self.pts[0].y() + self.pos().y() ) pt2 = ( self.pts[1].x() + self.pos().x(), self.pts[1].y() + self.pos().y() ) self.params.widget.updateControlItem(self.params, pt1, pt2) ############################################################################# class _AxisGraphicsLineItem(qt4.QGraphicsLineItem): def __init__(self, parent): qt4.QGraphicsLineItem.__init__(self, parent) self.parent = parent self.setPen(controlLinePen()) self.setZValue(2.) self.setFlag(qt4.QGraphicsItem.ItemIsMovable) def mouseReleaseEvent(self, event): """Notify finished.""" qt4.QGraphicsLineItem.mouseReleaseEvent(self, event) self.parent.updateWidget() def mouseMoveEvent(self, event): """Move the axis.""" qt4.QGraphicsLineItem.mouseMoveEvent(self, event) self.parent.doLineUpdate() class ControlAxisLine(object): """Controlling position of an axis.""" def __init__(self, widget, direction, minpos, maxpos, axispos, maxposn): self.widget = widget self.direction = direction if minpos > maxpos: minpos, maxpos = maxpos, minpos self.minpos = self.minzoom = self.minorig = minpos self.maxpos = self.maxzoom = self.maxorig = maxpos self.axisorigpos = self.axispos = axispos self.maxposn = maxposn def zoomed(self): """Is this a zoom?""" return self.minzoom != self.minorig or self.maxzoom != self.maxorig def moved(self): """Has axis moved?""" return ( self.minpos != self.minorig or self.maxpos != self.maxorig or self.axisorigpos != self.axispos ) def createGraphicsItem(self): return _GraphAxisLine(self) class _GraphAxisLine(qt4.QGraphicsItem): curs = {True: qt4.Qt.SizeVerCursor, False: qt4.Qt.SizeHorCursor} curs_zoom = {True: qt4.Qt.SplitVCursor, False: qt4.Qt.SplitHCursor} def __init__(self, params): """Line is about to be shown.""" qt4.QGraphicsItem.__init__(self) self.params = params self.pts = [ _ShapeCorner(self), _ShapeCorner(self), _ShapeCorner(self), _ShapeCorner(self) ] self.line = _AxisGraphicsLineItem(self) # set cursors and tooltips for items self.horz = (params.direction == 'horizontal') for p in self.pts[0:2]: p.setCursor(self.curs[not self.horz]) p.setToolTip("Move axis ends") for p in self.pts[2:]: p.setCursor(self.curs_zoom[not self.horz]) p.setToolTip("Change axis scale") self.line.setCursor( self.curs[self.horz] ) self.line.setToolTip("Move axis position") self.setZValue(2.) self.updatePos() def updatePos(self): """Set ends of line and line positions from stored values.""" par = self.params mxp = par.maxposn def _clip(*args): """Clip positions to bounds of box given coords.""" par.minpos = max(par.minpos, mxp[args[0]]) par.maxpos = min(par.maxpos, mxp[args[1]]) par.axispos = max(par.axispos, mxp[args[2]]) par.axispos = min(par.axispos, mxp[args[3]]) if self.horz: _clip(0, 2, 1, 3) # set positions if par.zoomed(): self.line.setPos(par.minzoom, par.axispos) self.line.setLine(0, 0, par.maxzoom-par.minzoom, 0) else: self.line.setPos(par.minpos, par.axispos) self.line.setLine(0, 0, par.maxpos-par.minpos, 0) self.pts[0].setPos(par.minpos, par.axispos) self.pts[1].setPos(par.maxpos, par.axispos) self.pts[2].setPos(par.minzoom, par.axispos-15) self.pts[3].setPos(par.maxzoom, par.axispos-15) else: _clip(1, 3, 0, 2) # set positions if par.zoomed(): self.line.setPos(par.axispos, par.minzoom) self.line.setLine(0, 0, 0, par.maxzoom-par.minzoom) else: self.line.setPos(par.axispos, par.minpos) self.line.setLine(0, 0, 0, par.maxpos-par.minpos) self.pts[0].setPos(par.axispos, par.minpos) self.pts[1].setPos(par.axispos, par.maxpos) self.pts[2].setPos(par.axispos+15, par.minzoom) self.pts[3].setPos(par.axispos+15, par.maxzoom) def updateFromCorner(self, corner, event): """Ends of axis have moved, so update values.""" par = self.params pt = (corner.y(), corner.x())[self.horz] # which end has moved? if corner is self.pts[0]: # horizonal or vertical axis? par.minpos = pt elif corner is self.pts[1]: par.maxpos = pt elif corner is self.pts[2]: par.minzoom = pt elif corner is self.pts[3]: par.maxzoom = pt # swap round end points if min > max if par.minpos > par.maxpos: par.minpos, par.maxpos = par.maxpos, par.minpos self.pts[0], self.pts[1] = self.pts[1], self.pts[0] self.updatePos() def doLineUpdate(self): """Line has moved, so update position.""" pos = self.line.pos() if self.horz: self.params.axispos = pos.y() else: self.params.axispos = pos.x() self.updatePos() def updateWidget(self): """Tell widget to update.""" self.params.widget.updateControlItem(self.params) def boundingRect(self): """Intentionally zero bounding rect.""" return qt4.QRectF(0, 0, 0, 0) def paint(self, painter, option, widget): """Intentionally empty painter.""" veusz-1.21.1/veusz/qtwidgets/0000775000175000017500000000000012376130063014322 5ustar jssjssveusz-1.21.1/veusz/qtwidgets/historyvaluecombo.py0000664000175000017500000000624312260623255020461 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """A combobox which remembers previous setting """ from __future__ import division from ..compat import crange from .. import qtall as qt4 from .. import setting class HistoryValueCombo(qt4.QComboBox): """This combobox records what value was previously saved """ def __init__(self, *args): qt4.QComboBox.__init__(self, *args) self.defaultlist = [] self.defaultval = None self.hasshown = False def getSettingName(self): """Get name for saving in settings.""" # get dialog for widget dialog = self.parent() while not isinstance(dialog, qt4.QDialog): dialog = dialog.parent() # combine dialog and object names to make setting return '%s_%s_HistoryValueCombo' % ( dialog.objectName(), self.objectName() ) def saveHistory(self): """Save contents of history combo to settings.""" # only save history if it has been loaded if not self.hasshown: return # collect current items history = [ self.itemText(i) for i in crange(self.count()) ] history.insert(0, self.currentText()) # remove dups histout = [] histset = set() for item in history: if item not in histset: histout.append(item) histset.add(item) # save the history setting.settingdb[self.getSettingName()] = histout def showEvent(self, event): """Show HistoryCombo and load history.""" qt4.QComboBox.showEvent(self, event) if self.hasshown: return self.clear() self.addItems(self.defaultlist) text = setting.settingdb.get(self.getSettingName(), self.defaultval) if text is not None: indx = self.findText(text) if indx < 0: if self.isEditable(): self.insertItem(0, text) indx = 0 self.setCurrentIndex(indx) self.hasshown = True def hideEvent(self, event): """Save history as widget is hidden.""" qt4.QComboBox.hideEvent(self, event) if self.hasshown: text = self.currentText() setting.settingdb[self.getSettingName()] = text veusz-1.21.1/veusz/qtwidgets/historycheck.py0000664000175000017500000000464312237406466017413 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division from .. import qtall as qt4 from .. import setting class HistoryCheck(qt4.QCheckBox): """Checkbox remembers its setting between calls """ def __init__(self, *args): qt4.QCheckBox.__init__(self, *args) self.default = False def getSettingName(self): """Get name for saving in settings.""" # get dialog for widget dialog = self.parent() while not isinstance(dialog, qt4.QDialog): dialog = dialog.parent() # combine dialog and object names to make setting return '%s_%s_HistoryCheck' % ( dialog.objectName(), self.objectName() ) def loadHistory(self): """Load contents of HistoryCheck from settings.""" checked = setting.settingdb.get(self.getSettingName(), self.default) # this is to ensure toggled() signals get sent self.setChecked(not checked) self.setChecked(checked) def saveHistory(self): """Save contents of HistoryCheck to settings.""" setting.settingdb[self.getSettingName()] = self.isChecked() def showEvent(self, event): """Show HistoryCheck and load history.""" qt4.QCheckBox.showEvent(self, event) # we do this now rather than in __init__ because the widget # has no name set at __init__ self.loadHistory() def hideEvent(self, event): """Save history as widget is hidden.""" qt4.QCheckBox.hideEvent(self, event) self.saveHistory() veusz-1.21.1/veusz/qtwidgets/historygroupbox.py0000644000175000017500000000565112327177747020210 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division from ..compat import cstr from .. import qtall as qt4 from .. import setting class HistoryGroupBox(qt4.QGroupBox): """Group box remembers settings of radio buttons inside it. emits radioClicked(radiowidget) when clicked """ radioClicked = qt4.pyqtSignal(qt4.QObject) def getSettingName(self): """Get name for saving in settings.""" # get dialog for widget dialog = self.parent() while not isinstance(dialog, qt4.QDialog): dialog = dialog.parent() # combine dialog and object names to make setting return '%s_%s_HistoryGroup' % ( dialog.objectName(), self.objectName() ) def loadHistory(self): """Load from settings.""" # connect up radio buttons to emit clicked signal for w in self.children(): if isinstance(w, qt4.QRadioButton): def doemit(widget): return lambda: self.radioClicked.emit(widget) w.clicked.connect(doemit(w)) # set item to be checked checked = setting.settingdb.get(self.getSettingName(), "") for w in self.children(): if isinstance(w, qt4.QRadioButton) and ( w.objectName() == checked or checked == ""): w.click() return def getRadioChecked(self): """Get name of radio button checked.""" for w in self.children(): if isinstance(w, qt4.QRadioButton) and w.isChecked(): return w return None def saveHistory(self): """Save to settings.""" name = cstr(self.getRadioChecked().objectName()) setting.settingdb[self.getSettingName()] = name def showEvent(self, event): """Show and load history.""" qt4.QGroupBox.showEvent(self, event) self.loadHistory() def hideEvent(self, event): """Save history as widget is hidden.""" qt4.QGroupBox.hideEvent(self, event) self.saveHistory() veusz-1.21.1/veusz/qtwidgets/recentfilesbutton.py0000644000175000017500000000536112327177747020456 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import os.path from .. import qtall as qt4 from ..compat import cstr from .. import setting def removeBadRecents(itemlist): """Remove duplicates from list and bad entries.""" previous = set() i = 0 while i < len(itemlist): if itemlist[i] in previous: del itemlist[i] elif not os.path.exists(itemlist[i]): del itemlist[i] else: previous.add(itemlist[i]) i += 1 # trim list del itemlist[10:] class RecentFilesButton(qt4.QPushButton): """A button for remembering recent files. emits filechosen(filename) if a file is chosen """ filechosen = qt4.pyqtSignal(cstr) def __init__(self, *args): qt4.QPushButton.__init__(self, *args) self.menu = qt4.QMenu() self.setMenu(self.menu) self.settingname = None def setSetting(self, name): """Specify settings to use when loading menu. Should be called before use.""" self.settingname = name self.fillMenu() def fillMenu(self): """Add filenames to menu.""" self.menu.clear() recent = setting.settingdb.get(self.settingname, []) removeBadRecents(recent) setting.settingdb[self.settingname] = recent for filename in recent: if os.path.exists(filename): act = self.menu.addAction( os.path.basename(filename) ) def loadrecentfile(f): return lambda: self.filechosen.emit(f) act.triggered.connect(loadrecentfile(filename)) def addFile(self, filename): """Add filename to list of recent files.""" recent = setting.settingdb.get(self.settingname, []) recent.insert(0, os.path.abspath(filename)) setting.settingdb[self.settingname] = recent self.fillMenu() veusz-1.21.1/veusz/qtwidgets/__init__.py0000664000175000017500000000307712237406466016453 0ustar jssjss# Copyright (C) 2011 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Veusz qtwidgets module.""" # insert history combo into the list of modules so that it can be found # by loadUi - yuck import sys from . import historycombo from . import historycheck from . import historyvaluecombo from . import historygroupbox from . import historyspinbox from . import recentfilesbutton from . import lineeditwithclear sys.modules['historycombo'] = historycombo sys.modules['historycheck'] = historycheck sys.modules['historyvaluecombo'] = historyvaluecombo sys.modules['historygroupbox'] = historygroupbox sys.modules['historyspinbox'] = historyspinbox sys.modules['recentfilesbutton'] = recentfilesbutton sys.modules['lineeditwithclear'] = lineeditwithclear veusz-1.21.1/veusz/qtwidgets/historycombo.py0000664000175000017500000001107712260623255017425 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """A combobox which remembers its history. The history is stored in the Veusz settings database. """ from __future__ import division from ..compat import crange from .. import qtall as qt4 from .. import setting class HistoryCombo(qt4.QComboBox): """This combobox records what items have been entered into it so the user can choose them again. Duplicates and blanks are ignored. """ def __init__(self, *args): qt4.QComboBox.__init__(self, *args) # sane defaults self.setEditable(True) self.setAutoCompletion(True) self.setMaxCount(50) self.setInsertPolicy(qt4.QComboBox.InsertAtTop) self.setDuplicatesEnabled(False) self.setSizePolicy( qt4.QSizePolicy(qt4.QSizePolicy.MinimumExpanding, qt4.QSizePolicy.Fixed) ) # stops combobox readjusting in size to fit contents self.setSizeAdjustPolicy( qt4.QComboBox.AdjustToMinimumContentsLengthWithIcon) self.default = [] self.hasshown = False def text(self): """Get text in combobox - this gives it the same interface as QLineEdit.""" return self.currentText() def setText(self, text): """Set text in combobox - gives same interface as QLineEdit.""" self.lineEdit().setText(text) def hasAcceptableInput(self): """Input valid? - gives same interface as QLineEdit.""" return self.lineEdit().hasAcceptableInput() def replaceAndAddHistory(self, item): """Replace the text and place item at top of history.""" self.lineEdit().setText(item) index = self.findText(item) # lookup for existing item (if any) if index != -1: # remove any old items matching this self.removeItem(index) # put new item in self.insertItem(0, item) # set selected item in drop down list match current item self.setCurrentIndex(0) def getSettingName(self): """Get name for saving in settings.""" # get dialog for widget dialog = self.parent() while not isinstance(dialog, qt4.QDialog): dialog = dialog.parent() # combine dialog and object names to make setting return '%s_%s_HistoryCombo' % ( dialog.objectName(), self.objectName() ) def loadHistory(self): """Load contents of history combo from settings.""" self.clear() history = setting.settingdb.get(self.getSettingName(), self.default) self.insertItems(0, history) self.hasshown = True def saveHistory(self): """Save contents of history combo to settings.""" # only save history if it has been loaded if not self.hasshown: return # collect current items history = [ self.itemText(i) for i in crange(self.count()) ] history.insert(0, self.currentText()) # remove dups histout = [] histset = set() for item in history: if item not in histset: histout.append(item) histset.add(item) # save the history setting.settingdb[self.getSettingName()] = histout def showEvent(self, event): """Show HistoryCombo and load history.""" qt4.QComboBox.showEvent(self, event) # we do this now rather than in __init__ because the widget # has no name set at __init__ if not self.hasshown: self.loadHistory() def hideEvent(self, event): """Save history as widget is hidden.""" qt4.QComboBox.hideEvent(self, event) self.saveHistory() veusz-1.21.1/veusz/qtwidgets/datasetbrowser.py0000644000175000017500000006721112327177747017752 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2011 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """A widget for navigating datasets.""" from __future__ import division import os.path import numpy as N import textwrap from ..compat import crange, citems, czip, cstr from .. import qtall as qt4 from .. import setting from .. import document from .. import utils from .lineeditwithclear import LineEditWithClear from ..utils.treemodel import TMNode, TreeModel def _(text, disambiguation=None, context="DatasetBrowser"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) def datasetLinkFile(ds): """Get a linked filename from a dataset.""" if ds.linked is None: return "/" else: return ds.linked.filename class DatasetNode(TMNode): """Node for a dataset.""" def __init__(self, doc, dsname, cols, parent): ds = doc.data[dsname] data = [] assert cols[0] == "name" for c in cols: if c == "name": data.append( dsname ) elif c == "size": data.append( ds.userSize() ) elif c == "type": data.append( ds.dstype ) elif c == "linkfile": data.append( os.path.basename(datasetLinkFile(ds)) ) TMNode.__init__(self, tuple(data), parent) self.doc = doc self.cols = cols def getPreviewPixmap(self, ds): """Get a preview pixmap for a dataset.""" size = (140, 70) if ds.dimensions != 1 or ds.datatype != "numeric": return None pixmap = qt4.QPixmap(*size) pixmap.fill(qt4.Qt.transparent) p = qt4.QPainter(pixmap) p.setRenderHint(qt4.QPainter.Antialiasing) # calculate data points try: if len(ds.data) < size[1]: y = ds.data else: intvl = len(ds.data)//size[1]+1 y = ds.data[::intvl] x = N.arange(len(y)) # plot data points on image minval, maxval = N.nanmin(y), N.nanmax(y) y = (y-minval) / (maxval-minval) * size[1] finite = N.isfinite(y) x, y = x[finite], y[finite] x = x * (1./len(x)) * size[0] poly = qt4.QPolygonF() utils.addNumpyToPolygonF(poly, x, size[1]-y) p.setPen( qt4.QPen(qt4.Qt.blue) ) p.drawPolyline(poly) # draw x axis if span 0 p.setPen( qt4.QPen(qt4.Qt.black) ) if minval <= 0 and maxval > 0: y0 = size[1] - (0-minval)/(maxval-minval)*size[1] p.drawLine(x[0], y0, x[-1], y0) else: p.drawLine(x[0], size[1], x[-1], size[1]) p.drawLine(x[0], 0, x[0], size[1]) except (ValueError, ZeroDivisionError): # zero sized array after filtering or min == max, so return None p.end() return None p.end() return pixmap def toolTip(self, column): """Return tooltip for column.""" try: ds = self.doc.data[self.data[0]] except KeyError: return None c = self.cols[column] if c == "name": text = ds.description() if text is None: text = '' if ds.tags: text += '\n\n' + _('Tags: %s') % (' '.join(sorted(ds.tags))) return textwrap.fill(text, 40) elif c == "size" or (c == 'type' and 'size' not in self.cols): text = ds.userPreview() # add preview of dataset if possible pix = self.getPreviewPixmap(ds) if pix: text = text.replace("\n", "
    ") text = "%s
    %s" % (text, utils.pixmapAsHtml(pix)) return text elif c == "linkfile" or c == "type": return textwrap.fill(ds.linkedInformation(), 40) return None def dataset(self): """Get associated dataset.""" try: return self.doc.data[self.data[0]] except KeyError: return None def datasetName(self): """Get dataset name.""" return self.data[0] def cloneTo(self, newroot): """Make a clone of self at the root given.""" return self.__class__(self.doc, self.data[0], self.cols, newroot) class FilenameNode(TMNode): """A special node for holding filenames of files.""" def nodeData(self, column): """basename of filename for data.""" if column == 0: if self.data[0] == "/": return "/" else: return os.path.basename(self.data[0]) return None def filename(self): """Return filename.""" return self.data[0] def toolTip(self, column): """Full filename for tooltip.""" if column == 0: return self.data[0] return None def treeFromList(nodelist, rootdata): """Construct a tree from a list of nodes.""" tree = TMNode( rootdata, None ) for node in nodelist: tree.insertChildSorted(node) return tree class DatasetRelationModel(TreeModel): """A model to show how the datasets are related to each file.""" def __init__(self, doc, grouping="filename", readonly=False, filterdims=None, filterdtype=None): """Model parameters: doc: document group: how to group datasets readonly: no modification of data filterdims/filterdtype: filter dimensions and datatypes. """ TreeModel.__init__(self, (_("Dataset"), _("Size"), _("Type"))) self.doc = doc self.linkednodes = {} self.grouping = grouping self.filter = "" self.readonly = readonly self.filterdims = filterdims self.filterdtype = filterdtype self.refresh() doc.signalModified.connect(self.refresh) def datasetFilterOut(self, ds, node): """Should dataset be filtered out by filter options.""" filterout = False # is filter text not in node text or text keep = True if self.filter != "": keep = False if any([t.find(self.filter) >= 0 for t in ds.tags]): keep = True if any([t.find(self.filter) >= 0 for t in node.data]): keep = True # check dimensions haven't been filtered if ( self.filterdims is not None and ds.dimensions not in self.filterdims ): filterout = True # check type hasn't been filtered if ( self.filterdtype is not None and ds.datatype not in self.filterdtype ): filterout = True if filterout: return True return not keep def makeGrpTreeNone(self): """Make tree with no grouping.""" tree = TMNode( (_("Dataset"), _("Size"), _("Type"), _("File")), None ) for name, ds in citems(self.doc.data): child = DatasetNode( self.doc, name, ("name", "size", "type", "linkfile"), None ) # add if not filtered for filtering if not self.datasetFilterOut(ds, child): tree.insertChildSorted(child) return tree def makeGrpTree(self, coltitles, colitems, grouper, GrpNodeClass): """Make a tree grouping with function: coltitles: tuple of titles of columns for user colitems: tuple of items to lookup in DatasetNode grouper: function of dataset to return text for grouping GrpNodeClass: class for creating grouping nodes """ grpnodes = {} for name, ds in citems(self.doc.data): child = DatasetNode(self.doc, name, colitems, None) # check whether filtered out if not self.datasetFilterOut(ds, child): # get group grps = grouper(ds) for grp in grps: if grp not in grpnodes: grpnodes[grp] = GrpNodeClass( (grp,), None ) # add to group grpnodes[grp].insertChildSorted(child) return treeFromList(list(grpnodes.values()), coltitles) def makeGrpTreeFilename(self): """Make a tree of datasets grouped by linked file.""" return self.makeGrpTree( (_("Dataset"), _("Size"), _("Type")), ("name", "size", "type"), lambda ds: (datasetLinkFile(ds),), FilenameNode ) def makeGrpTreeSize(self): """Make a tree of datasets grouped by dataset size.""" return self.makeGrpTree( (_("Dataset"), _("Type"), _("Filename")), ("name", "type", "linkfile"), lambda ds: (ds.userSize(),), TMNode ) def makeGrpTreeType(self): """Make a tree of datasets grouped by dataset type.""" return self.makeGrpTree( (_("Dataset"), _("Size"), _("Filename")), ("name", "size", "linkfile"), lambda ds: (ds.dstype,), TMNode ) def makeGrpTreeTags(self): """Make a tree of datasets grouped by tags.""" def getgrp(ds): if ds.tags: return sorted(ds.tags) else: return [_("None")] return self.makeGrpTree( (_("Dataset"), _("Size"), _("Type"), _("Filename")), ("name", "size", "type", "linkfile"), getgrp, TMNode ) def flags(self, idx): """Return model flags for index.""" f = TreeModel.flags(self, idx) # allow dataset names to be edited if ( idx.isValid() and isinstance(self.objFromIndex(idx), DatasetNode) and not self.readonly and idx.column() == 0 ): f |= qt4.Qt.ItemIsEditable return f def setData(self, idx, newname, role): """Rename dataset.""" dsnode = self.objFromIndex(idx) if not utils.validateDatasetName(newname) or newname in self.doc.data: return False self.doc.applyOperation( document.OperationDatasetRename(dsnode.data[0], newname)) self.dataChanged.emit(idx, idx) return True @qt4.pyqtSlot() def refresh(self): """Update tree of datasets when document changes.""" tree = { "none": self.makeGrpTreeNone, "filename": self.makeGrpTreeFilename, "size": self.makeGrpTreeSize, "type": self.makeGrpTreeType, "tags": self.makeGrpTreeTags, }[self.grouping]() self.syncTree(tree) class DatasetsNavigatorTree(qt4.QTreeView): """Tree view for dataset names.""" updateitem = qt4.pyqtSignal() selecteddatasets = qt4.pyqtSignal(list) def __init__(self, doc, mainwin, grouping, parent, readonly=False, filterdims=None, filterdtype=None): """Initialise the dataset tree view. doc: veusz document mainwin: veusz main window (or None if readonly) grouping: grouping mode of datasets parent: parent window or None filterdims: if set, only show datasets with dimensions given filterdtype: if set, only show datasets with type given """ qt4.QTreeView.__init__(self, parent) self.doc = doc self.mainwindow = mainwin self.model = DatasetRelationModel(doc, grouping, readonly=readonly, filterdims=filterdims, filterdtype=filterdtype) self.setModel(self.model) self.setSelectionMode(qt4.QTreeView.ExtendedSelection) self.setSelectionBehavior(qt4.QTreeView.SelectRows) self.setUniformRowHeights(True) self.setContextMenuPolicy(qt4.Qt.CustomContextMenu) if not readonly: self.customContextMenuRequested.connect(self.showContextMenu) self.model.refresh() self.expandAll() # stretch of columns hdr = self.header() hdr.setStretchLastSection(False) hdr.setResizeMode(0, qt4.QHeaderView.Stretch) for col in crange(1, 3): hdr.setResizeMode(col, qt4.QHeaderView.ResizeToContents) # when documents have finished opening, expand all nodes if mainwin is not None: mainwin.documentOpened.connect(self.expandAll) # keep track of selection self.selectionModel().selectionChanged.connect(self.slotNewSelection) # expand nodes by default self.model.rowsInserted.connect(self.slotNewRow) def changeGrouping(self, grouping): """Change the tree grouping behaviour.""" self.model.grouping = grouping self.model.refresh() self.expandAll() def changeFilter(self, filtertext): """Change filtering text.""" self.model.filter = filtertext self.model.refresh() self.expandAll() def selectDataset(self, dsname): """Find, and if possible select dataset name.""" matches = self.model.match( self.model.index(0, 0, qt4.QModelIndex()), qt4.Qt.DisplayRole, dsname, -1, qt4.Qt.MatchFixedString | qt4.Qt.MatchCaseSensitive | qt4.Qt.MatchRecursive ) for idx in matches: if isinstance(self.model.objFromIndex(idx), DatasetNode): self.selectionModel().setCurrentIndex( idx, qt4.QItemSelectionModel.SelectCurrent | qt4.QItemSelectionModel.Clear | qt4.QItemSelectionModel.Rows ) def showContextMenu(self, pt): """Context menu for nodes.""" # get selected nodes idxs = self.selectionModel().selection().indexes() nodes = [ self.model.objFromIndex(i) for i in idxs if i.column() == 0 ] # unique list of types of nodes types = utils.unique([ type(n) for n in nodes ]) menu = qt4.QMenu() # put contexts onto submenus if multiple types selected if DatasetNode in types: thismenu = menu if len(types) > 1: thismenu = menu.addMenu(_("Datasets")) self.datasetContextMenu( [n for n in nodes if isinstance(n, DatasetNode)], thismenu) elif FilenameNode in types: thismenu = menu if len(types) > 1: thismenu = menu.addMenu(_("Files")) self.filenameContextMenu( [n for n in nodes if isinstance(n, FilenameNode)], thismenu) def _paste(): """Paste dataset(s).""" if document.isClipboardDataMime(): mime = qt4.QApplication.clipboard().mimeData() self.doc.applyOperation(document.OperationDataPaste(mime)) # if there is data to paste, add menu item if document.isClipboardDataMime(): menu.addAction(_("Paste"), _paste) if len( menu.actions() ) != 0: menu.exec_(self.mapToGlobal(pt)) def datasetContextMenu(self, dsnodes, menu): """Return context menu for datasets.""" from ..dialogs import dataeditdialog datasets = [d.dataset() for d in dsnodes] dsnames = [d.datasetName() for d in dsnodes] def _edit(): """Open up dialog box to recreate dataset.""" for dataset, dsname in czip(datasets, dsnames): if type(dataset) in dataeditdialog.recreate_register: dataeditdialog.recreate_register[type(dataset)]( self.mainwindow, self.doc, dataset, dsname) def _edit_data(): """Open up data edit dialog.""" for dataset, dsname in czip(datasets, dsnames): if type(dataset) not in dataeditdialog.recreate_register: self.mainwindow.slotDataEdit(editdataset=dsname) def _delete(): """Simply delete dataset.""" self.doc.applyOperation( document.OperationMultiple( [document.OperationDatasetDelete(n) for n in dsnames], descr=_('delete dataset(s)'))) def _unlink_file(): """Unlink dataset from file.""" self.doc.applyOperation( document.OperationMultiple( [document.OperationDatasetUnlinkFile(n) for d,n in czip(datasets,dsnames) if d.canUnlink() and d.linked], descr=_('unlink dataset(s)'))) def _unlink_relation(): """Unlink dataset from relation.""" self.doc.applyOperation( document.OperationMultiple( [document.OperationDatasetUnlinkRelation(n) for d,n in czip(datasets,dsnames) if d.canUnlink() and not d.linked], descr=_('unlink dataset(s)'))) def _copy(): """Copy data to clipboard.""" mime = document.generateDatasetsMime(dsnames, self.doc) qt4.QApplication.clipboard().setMimeData(mime) # editing recreate = [type(d) in dataeditdialog.recreate_register for d in datasets] if any(recreate): menu.addAction(_("Edit"), _edit) if not all(recreate): menu.addAction(_("Edit data"), _edit_data) # deletion menu.addAction(_("Delete"), _delete) # linking unlink_file = [d.canUnlink() and d.linked for d in datasets] if any(unlink_file): menu.addAction(_("Unlink file"), _unlink_file) unlink_relation = [d.canUnlink() and not d.linked for d in datasets] if any(unlink_relation): menu.addAction(_("Unlink relation"), _unlink_relation) # tagging submenu tagmenu = menu.addMenu(_("Tags")) for tag in self.doc.datasetTags(): def toggle(tag=tag): state = [tag in d.tags for d in datasets] if all(state): op = document.OperationDataUntag else: op = document.OperationDataTag self.doc.applyOperation(op(tag, dsnames)) a = tagmenu.addAction(tag, toggle) a.setCheckable(True) state = [tag in d.tags for d in datasets] a.setChecked( all(state) ) def addtag(): tag, ok = qt4.QInputDialog.getText( self, _("New tag"), _("Enter new tag")) if ok: tag = tag.strip().replace(' ', '') if tag: self.doc.applyOperation( document.OperationDataTag( tag, dsnames) ) tagmenu.addAction(_("Add..."), addtag) # copy menu.addAction(_("Copy"), _copy) if len(datasets) == 1: useasmenu = menu.addMenu(_("Use as")) self.getMenuUseAs(useasmenu, datasets[0]) def filenameContextMenu(self, nodes, menu): """Return context menu for filenames.""" from ..dialogs.reloaddata import ReloadData filenames = [n.filename() for n in nodes if n.filename() != '/'] if not filenames: return def _reload(): """Reload data in this file.""" d = ReloadData(self.doc, self.mainwindow, filenames=set(filenames)) self.mainwindow.showDialog(d) def _unlink_all(): """Unlink all datasets associated with file.""" self.doc.applyOperation( document.OperationMultiple( [document.OperationDatasetUnlinkByFile(f) for f in filenames], descr=_('unlink by file'))) def _delete_all(): """Delete all datasets associated with file.""" self.doc.applyOperation( document.OperationMultiple( [document.OperationDatasetDeleteByFile(f) for f in filenames], descr=_('delete by file'))) menu.addAction(_("Reload"), _reload) menu.addAction(_("Unlink all"), _unlink_all) menu.addAction(_("Delete all"), _delete_all) def getMenuUseAs(self, menu, dataset): """Build up menu of widget settings to use dataset in.""" def addifdatasetsetting(path, setn): def _setdataset(): self.doc.applyOperation( document.OperationSettingSet( path, self.doc.datasetName(dataset)) ) if ( isinstance(setn, setting.Dataset) and setn.dimensions == dataset.dimensions and setn.datatype == dataset.datatype and path[:12] != "/StyleSheet/" ): menu.addAction(path, _setdataset) self.doc.walkNodes(addifdatasetsetting, nodetypes=("setting",)) def keyPressEvent(self, event): """Enter key selects widget.""" if event.key() in (qt4.Qt.Key_Return, qt4.Qt.Key_Enter): self.updateitem.emit() return qt4.QTreeView.keyPressEvent(self, event) def mouseDoubleClickEvent(self, event): """Emit updateitem signal if double clicked.""" retn = qt4.QTreeView.mouseDoubleClickEvent(self, event) self.updateitem.emit() return retn def slotNewSelection(self, selected, deselected): """Emit selecteditem signal on new selection.""" self.selecteddatasets.emit(self.getSelectedDatasets()) def slotNewRow(self, parent, start, end): """Expand parent if added.""" self.expand(parent) def getSelectedDatasets(self): """Returns list of selected datasets.""" datasets = [] for idx in self.selectionModel().selectedRows(): node = self.model.objFromIndex(idx) try: name = node.datasetName() if name in self.doc.data: datasets.append(name) except AttributeError: pass return datasets class DatasetBrowser(qt4.QWidget): """Widget which shows the document's datasets.""" # how datasets can be grouped grpnames = ("none", "filename", "type", "size", "tags") grpentries = { "none": _("None"), "filename": _("Filename"), "type": _("Type"), "size": _("Size"), "tags": _("Tags"), } def __init__(self, thedocument, mainwin, parent, readonly=False, filterdims=None, filterdtype=None): """Initialise widget: thedocument: document to show mainwin: main window of application (or None if readonly) parent: parent of widget. readonly: for choosing datasets only filterdims: if set, only show datasets with dimensions given filterdtype: if set, only show datasets with type given """ qt4.QWidget.__init__(self, parent) self.layout = qt4.QVBoxLayout() self.setLayout(self.layout) # options for navigator are in this layout self.optslayout = qt4.QHBoxLayout() # grouping options - use a menu to choose the grouping self.grpbutton = qt4.QPushButton(_("Group")) self.grpmenu = qt4.QMenu() self.grouping = setting.settingdb.get("navtree_grouping", "filename") self.grpact = qt4.QActionGroup(self) self.grpact.setExclusive(True) for name in self.grpnames: a = self.grpmenu.addAction(self.grpentries[name]) a.grpname = name a.setCheckable(True) if name == self.grouping: a.setChecked(True) self.grpact.addAction(a) self.grpact.triggered.connect(self.slotGrpChanged) self.grpbutton.setMenu(self.grpmenu) self.grpbutton.setToolTip(_("Group datasets with property given")) self.optslayout.addWidget(self.grpbutton) # filtering by entering text self.optslayout.addWidget(qt4.QLabel(_("Filter"))) self.filteredit = LineEditWithClear() self.filteredit.setToolTip(_("Enter text here to filter datasets")) self.filteredit.textChanged.connect(self.slotFilterChanged) self.optslayout.addWidget(self.filteredit) self.layout.addLayout(self.optslayout) # the actual widget tree self.navtree = DatasetsNavigatorTree( thedocument, mainwin, self.grouping, None, readonly=readonly, filterdims=filterdims, filterdtype=filterdtype) self.layout.addWidget(self.navtree) def slotGrpChanged(self, action): """Grouping changed by user.""" self.navtree.changeGrouping(action.grpname) setting.settingdb["navtree_grouping"] = action.grpname def slotFilterChanged(self, filtertext): """Filtering changed by user.""" self.navtree.changeFilter(filtertext) def selectDataset(self, dsname): """Find, and if possible select dataset name.""" self.navtree.selectDataset(dsname) class DatasetBrowserPopup(DatasetBrowser): """Popup window for dataset browser for selecting datasets. This is used by setting.controls.Dataset """ closing = qt4.pyqtSignal() newdataset = qt4.pyqtSignal(cstr) def __init__(self, document, dsname, parent, filterdims=None, filterdtype=None): """Open popup window for document dsname: dataset name parent: window parent filterdims: if set, only show datasets with dimensions given filterdtype: if set, only show datasets with type given """ DatasetBrowser.__init__(self, document, None, parent, readonly=True, filterdims=filterdims, filterdtype=filterdtype) self.setWindowFlags(qt4.Qt.Popup) self.setAttribute(qt4.Qt.WA_DeleteOnClose) self.spacing = self.fontMetrics().height() utils.positionFloatingPopup(self, parent) self.selectDataset(dsname) self.installEventFilter(self) self.navtree.setFocus() self.navtree.updateitem.connect(self.slotUpdateItem) def eventFilter(self, node, event): """Grab clicks outside this window to close it.""" if ( isinstance(event, qt4.QMouseEvent) and event.buttons() != qt4.Qt.NoButton ): frame = qt4.QRect(0, 0, self.width(), self.height()) if not frame.contains(event.pos()): self.close() return True return qt4.QTextEdit.eventFilter(self, node, event) def sizeHint(self): """A reasonable size for the text editor.""" return qt4.QSize(self.spacing*30, self.spacing*20) def closeEvent(self, event): """Tell the calling widget that we are closing.""" self.closing.emit() event.accept() def slotUpdateItem(self): """Emit new dataset signal.""" selected = self.navtree.selectionModel().currentIndex() if selected.isValid(): n = self.navtree.model.objFromIndex(selected) if isinstance(n, DatasetNode): self.newdataset.emit(n.data[0]) self.close() veusz-1.21.1/veusz/qtwidgets/historyspinbox.py0000664000175000017500000000432312237406466020013 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division from .. import qtall as qt4 from .. import setting class HistorySpinBox(qt4.QSpinBox): """A SpinBox which remembers its setting between calls.""" def __init__(self, *args): qt4.QSpinBox.__init__(self, *args) self.default = 0 def getSettingName(self): """Get name for saving in settings.""" # get dialog for widget dialog = self.parent() while not isinstance(dialog, qt4.QDialog): dialog = dialog.parent() # combine dialog and object names to make setting return "%s_%s_HistorySpinBox" % ( dialog.objectName(), self.objectName() ) def loadHistory(self): """Load contents of HistorySpinBox from settings.""" num = setting.settingdb.get(self.getSettingName(), self.default) self.setValue(num) def saveHistory(self): """Save contents of HistorySpinBox to settings.""" setting.settingdb[self.getSettingName()] = self.value() def showEvent(self, event): """Show HistorySpinBox and load history.""" qt4.QSpinBox.showEvent(self, event) self.loadHistory() def hideEvent(self, event): """Save history as widget is hidden.""" qt4.QSpinBox.hideEvent(self, event) self.saveHistory() veusz-1.21.1/veusz/qtwidgets/lineeditwithclear.py0000644000175000017500000000526012327177747020415 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2011 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division from .. import qtall as qt4 from .. import utils class LineEditWithClear(qt4.QLineEdit): """This is a line edit widget which supplies a clear button to delete the text if it is clicked. Adapted from: http://labs.qt.nokia.com/2007/06/06/lineedit-with-a-clear-button/ """ def __init__(self, *args): """Initialise the line edit.""" qt4.QLineEdit.__init__(self, *args) # the clear button itself, with no padding self.clearbutton = cb = qt4.QToolButton(self) cb.setIcon( utils.getIcon('kde-edit-delete') ) cb.setCursor(qt4.Qt.ArrowCursor) cb.setStyleSheet('QToolButton { border: none; padding: 0px; }') cb.setToolTip("Clear text") cb.hide() cb.clicked.connect(self.clear) # button should appear if there is text self.textChanged.connect(self.updateCloseButton) # positioning of the button fw = self.style().pixelMetric(qt4.QStyle.PM_DefaultFrameWidth) self.setStyleSheet("QLineEdit { padding-right: %ipx; } " % (cb.sizeHint().width() + fw + 1)) msz = self.minimumSizeHint() mx = cb.sizeHint().height()+ fw*2 + 2 self.setMinimumSize( max(msz.width(), mx), max(msz.height(), mx) ) def resizeEvent(self, evt): """Move button if widget resized.""" sz = self.clearbutton.sizeHint() fw = self.style().pixelMetric(qt4.QStyle.PM_DefaultFrameWidth) r = self.rect() self.clearbutton.move( r.right() - fw - sz.width(), (r.bottom() + 1 - sz.height())//2 ) def updateCloseButton(self, text): """Button should only appear if there is text.""" self.clearbutton.setVisible(text != '') veusz-1.21.1/veusz/veusz_main.py0000755000175000017500000002425112327177747015066 0ustar jssjss#!/usr/bin/env python # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## '''Main Veusz executable.''' from __future__ import division import sys import os.path import signal import optparse # trick to make sure veusz is on the path, if being run as a script try: import veusz except ImportError: sys.path.append( os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ) import veusz from veusz.compat import czip, cbytes from veusz import qtall as qt4 from veusz import utils copyr='''Veusz %s Copyright (C) Jeremy Sanders 2003-2014 and contributors Licenced under the GNU General Public Licence (version 2 or greater) ''' splashcopyr='''Veusz %s
    Copyright (C) Jeremy Sanders 2003-2014 and contributors
    Licenced under the GPL (version 2 or greater) ''' def handleIntSignal(signum, frame): '''Ask windows to close if Ctrl+C pressed.''' qt4.qApp.closeAllWindows() def makeSplashLogo(): '''Make a splash screen logo.''' border = 16 xw, yw = 520, 240 pix = qt4.QPixmap(xw, yw) pix.fill() p = qt4.QPainter(pix) # draw logo on pixmap logo = utils.getPixmap('logo.png') p.drawPixmap( xw//2 - logo.width()//2, border, logo ) # add copyright text doc = qt4.QTextDocument() doc.setPageSize( qt4.QSizeF(xw, yw - 3*border - logo.height()) ) f = qt4.qApp.font() f.setPointSize(14) doc.setDefaultFont(f) doc.setDefaultTextOption( qt4.QTextOption(qt4.Qt.AlignCenter) ) doc.setHtml(splashcopyr % utils.version()) p.translate(0, 2*border + logo.height()) doc.drawContents(p) p.end() return pix def excepthook(excepttype, exceptvalue, tracebackobj): '''Show exception dialog if an exception occurs.''' from veusz.dialogs.exceptiondialog import ExceptionDialog if not isinstance(exceptvalue, utils.IgnoreException): # next exception is ignored to clear out the stack frame of the # previous exception - yuck d = ExceptionDialog((excepttype, exceptvalue, tracebackobj), None) d.exec_() def listen(args, quiet): '''For running with --listen option.''' from veusz.veusz_listen import openWindow openWindow(args, quiet=quiet) def export(exports, args): '''A shortcut to load a set of files and export them.''' from veusz import document from veusz import utils for expfn, vsz in czip(exports, args[1:]): doc = document.Document() ci = document.CommandInterpreter(doc) ci.Load(vsz) ci.run('Export(%s)' % repr(expfn)) def convertArgsUnicode(args): '''Convert set of arguments to unicode. Arguments in argv use current file system encoding ''' enc = sys.getfilesystemencoding() # bail out if not supported if enc is None: return args out = [] for a in args: if isinstance(a, cbytes): out.append( a.decode(enc) ) else: out.append(a) return out class ImportThread(qt4.QThread): '''Do import of main code within another thread. Main application runs when this is done ''' def run(self): from veusz import setting from veusz import widgets from veusz import dataimport class VeuszApp(qt4.QApplication): """Event which can open mac files.""" def __init__(self, args): qt4.QApplication.__init__(self, args) self.lastWindowClosed.connect(self.quit) # register a signal handler to catch ctrl+C signal.signal(signal.SIGINT, handleIntSignal) # parse command line options parser = optparse.OptionParser( usage='%prog [options] filename.vsz ...', version=copyr % utils.version()) parser.add_option('--unsafe-mode', action='store_true', help='disable safety checks when running documents' ' or scripts') parser.add_option('--listen', action='store_true', help='read and execute Veusz commands from stdin,' ' replacing veusz_listen') parser.add_option('--quiet', action='store_true', help='if in listening mode, do not open a window but' ' execute commands quietly') parser.add_option('--export', action='append', metavar='FILE', help='export the next document to this' ' output image file, exiting when finished') parser.add_option('--embed-remote', action='store_true', help=optparse.SUPPRESS_HELP) parser.add_option('--plugin', action='append', metavar='FILE', help='load the plugin from the file given for ' 'the session') parser.add_option('--translation', metavar='FILE', help='load the translation .qm file given') options, args = parser.parse_args(self.argv()) # export files to make images if options.export and len(options.export) != len(args)-1: parser.error( 'export option needs same number of documents and ' 'output files') # convert args to unicode from filesystem strings self.args = convertArgsUnicode(args) self.options = options self.openeventfiles = [] self.startupdone = False self.splash = None def openMainWindow(self, args): """Open the main window with any loaded files.""" from veusz.windows.mainwindow import MainWindow emptywins = [] for w in self.topLevelWidgets(): if isinstance(w, MainWindow) and w.document.isBlank(): emptywins.append(w) if len(args) > 1: # load in filenames given for filename in args[1:]: if not emptywins: MainWindow.CreateWindow(filename) else: emptywins[0].openFile(filename) else: # create blank window MainWindow.CreateWindow() def checkOpen(self): """If startup complete, open any files.""" if self.startupdone: self.openMainWindow([None] + self.openeventfiles) del self.openeventfiles[:] else: qt4.QTimer.singleShot(100, self.checkOpen) def event(self, event): """Handle events. This is the only way to get the FileOpen event.""" if event.type() == qt4.QEvent.FileOpen: self.openeventfiles.append(event.file()) # need to wait until startup has finished qt4.QTimer.singleShot(100, self.checkOpen) return True return qt4.QApplication.event(self, event) def startup(self): """Do startup.""" if not (self.options.listen or self.options.export): # show the splash screen on normal start self.splash = qt4.QSplashScreen(makeSplashLogo()) self.splash.show() # optionally load a translation if self.options.translation: trans = qt4.QTranslator() trans.load(self.options.translation) self.installTranslator(trans) self.thread = ImportThread() self.thread.finished.connect(self.slotStartApplication) self.thread.start() def slotStartApplication(self): """Start app, after modules imported.""" options = self.options args = self.args from veusz.utils import vzdbus, vzsamp vzdbus.setup() vzsamp.setup() from veusz import document from veusz import setting # install exception hook after thread has finished sys.excepthook = excepthook # for people who want to run any old script setting.transient_settings['unsafe_mode'] = bool( options.unsafe_mode) # load any requested plugins if options.plugin: document.Document.loadPlugins(pluginlist=options.plugin) # different modes if options.listen: # listen to incoming commands listen(args, quiet=options.quiet) elif options.export: export(options.export, args) self.quit() sys.exit(0) else: # standard start main window self.openMainWindow(args) self.startupdone = True # clear splash when startup done if self.splash is not None: self.splash.finish(self.topLevelWidgets()[0]) def run(): '''Run the main application.''' # nasty workaround for bug that causes non-modal windows not to # appear on mac see # https://github.com/jeremysanders/veusz/issues/39 if sys.platform == 'darwin': import glob for f in glob.glob(os.environ['HOME'] + '/Library/Saved Application State/org.python.veusz.*/*'): os.unlink(f) # jump to the embedding client entry point if required if len(sys.argv) == 2 and sys.argv[1] == '--embed-remote': from veusz.embed_remote import runremote runremote() return # this function is spaghetti-like and has nasty code paths. # the idea is to postpone the imports until the splash screen # is shown app = VeuszApp(sys.argv) app.startup() app.exec_() # if ran as a program if __name__ == '__main__': #import cProfile #cProfile.run('run()', 'outprofile.dat') run() veusz-1.21.1/veusz/veusz_listen.py0000755000175000017500000001146312327177747015441 0ustar jssjss#!/usr/bin/env python # Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """ Veusz interface which listens to stdin, and receives commands. Results are written to stdout All commands in CommandInterface are supported, plus further commands: Quit: exit the listening program Zoom x: Change the zoom factor of the plot to x """ from __future__ import division import sys from . import qtall as qt4 from .compat import cstr from .windows.simplewindow import SimpleWindow from . import document class ReadingThread(qt4.QThread): """Stdin reading thread. Emits newline signals with new data. We could use a QSocketNotifier on Unix, but this doesn't work on Windows as its stdin is a weird object """ newline = qt4.pyqtSignal(cstr) def run(self): """Emit lines read from stdin.""" while True: line = sys.stdin.readline() if line == '': break self.newline.emit(line) class InputListener(qt4.QObject): """Class reads text from stdin, in order to send commands to a document.""" def __init__(self, window): """Initialse the listening object to send commands to the document given by window.""" qt4.QObject.__init__(self) self.window = window self.document = window.document self.plot = window.plot self.pickle = False self.ci = document.CommandInterpreter(self.document) self.ci.addCommand('Quit', self.quitProgram) self.ci.addCommand('Zoom', self.plotZoom) self.ci.addCommand('EnableToolbar', self.enableToolbar) self.ci.addCommand('Pickle', self.enablePickle) self.ci.addCommand('ResizeWindow', self.resizeWindow) self.ci.addCommand('SetUpdateInterval', self.setUpdateInterval) self.ci.addCommand('MoveToPage', self.moveToPage) # reading is done in a separate thread so as not to block self.readthread = ReadingThread(self) self.readthread.newline.connect(self.processLine) self.readthread.start() def resizeWindow(self, width, height): """ResizeWindow(width, height) Resize the window to be width x height pixels.""" self.window.resize(width, height) def setUpdateInterval(self, interval): """SetUpdateInterval(interval) Set graph update interval. interval is in milliseconds (ms) set to zero to disable updates """ self.plot.setTimeout(interval) def moveToPage(self, pagenum): """MoveToPage(pagenum) Tell window to show specified pagenumber (starting from 1). """ self.plot.setPageNumber(pagenum-1) def quitProgram(self): """Exit the program.""" self.window.close() def plotZoom(self, zoomfactor): """Set the plot zoom factor.""" self.window.setZoom(zoomfactor) def enableToolbar(self, enable=True): """Enable plot toolbar.""" self.window.enableToolbar(enable) def enablePickle(self, on=True): """Enable/disable pickling of commands to/data from veusz""" self.pickle = on def processLine(self, line): """Process inputted line.""" if self.pickle: # line is repr form of pickled string get get rid of \n retn = self.ci.runPickle( eval(line.strip()) ) sys.stdout.write('%s\n' % repr(retn)) sys.stdout.flush() else: self.ci.run(line) def openWindow(args, quiet=False): '''Opening listening window. args is a list of arguments to the program ''' global _win global _listen if len(args) > 1: name = args[1] else: name = 'Veusz output' _win = SimpleWindow(name) if not quiet: _win.show() _listen = InputListener(_win) def run(): '''Actually run the program.''' app = qt4.QApplication(sys.argv) openWindow(sys.argv) app.exec_() # if ran as a program if __name__ == '__main__': run() veusz-1.21.1/veusz/embed.py0000664000175000017500000004652112346112416013744 0ustar jssjss# A module for embedding Veusz within another python program # Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """This module allows veusz to be embedded within other Python programs. For example: import time import numpy import veusz.embed as veusz g = veusz.Embedded('new win') g.To( g.Add('page') ) g.To( g.Add('graph') ) g.SetData('x', numpy.arange(20)) g.SetData('y', numpy.arange(20)**2) g.Add('xy') g.Zoom(0.5) time.sleep(60) g.Close() More than one embedded window can be opened at once """ from __future__ import division import atexit import sys import os import os.path import struct import socket import subprocess import time import uuid import functools import types # python3 compatibility try: import cPickle as pickle except ImportError: import pickle # check remote process has this API version API_VERSION = 2 def findOnPath(cmd): """Find a command on the system path, or None if does not exist.""" path = os.getenv('PATH', os.path.defpath) pathparts = path.split(os.path.pathsep) for dirname in pathparts: cmdtry = os.path.join(dirname, cmd) if os.path.isfile(cmdtry): return cmdtry return None class Embedded(object): """An embedded instance of Veusz. This embedded instance supports all the normal veusz functions """ remote = None def __init__(self, name='Veusz', copyof=None, hidden=False): """Initialse the embedded veusz window. name is the name of the window to show. copyof duplicates a view of the document in the Embedded instance given hidden makes a hidden window (useful for batch scripting) """ if not Embedded.remote: Embedded.startRemote() if not copyof: retval = self.sendCommand( (-1, '_NewWindow', (name,), {'hidden': hidden}) ) else: retval = self.sendCommand( (-1, '_NewWindowCopy', (name, copyof.winno), {'hidden': hidden}) ) self.winno, cmds = retval # add methods corresponding to Veusz commands for name, doc in cmds: func = functools.partial(self.runCommand, name) func.__doc__ = doc # set docstring func.__name__ = name # make name match what it calls method = types.MethodType(func, self) setattr(self, name, method) # assign to self # check API version is same try: remotever = self._apiVersion() except AttributeError: remotever = 0 if remotever != API_VERSION: raise RuntimeError("Remote Veusz instance reports version %i of" " API. This embed.py supports version %i." % (remotever, API_VERSION)) # define root object self.Root = WidgetNode(self, 'widget', '/') def StartSecondView(self, name = 'Veusz'): """Provides a second view onto the document of this window. Returns an Embedded instance """ return Embedded(name=name, copyof=self) def WaitForClose(self): """Wait for the window to close.""" # this is messy, polling for closure, but cleaner than doing # it in the remote client while not self.IsClosed(): time.sleep(0.1) @classmethod def makeSockets(cls): """Make socket(s) to communicate with remote process. Returns string to send to remote process """ if ( hasattr(socket, 'AF_UNIX') and hasattr(socket, 'socketpair') ): # convenient interface cls.sockfamily = socket.AF_UNIX sock, socket2 = socket.socketpair(cls.sockfamily, socket.SOCK_STREAM) # socket is closed on popen in Python 3.4+ without this (PEP 446) try: os.set_inheritable(socket2.fileno(), True) except AttributeError: pass sendtext = 'unix %i\n' % socket2.fileno() cls.socket2 = socket2 # prevent socket being destroyed waitaccept = False else: # otherwise mess around with internet sockets # * This is required for windows, which doesn't have AF_UNIX # * It is required where socketpair is not supported cls.sockfamily = socket.AF_INET sock = socket.socket(cls.sockfamily, socket.SOCK_STREAM) sock.bind( ('localhost', 0) ) interface, port = sock.getsockname() sock.listen(1) sendtext = 'internet %s %i\n' % (interface, port) waitaccept = True return (sock, sendtext.encode('ascii'), waitaccept) @classmethod def makeRemoteProcess(cls): """Try to find veusz process for remote program.""" # here's where to look for embed_remote.py thisdir = os.path.dirname(os.path.abspath(__file__)) # build up a list of possible command lines to start the remote veusz if sys.platform == 'win32': # windows is a special case # we need to run embed_remote.py under pythonw.exe, not python.exe # look for the python windows interpreter on path findpython = findOnPath('pythonw.exe') if not findpython: # if it wasn't on the path, use sys.prefix instead findpython = os.path.join(sys.prefix, 'pythonw.exe') # look for veusz executable on path findexe = findOnPath('veusz.exe') if not findexe: try: # add the usual place as a guess :-( findexe = os.path.join(os.environ['ProgramFiles'], 'Veusz', 'veusz.exe') except KeyError: pass # here is the list of commands to try possiblecommands = [ [findpython, os.path.join(thisdir, 'veusz_main.py')], [findexe] ] else: executable = sys.executable # try embed_remote.py in this directory, veusz in this directory # or veusz on the path in order possiblecommands = [ [executable, os.path.join(thisdir, 'veusz_main.py')], [os.path.join(thisdir, 'veusz')], [findOnPath('veusz')] ] # cheat and look for Veusz app for MacOS under the standard application # directory. I don't know how else to find it :-( if sys.platform == 'darwin': findbundle = findOnPath('Veusz.app') if findbundle: possiblecommands += [ [findbundle+'/Contents/MacOS/Veusz'] ] else: possiblecommands += [[ '/Applications/Veusz.app/Contents/MacOS/Veusz' ]] possiblecommands += [[ os.path.expanduser('~/Applications/Veusz.app/Contents/MacOS/Veusz')]] for cmd in possiblecommands: # only try to run commands that exist as error handling # does not work well when interfacing with OS (especially Windows) if ( None not in cmd and False not in [os.path.isfile(c) for c in cmd] ): try: # we don't use stdout below, but works around windows bug # http://bugs.python.org/issue1124861 cls.remote = subprocess.Popen(cmd + ['--embed-remote'], shell=False, bufsize=0, close_fds=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE) return except OSError: pass raise RuntimeError('Unable to find a veusz executable on system path') @classmethod def startRemote(cls): """Start remote process.""" cls.serv_socket, sendtext, waitaccept = cls.makeSockets() cls.makeRemoteProcess() stdin = cls.remote.stdin # send socket number over pipe stdin.write( sendtext ) # accept connection if necessary if waitaccept: cls.serv_socket, address = cls.serv_socket.accept() # Send a secret to the remote program by secure route and # check it comes back. This is to check that no program has # secretly connected on our port, which isn't really useful # for AF_UNIX sockets. secret = (str(uuid.uuid4()) + '\n').encode('ascii') stdin.write(secret) secretback = cls.readLenFromSocket(cls.serv_socket, len(secret)) if secret != secretback: raise RuntimeError("Security between client and server broken") # packet length for command bytes cls.cmdlen = struct.calcsize('" % (self.__class__.__name__, repr(self._path), self._type) def fromPath(self, path): """Return a new Node for the path given.""" wtype = self._ci.NodeType(path) if wtype == 'widget': return WidgetNode(self._ci, wtype, path) elif wtype == 'setting': return SettingNode(self._ci, wtype, path) else: return SettingGroupNode(self._ci, wtype, path) @property def path(self): """Veusz full path to node""" return self._path @property def type(self): """Type of node: 'widget', 'settinggroup', or 'setting'""" return self._type def _joinPath(self, child): """Return new path of child.""" if self._path == '/': return '/' + child else: return self._path + '/' + child def __getitem__(self, key): """Return a child widget, settinggroup or setting.""" if self._type != 'setting': try: return self.fromPath(self._joinPath(key)) except ValueError: pass raise KeyError("%s does not have key or child '%s'" % ( self.__class__.__name__, key)) def __getattr__(self, attr): """Return a child widget, settinggroup or setting.""" if self._type == 'setting': pass elif attr[:2] != '__': try: return self.fromPath(self._joinPath(attr)) except ValueError: pass raise AttributeError("%s does not have attribute or child '%s'" % ( self.__class__.__name__, attr)) # boring ways to get children of nodes @property def children(self): """Generator to get children as Nodes.""" for c in self._ci.NodeChildren(self._path): yield self.fromPath(self._joinPath(c)) @property def children_widgets(self): """Generator to get child widgets as Nodes.""" for c in self._ci.NodeChildren(self._path, types='widget'): yield self.fromPath(self._joinPath(c)) @property def children_settings(self): """Generator to get child settings as Nodes.""" for c in self._ci.NodeChildren(self._path, types='setting'): yield self.fromPath(self._joinPath(c)) @property def children_settinggroups(self): """Generator to get child settingsgroups as Nodes.""" for c in self._ci.NodeChildren(self._path, types='settinggroup'): yield self.fromPath(self._joinPath(c)) @property def childnames(self): """Get names of children.""" return self._ci.NodeChildren(self._path) @property def childnames_widgets(self): """Get names of children widgets.""" return self._ci.NodeChildren(self._path, types='widget') @property def childnames_settings(self): """Get names of child settings.""" return self._ci.NodeChildren(self._path, types='setting') @property def childnames_settinggroups(self): """Get names of child setting groups""" return self._ci.NodeChildren(self._path, types='settinggroup') @property def parent(self): """Return parent of node.""" if self._path == '/': raise TypeError("Cannot get parent node of root node""") p = self._path.split('/')[:-1] if p == ['']: newpath = '/' else: newpath = '/'.join(p) return self.fromPath(newpath) @property def name(self): """Get name of node.""" if self._path == '/': return self._path else: return self._path.split('/')[-1] class SettingNode(Node): """A node which is a setting.""" def _getVal(self): """The value of a setting.""" if self._type == 'setting': return self._ci.Get(self._path) raise TypeError("Cannot get value unless is a setting""") def _setVal(self, val): if self._type == 'setting': self._ci.Set(self._path, val) else: raise TypeError("Cannot set value unless is a setting.""") val = property(_getVal, _setVal) @property def isreference(self): """Is this setting set to a reference to another setting?.""" ref = self._ci.ResolveReference(self._path) return bool(ref) def resolveReference(self): """If this is set to a reference to a setting, return a new SettingNode to the original setting. If there are a chain of references, follow them to the target. Returns None if this setting is not set to a reference. """ real = self._ci.ResolveReference(self._path) if not real: return None return self.fromPath(real) def setToReference(self, othernode): """Make this setting point to another setting, by creating a reference. References can be chained. Note that the absolute path is used to specify a reference, so moving affected widgets around will destroy the link.""" if not isinstance(othernode, SettingNode): raise ValueError("othernode is not a SettingNode") self._ci.SetToReference(self._path, othernode._path) @property def settingtype(self): """Get the type of setting, which is a string.""" return self._ci.SettingType(self._path) class SettingGroupNode(Node): """A node containing a group of settings.""" pass class WidgetNode(Node): """A node pointing to a widget.""" @property def widgettype(self): """Get Veusz type of widget.""" return self._ci.WidgetType(self.path) def WalkWidgets(self, widgettype=None): """Generator to walk widget tree and get this widget and the widgets below this WidgetNode of type given. widgettype is a Veusz widget type name or None to get all widgets.""" if widgettype is None or self._ci.WidgetType(self._path) == widgettype: yield self for child in self.children_widgets: for w in child.WalkWidgets(widgettype=widgettype): yield w def Add(self, widgettype, *args, **args_opt): """Add a widget of the type given, returning the Node instance. """ args_opt['widget'] = self._path name = self._ci.Add(widgettype, *args, **args_opt) return WidgetNode( self._ci, 'widget', self._joinPath(name) ) def Rename(self, newname): """Renames widget to name given.""" if self._path == '/': raise RuntimeError("Cannot rename root widget") self._ci.Rename(self._path, newname) self._path = '/'.join( self._path.split('/')[:-1] + [newname] ) def Action(self, action): """Applies action on widget.""" self._ci.Action(action, widget=self._path) def Remove(self): """Removes a widget and its children.""" self._ci.Remove(self._path) def Clone(self, newparent, newname=None): """Clone widget, placing at newparent. Uses newname if given. Returns new node.""" path = self._ci.CloneWidget(self._path, newparent._path, newname=newname) return WidgetNode( self._ci, 'widget', path ) veusz-1.21.1/veusz/setting/0000775000175000017500000000000012376130063013764 5ustar jssjssveusz-1.21.1/veusz/setting/collections.py0000644000175000017500000003524012327177747016676 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Collections of predefined settings for common settings.""" from __future__ import division from .. import qtall as qt4 from . import setting from .settings import Settings from .reference import Reference def _(text, disambiguation=None, context="Setting"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class Line(Settings): '''For holding properities of a line.''' def __init__(self, name, **args): Settings.__init__(self, name, **args) self.add( setting.Color('color', setting.Reference('/StyleSheet/Line/color'), descr = _('Color of line'), usertext=_('Color')) ) self.add( setting.DistancePt('width', setting.Reference('/StyleSheet/Line/width'), descr = _('Width of line'), usertext=_('Width')) ) self.add( setting.LineStyle('style', 'solid', descr = _('Line style'), usertext=_('Style')) ) self.add( setting.Int( 'transparency', 0, descr = _('Transparency percentage'), usertext = _('Transparency'), minval = 0, maxval = 100 ) ) self.add( setting.Bool('hide', False, descr = _('Hide the line'), usertext=_('Hide')) ) def makeQPen(self, painthelper): '''Make a QPen from the description. This currently ignores the hide attribute ''' color = qt4.QColor(self.color) color.setAlphaF( (100-self.transparency) / 100.) width = self.get('width').convert(painthelper) style, dashpattern = setting.LineStyle._linecnvt[self.style] pen = qt4.QPen( color, width, style ) if dashpattern: pen.setDashPattern(dashpattern) return pen def makeQPenWHide(self, painthelper): """Make a pen, taking account of hide attribute.""" if self.hide: return qt4.QPen(qt4.Qt.NoPen) else: return self.makeQPen(painthelper) class XYPlotLine(Line): '''A plot line for plotting data, allowing histogram-steps to be plotted.''' def __init__(self, name, **args): Line.__init__(self, name, **args) self.add( setting.Choice('steps', ['off', 'left', 'centre', 'right', 'left-shift-points', 'right-shift-points', 'vcentre'], 'off', descr=_('Plot horizontal steps ' 'instead of a line'), usertext=_('Steps')), 0 ) self.add( setting.Bool('bezierJoin', False, descr=_('Connect points with a cubic Bezier curve'), usertext=_('Bezier join')), 1 ) self.get('color').newDefault( Reference('../color') ) class MarkerLine(Line): '''A line for marker border.''' def __init__(self, name, **args): Line.__init__(self, name, **args) self.add( setting.Bool('scaleLine', True, descr=_('Scale line width with marker if scaling' ' enabled'), usertext=_('Scale')), 4 ) class ErrorBarLine(Line): '''A line style for error bar plotting.''' def __init__(self, name, **args): Line.__init__(self, name, **args) self.add( setting.Float('endsize', 1.0, minval = 0., descr=_('Scale ends of error bars by this factor'), usertext = _('End size')) ) self.add( setting.Bool('hideHorz', False, descr = _('Hide horizontal errors'), usertext=_('Hide horz.')) ) self.add( setting.Bool('hideVert', False, descr = _('Hide vertical errors'), usertext=_('Hide vert.')) ) class Brush(Settings): '''Settings of a fill.''' def __init__(self, name, **args): Settings.__init__(self, name, **args) self.add( setting.Color( 'color', 'black', descr = _('Fill colour'), usertext=_('Color')) ) self.add( setting.FillStyle( 'style', 'solid', descr = _('Fill style'), usertext=_('Style')) ) self.add( setting.Int( 'transparency', 0, descr = _('Transparency percentage'), usertext = _('Transparency'), minval = 0, maxval = 100 ) ) self.add( setting.Bool( 'hide', False, descr = _('Hide the fill'), usertext=_('Hide')) ) def makeQBrush(self): '''Make a qbrush from the settings.''' color = qt4.QColor(self.color) color.setAlphaF( (100-self.transparency) / 100.) return qt4.QBrush( color, self.get('style').qtStyle() ) def makeQBrushWHide(self): """Make a brush, taking account of hide attribute.""" if self.hide: return qt4.QBrush() else: return self.makeQBrush() class BrushExtended(Settings): '''Extended brush style.''' def __init__(self, name, **args): Settings.__init__(self, name, **args) self.add( setting.Color( 'color', 'black', descr = _('Fill colour'), usertext=_('Color')) ) self.add( setting.FillStyleExtended( 'style', 'solid', descr = _('Fill style'), usertext=_('Style')) ) self.add( setting.Bool( 'hide', False, descr = _('Hide the fill'), usertext=_('Hide')) ) self.add( setting.Int( 'transparency', 0, descr = _('Transparency percentage'), usertext = _('Transparency'), minval = 0, maxval = 100 ) ) self.add( setting.DistancePt( 'linewidth', '0.5pt', descr = _('Width of hatch or pattern line'), usertext=_('Line width')) ) self.add( setting.LineStyle( 'linestyle', 'solid', descr = _('Hatch or pattern line style'), usertext=_('Line style')) ) self.add( setting.DistancePt( 'patternspacing', '5pt', descr = _('Hatch or pattern spacing'), usertext = _('Spacing')) ) self.add( setting.Color( 'backcolor', 'white', descr = _('Hatch or pattern background color'), usertext = _('Back color') ) ) self.add( setting.Int( 'backtransparency', 0, descr = _('Hatch or pattern background transparency percentage'), usertext = _('Back trans.'), minval = 0, maxval = 100 ) ) self.add( setting.Bool( 'backhide', True, descr = _('Hide hatch or pattern background'), usertext=_('Back hide')) ) class KeyBrush(BrushExtended): '''Fill used for back of key.''' def __init__(self, name, **args): BrushExtended.__init__(self, name, **args) self.get('color').newDefault('white') class BoxPlotMarkerFillBrush(Brush): '''Fill used for points on box plots.''' def __init__(self, name, **args): Brush.__init__(self, name, **args) self.get('color').newDefault('white') class GraphBrush(BrushExtended): '''Fill used for back of graph.''' def __init__(self, name, **args): BrushExtended.__init__(self, name, **args) self.get('color').newDefault('white') class PlotterFill(BrushExtended): '''Filling used for filling on plotters.''' def __init__(self, name, **args): BrushExtended.__init__(self, name, **args) self.get('hide').newDefault(True) class PointFill(BrushExtended): '''Filling used for filling above/below line or inside error region for xy-point plotters. ''' def __init__(self, name, **args): BrushExtended.__init__(self, name, **args) hide = self.get('hide') hide.newDefault(True) hide.usertext = _('Hide edge fill') hide.descr = _('Hide the filled region to the edge of the plot') self.get('color').newDefault('grey') self.add( setting.Choice( 'fillto', ['top', 'bottom', 'left', 'right'], 'top', descr=_('Edge to fill towards'), usertext=_('Fill to')), 0) self.add( setting.Bool( 'hideerror', False, descr = _('Hide the filled region inside the error bars'), usertext=_('Hide error fill')) ) class ShapeFill(BrushExtended): '''Filling used for filling shapes.''' def __init__(self, name, **args): BrushExtended.__init__(self, name, **args) self.get('hide').newDefault(True) self.get('color').newDefault('white') class ArrowFill(Brush): """Brush for filling arrow heads""" def __init__(self, name, **args): Brush.__init__(self, name, **args) self.get('color').newDefault( setting.Reference( '../Line/color') ) class Text(Settings): '''Text settings.''' # need to examine font table to see what's available # this is set on app startup defaultfamily = None families = None def __init__(self, name, **args): Settings.__init__(self, name, **args) self.add( setting.FontFamily('font', setting.Reference('/StyleSheet/Font/font'), descr = _('Font name'), usertext=_('Font')) ) self.add( setting.DistancePt('size', setting.Reference('/StyleSheet/Font/size'), descr = _('Font size'), usertext=_('Size') ) ) self.add( setting.Color( 'color', setting.Reference('/StyleSheet/Font/color'), descr = _('Font color'), usertext=_('Color') ) ) self.add( setting.Bool( 'italic', False, descr = _('Italic font'), usertext=_('Italic') ) ) self.add( setting.Bool( 'bold', False, descr = _('Bold font'), usertext=_('Bold') ) ) self.add( setting.Bool( 'underline', False, descr = _('Underline font'), usertext=_('Underline') ) ) self.add( setting.Bool( 'hide', False, descr = _('Hide the text'), usertext=_('Hide')) ) def copy(self): """Make copy of settings.""" c = Settings.copy(self) c.defaultfamily = self.defaultfamily c.families = self.families return c def makeQFont(self, painthelper): '''Return a qt4.QFont object corresponding to the settings.''' size = self.get('size').convertPts(painthelper) weight = qt4.QFont.Normal if self.bold: weight = qt4.QFont.Bold f = qt4.QFont(self.font, size, weight, self.italic) if self.underline: f.setUnderline(True) f.setStyleHint(qt4.QFont.Times) return f def makeQPen(self): """ Return a qt4.QPen object for the font pen """ return qt4.QPen(qt4.QColor(self.color)) class PointLabel(Text): """For labelling points on plots.""" def __init__(self, name, **args): Text.__init__(self, name, **args) self.add( setting.Float('angle', 0., descr=_('Angle of the labels in degrees'), usertext=_('Angle'), formatting=True), 0 ) self.add( setting.AlignVert('posnVert', 'centre', descr=_('Vertical position of label'), usertext=_('Vert position'), formatting=True), 0 ) self.add( setting.AlignHorz('posnHorz', 'right', descr=_('Horizontal position of label'), usertext=_('Horz position'), formatting=True), 0 ) class MarkerColor(Settings): """Settings for a coloring points using data values.""" def __init__(self, name): Settings.__init__(self, name, setnsmode='groupedsetting') self.add( setting.DatasetExtended( 'points', '', descr = _('Use color value (0-1) in dataset to paint points'), usertext=_('Color markers')), 7 ) self.add( setting.Float( 'min', 0., descr = _('Minimum value of color dataset'), usertext = _('Min val') )) self.add( setting.Float( 'max', 1., descr = _('Maximum value of color dataset'), usertext = _('Max val') )) self.add( setting.Choice( 'scaling', ['linear', 'sqrt', 'log', 'squared'], 'linear', descr = _('Scaling to transform numbers to color'), usertext=_('Scaling'))) veusz-1.21.1/veusz/setting/settings.py0000644000175000017500000002117512327177747016222 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Module for holding collections of settings.""" from __future__ import division from ..compat import citems from .reference import Reference, ReferenceMultiple class Settings(object): """A class for holding collections of settings.""" # differentiate widgets, settings and setting nodetype = 'settings' def __init__(self, name, descr = '', usertext='', pixmap='', setnsmode='formatting'): """A new Settings with a name. name: name in hierarchy descr: description (for user) usertext: name for user of class pixmap: pixmap to show in tab (if appropriate) setnsmode: type of Settings class, one of ('formatting', 'groupedsetting', 'widgetsettings', 'stylesheet') """ self.__dict__['setdict'] = {} self.name = name self.descr = descr self.usertext = usertext self.pixmap = pixmap self.setnsmode = setnsmode self.setnames = [] # a list of names self.parent = None def copy(self): """Make a copy of the settings and its subsettings.""" s = Settings( self.name, descr=self.descr, usertext=self.usertext, pixmap=self.pixmap, setnsmode=self.setnsmode ) for name in self.setnames: s.add( self.setdict[name].copy() ) return s def isWidget(self): """Is this object a widget?""" return False def getList(self): """Get a list of setting or settings types.""" return [self.setdict[n] for n in self.setnames] def getSettingList(self): """Get a list of setting types.""" return [self.setdict[n] for n in self.setnames if not isinstance(self.setdict[n], Settings)] def getSettingsList(self): """Get a list of settings types.""" return [self.setdict[n] for n in self.setnames if isinstance(self.setdict[n], Settings)] def getNames(self): """Return list of names.""" return self.setnames def getSettingNames(self): """Get list of setting names.""" return [n for n in self.setnames if not isinstance(self.setdict[n], Settings)] def getSettingsNames(self): """Get list of settings names.""" return [n for n in self.setnames if isinstance(self.setdict[n], Settings)] def isSetting(self, name): """Is the name a supported setting?""" return name in self.setdict def add(self, setting, posn = -1, readonly = False, pixmap=None): """Add a new setting with the name, or a set of subsettings.""" name = setting.name if name in self.setdict: raise RuntimeError("Name already in settings dictionary") self.setdict[name] = setting if posn < 0: self.setnames.append(name) else: self.setnames.insert(posn, name) setting.parent = self if pixmap: setting.pixmap = pixmap if readonly: setting.readonly = True def remove(self, name): """Remove name from the list of settings.""" del self.setnames[ self.setnames.index( name ) ] del self.setdict[ name ] def __setattr__(self, name, val): """Allow us to do foo.setname = 42 """ d = self.__dict__['setdict'] if name in d: d[name].val = val else: self.__dict__[name] = val def __getattr__(self, name): """Allow us to do print foo.setname """ try: s = self.__dict__['setdict'][name] if isinstance(s, Settings): return s return s.val except KeyError: pass try: return self.__dict__[name] except KeyError: raise AttributeError("'%s' is not a setting" % name) def __getitem__(self, name): """Also allows us to do print foo['setname'] """ d = self.__dict__['setdict'] try: s = d[name] if isinstance(s, Settings): return s else: return s.val except KeyError: raise KeyError("'%s' is not a setting" % name) def __contains__(self, name): """Whether settings contains name.""" return name in self.__dict__['setdict'] def get(self, name = None): """Get the setting variable.""" if name is None: return self else: return self.setdict[name] def getFromPath(self, path): """Get setting according to the path given as a list.""" name = path[0] if name in self.setdict: val = self.setdict[name] if len(path) == 1: if isinstance(val, Settings): raise ValueError( '"%s" is a list of settings, not a setting' % name) else: return val else: if isinstance(val, Settings): return val.getFromPath(path[1:]) else: raise ValueError('"%s" not a valid subsetting' % name) else: raise ValueError('"%s" is not a setting' % name) def saveText(self, saveall, rootname = None): """Return the text which would reload the settings. if saveall is true, save those which haven't been modified. rootname is the part to stick on the front of the settings """ # we want to build the root up if we're not the first item # (first item is implicit) if rootname is None: rootname = '' else: rootname += self.name + '/' text = ''.join( [self.setdict[name].saveText(saveall, rootname) for name in self.setnames] ) return text def readDefaults(self, root, widgetname): """Return default values from saved text. root is the path of the setting in the db, built up by settings above this one widgetname is the name of the widget this setting belongs to """ root = '%s/%s' % (root, self.name) for s in list(self.setdict.values()): s.readDefaults(root, widgetname) def linkToStylesheet(self, _root=None): """Link the settings within this Settings to a stylesheet. _root is an internal parameter as this function is recursive.""" # build up root part of pathname to reference if _root is None: path = [] obj = self while not obj.parent.isWidget(): path.insert(0, obj.name) obj = obj.parent path = ['', 'StyleSheet', obj.parent.typename] + path + [''] _root = '/'.join(path) # iterate over subsettings for name, setn in citems(self.setdict): thispath = _root + name if isinstance(setn, Settings): # call recursively if this is a Settings setn.linkToStylesheet(_root=thispath+'/') else: # check that reference resolves ref = Reference(thispath) try: ref.resolve(setn) except Reference.ResolveException: # leave it as it was pass else: if setn.isReference() and setn.getReference().split[0] == '..': # convert a relative path to multiple references paths = [thispath, setn.getReference().value] ref = ReferenceMultiple(paths) setn.set(ref) setn.default = ref veusz-1.21.1/veusz/setting/setting.py0000664000175000017500000016627312376130006016027 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Module for holding setting values. e.g. s = Int('foo', 5) s.get() s.set(42) s.fromText('42') """ from __future__ import division import re import sys import numpy as N from ..compat import cbasestr, cstr, crepr from .. import qtall as qt4 from . import controls from .settingdb import settingdb, uilocale, ui_floattostring, ui_stringtofloat from .reference import ReferenceBase, Reference from .. import utils class OnModified(qt4.QObject): """onmodified is emitted from an object contained in each setting.""" onModified = qt4.pyqtSignal() class Setting(object): """A class to store a value with a particular type.""" # differentiate widgets, settings and setting nodetype = 'setting' typename = 'setting' def __init__(self, name, value, descr='', usertext='', formatting=False, hidden=False): """Initialise the values. name: setting name value: default value and initial value descr: description of the setting usertext: name of setting for user formatting: whether setting applies to formatting hidden: hide widget from user """ self.readonly = False self.parent = None self.name = name self.descr = descr self.usertext = usertext self.formatting = formatting self.hidden = hidden self.default = value self.onmodified = OnModified() self._val = None # calls the set function for the val property self.val = value def isWidget(self): """Is this object a widget?""" return False def _copyHelper(self, before, after, optional): """Help copy an object. before are arguments before val after are arguments after val optinal as optional arguments """ if isinstance(self._val, ReferenceBase): val = self._val else: val = self.val args = (self.name,) + before + (val,) + after opt = optional.copy() opt['descr'] = self.descr opt['usertext'] = self.usertext opt['formatting'] = self.formatting opt['hidden'] = self.hidden obj = self.__class__(*args, **opt) obj.readonly = self.readonly obj.default = self.default return obj def copy(self): """Make a setting which has its values copied from this one. This needs to be overridden if the constructor changes """ return self._copyHelper((), (), {}) def get(self): """Get the value.""" if isinstance(self._val, ReferenceBase): return self._val.resolve(self).get() else: return self.convertFrom(self._val) def set(self, v): """Set the value.""" if isinstance(v, ReferenceBase): self._val = v else: # this also removes the linked value if there is one set self._val = self.convertTo(v) self.onmodified.onModified.emit() val = property(get, set, None, 'Get or modify the value of the setting') def isReference(self): """Is this a setting a reference to another object.""" return isinstance(self._val, ReferenceBase) def getReference(self): """Return the reference object. Raise ValueError if not a reference""" if isinstance(self._val, ReferenceBase): return self._val else: raise ValueError("Setting is not a reference") def getStylesheetLink(self): """Get text that this setting should default to linked to the stylesheet.""" path = [] obj = self while not obj.parent.isWidget(): path.insert(0, obj.name) obj = obj.parent path = ['', 'StyleSheet', obj.parent.typename] + path return '/'.join(path) def linkToStylesheet(self): """Make this setting link to stylesheet setting, if possible.""" self.set( Reference(self.getStylesheetLink()) ) def _path(self): """Return full path of setting.""" path = [] obj = self while obj is not None: # logic easier to understand here # do not add settings name for settings of widget if not obj.isWidget() and obj.parent.isWidget(): pass else: if obj.name == '/': path.insert(0, '') else: path.insert(0, obj.name) obj = obj.parent return '/'.join(path) path = property(_path, None, None, 'Return the full path of the setting') def toText(self): """Convert the type to text for saving.""" return "" def fromText(self, text): """Convert text to type suitable for setting. Raises utils.InvalidType if cannot convert.""" return None def readDefaults(self, root, widgetname): """Check whether the user has a default for this setting.""" deftext = None unnamedpath = '%s/%s' % (root, self.name) try: deftext = settingdb[unnamedpath] except KeyError: pass # named defaults supersedes normal defaults namedpath = '%s_NAME:%s' % (widgetname, unnamedpath) try: deftext = settingdb[namedpath] except KeyError: pass if deftext is not None: self.val = self.fromText(deftext) self.default = self.val def removeDefault(self): """Remove the default setting for this setting.""" # build up setting path path = '' item = self while not item.isWidget(): path = '/%s%s' % (item.name, path) item = item.parent # remove the settings (ignore if they are not set) if path in settingdb: del settingdb[path] # specific setting to this widgetname namedpath = '%s_NAME:%s' % (item.name, path) if namedpath in settingdb: del settingdb[namedpath] def setAsDefault(self, withwidgetname = False): """Set the current value of this setting as the default value If withwidthname is True, then it is only the default for widgets of the particular name this setting is contained within.""" # build up setting path path = '' item = self while not item.isWidget(): path = '/%s%s' % (item.name, path) item = item.parent # if the setting is only for widgets with a certain name if withwidgetname: path = '%s_NAME:%s' % (item.name, path) # set the default settingdb[path] = self.toText() def saveText(self, saveall, rootname = ''): """Return text to restore the value of this setting.""" if (saveall or not self.isDefault()) and not self.readonly: if isinstance(self._val, ReferenceBase): return "SetToReference('%s%s', %s)\n" % (rootname, self.name, crepr(self._val.value)) else: return "Set('%s%s', %s)\n" % ( rootname, self.name, crepr(self.val) ) else: return '' def setOnModified(self, fn): """Set the function to be called on modification (passing True).""" self.onmodified.onModified.connect(fn) if isinstance(self._val, ReferenceBase): # tell references to notify us if they are modified self._val.setOnModified(self, fn) def removeOnModified(self, fn): """Remove the function from the list of function to be called.""" self.onmodified.onModified.disconnect(fn) def newDefault(self, value): """Update the default and the value.""" self.default = value self.val = value def isDefault(self): """Is the current value a default? This also returns true if it is linked to the appropriate stylesheet """ if ( isinstance(self._val, ReferenceBase) and isinstance(self.default, ReferenceBase) ): return self._val.value == self.default.value else: return self.val == self.default def isDefaultLink(self): """Is this a link to the default stylesheet value.""" return ( isinstance(self._val, ReferenceBase) and self._val.value == self.getStylesheetLink() ) def setSilent(self, val): """Set the setting, without propagating modified flags. This shouldn't often be used as it defeats the automatic updation. Used for temporary modifications.""" self._val = self.convertTo(val) def convertTo(self, val): """Convert for storage.""" return val def convertFrom(self, val): """Convert to storage.""" return val def makeControl(self, *args): """Make a qt control for editing the setting. The control emits settingValueChanged() when the setting has changed value.""" return None def getDocument(self): """Return document.""" p = self.parent while p: try: return p.getDocument() except AttributeError: pass p = p.parent return None def getWidget(self): """Return associated widget.""" w = self.parent while not w.isWidget(): w = w.parent return w def safeEvalHelper(self, text): """Evaluate an expression, catching naughtiness.""" try: comp = self.getDocument().compileCheckedExpression( text) if comp is None: raise utils.InvalidType return float( eval(comp, self.getDocument().eval_context) ) except: raise utils.InvalidType # forward setting to another setting class SettingBackwardCompat(Setting): """Forward setting requests to another setting. This is used for backward-compatibility. """ typename = 'backward-compat' def __init__(self, name, newrelpath, val, translatefn = None, **args): """Point this setting to another. newrelpath is a path relative to this setting's parent """ self.translatefn = translatefn Setting.__init__(self, name, val, **args) self.relpath = newrelpath.split('/') def getForward(self): """Get setting this setting forwards to.""" return self.parent.getFromPath(self.relpath) def convertTo(self, val): if self.parent is not None: return self.getForward().convertTo(val) def toText(self): return self.getForward().toText() def fromText(self, val): return self.getForward().fromText(val) def set(self, val): if self.parent is not None and not isinstance(val, ReferenceBase): if self.translatefn: val = self.translatefn(val) self.getForward().set(val) def isDefault(self): return self.getForward().isDefault() def get(self): return self.getForward().get() def copy(self): return self._copyHelper(('/'.join(self.relpath),), (), {'translatefn': self.translatefn}) def makeControl(self, *args): return None def saveText(self, saveall, rootname = ''): return '' def linkToStylesheet(self): """Do nothing for backward compatibility settings.""" pass # Store strings class Str(Setting): """String setting.""" typename = 'str' def convertTo(self, val): if isinstance(val, cbasestr): return val raise utils.InvalidType def toText(self): return self.val def fromText(self, text): return text def makeControl(self, *args): return controls.String(self, *args) class Notes(Str): """String for making notes.""" typename = 'str-notes' def makeControl(self, *args): return controls.Notes(self, *args) # Store bools class Bool(Setting): """Bool setting.""" typename = 'bool' def convertTo(self, val): if type(val) in (bool, int): return bool(val) raise utils.InvalidType def toText(self): if self.val: return 'True' else: return 'False' def fromText(self, text): t = text.strip().lower() if t in ('true', '1', 't', 'y', 'yes'): return True elif t in ('false', '0', 'f', 'n', 'no'): return False else: raise utils.InvalidType def makeControl(self, *args): return controls.Bool(self, *args) # Storing integers class Int(Setting): """Integer settings.""" typename = 'int' def __init__(self, name, value, minval=-1000000, maxval=1000000, **args): """Initialise the values. minval is minimum possible value of setting maxval is maximum possible value of setting """ self.minval = minval self.maxval = maxval Setting.__init__(self, name, value, **args) def copy(self): """Make a setting which has its values copied from this one. This needs to be overridden if the constructor changes """ return self._copyHelper((), (), {'minval': self.minval, 'maxval': self.maxval}) def convertTo(self, val): if isinstance(val, int): if val >= self.minval and val <= self.maxval: return val else: raise utils.InvalidType('Out of range allowed') raise utils.InvalidType def toText(self): return uilocale.toString(self.val) def fromText(self, text): i, ok = uilocale.toLongLong(text) if not ok: raise ValueError if i >= self.minval and i <= self.maxval: return i else: raise utils.InvalidType('Out of range allowed') def makeControl(self, *args): return controls.Int(self, *args) def _finiteRangeFloat(f, minval=-1e300, maxval=1e300): """Return a finite float in range or raise exception otherwise.""" f = float(f) if not N.isfinite(f): raise utils.InvalidType('Finite values only allowed') if f < minval or f > maxval: raise utils.InvalidType('Out of range allowed') return f # for storing floats class Float(Setting): """Float settings.""" typename = 'float' def __init__(self, name, value, minval=-1e200, maxval=1e200, **args): """Initialise the values. minval is minimum possible value of setting maxval is maximum possible value of setting """ self.minval = minval self.maxval = maxval Setting.__init__(self, name, value, **args) def copy(self): """Make a setting which has its values copied from this one. This needs to be overridden if the constructor changes """ return self._copyHelper((), (), {'minval': self.minval, 'maxval': self.maxval}) def convertTo(self, val): if isinstance(val, int) or isinstance(val, float): return _finiteRangeFloat(val, minval=self.minval, maxval=self.maxval) raise utils.InvalidType def toText(self): return ui_floattostring(self.val) def fromText(self, text): try: f = ui_stringtofloat(text) except ValueError: # try to evaluate f = self.safeEvalHelper(text) return self.convertTo(f) def makeControl(self, *args): return controls.Edit(self, *args) class FloatOrAuto(Float): """Save a float or text auto.""" typename = 'float-or-auto' def convertTo(self, val): if type(val) in (int, float): return _finiteRangeFloat(val, minval=self.minval, maxval=self.maxval) elif isinstance(val, cbasestr) and val.strip().lower() == 'auto': return None else: raise utils.InvalidType def convertFrom(self, val): if val is None: return 'Auto' else: return Float.convertFrom(self, val) def toText(self): if self.val is None or (isinstance(self.val, cbasestr) and self.val.lower() == 'auto'): return 'Auto' else: return ui_floattostring(self.val) def fromText(self, text): if text.strip().lower() == 'auto': return 'Auto' else: return Float.fromText(self, text) def makeControl(self, *args): return controls.Choice(self, True, ['Auto'], *args) class IntOrAuto(Setting): """Save an int or text auto.""" typename = 'int-or-auto' def convertTo(self, val): if isinstance(val, int): return val elif isinstance(val, cbasestr) and val.strip().lower() == 'auto': return None else: raise utils.InvalidType def convertFrom(self, val): if val is None: return 'Auto' else: return val def toText(self): if self.val is None or (isinstance(self.val, cbasestr) and self.val.lower() == 'auto'): return 'Auto' else: return uilocale.toString(self.val) def fromText(self, text): if text.strip().lower() == 'auto': return 'Auto' else: i, ok = uilocale.toLongLong(text) if not ok: raise utils.InvalidType return i def makeControl(self, *args): return controls.Choice(self, True, ['Auto'], *args) # these are functions used by the distance setting below. # they don't work as class methods def _distPhys(match, painter, mult): """Convert a physical unit measure in multiples of points.""" return (painter.pixperpt * mult * float(match.group(1)) * painter.scaling) def _idistval(val, unit): """Convert value to text, dropping zeros and . points on right.""" return ("%.3f" % val).rstrip('0').rstrip('.') + unit def _distInvPhys(pixdist, painter, mult, unit): """Convert number of pixels into physical distance.""" return _idistval( pixdist / (mult * painter.pixperpt * painter.scaling), unit ) def _distPerc(match, painter): """Convert from a percentage of maxsize.""" return painter.maxsize * 0.01 * float(match.group(1)) def _distInvPerc(pixdist, painter): """Convert pixel distance into percentage.""" return _idistval(pixdist * 100. / painter.maxsize, '%') def _distFrac(match, painter): """Convert from a fraction a/b of maxsize.""" try: return painter.maxsize * float(match.group(1))/float(match.group(4)) except ZeroDivisionError: return 0. def _distRatio(match, painter): """Convert from a simple 0.xx ratio of maxsize.""" # if it's greater than 1 then assume it's a point measurement if float(match.group(1)) > 1.: return _distPhys(match, painter, 1) return painter.maxsize * float(match.group(1)) # regular expression to match distances distre_expr = r'''^ [ ]* # optional whitespace (\.?[0-9]+|[0-9]+\.[0-9]*) # a floating point number [ ]* # whitespace (cm|pt|mm|inch|in|"|%|| # ( unit, no unit, (?P/) ) # or / ) (?(slash)[ ]* # if it was a slash, match any whitespace (\.?[0-9]+|[0-9]+\.[0-9]*)) # and match following fp number [ ]* # optional whitespace $''' class Distance(Setting): """A veusz distance measure, e.g. 1pt or 3%.""" typename = 'distance' # match a distance distre = re.compile(distre_expr, re.VERBOSE) # functions to convert from unit values to points unit_func = { 'cm': lambda match, painter: _distPhys(match, painter, 720/25.4), 'pt': lambda match, painter: _distPhys(match, painter, 1.), 'mm': lambda match, painter: _distPhys(match, painter, 72/25.4), 'in': lambda match, painter: _distPhys(match, painter, 72.), 'inch': lambda match, painter: _distPhys(match, painter, 72.), '"': lambda match, painter: _distPhys(match, painter, 72.), '%': _distPerc, '/': _distFrac, '': _distRatio } # inverse functions for converting points to units inv_unit_func = { 'cm': lambda match, painter: _distInvPhys(match, painter, 720/25.4, 'cm'), 'pt': lambda match, painter: _distInvPhys(match, painter, 1., 'pt'), 'mm': lambda match, painter: _distInvPhys(match, painter, 72/25.4, 'mm'), 'in': lambda match, painter: _distInvPhys(match, painter, 72., 'in'), 'inch': lambda match, painter: _distInvPhys(match, painter, 72., 'in'), '"': lambda match, painter: _distInvPhys(match, painter, 72., 'in'), '%': _distInvPerc, '/': _distInvPerc, '': _distInvPerc } @classmethod def isDist(kls, dist): """Is the text a valid distance measure?""" return kls.distre.match(dist) is not None def convertTo(self, val): if self.distre.match(val) is not None: return val else: raise utils.InvalidType def toText(self): # convert decimal point to display locale return self.val.replace('.', uilocale.decimalPoint()) def fromText(self, text): # convert decimal point from display locale text = text.replace(uilocale.decimalPoint(), '.') if self.isDist(text): return text else: raise utils.InvalidType def makeControl(self, *args): return controls.Distance(self, *args) @classmethod def convertDistance(kls, painter, dist): '''Convert a distance to plotter units. dist: eg 0.1 (fraction), 10% (percentage), 1/10 (fraction), 10pt, 1cm, 20mm, 1inch, 1in, 1" (size) maxsize: size fractions are relative to painter: painter to get metrics to convert physical sizes ''' # match distance against expression m = kls.distre.match(dist) if m is not None: # lookup function to call to do conversion func = kls.unit_func[m.group(2)] return func(m, painter) # none of the regexps match raise ValueError( "Cannot convert distance in form '%s'" % dist ) def convert(self, painter): """Convert this setting's distance as above""" return self.convertDistance(painter, self.val) def convertPts(self, painter): """Get the distance in points.""" return self.convert(painter) / painter.pixperpt def convertInverse(self, distpix, painter): """Convert distance in pixels into units of this distance. """ m = self.distre.match(self.val) if m is not None: # if it matches convert back inversefn = self.inv_unit_func[m.group(2)] else: # otherwise force unit inversefn = self.inv_unit_func['cm'] # do inverse mapping return inversefn(distpix, painter) class DistancePt(Distance): """For a distance in points.""" def makeControl(self, *args): return controls.DistancePt(self, *args) class DistancePhysical(Distance): """For physical distances (no fractional).""" def isDist(self, val): m = self.distre.match(val) if m: # disallow non-physical distances if m.group(2) not in ('/', '', '%'): return True return False def makeControl(self, *args): return controls.Distance(self, *args, physical=True) class DistanceOrAuto(Distance): """A distance or the value Auto""" typename = 'distance-or-auto' distre = re.compile( distre_expr + r'|^Auto$', re.VERBOSE ) def isAuto(self): return self.val == 'Auto' def makeControl(self, *args): return controls.Distance(self, allowauto=True, *args) class Choice(Setting): """One out of a list of strings.""" # maybe should be implemented as a dict to speed up checks typename = 'choice' def __init__(self, name, vallist, val, **args): """Setting val must be in vallist. descriptions is an optional addon to put a tooltip on each item in the control. """ assert type(vallist) in (list, tuple) self.vallist = vallist self.descriptions = args.get('descriptions', None) if self.descriptions: del args['descriptions'] Setting.__init__(self, name, val, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((self.vallist,), (), {}) def convertTo(self, val): if val in self.vallist: return val else: raise utils.InvalidType def toText(self): return self.val def fromText(self, text): if text in self.vallist: return text else: raise utils.InvalidType def makeControl(self, *args): argsv = {'descriptions': self.descriptions} return controls.Choice(self, False, self.vallist, *args, **argsv) class ChoiceOrMore(Setting): """One out of a list of strings, or anything else.""" # maybe should be implemented as a dict to speed up checks typename = 'choice-or-more' def __init__(self, name, vallist, val, **args): """Setting has val must be in vallist. descriptions is an optional addon to put a tooltip on each item in the control """ self.vallist = vallist self.descriptions = args.get('descriptions', None) if self.descriptions: del args['descriptions'] Setting.__init__(self, name, val, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((self.vallist,), (), {}) def convertTo(self, val): return val def toText(self): return self.val def fromText(self, text): return text def makeControl(self, *args): argsv = {'descriptions': self.descriptions} return controls.Choice(self, True, self.vallist, *args, **argsv) class FloatChoice(ChoiceOrMore): """A numeric value, which can also be chosen from the list of values.""" typename = 'float-choice' def convertTo(self, val): if isinstance(val, int) or isinstance(val, float): return _finiteRangeFloat(val) raise utils.InvalidType def toText(self): return ui_floattostring(self.val) def fromText(self, text): try: f = ui_stringtofloat(text) except ValueError: # try to evaluate f = self.safeEvalHelper(text) return self.convertTo(f) def makeControl(self, *args): argsv = {'descriptions': self.descriptions} strings = [ui_floattostring(x) for x in self.vallist] return controls.Choice(self, True, strings, *args, **argsv) class FloatDict(Setting): """A dictionary, taking floats as values.""" typename = 'float-dict' def convertTo(self, val): if type(val) != dict: raise utils.InvalidType for v in val.values(): if type(v) not in (float, int): raise utils.InvalidType # return copy return dict(val) def toText(self): text = ['%s = %s' % (k, ui_floattostring(self.val[k])) for k in sorted(self.val)] return '\n'.join(text) def fromText(self, text): """Do conversion from list of a=X\n values.""" out = {} # break up into lines for l in text.split('\n'): l = l.strip() if len(l) == 0: continue # break up using = p = l.strip().split('=') if len(p) != 2: raise utils.InvalidType try: v = ui_stringtofloat(p[1]) except ValueError: raise utils.InvalidType out[ p[0].strip() ] = v return out def makeControl(self, *args): return controls.MultiLine(self, *args) class FloatList(Setting): """A list of float values.""" typename = 'float-list' def convertTo(self, val): if type(val) not in (list, tuple): raise utils.InvalidType # horribly slow test for invalid entries out = [] for i in val: if type(i) not in (float, int): raise utils.InvalidType else: out.append( float(i) ) return out def toText(self): """Make a string a, b, c.""" # can't use the comma for splitting if used as a decimal point join = ', ' if uilocale.decimalPoint() == ',': join = '; ' return join.join( [ui_floattostring(x) for x in self.val] ) def fromText(self, text): """Convert from a, b, c or a b c.""" # don't use commas if it is the decimal separator splitre = r'[\t\n, ]+' if uilocale.decimalPoint() == ',': splitre = r'[\t\n; ]+' out = [] for x in re.split(splitre, text.strip()): if x: try: out.append( ui_stringtofloat(x) ) except ValueError: out.append( self.safeEvalHelper(x) ) return out def makeControl(self, *args): return controls.String(self, *args) class WidgetPath(Str): """A setting holding a path to a widget. This is checked for validity.""" typename = 'widget-path' def __init__(self, name, val, relativetoparent=True, allowedwidgets = None, **args): """Initialise the setting. The widget is located relative to parent if relativetoparent is True, otherwise this widget. If allowedwidgets is not None, only those widgets types in the list are allowed by this setting. """ Str.__init__(self, name, val, **args) self.relativetoparent = relativetoparent self.allowedwidgets = allowedwidgets def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {'relativetoparent': self.relativetoparent, 'allowedwidgets': self.allowedwidgets}) def getReferredWidget(self, val = None): """Get the widget referred to. We double-check here to make sure it's the one. Returns None if setting is blank utils.InvalidType is raised if there's a problem """ # this is a bit of a hack, so we don't have to pass a value # for the setting (which we need to from convertTo) if val is None: val = self.val if val == '': return None # find the widget associated with this setting widget = self while not widget.isWidget(): widget = widget.parent # usually makes sense to give paths relative to a parent of a widget if self.relativetoparent: widget = widget.parent # resolve the text to a widget try: widget = widget.document.resolve(widget, val) except ValueError: raise utils.InvalidType # check the widget against the list of allowed types if given if self.allowedwidgets is not None: allowed = False for c in self.allowedwidgets: if isinstance(widget, c): allowed = True if not allowed: raise utils.InvalidType return widget class Dataset(Str): """A setting to choose from the possible datasets.""" typename = 'dataset' def __init__(self, name, val, dimensions=1, datatype='numeric', **args): """ dimensions is the number of dimensions the dataset needs """ self.dimensions = dimensions self.datatype = datatype Setting.__init__(self, name, val, **args) def copy(self): """Make a setting which has its values copied from this one.""" return self._copyHelper((), (), {'dimensions': self.dimensions, 'datatype': self.datatype}) def makeControl(self, *args): """Allow user to choose between the datasets.""" return controls.Dataset(self, self.getDocument(), self.dimensions, self.datatype, *args) def getData(self, doc): """Return a list of datasets entered.""" d = doc.data.get(self.val) if ( d is not None and d.datatype == self.datatype and d.dimensions == self.dimensions ): return d class Strings(Setting): """A multiple set of strings.""" typename = 'str-multi' def convertTo(self, val): """Takes a tuple/list of strings: ('ds1','ds2'...) """ if isinstance(val, cbasestr): return (val, ) if type(val) not in (list, tuple): raise utils.InvalidType # check each entry in the list is appropriate for ds in val: if not isinstance(ds, cbasestr): raise utils.InvalidType return tuple(val) def makeControl(self, *args): """Allow user to choose between the datasets.""" return controls.Strings(self, self.getDocument(), *args) class Datasets(Setting): """A setting to choose one or more of the possible datasets.""" typename = 'dataset-multi' def __init__(self, name, val, dimensions=1, datatype='numeric', **args): """ dimensions is the number of dimensions the dataset needs """ Setting.__init__(self, name, val, **args) self.dimensions = dimensions self.datatype = datatype def convertTo(self, val): """Takes a tuple/list of strings: ('ds1','ds2'...) """ if isinstance(val, cbasestr): return (val, ) if type(val) not in (list, tuple): raise utils.InvalidType # check each entry in the list is appropriate for ds in val: if not isinstance(ds, cbasestr): raise utils.InvalidType return tuple(val) def copy(self): """Make a setting which has its values copied from this one.""" return self._copyHelper((), (), {'dimensions': self.dimensions, 'datatype': self.datatype}) def makeControl(self, *args): """Allow user to choose between the datasets.""" return controls.Datasets(self, self.getDocument(), self.dimensions, self.datatype, *args) def getData(self, doc): """Return a list of datasets entered.""" out = [] for name in self.val: d = doc.data.get(name) if ( d is not None and d.datatype == self.datatype and d.dimensions == self.dimensions ): out.append(d) return out class DatasetExtended(Dataset): """Choose a dataset, give an expression or specify a list of float values.""" typename = 'dataset-extended' def convertTo(self, val): """Check is a string (dataset name or expression) or a list of floats (numbers). """ if isinstance(val, cbasestr): return val elif self.dimensions == 1: # list of numbers only allowed for 1d datasets if isinstance(val, float) or isinstance(val, int): return [val] else: try: return [float(x) for x in val] except (TypeError, ValueError): pass raise utils.InvalidType def toText(self): if isinstance(self.val, cbasestr): return self.val else: # join based on , or ; depending on decimal point join = ', ' if uilocale.decimalPoint() == ',': join = '; ' return join.join( [ ui_floattostring(x) for x in self.val ] ) def fromText(self, text): """Convert from text.""" text = text.strip() if self.dimensions > 1: return text # split based on , or ; depending on decimal point splitre = r'[\t\n, ]+' if uilocale.decimalPoint() == ',': splitre = r'[\t\n; ]+' out = [] for x in re.split(splitre, text): if x: try: out.append( ui_stringtofloat(x) ) except ValueError: # fail conversion, so exit with text return text return out def getFloatArray(self, doc): """Get a numpy of values or None.""" if isinstance(self.val, cbasestr): ds = doc.evalDatasetExpression( self.val, datatype=self.datatype, dimensions=self.dimensions) if ds: # get numpy array of values return N.array(ds.data) else: # list of values return N.array(self.val) return None def isDataset(self, doc): """Is this setting a dataset?""" return (isinstance(self.val, cbasestr) and doc.data.get(self.val)) def isEmpty(self): """Is this unset?""" return self.val == [] or self.val == '' def getData(self, doc): """Return veusz dataset""" if isinstance(self.val, cbasestr): return doc.evalDatasetExpression( self.val, datatype=self.datatype, dimensions=self.dimensions) else: return doc.valsToDataset( self.val, self.datatype, self.dimensions) class DatasetOrStr(Dataset): """Choose a dataset or enter a string. Non string datasets are converted to string arrays using this. """ typename = 'dataset-or-str' def __init__(self, name, val, **args): Dataset.__init__(self, name, val, datatype='text', **args) def getData(self, doc, checknull=False): """Return either a list of strings, a single item list. If checknull then None is returned if blank """ if doc: ds = doc.data.get(self.val) if ds: return doc.formatValsWithDatatypeToText( ds.data, ds.displaytype) if checknull and not self.val: return None else: return [cstr(self.val)] def makeControl(self, *args): return controls.DatasetOrString(self, self.getDocument(), *args) def copy(self): """Make a setting which has its values copied from this one.""" return self._copyHelper((), (), {}) class Color(ChoiceOrMore): """A color setting.""" typename = 'color' _colors = [ 'white', 'black', 'red', 'green', 'blue', 'cyan', 'magenta', 'yellow', 'grey', 'darkred', 'darkgreen', 'darkblue', 'darkcyan', 'darkmagenta' ] controls.Color._colors = _colors def __init__(self, name, value, **args): """Initialise the color setting with the given name, default and description.""" ChoiceOrMore.__init__(self, name, self._colors, value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) def color(self): """Return QColor for color.""" return qt4.QColor(self.val) def makeControl(self, *args): return controls.Color(self, *args) class FillStyle(Choice): """A setting for the different fill styles provided by Qt.""" typename = 'fill-style' _fillstyles = [ 'solid', 'horizontal', 'vertical', 'cross', 'forward diagonals', 'backward diagonals', 'diagonal cross', '94% dense', '88% dense', '63% dense', '50% dense', '37% dense', '12% dense', '6% dense' ] _fillcnvt = { 'solid': qt4.Qt.SolidPattern, 'horizontal': qt4.Qt.HorPattern, 'vertical': qt4.Qt.VerPattern, 'cross': qt4.Qt.CrossPattern, 'forward diagonals': qt4.Qt.FDiagPattern, 'backward diagonals': qt4.Qt.BDiagPattern, 'diagonal cross': qt4.Qt.DiagCrossPattern, '94% dense': qt4.Qt.Dense1Pattern, '88% dense': qt4.Qt.Dense2Pattern, '63% dense': qt4.Qt.Dense3Pattern, '50% dense': qt4.Qt.Dense4Pattern, '37% dense': qt4.Qt.Dense5Pattern, '12% dense': qt4.Qt.Dense6Pattern, '6% dense': qt4.Qt.Dense7Pattern } controls.FillStyle._fills = _fillstyles controls.FillStyle._fillcnvt = _fillcnvt def __init__(self, name, value, **args): Choice.__init__(self, name, self._fillstyles, value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) def qtStyle(self): """Return Qt ID of fill.""" return self._fillcnvt[self.val] def makeControl(self, *args): return controls.FillStyle(self, *args) class LineStyle(Choice): """A setting choosing a particular line style.""" typename = 'line-style' # list of allowed line styles _linestyles = ['solid', 'dashed', 'dotted', 'dash-dot', 'dash-dot-dot', 'dotted-fine', 'dashed-fine', 'dash-dot-fine', 'dot1', 'dot2', 'dot3', 'dot4', 'dash1', 'dash2', 'dash3', 'dash4', 'dash5', 'dashdot1', 'dashdot2', 'dashdot3'] # convert from line styles to Qt constants and a custom pattern (if any) _linecnvt = { 'solid': (qt4.Qt.SolidLine, None), 'dashed': (qt4.Qt.DashLine, None), 'dotted': (qt4.Qt.DotLine, None), 'dash-dot': (qt4.Qt.DashDotLine, None), 'dash-dot-dot': (qt4.Qt.DashDotDotLine, None), 'dotted-fine': (qt4.Qt.CustomDashLine, [2, 4]), 'dashed-fine': (qt4.Qt.CustomDashLine, [8, 4]), 'dash-dot-fine': (qt4.Qt.CustomDashLine, [8, 4, 2, 4]), 'dot1': (qt4.Qt.CustomDashLine, [0.1, 2]), 'dot2': (qt4.Qt.CustomDashLine, [0.1, 4]), 'dot3': (qt4.Qt.CustomDashLine, [0.1, 6]), 'dot4': (qt4.Qt.CustomDashLine, [0.1, 8]), 'dash1': (qt4.Qt.CustomDashLine, [4, 4]), 'dash2': (qt4.Qt.CustomDashLine, [4, 8]), 'dash3': (qt4.Qt.CustomDashLine, [8, 8]), 'dash4': (qt4.Qt.CustomDashLine, [16, 8]), 'dash5': (qt4.Qt.CustomDashLine, [16, 16]), 'dashdot1': (qt4.Qt.CustomDashLine, [0.1, 4, 4, 4]), 'dashdot2': (qt4.Qt.CustomDashLine, [0.1, 4, 8, 4]), 'dashdot3': (qt4.Qt.CustomDashLine, [0.1, 2, 4, 2]), } controls.LineStyle._lines = _linestyles def __init__(self, name, default, **args): Choice.__init__(self, name, self._linestyles, default, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) def qtStyle(self): """Get Qt ID of chosen line style.""" return self._linecnvt[self.val] def makeControl(self, *args): return controls.LineStyle(self, *args) class Axis(Str): """A setting to hold the name of an axis. direction is 'horizontal', 'vertical' or 'both' """ typename = 'axis' def __init__(self, name, val, direction, **args): """Initialise using the document, so we can get the axes later. direction is horizontal or vertical to specify the type of axis to show """ Setting.__init__(self, name, val, **args) self.direction = direction def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (self.direction,), {}) def makeControl(self, *args): """Allows user to choose an axis or enter a name.""" return controls.Axis(self, self.getDocument(), self.direction, *args) class WidgetChoice(Str): """Hold the name of a child widget.""" typename = 'widget-choice' def __init__(self, name, val, widgettypes={}, **args): """Choose widgets from (named) type given.""" Setting.__init__(self, name, val, **args) self.widgettypes = widgettypes def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {'widgettypes': self.widgettypes}) def buildWidgetList(self, level, widget, outdict): """A recursive helper to build up a list of possible widgets. This iterates over widget's children, and adds widgets as tuples to outdict using outdict[name] = (widget, level) Lower level images of the same name outweigh other images further down the tree """ for child in widget.children: if child.typename in self.widgettypes: if (child.name not in outdict) or (outdict[child.name][1]>level): outdict[child.name] = (child, level) else: self.buildWidgetList(level+1, child, outdict) def getWidgetList(self): """Return a dict of valid widget names and the corresponding objects.""" # find widget which contains setting widget = self.parent while not widget.isWidget() and widget is not None: widget = widget.parent # get widget's parent if widget is not None: widget = widget.parent # get list of widgets from recursive find widgets = {} if widget is not None: self.buildWidgetList(0, widget, widgets) # turn (object, level) pairs into object outdict = {} for name, val in widgets.items(): outdict[name] = val[0] return outdict def findWidget(self): """Find the image corresponding to this setting. Returns Image object if succeeds or None if fails """ widgets = self.getWidgetList() try: return widgets[self.get()] except KeyError: return None def makeControl(self, *args): """Allows user to choose an image widget or enter a name.""" return controls.WidgetChoice(self, self.getDocument(), *args) class Marker(Choice): """Choose a marker type from one allowable.""" typename = 'marker' def __init__(self, name, value, **args): Choice.__init__(self, name, utils.MarkerCodes, value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) def makeControl(self, *args): return controls.Marker(self, *args) class Arrow(Choice): """Choose an arrow type from one allowable.""" typename = 'arrow' def __init__(self, name, value, **args): Choice.__init__(self, name, utils.ArrowCodes, value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) def makeControl(self, *args): return controls.Arrow(self, *args) class LineSet(Setting): """A setting which corresponds to a set of lines. """ typename='line-multi' def convertTo(self, val): """Takes a tuple/list of tuples: [('dotted', '1pt', 'color', , False), ...] These are style, width, color, and hide or style, widget, color, transparency, hide """ if type(val) not in (list, tuple): raise utils.InvalidType # check each entry in the list is appropriate for line in val: try: style, width, color, hide = line except ValueError: raise utils.InvalidType if ( not isinstance(color, cbasestr) or not Distance.isDist(width) or style not in LineStyle._linestyles or type(hide) not in (int, bool) ): raise utils.InvalidType return val def makeControl(self, *args): """Make specialised lineset control.""" return controls.LineSet(self, *args) def makePen(self, painter, row): """Make a pen for the painter using row. If row is outside of range, then cycle """ if len(self.val) == 0: return qt4.QPen(qt4.Qt.NoPen) else: row = row % len(self.val) v = self.val[row] style, width, color, hide = v width = Distance.convertDistance(painter, width) style, dashpattern = LineStyle._linecnvt[style] col = utils.extendedColorToQColor(color) pen = qt4.QPen(col, width, style) if dashpattern: pen.setDashPattern(dashpattern) if hide: pen.setStyle(qt4.Qt.NoPen) return pen class FillSet(Setting): """A setting which corresponds to a set of fills. This setting keeps an internal array of LineSettings. """ typename = 'fill-multi' def convertTo(self, val): """Takes a tuple/list of tuples: [('solid', 'color', False), ...] These are color, fill style, and hide or color, fill style, and hide (style, color, hide, [optional transparency, linewidth, linestyle, spacing, backcolor, backtrans, backhide]]) """ if type(val) not in (list, tuple): raise utils.InvalidType # check each entry in the list is appropriate for fill in val: try: style, color, hide = fill[:3] except ValueError: raise utils.InvalidType if ( not isinstance(color, cbasestr) or style not in utils.extfillstyles or type(hide) not in (int, bool) or len(fill) not in (3, 10) ): raise utils.InvalidType return val def makeControl(self, *args): """Make specialised lineset control.""" return controls.FillSet(self, *args) def returnBrushExtended(self, row): """Return BrushExtended for the row.""" from . import collections s = collections.BrushExtended('tempbrush') if len(self.val) == 0: s.hide = True else: v = self.val[row % len(self.val)] s.style = v[0] col = utils.extendedColorToQColor(v[1]) if col.alpha() != 255: s.transparency = int(100 - col.alphaF()*100) col.setAlpha(255) s.color = col.name() else: s.color = v[1] s.hide = v[2] if len(v) == 10: (s.transparency, s.linewidth, s.linestyle, s.patternspacing, s.backcolor, s.backtransparency, s.backhide) = v[3:] return s class Filename(Str): """Represents a filename setting.""" typename = 'filename' def makeControl(self, *args): return controls.Filename(self, 'file', *args) def convertTo(self, val): if sys.platform == 'win32': val = val.replace('\\', '/') return val class ImageFilename(Filename): """Represents an image filename setting.""" typename = 'filename-image' def makeControl(self, *args): return controls.Filename(self, 'image', *args) class FontFamily(Str): """Represents a font family.""" typename = 'font-family' def makeControl(self, *args): """Make a special font combobox.""" return controls.FontFamily(self, *args) class ErrorStyle(Choice): """Error bar style. The allowed values are below in _errorstyles. """ typename = 'errorbar-style' _errorstyles = ( 'none', 'bar', 'barends', 'box', 'diamond', 'curve', 'barbox', 'bardiamond', 'barcurve', 'boxfill', 'diamondfill', 'curvefill', 'fillvert', 'fillhorz', 'linevert', 'linehorz', 'linevertbar', 'linehorzbar' ) controls.ErrorStyle._errorstyles = _errorstyles def __init__(self, name, value, **args): Choice.__init__(self, name, self._errorstyles, value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) def makeControl(self, *args): return controls.ErrorStyle(self, *args) class AlignHorz(Choice): """Alignment horizontally.""" typename = 'align-horz' def __init__(self, name, value, **args): Choice.__init__(self, name, ['left', 'centre', 'right'], value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) class AlignVert(Choice): """Alignment vertically.""" typename = 'align-vert' def __init__(self, name, value, **args): Choice.__init__(self, name, ['top', 'centre', 'bottom'], value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) class AlignHorzWManual(Choice): """Alignment horizontally.""" typename = 'align-horz-+manual' def __init__(self, name, value, **args): Choice.__init__(self, name, ['left', 'centre', 'right', 'manual'], value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) class AlignVertWManual(Choice): """Alignment vertically.""" typename = 'align-vert-+manual' def __init__(self, name, value, **args): Choice.__init__(self, name, ['top', 'centre', 'bottom', 'manual'], value, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) # Bool which shows/hides other settings class BoolSwitch(Bool): """Bool switching setting.""" def __init__(self, name, value, settingsfalse=[], settingstrue=[], **args): """Enables/disables a set of settings if True or False settingsfalse and settingstrue are lists of names of settings which are hidden/shown to user """ self.sfalse = settingsfalse self.strue = settingstrue Bool.__init__(self, name, value, **args) def makeControl(self, *args): return controls.BoolSwitch(self, *args) def copy(self): return self._copyHelper((), (), {'settingsfalse': self.sfalse, 'settingstrue': self.strue}) class ChoiceSwitch(Choice): """Show or hide other settings based on the choice given here.""" def __init__(self, name, vallist, value, settingstrue=[], settingsfalse=[], showfn=lambda val: True, **args): """Enables/disables a set of settings if True or False settingsfalse and settingstrue are lists of names of settings which are hidden/shown to user depending on showfn(val).""" self.sfalse = settingsfalse self.strue = settingstrue self.showfn = showfn Choice.__init__(self, name, vallist, value, **args) def makeControl(self, *args): return controls.ChoiceSwitch(self, False, self.vallist, *args) def copy(self): return self._copyHelper((self.vallist,), (), {'settingsfalse': self.sfalse, 'settingstrue': self.strue, 'showfn': self.showfn}) class FillStyleExtended(ChoiceSwitch): """A setting for the different fill styles provided by Qt.""" typename = 'fill-style-ext' _strue = ( 'linewidth', 'linestyle', 'patternspacing', 'backcolor', 'backtransparency', 'backhide' ) @staticmethod def _ishatch(val): """Is this a hatching fill?""" return not ( val == 'solid' or val.find('dense') >= 0 ) def __init__(self, name, value, **args): ChoiceSwitch.__init__(self, name, utils.extfillstyles, value, settingstrue=self._strue, settingsfalse=(), showfn=self._ishatch, **args) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) def makeControl(self, *args): return controls.FillStyleExtended(self, *args) class RotateInterval(Choice): '''Rotate a label with intervals given.''' def __init__(self, name, val, **args): Choice.__init__(self, name, ('-180', '-135', '-90', '-45', '0', '45', '90', '135', '180'), val, **args) def convertTo(self, val): """Store rotate angle.""" # backward compatibility with rotate option # False: angle 0 # True: angle 90 if val == False: val = '0' elif val == True: val = '90' return Choice.convertTo(self, val) def copy(self): """Make a copy of the setting.""" return self._copyHelper((), (), {}) class Colormap(Str): """A setting to set the color map used in an image. This is based on a Str rather than Choice as the list might change later. """ def makeControl(self, *args): return controls.Colormap(self, self.getDocument(), *args) class AxisBound(FloatOrAuto): """Axis bound - either numeric, Auto or date.""" typename = 'axis-bound' def makeControl(self, *args): return controls.AxisBound(self, *args) def toText(self): """Convert to text, taking into account mode of Axis. Displays datetimes in date format if used """ try: mode = self.parent.mode except AttributeError: mode = None v = self.val if ( not isinstance(v, cbasestr) and v is not None and mode == 'datetime' ): return utils.dateFloatToString(v) return FloatOrAuto.toText(self) def fromText(self, txt): """Convert from text, allowing datetimes.""" v = utils.dateStringToDate(txt) if N.isfinite(v): return v else: return FloatOrAuto.fromText(self, txt) veusz-1.21.1/veusz/setting/controls.py0000644000175000017500000016476712327177747016244 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Module for creating QWidgets for the settings, to enable their values to be changed. These widgets emit settingChanged(control, setting, val) when the setting is changed. The creator should use this to change the setting. """ from __future__ import division import re import numpy as N from ..compat import crange, czip, citems, cstr from .. import qtall as qt4 from .settingdb import settingdb from .. import utils def _(text, disambiguation=None, context="Setting"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) def styleClear(widget): """Return widget to default""" widget.setStyleSheet("") def styleError(widget): """Show error state on widget.""" widget.setStyleSheet("background-color: " + settingdb.color('error').name() ) class DotDotButton(qt4.QPushButton): """A button for opening up more complex editor.""" def __init__(self, tooltip=None, checkable=True): qt4.QPushButton.__init__(self, "..", flat=True, checkable=checkable, maximumWidth=16, maximumHeight=16) if tooltip: self.setToolTip(tooltip) self.setSizePolicy(qt4.QSizePolicy.Maximum, qt4.QSizePolicy.Maximum) class Edit(qt4.QLineEdit): """Main control for editing settings which are text.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, parent): """Initialise the setting widget.""" qt4.QLineEdit.__init__(self, parent) self.setting = setting self.setText( setting.toText() ) self.editingFinished.connect(self.validateAndSet) self.setting.setOnModified(self.onModified) if setting.readonly: self.setReadOnly(True) def validateAndSet(self): """Check the text is a valid setting and update it.""" text = self.text() try: val = self.setting.fromText(text) styleClear(self) self.sigSettingChanged.emit(self, self.setting, val) except utils.InvalidType: styleError(self) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" self.setText( self.setting.toText() ) class _EditBox(qt4.QTextEdit): """A popup edit box to support editing long text sections. Emits closing(text) when the box closes """ closing = qt4.pyqtSignal(cstr) def __init__(self, origtext, readonly, parent): """Make a popup, framed widget containing a text editor.""" qt4.QTextEdit.__init__(self, parent) self.setWindowFlags(qt4.Qt.Popup) self.setAttribute(qt4.Qt.WA_DeleteOnClose) self.spacing = self.fontMetrics().height() self.origtext = origtext self.setPlainText(origtext) cursor = self.textCursor() cursor.movePosition(qt4.QTextCursor.End) self.setTextCursor(cursor) if readonly: self.setReadOnly(True) utils.positionFloatingPopup(self, parent) self.installEventFilter(self) def eventFilter(self, obj, event): """Grab clicks outside this window to close it.""" if ( isinstance(event, qt4.QMouseEvent) and event.buttons() != qt4.Qt.NoButton ): frame = qt4.QRect(0, 0, self.width(), self.height()) if not frame.contains(event.pos()): self.close() return True return qt4.QTextEdit.eventFilter(self, obj, event) def keyPressEvent(self, event): """Close if escape or return is pressed.""" qt4.QTextEdit.keyPressEvent(self, event) key = event.key() if key == qt4.Qt.Key_Escape: # restore original content self.setPlainText(self.origtext) self.close() elif key == qt4.Qt.Key_Return: # keep changes self.close() def sizeHint(self): """A reasonable size for the text editor.""" return qt4.QSize(self.spacing*40, self.spacing*3) def closeEvent(self, event): """Tell the calling widget that we are closing, and provide the new text.""" text = self.toPlainText() text = text.replace('\n', '') self.closing.emit(text) event.accept() class String(qt4.QWidget): """A line editor which allows editting in a larger popup window.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, parent): qt4.QWidget.__init__(self, parent) self.setting = setting layout = qt4.QHBoxLayout() layout.setSpacing(0) layout.setMargin(0) self.setLayout(layout) self.edit = qt4.QLineEdit() layout.addWidget(self.edit) b = self.button = DotDotButton(tooltip="Edit text") layout.addWidget(b) # set the text of the widget to the self.edit.setText( setting.toText() ) self.edit.editingFinished.connect(self.validateAndSet) b.toggled.connect(self.buttonToggled) self.setting.setOnModified(self.onModified) if setting.readonly: self.edit.setReadOnly(True) def buttonToggled(self, on): """Button is pressed to bring popup up / down.""" # if button is down and there's no existing popup, bring up a new one if on: e = _EditBox( self.edit.text(), self.setting.readonly, self.button) # we get notified with text when the popup closes e.closing.connect(self.boxClosing) e.show() def boxClosing(self, text): """Called when the popup edit box closes.""" # update the text if we can if not self.setting.readonly: self.edit.setText(text) self.edit.setFocus() self.parentWidget().setFocus() self.edit.setFocus() self.button.setChecked(False) def validateAndSet(self): """Check the text is a valid setting and update it.""" text = self.edit.text() try: val = self.setting.fromText(text) styleClear(self.edit) self.sigSettingChanged.emit(self, self.setting, val) except utils.InvalidType: styleError(self.edit) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" self.edit.setText( self.setting.toText() ) class Int(qt4.QSpinBox): """A control for changing an integer.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, parent): qt4.QSpinBox.__init__(self, parent) self.ignorechange = False self.setting = setting self.setMinimum(setting.minval) self.setMaximum(setting.maxval) self.setValue(setting.val) self.valueChanged[int].connect(self.slotChanged) self.setting.setOnModified(self.onModified) if setting.readonly: self.setEnabled(False) def slotChanged(self, value): """If check box changes.""" # this is emitted by setValue, so ignore onModified doing this if not self.ignorechange: self.sigSettingChanged.emit(self, self.setting, value) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" self.ignorechange = True self.setValue( self.setting.val ) self.ignorechange = False class Bool(qt4.QCheckBox): """A check box for changing a bool setting.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, parent): qt4.QCheckBox.__init__(self, parent) self.setSizePolicy( qt4.QSizePolicy( qt4.QSizePolicy.MinimumExpanding, qt4.QSizePolicy.Fixed) ) self.ignorechange = False self.setting = setting self.setChecked(setting.val) # we get a signal when the button is toggled self.toggled.connect(self.slotToggled) self.setting.setOnModified(self.onModified) if setting.readonly: self.setEnabled(False) def slotToggled(self, state): """Emitted when checkbox toggled.""" # this is emitted by setChecked, so ignore onModified doing this if not self.ignorechange: self.sigSettingChanged.emit(self, self.setting, state) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" self.ignorechange = True self.setChecked( self.setting.val ) self.ignorechange = False class BoolSwitch(Bool): """Bool for switching off/on other settings.""" def showEvent(self, event): Bool.showEvent(self, event) self.updateState() def slotToggled(self, state): Bool.slotToggled(self, state) self.updateState() def updateState(self): """Set hidden state of settings.""" s1, s2 = self.setting.strue, self.setting.sfalse if self.setting.val: show, hide = s1, s2 else: show, hide = s2, s1 if hasattr(self.parent(), 'showHideSettings'): self.parent().showHideSettings(show, hide) class Choice(qt4.QComboBox): """For choosing between a set of values.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, iseditable, vallist, parent, icons=None, descriptions=None): qt4.QComboBox.__init__(self, parent) self.setting = setting self.setEditable(iseditable) # stops combobox readjusting in size to fit contents self.setSizeAdjustPolicy( qt4.QComboBox.AdjustToMinimumContentsLengthWithIcon) if icons is None: # add items to list (text only) self.addItems( list(vallist) ) else: # add pixmaps and text to list for icon, text in czip(icons, vallist): self.addItem(icon, text) # use tooltip descriptions if requested if descriptions is not None: for i, descr in enumerate(descriptions): self.setItemData(i, descr, qt4.Qt.ToolTipRole) # choose the correct setting try: index = list(vallist).index(setting.toText()) self.setCurrentIndex(index) except ValueError: # for cases when this is editable # set the text of the widget to the setting assert iseditable self.setEditText( setting.toText() ) # if a different item is selected self.activated[str].connect(self.slotActivated) self.setting.setOnModified(self.onModified) if setting.readonly: self.setEnabled(False) # make completion case sensitive (to help fix case typos) if self.completer(): self.completer().setCaseSensitivity(qt4.Qt.CaseSensitive) def focusOutEvent(self, *args): """Allows us to check the contents of the widget.""" qt4.QComboBox.focusOutEvent(self, *args) self.slotActivated('') def slotActivated(self, val): """If a different item is chosen.""" text = self.currentText() try: val = self.setting.fromText(text) styleClear(self) self.sigSettingChanged.emit(self, self.setting, val) except utils.InvalidType: styleError(self) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" text = self.setting.toText() index = self.findText(text) if index >= 0: self.setCurrentIndex(index) if self.isEditable(): self.setEditText(text) class ChoiceSwitch(Choice): """Show or hide other settings based on value.""" def showEvent(self, event): Choice.showEvent(self, event) self.updateState() @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" Choice.onModified(self) self.updateState() def updateState(self): """Set hidden state of settings.""" s1, s2 = self.setting.strue, self.setting.sfalse if self.setting.showfn(self.setting.val): show, hide = s1, s2 else: show, hide = s2, s1 if hasattr(self.parent(), 'showHideSettings'): self.parent().showHideSettings(show, hide) class FillStyleExtended(ChoiceSwitch): """Extended fill style list.""" _icons = None def __init__(self, setting, parent): if self._icons is None: self._generateIcons() ChoiceSwitch.__init__(self, setting, False, utils.extfillstyles, parent, icons=self._icons) @classmethod def _generateIcons(cls): """Generate a list of pixmaps for drop down menu.""" from . import collections brush = collections.BrushExtended("") brush.color = 'black' brush.patternspacing = '5pt' brush.linewidth = '0.5pt' size = 12 cls._icons = icons = [] path = qt4.QPainterPath() path.addRect(0, 0, size, size) for f in utils.extfillstyles: pix = qt4.QPixmap(size, size) pix.fill() painter = qt4.QPainter(pix) painter.pixperpt = 1. painter.scaling = 1. painter.setRenderHint(qt4.QPainter.Antialiasing) brush.style = f utils.brushExtFillPath(painter, brush, path) painter.end() icons.append( qt4.QIcon(pix) ) class MultiLine(qt4.QTextEdit): """For editting multi-line settings.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, parent): """Initialise the widget.""" qt4.QTextEdit.__init__(self, parent) self.setting = setting self.setWordWrapMode(qt4.QTextOption.NoWrap) self.setTabChangesFocus(True) # set the text of the widget to the self.setPlainText( setting.toText() ) self.setting.setOnModified(self.onModified) if setting.readonly: self.setReadOnly(True) self.document().contentsChanged.connect(self.onSizeChange) self.document().documentLayout().documentSizeChanged.connect( self.onSizeChange) self.heightmin = 0 self.heightmax = 2048 # recalculate size of document to fix size self.document().adjustSize() self.onSizeChange() def onSizeChange(self): """Make size match content size.""" m = self.contentsMargins() docheight = self.document().size().height() + m.top() + m.bottom() docheight = min(self.heightmax, max(self.heightmin, docheight)) self.setFixedHeight(docheight) def focusOutEvent(self, *args): """Allows us to check the contents of the widget.""" qt4.QTextEdit.focusOutEvent(self, *args) text = self.toPlainText() try: val = self.setting.fromText(text) styleClear(self) self.sigSettingChanged.emit(self, self.setting, val) except utils.InvalidType: styleError(self) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" self.setPlainText( self.setting.toText() ) class Notes(MultiLine): """For editing notes.""" def __init__(self, setting, parent): MultiLine.__init__(self, setting, parent) self.setWordWrapMode(qt4.QTextOption.WordWrap) class Distance(Choice): """For editing distance settings.""" # used to remove non-numerics from the string # we also remove X/ from X/num stripnumre = re.compile(r"[0-9]*/|[^0-9.,]") # remove spaces stripspcre = re.compile(r"\s") def __init__(self, setting, parent, allowauto=False, physical=False): '''Initialise with blank list, then populate with sensible units.''' Choice.__init__(self, setting, True, [], parent) self.allowauto = allowauto self.physical = physical self.updateComboList() def updateComboList(self): '''Populates combo list with sensible list of other possible units.''' # turn off signals, so our modifications don't create more signals self.blockSignals(True) # get current text text = self.currentText() # get rid of non-numeric things from the string num = self.stripnumre.sub('', text) # here are a list of possible different units the user can choose # between. should this be in utils? newitems = [ num+'pt', num+'cm', num+'mm', num+'in' ] if not self.physical: newitems += [ num+'%', '1/'+num ] if self.allowauto: newitems.insert(0, 'Auto') # if we're already in this list, we position the current selection # to the correct item (up and down keys work properly then) # spaces are removed to make sure we get sensible matches spcfree = self.stripspcre.sub('', text) try: index = newitems.index(spcfree) except ValueError: index = 0 newitems.insert(0, text) # get rid of existing items in list (clear doesn't work here) for i in crange(self.count()): self.removeItem(0) # put new items in and select the correct option self.addItems(newitems) self.setCurrentIndex(index) # must remember to do this! self.blockSignals(False) def slotActivated(self, val): '''Populate the drop down list before activation.''' self.updateComboList() Choice.slotActivated(self, val) class DistancePt(Choice): """For editing distances with defaults in points.""" points = ( '0pt', '0.25pt', '0.5pt', '1pt', '1.5pt', '2pt', '3pt', '4pt', '5pt', '6pt', '8pt', '10pt', '12pt', '14pt', '16pt', '18pt', '20pt', '22pt', '24pt', '26pt', '28pt', '30pt', '34pt', '40pt', '44pt', '50pt', '60pt', '70pt' ) def __init__(self, setting, parent, allowauto=False): '''Initialise with blank list, then populate with sensible units.''' Choice.__init__(self, setting, True, DistancePt.points, parent) class Dataset(qt4.QWidget): """Allow the user to choose between the possible datasets.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, document, dimensions, datatype, parent): """Initialise the combobox. The list is populated with datasets. dimensions specifies the dimension of the dataset to list Changes on the document refresh the list of datasets.""" qt4.QWidget.__init__(self, parent) self.choice = Choice(setting, True, [], None) self.choice.sigSettingChanged.connect(self.sigSettingChanged) b = self.button = DotDotButton(tooltip=_("Select using dataset browser")) b.toggled.connect(self.slotButtonToggled) self.document = document self.dimensions = dimensions self.datatype = datatype self.lastdatasets = None self._populateEntries() document.signalModified.connect(self.slotModified) layout = qt4.QHBoxLayout() layout.setSpacing(0) layout.setMargin(0) layout.addWidget(self.choice) layout.addWidget(b) self.setLayout(layout) def _populateEntries(self): """Put the list of datasets into the combobox.""" # get datasets of the correct dimension datasets = [] for name, ds in citems(self.document.data): if ds.dimensions == self.dimensions and ( ds.datatype == self.datatype or self.datatype == 'all' or ds.datatype in self.datatype ): datasets.append(name) datasets.sort() if datasets != self.lastdatasets: utils.populateCombo(self.choice, datasets) self.lastdatasets = datasets @qt4.pyqtSlot(int) def slotModified(self, modified): """Update the list of datasets if the document is modified.""" self._populateEntries() def slotButtonToggled(self, on): """Bring up list of datasets.""" if on: from ..qtwidgets.datasetbrowser import DatasetBrowserPopup d = DatasetBrowserPopup(self.document, self.choice.currentText(), self.button, filterdims=set((self.dimensions,)), filterdtype=set((self.datatype,)) ) d.closing.connect(self.boxClosing) d.newdataset.connect(self.newDataset) d.show() def boxClosing(self): """Called when the popup edit box closes.""" self.button.setChecked(False) def newDataset(self, dsname): """New dataset selected.""" self.sigSettingChanged.emit(self, self.choice.setting, dsname) class DatasetOrString(Dataset): """Allow use to choose a dataset or enter some text.""" def __init__(self, setting, document, parent): Dataset.__init__(self, setting, document, 1, 'all', parent) b = self.textbutton = DotDotButton() self.layout().addWidget(b) b.toggled.connect(self.textButtonToggled) def textButtonToggled(self, on): """Button is pressed to bring popup up / down.""" # if button is down and there's no existing popup, bring up a new one if on: e = _EditBox( self.choice.currentText(), self.choice.setting.readonly, self.textbutton) # we get notified with text when the popup closes e.closing.connect(self.textBoxClosing) e.show() def textBoxClosing(self, text): """Called when the popup edit box closes.""" self.textbutton.setChecked(False) # update the text if we can if not self.choice.setting.readonly: self.choice.setEditText(text) self.choice.setFocus() self.parentWidget().setFocus() self.choice.setFocus() class FillStyle(Choice): """For choosing between fill styles.""" _icons = None _fills = None _fillcnvt = None def __init__(self, setting, parent): if self._icons is None: self._generateIcons() Choice.__init__(self, setting, False, self._fills, parent, icons=self._icons) @classmethod def _generateIcons(cls): """Generate a list of pixmaps for drop down menu.""" size = 12 icons = [] c = qt4.QColor('grey') for f in cls._fills: pix = qt4.QPixmap(size, size) pix.fill() painter = qt4.QPainter(pix) painter.setRenderHint(qt4.QPainter.Antialiasing) brush = qt4.QBrush(c, cls._fillcnvt[f]) painter.fillRect(0, 0, size, size, brush) painter.end() icons.append( qt4.QIcon(pix) ) cls._icons = icons class Marker(Choice): """A control to let the user choose a marker.""" _icons = None def __init__(self, setting, parent): if self._icons is None: self._generateIcons() Choice.__init__(self, setting, False, utils.MarkerCodes, parent, icons=self._icons) @classmethod def _generateIcons(cls): size = 16 icons = [] brush = qt4.QBrush( qt4.QColor('darkgrey') ) pen = qt4.QPen( qt4.QBrush(qt4.Qt.black), 1. ) for marker in utils.MarkerCodes: pix = qt4.QPixmap(size, size) pix.fill() painter = qt4.QPainter(pix) painter.setRenderHint(qt4.QPainter.Antialiasing) painter.setBrush(brush) painter.setPen(pen) utils.plotMarker(painter, size*0.5, size*0.5, marker, size*0.33) painter.end() icons.append( qt4.QIcon(pix) ) cls._icons = icons class Arrow(Choice): """A control to let the user choose an arrowhead.""" _icons = None def __init__(self, setting, parent): if self._icons is None: self._generateIcons() Choice.__init__(self, setting, False, utils.ArrowCodes, parent, icons=self._icons) @classmethod def _generateIcons(cls): size = 16 icons = [] brush = qt4.QBrush(qt4.Qt.black) pen = qt4.QPen( qt4.QBrush(qt4.Qt.black), 1. ) for arrow in utils.ArrowCodes: pix = qt4.QPixmap(size, size) pix.fill() painter = qt4.QPainter(pix) painter.setRenderHint(qt4.QPainter.Antialiasing) painter.setBrush(brush) painter.setPen(pen) utils.plotLineArrow(painter, size*0.4, size*0.5, size*2, 0., arrowsize=size*0.2, arrowleft=arrow, arrowright=arrow) painter.end() icons.append( qt4.QIcon(pix) ) cls._icons = icons class LineStyle(Choice): """For choosing between line styles.""" _icons = None _lines = None _linecnvt = None size = (24, 8) def __init__(self, setting, parent): if self._icons is None: self._generateIcons() Choice.__init__(self, setting, False, self._lines, parent, icons=self._icons) self.setIconSize( qt4.QSize(*self.size) ) @classmethod def _generateIcons(cls): """Generate a list of icons for drop down menu.""" # import later for dependency issues from . import collections from .. import document icons = [] size = cls.size setn = collections.Line('temp') setn.get('color').set('black') setn.get('width').set('1pt') for lstyle in cls._lines: pix = qt4.QPixmap(*size) pix.fill() ph = document.PaintHelper( (1, 1) ) painter = qt4.QPainter(pix) painter.setRenderHint(qt4.QPainter.Antialiasing) setn.get('style').set(lstyle) painter.setPen( setn.makeQPen(ph) ) painter.drawLine( int(size[0]*0.1), size[1]/2, int(size[0]*0.9), size[1]/2 ) painter.end() icons.append( qt4.QIcon(pix) ) cls._icons = icons class _ColNotifier(qt4.QObject): sigNewColor = qt4.pyqtSignal(cstr) class Color(qt4.QWidget): """A control which lets the user choose a color. A drop down list and a button to bring up a dialog are used """ sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) _icons = None _colors = None _colnotifier = None def __init__(self, setting, parent): qt4.QWidget.__init__(self, parent) if self._icons is None: self._generateIcons() self.setting = setting # combo box c = self.combo = qt4.QComboBox() c.setEditable(True) for color in self._colors: c.addItem(self._icons[color], color) c.activated[str].connect(self.slotActivated) # add color if a color is added by a different combo box Color._colnotifier.sigNewColor.connect(self.addcolorSlot) # button for selecting colors b = self.button = qt4.QPushButton() b.setFlat(True) b.setSizePolicy(qt4.QSizePolicy.Maximum, qt4.QSizePolicy.Maximum) b.setMaximumHeight(24) b.setMaximumWidth(24) b.clicked.connect(self.slotButtonClicked) if setting.readonly: c.setEnabled(False) b.setEnabled(False) layout = qt4.QHBoxLayout() layout.setSpacing(0) layout.setMargin(0) layout.addWidget(c) layout.addWidget(b) self.setColor( setting.toText() ) self.setLayout(layout) self.setting.setOnModified(self.onModified) def addcolorSlot(self, color): """When another Color combo adds a color, add one to this one""" self.combo.addItem(self._icons[color], color) @classmethod def _generateIcons(cls): """Generate a list of icons for drop down menu. Does not generate existing icons """ size = 12 if cls._icons is None: cls._icons = {} icons = cls._icons for c in cls._colors: if c not in icons: pix = qt4.QPixmap(size, size) pix.fill( qt4.QColor(c) ) icons[c] = qt4.QIcon(pix) if cls._colnotifier is not None: # tell other combo boxes a color has been added cls._colnotifier.sigNewColor.emit(c) if cls._colnotifier is None: cls._colnotifier = _ColNotifier() def slotButtonClicked(self): """Open dialog to edit color.""" col = qt4.QColorDialog.getColor(self.setting.color(), self) if col.isValid(): # change setting val = col.name() self.sigSettingChanged.emit(self, self.setting, val) def slotActivated(self, val): """A different value is selected.""" text = self.combo.currentText() val = self.setting.fromText(text) self.sigSettingChanged.emit(self, self.setting, val) def setColor(self, color): """Update control with color given.""" # construct color icon if not there if color not in Color._icons: Color._colors.append(color) Color._generateIcons() # add text to combo if not there index = self.combo.findText(color) # set correct index in combobox self.combo.setCurrentIndex(index) self.button.setIcon( self.combo.itemIcon(index) ) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" self.setColor( self.setting.toText() ) class WidgetSelector(Choice): """For choosing from a list of widgets.""" def __init__(self, setting, document, parent): """Initialise and populate combobox.""" Choice.__init__(self, setting, True, [], parent) self.document = document document.signalModified.connect(self.slotModified) def _populateEntries(self): pass @qt4.pyqtSlot(int) def slotModified(self, modified): """Update list of axes.""" self._populateEntries() class WidgetChoice(WidgetSelector): """Choose a widget.""" def __init__(self, setting, document, parent): """Initialise and populate combobox.""" WidgetSelector.__init__(self, setting, document, parent) self._populateEntries() def _populateEntries(self): """Build up a list of widgets for combobox.""" widgets = self.setting.getWidgetList() # we only need the list of names names = list(widgets.keys()) names.sort() utils.populateCombo(self, names) class Axis(WidgetSelector): """Choose an axis to plot against.""" def __init__(self, setting, document, direction, parent): """Initialise and populate combobox.""" WidgetSelector.__init__(self, setting, document, parent) self.direction = direction self._populateEntries() def _populateEntries(self): """Build up a list of possible axes.""" # get parent widget widget = self.setting.parent while not widget.isWidget() and widget is not None: widget = widget.parent # get list of axis widgets up the tree axes = set() while widget is not None: for w in widget.children: if ( w.isaxis and ( self.direction == 'both' or w.settings.direction == self.direction) ): axes.add(w.name) widget = widget.parent names = sorted(axes) utils.populateCombo(self, names) class ListSet(qt4.QFrame): """A widget for constructing settings which are lists of other properties. This code is pretty nasty and horrible, so we abstract it in this base widget """ sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) pixsize = 12 def __init__(self, defaultval, setting, parent): """Initialise this base widget. defaultval is the default entry to add if add is clicked with no current entries setting is the setting this widget corresponds to parent is the parent widget. """ qt4.QFrame.__init__(self, parent) self.setFrameStyle(qt4.QFrame.Box) self.defaultval = defaultval self.setting = setting self.controls = [] self.layout = qt4.QGridLayout(self) self.layout.setMargin( self.layout.margin()//2 ) self.layout.setSpacing( self.layout.spacing()//4 ) # ignore changes if this set self.ignorechange = False self.populate() self.setting.setOnModified(self.onModified) def populateRow(self, row, val): """Populate the row in the control. Returns a list of the widgets created. """ return None def populate(self): """Construct the list of controls.""" # delete all children in case of refresh self.controls = [] for c in self.children(): if isinstance(c, qt4.QWidget): self.layout.removeWidget(c) c.deleteLater() c = None # iterate over each row row = -1 for row, val in enumerate(self.setting.val): cntrls = self.populateRow(row, val) for col in crange(len(cntrls)): self.layout.addWidget(cntrls[col], row, col) for c in cntrls: c.show() self.controls.append(cntrls) # buttons at end bbox = qt4.QWidget() h = qt4.QHBoxLayout(bbox) h.setMargin(0) bbox.setLayout(h) self.layout.addWidget(bbox, row+1, 0, 1, -1) # a button to add a new entry b = qt4.QPushButton('Add') h.addWidget(b) b.clicked.connect(self.onAddClicked) b.show() # a button to delete the last entry b = qt4.QPushButton('Delete') h.addWidget(b) b.clicked.connect(self.onDeleteClicked) b.setEnabled( len(self.setting.val) > 0 ) b.show() def onAddClicked(self): """Add a line style to the list given.""" rows = list(self.setting.val) if len(rows) != 0: rows.append(rows[-1]) else: rows.append(self.defaultval) self.sigSettingChanged.emit(self, self.setting, rows) def onDeleteClicked(self): """Remove final entry in settings list.""" rows = list(self.setting.val)[:-1] self.sigSettingChanged.emit(self, self.setting, rows) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" if not self.ignorechange: self.populate() else: self.ignorechange = False def identifyPosn(self, widget): """Identify the position this widget is in. Returns (row, col) or (None, None) if not found. """ for row, cntrls in enumerate(self.controls): for col, cntrl in enumerate(cntrls): if cntrl == widget: return (row, col) return (None, None) def addColorButton(self, row, col, tooltip): """Add a color button to the list at the position specified.""" color = self.setting.val[row][col] wcolor = qt4.QPushButton() wcolor.setFlat(True) wcolor.setSizePolicy(qt4.QSizePolicy.Maximum, qt4.QSizePolicy.Maximum) wcolor.setMaximumHeight(24) wcolor.setMaximumWidth(24) pix = qt4.QPixmap(self.pixsize, self.pixsize) pix.fill( utils.extendedColorToQColor(color) ) wcolor.setIcon( qt4.QIcon(pix) ) wcolor.setToolTip(tooltip) wcolor.clicked.connect(self.onColorClicked) return wcolor def addToggleButton(self, row, col, tooltip): """Make a toggle button.""" toggle = self.setting.val[row][col] wtoggle = qt4.QCheckBox() wtoggle.setChecked(toggle) wtoggle.setToolTip(tooltip) wtoggle.toggled.connect(self.onToggled) return wtoggle def addCombo(self, row, col, tooltip, values, icons, texts): """Make an enumeration combo - choose from a set of icons.""" val = self.setting.val[row][col] wcombo = qt4.QComboBox() if texts is None: for icon in icons: wcombo.addItem(icon, "") else: for text, icon in czip(texts, icons): wcombo.addItem(icon, text) wcombo.setCurrentIndex(values.index(val)) wcombo.setToolTip(tooltip) wcombo.activated[int].connect(self.onComboChanged) wcombo._vz_values = values return wcombo def _updateRowCol(self, row, col, val): """Update value on row and column.""" rows = list(self.setting.val) items = list(rows[row]) items[col] = val rows[row] = tuple(items) self.ignorechange = True self.sigSettingChanged.emit(self, self.setting, rows) def onToggled(self, on): """Checkbox toggled.""" row, col = self.identifyPosn(self.sender()) self._updateRowCol(row, col, on) def onComboChanged(self, val): """Update the setting if the combo changes.""" sender = self.sender() row, col = self.identifyPosn(sender) self._updateRowCol(row, col, sender._vz_values[val]) def onColorClicked(self): """Color button clicked for line.""" sender = self.sender() row, col = self.identifyPosn(sender) rows = self.setting.val color = qt4.QColorDialog.getColor( utils.extendedColorToQColor(rows[row][col]), self, "Choose color", qt4.QColorDialog.ShowAlphaChannel ) if color.isValid(): # change setting # this is a bit irritating, as have to do lots of # tedious conversions color = utils.extendedColorFromQColor(color) self._updateRowCol(row, col, color) # change the color pix = qt4.QPixmap(self.pixsize, self.pixsize) pix.fill( utils.extendedColorToQColor(color) ) sender.setIcon( qt4.QIcon(pix) ) class LineSet(ListSet): """A list of line styles. """ def __init__(self, setting, parent): ListSet.__init__(self, ('solid', '1pt', 'black', False), setting, parent) def populateRow(self, row, val): """Add the widgets for the row given.""" # create line icons if not already created if LineStyle._icons is None: LineStyle._generateIcons() # make line style selector wlinestyle = self.addCombo(row, 0, _('Line style'), LineStyle._lines, LineStyle._icons, None) # make line width edit box wwidth = qt4.QLineEdit() wwidth.setText(self.setting.val[row][1]) wwidth.setToolTip('Line width') wwidth.editingFinished.connect(self.onWidthChanged) # make color selector button wcolor = self.addColorButton(row, 2, _('Line color')) # make hide checkbox whide = self.addToggleButton(row, 3, _('Hide line')) # return created controls return [wlinestyle, wwidth, wcolor, whide] def onWidthChanged(self): """Width has changed - validate.""" sender = self.sender() row, col = self.identifyPosn(sender) text = sender.text() from . import setting if setting.Distance.isDist(text): # valid distance styleClear(sender) self._updateRowCol(row, col, text) else: # invalid distance styleError(sender) class _FillBox(qt4.QScrollArea): """Pop up box for extended fill settings.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) closing = qt4.pyqtSignal(int) def __init__(self, doc, thesetting, row, button, parent): """Initialse widget. This is based on a PropertyList widget. FIXME: we have to import at runtime, so we should improve the inheritance here. Is using PropertyList window a hack? """ qt4.QScrollArea.__init__(self, parent) self.setWindowFlags(qt4.Qt.Popup) self.setAttribute(qt4.Qt.WA_DeleteOnClose) self.parent = parent self.row = row self.setting = thesetting self.extbrush = thesetting.returnBrushExtended(row) from ..windows.treeeditwindow import SettingsProxySingle, \ PropertyList fbox = self class DirectSetProxy(SettingsProxySingle): """Class to intercept changes of settings from UI.""" def onSettingChanged(self, control, setting, val): # set value in setting setting.val = val # tell box to update setting fbox.onSettingChanged() # actual widget for changing the fill plist = PropertyList(doc) plist.updateProperties( DirectSetProxy(doc, self.extbrush) ) self.setWidget(plist) utils.positionFloatingPopup(self, button) self.installEventFilter(self) def onSettingChanged(self): """Called when user changes a fill property.""" # get value of brush and get data for row e = self.extbrush rowdata = [e.style, e.color, e.hide] if e.style != 'solid': rowdata += [ e.transparency, e.linewidth, e.linestyle, e.patternspacing, e.backcolor, e.backtransparency, e.backhide ] rowdata = tuple(rowdata) if self.setting.val[self.row] != rowdata: # if row different, send update signal val = list(self.setting.val) val[self.row] = rowdata self.sigSettingChanged.emit(self, self.setting, val) def eventFilter(self, obj, event): """Grab clicks outside this window to close it.""" if ( isinstance(event, qt4.QMouseEvent) and event.buttons() != qt4.Qt.NoButton ): frame = qt4.QRect(0, 0, self.width(), self.height()) if not frame.contains(event.pos()): self.close() return True return qt4.QScrollArea.eventFilter(self, obj, event) def keyPressEvent(self, event): """Close if escape or return is pressed.""" qt4.QScrollArea.keyPressEvent(self, event) key = event.key() if key == qt4.Qt.Key_Escape: self.close() def closeEvent(self, event): """Tell the calling widget that we are closing, and provide the new text.""" self.closing.emit(self.row) qt4.QScrollArea.closeEvent(self, event) class FillSet(ListSet): """A list of fill settings.""" def __init__(self, setting, parent): ListSet.__init__(self, ('solid', 'black', False), setting, parent) def populateRow(self, row, val): """Add the widgets for the row given.""" # construct fill icons if not already done if FillStyle._icons is None: FillStyle._generateIcons() # make fill style selector wfillstyle = self.addCombo(row, 0, _("Fill style"), FillStyle._fills, FillStyle._icons, FillStyle._fills) wfillstyle.setMinimumWidth(self.pixsize) # make color selector button wcolor = self.addColorButton(row, 1, _("Fill color")) # make hide checkbox whide = self.addToggleButton(row, 2, _("Hide fill")) # extended options wmore = DotDotButton(tooltip=_("More options")) wmore.toggled.connect(lambda on, row=row: self.editMore(on, row)) # return widgets return [wfillstyle, wcolor, whide, wmore] def buttonAtRow(self, row): """Get .. button on row.""" return self.layout.itemAtPosition(row, 3).widget() def editMore(self, on, row): if on: fb = _FillBox(self.setting.getDocument(), self.setting, row, self.buttonAtRow(row), self.parent()) fb.closing.connect(self.boxClosing) fb.sigSettingChanged.connect(self.sigSettingChanged) fb.show() def boxClosing(self, row): """Called when the popup edit box closes.""" # uncheck the .. button self.buttonAtRow(row).setChecked(False) class MultiSettingWidget(qt4.QWidget): """A widget for storing multiple values in a tuple, with + and - signs by each entry.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, doc, *args): """Construct widget as combination of LineEdit and PushButton for browsing.""" qt4.QWidget.__init__(self, *args) self.setting = setting self.document = doc self.grid = layout = qt4.QGridLayout() layout.setHorizontalSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.last = () self.controls = [] self.setting.setOnModified(self.onModified) def makeRow(self): """Make new row at end""" row = len(self.controls) cntrl = self.makeControl(row) cntrl.installEventFilter(self) addbutton = qt4.QPushButton('+') addbutton.setFixedWidth(24) addbutton.setFlat(True) addbutton.setToolTip('Add another item') subbutton = qt4.QPushButton('-') subbutton.setToolTip('Remove item') subbutton.setFixedWidth(24) subbutton.setFlat(True) self.controls.append((cntrl, addbutton, subbutton)) self.grid.addWidget(cntrl, row, 0) self.grid.addWidget(addbutton, row, 1) self.grid.addWidget(subbutton, row, 2) addbutton.clicked.connect(lambda: self.addPressed(row)) subbutton.clicked.connect(lambda: self.subPressed(row)) if len(self.controls) == 2: # enable first subtraction button self.controls[0][2].setEnabled(True) elif len(self.controls) == 1: # or disable self.controls[0][2].setEnabled(False) def eventFilter(self, obj, event): """Capture loss of focus by controls.""" if event.type() == qt4.QEvent.FocusOut: for row, c in enumerate(self.controls): if c[0] is obj: self.dataChanged(row) break return qt4.QWidget.eventFilter(self, obj, event) def deleteRow(self): """Remove last row""" for w in self.controls[-1]: self.grid.removeWidget(w) w.deleteLater() self.controls.pop(-1) # disable first subtraction button if len(self.controls) == 1: self.controls[0][2].setEnabled(False) def addPressed(self, row): """User adds a new row.""" val = list(self.setting.val) val.insert(row+1, '') self.sigSettingChanged.emit(self, self.setting, tuple(val)) def subPressed(self, row): """User deletes a row.""" val = list(self.setting.val) val.pop(row) self.sigSettingChanged.emit(self, self.setting, tuple(val)) @qt4.pyqtSlot() def onModified(self): """Called when the setting is changed remotely, or when control is opened""" s = self.setting if self.last == s.val: return self.last = s.val # update number of rows while len(self.setting.val) > len(self.controls): self.makeRow() while len(self.setting.val) < len(self.controls): self.deleteRow() # update values self.updateControls() def makeControl(self, row): """Override this to make an editing widget.""" return None def updateControls(self): """Override this to update values in controls.""" pass def readControl(self, cntrl): """Read value from control.""" return None def dataChanged(self, row): """Update row of setitng with new data""" val = list(self.setting.val) val[row] = self.readControl( self.controls[row][0] ) self.sigSettingChanged.emit(self, self.setting, tuple(val)) class Datasets(MultiSettingWidget): """A control for editing a list of datasets.""" def __init__(self, setting, doc, dimensions, datatype, *args): """Contruct set of comboboxes""" MultiSettingWidget.__init__(self, setting, doc, *args) self.dimensions = dimensions self.datatype = datatype self.lastdatasets = [] # force updating to initialise self.onModified() def makeControl(self, row): """Make QComboBox edit widget.""" combo = qt4.QComboBox() combo.setEditable(True) combo.lineEdit().editingFinished.connect(lambda: self.dataChanged(row)) # if a different item is selected combo.activated[str].connect(lambda x: self.dataChanged(row)) utils.populateCombo(combo, self.getDatasets()) return combo def readControl(self, control): """Get text for control.""" return control.lineEdit().text() def getDatasets(self): """Get applicable datasets (sorted).""" datasets = [] for name, ds in citems(self.document.data): if (ds.dimensions == self.dimensions and ds.datatype == self.datatype): datasets.append(name) datasets.sort() return datasets def updateControls(self): """Set values of controls.""" for cntrls, val in czip(self.controls, self.setting.val): cntrls[0].lineEdit().setText(val) @qt4.pyqtSlot() def onModified(self): """Called when the setting is changed remotely, or when control is opened""" MultiSettingWidget.onModified(self) datasets = self.getDatasets() if self.lastdatasets == datasets: return self.lastdatasets = datasets # update list of datasets for cntrls in self.controls: utils.populateCombo(cntrls[0], datasets) class Strings(MultiSettingWidget): """A list of strings.""" def __init__(self, setting, doc, *args): """Construct widget as combination of LineEdit and PushButton for browsing.""" MultiSettingWidget.__init__(self, setting, doc, *args) self.onModified() def makeControl(self, row): """Make edit widget.""" lineedit = qt4.QLineEdit() lineedit.editingFinished.connect(lambda: self.dataChanged(row)) return lineedit def readControl(self, control): """Get text for control.""" return control.text() def updateControls(self): """Set values of controls.""" for cntrls, val in czip(self.controls, self.setting.val): cntrls[0].setText(val) class Filename(qt4.QWidget): """A widget for selecting a filename with a browse button.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, mode, parent): """Construct widget as combination of LineEdit and PushButton for browsing. mode is 'image' or 'file' """ qt4.QWidget.__init__(self, parent) self.mode = mode self.setting = setting layout = qt4.QHBoxLayout() layout.setSpacing(0) layout.setMargin(0) self.setLayout(layout) # the actual edit control self.edit = qt4.QLineEdit() self.edit.setText( setting.toText() ) layout.addWidget(self.edit) b = self.button = DotDotButton(checkable=False, tooltip=_("Browse for file")) layout.addWidget(b) # connect up signals self.edit.editingFinished.connect(self.validateAndSet) b.clicked.connect(self.buttonClicked) # completion support c = self.filenamecompleter = qt4.QCompleter(self) model = qt4.QDirModel(c) c.setModel(model) self.edit.setCompleter(c) # for read only filenames if setting.readonly: self.edit.setReadOnly(True) self.setting.setOnModified(self.onModified) def buttonClicked(self): """Button clicked - show file open dialog.""" title = _('Choose file') filefilter = _("All files (*)") if self.mode == 'image': title = _('Choose image') filefilter = ("Images (*.png *.jpg *.jpeg *.bmp *.svg *.tiff *.tif " "*.gif *.xbm *.xpm);;" + filefilter) filename = qt4.QFileDialog.getOpenFileName( self, title, self.edit.text(), filefilter) if filename: self.sigSettingChanged.emit(self, self.setting, filename) def validateAndSet(self): """Check the text is a valid setting and update it.""" text = self.edit.text() try: val = self.setting.fromText(text) styleClear(self.edit) self.sigSettingChanged.emit(self, self.setting, val) except utils.InvalidType: styleError(self.edit) @qt4.pyqtSlot() def onModified(self): """called when the setting is changed remotely""" self.edit.setText( self.setting.toText() ) class FontFamily(qt4.QFontComboBox): """List the font families, showing each font.""" sigSettingChanged = qt4.pyqtSignal(qt4.QObject, object, object) def __init__(self, setting, parent): """Create the combobox.""" qt4.QFontComboBox.__init__(self, parent) self.setting = setting self.setFontFilters( qt4.QFontComboBox.ScalableFonts ) # set initial value self.onModified() # stops combobox readjusting in size to fit contents self.setSizeAdjustPolicy( qt4.QComboBox.AdjustToMinimumContentsLengthWithIcon) self.setting.setOnModified(self.onModified) # if a different item is selected self.activated[str].connect(self.slotActivated) def focusOutEvent(self, *args): """Allows us to check the contents of the widget.""" qt4.QFontComboBox.focusOutEvent(self, *args) self.slotActivated('') def slotActivated(self, val): """Update setting if a different item is chosen.""" newval = self.currentText() self.sigSettingChanged.emit(self, self.setting, newval) @qt4.pyqtSlot() def onModified(self): """Make control reflect chosen setting.""" self.setCurrentFont( qt4.QFont(self.setting.toText()) ) class ErrorStyle(Choice): """Choose different error bar styles.""" _icons = None # generated icons _errorstyles = None # copied in by setting.py def __init__(self, setting, parent): if self._icons is None: self._generateIcons() Choice.__init__(self, setting, False, self._errorstyles, parent, icons=self._icons) @classmethod def _generateIcons(cls): """Generate a list of pixmaps for drop down menu.""" cls._icons = [] for errstyle in cls._errorstyles: cls._icons.append( utils.getIcon('error_%s' % errstyle) ) class Colormap(Choice): """Give the user a preview of colormaps. Based on Choice to make life easier """ _icons = {} size = (32, 12) def __init__(self, setn, document, parent): names = sorted(document.colormaps) icons = Colormap._generateIcons(document, names) Choice.__init__(self, setn, True, names, parent, icons=icons) self.setIconSize( qt4.QSize(*self.size) ) @classmethod def _generateIcons(kls, document, names): """Generate a list of icons for drop down menu.""" # create a fake dataset smoothly varying from 0 to size[0]-1 size = kls.size fakedataset = N.fromfunction(lambda x, y: y, (size[1], size[0])) # keep track of icons to return retn = [] # iterate over colour maps for name in names: val = document.colormaps.get(name, None) if val in kls._icons: icon = kls._icons[val] else: if val is None: # empty icon pixmap = qt4.QPixmap(*size) pixmap.fill(qt4.Qt.transparent) else: # generate icon image = utils.applyColorMap(val, 'linear', fakedataset, 0., size[0]-1., 0) pixmap = qt4.QPixmap.fromImage(image) icon = qt4.QIcon(pixmap) kls._icons[val] = icon retn.append(icon) return retn class AxisBound(Choice): """Control for setting bounds of axis. This is to allow dates etc """ def __init__(self, setting, *args): Choice.__init__(self, setting, True, ['Auto'], *args) modesetn = setting.parent.get('mode') modesetn.setOnModified(self.modeChange) @qt4.pyqtSlot() def modeChange(self): """Called if the mode of the axis changes. Re-set text as float or date.""" if self.currentText().lower() != 'auto': self.setEditText( self.setting.toText() ) veusz-1.21.1/veusz/setting/__init__.py0000644000175000017500000000210512327177747016111 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from .settingdb import * from .reference import Reference, ReferenceMultiple from .setting import * from .settings import * from .collections import * from .stylesheet import * veusz-1.21.1/veusz/setting/stylesheet.py0000664000175000017500000001121612237406466016541 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division, print_function import sys from .settings import Settings from . import setting from . import collections from .. import qtall as qt4 def _(text, disambiguation=None, context="Setting"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class StyleSheet(Settings): """A class for handling default values of settings. Settings are registered to be added to the stylesheet.""" registeredsettings = [] @classmethod def register(kls, settingskls, posn=None): """Register a settings object with the stylesheet. This settings object is copied for each new document. """ if posn is None: kls.registeredsettings.append(settingskls) else: kls.registeredsettings.insert(posn, settingskls) def __init__(self, **args): """Create the default settings.""" Settings.__init__(self, 'StyleSheet', setnsmode='stylesheet', **args) self.pixmap = 'settings_stylesheet' for subset in self.registeredsettings: self.add( subset() ) class StylesheetLine(Settings): """Hold the properties of the default line.""" def __init__(self): Settings.__init__(self, 'Line', pixmap='settings_plotline', descr=_('Default line style for document'), usertext=_('Line')) self.add( setting.DistancePt('width', '0.5pt', descr=_('Default line width'), usertext=_('Width'), formatting=True) ) self.add( setting.Color('color', 'black', descr=_('Default line color'), usertext=_('Color'), formatting=True) ) # register these properties with the stylesheet StyleSheet.register(StylesheetLine) def _registerFontStyleSheet(): """Get fonts, and register default with StyleSheet and Text class.""" families = qt4.QFontDatabase().families() deffont = None for f in ('Times New Roman', 'Bitstream Vera Serif', 'Times', 'Utopia', 'Serif'): if f in families: deffont = f break if deffont is None: print("Warning: did not find a sensible default font. Choosing first font.", file=sys.stderr) deffont = families[0] collections.Text.defaultfamily = deffont collections.Text.families = families StylesheetText.defaultfamily = deffont StylesheetText.families = families class StylesheetText(Settings): """Hold properties of default text font.""" defaultfamily = None families = None def __init__(self): """Initialise with default font family and list of families.""" Settings.__init__(self, 'Font', pixmap='settings_axislabel', descr=_('Default font for document'), usertext=_('Font')) if StylesheetText.defaultfamily is None: _registerFontStyleSheet() self.add( setting.FontFamily('font', StylesheetText.defaultfamily, descr=_('Font name'), usertext=_('Font'), formatting=True)) self.add( setting.DistancePt('size', '14pt', descr=_('Default font size'), usertext=_('Size'), formatting=True)) self.add( setting.Color('color', 'black', descr=_('Default font color'), usertext=_('Color'), formatting=True)) StyleSheet.register(StylesheetText) veusz-1.21.1/veusz/setting/reference.py0000644000175000017500000001266012327177747016317 0ustar jssjss# Copyright (C) 2009 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division class ReferenceBase(object): """Reference objects are inherited from this base class. They should have a "value" property. """ class ResolveException(ValueError): pass def __init__(self, value): self.value = value def getPaths(self): """Return list of paths linked by reference.""" def resolve(self, setn): """Return setting this is linked to.""" def setOnModified(self, setn, fn): """Set on modified on settings pointed to by this reference.""" class Reference(ReferenceBase): """A value a setting can have to point to another setting. Formats of a reference are like /foo/bar/setting or ../Line/width alternatively style sheets can be used with the format, e.g. /StyleSheet/linewidth """ def __init__(self, value): """Initialise reference with value, which is a string as above.""" ReferenceBase.__init__(self, value) self.split = value.split('/') self.resolved = None def getPaths(self): """Path linked by setting.""" return [self.value] def resolve(self, thissetting): """Return the setting object associated with the reference.""" # this is for stylesheet references which don't move if self.resolved: return self.resolved item = thissetting.parent parts = list(self.split) if parts[0] == '': # need root widget if begins with slash while item.parent is not None: item = item.parent parts = parts[1:] # do an iterative lookup of the setting for p in parts: if p == '..': if item.parent is not None: item = item.parent elif p == '': pass else: if item.isWidget(): child = item.getChild(p) if not child: try: item = item.settings.get(p) except KeyError: raise self.ResolveException() else: item = child else: try: item = item.get(p) except KeyError: raise self.ResolveException() # shortcut to resolve stylesheets # hopefully this won't ever change if len(self.split) > 2 and self.split[1] == 'StyleSheet': self.resolved = item return item def setOnModified(self, setn, fn): """Set on modified on settings pointed to by this reference.""" resolved = self.resolve(setn) resolved.setOnModified(fn) class ReferenceMultiple(ReferenceBase): """A reference to more than one item. This allows references to override other references. If one is not the default value, this overrides the others. References to the right of the list override those on the left. """ def __init__(self, paths): """Initialise with a list of paths.""" ReferenceBase.__init__(self, paths) self.refs = [Reference(p) for p in paths] def getPaths(self): """List of paths linked by setting.""" return self.value def resolve(self, thissetting): """Resolve to setting. We prefer destination settings which are: - To the right of the list of paths - Which are closer in terms of the number of reference jumps Hopefully this algorithm isn't too slow... """ retn = None minjumps = 99999 for ref in self.refs: try: setn = ref.resolve(thissetting) jumps = 1 while isinstance(setn._val, ReferenceBase): setn = setn._val.resolve(setn) jumps += 1 if retn is None: retn = setn minjumps = jumps else: if jumps <= minjumps and not setn.isDefault(): retn = setn minjumps = jumps except self.ResolveException: pass if retn is None: raise self.ResolveException("Not linked to any settings") return retn def setOnModified(self, setn, fn): """Set on modified on settings pointed to by this reference.""" for ref in self.refs: resolved = ref.resolve(setn) resolved.setOnModified(fn) veusz-1.21.1/veusz/setting/settingdb.py0000664000175000017500000001632412376130006016324 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """A database for default values of settings.""" from __future__ import division, print_function import sys import numpy as N from .. import qtall as qt4 def _(text, disambiguation=None, context="Preferences"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) # default values to some settings in case the user does not have these defaultValues = { # export options 'export_DPI': 100, 'export_DPI_PDF': 150, 'export_color': True, 'export_antialias': True, 'export_quality': 85, 'export_background': '#ffffff00', 'export_SVG_text_as_text': False, # plot options 'plot_updatepolicy': -1, # update on document changed 'plot_antialias': True, 'plot_numthreads': 2, # recent files list 'main_recentfiles': [], # default stylesheet 'stylesheet_default': '', # default custom definitons 'custom_default': '', # colors (isdefault, 'notdefaultcolor') 'color_page': (True, 'white'), 'color_error': (True, 'red'), 'color_command': (True, 'blue'), 'color_cntrlline': (True, 'blue'), 'color_cntrlcorner': (True, 'black'), # further ui options 'toolbar_size': 24, # if set to true, do UI formatting in US/English 'ui_english': False, # use cwd as starting directory 'dirname_usecwd': False, # ask tutorial before? 'ask_tutorial': False, # log picked points to clipboard or to console 'picker_to_clipboard': False, 'picker_to_console': True } class _SettingDB(object): """A class which provides access to a persistant settings database. Items are accesses as a dict, with items as key=value """ # list of colors colors = ('page', 'error', 'command', 'cntrlline', 'cntrlcorner') # default colors if isdefault is set in the setting color_defaults = { 'page': 'LightBase', 'error': 'red', 'command': 'blue', 'cntrlline': 'blue', 'cntrlcorner': 'black', } def __init__(self): """Initialise the object, reading the settings.""" # This domain name is fictional! self.domain = 'veusz.org' self.product = 'veusz' self.database = {} self.sepchars = "%%%" # read settings using QSettings self.readSettings() def color(self, name): """Get a color setting as a QColor.""" val = self.database['color_' + name] if val[0]: default = self.color_defaults[name] if default == 'LightBase': base = qt4.qApp.palette().color(qt4.QPalette.Base) if base.value() < 127: base = qt4.QColor(qt4.Qt.white) return base return qt4.QColor(default) else: return qt4.QColor(val[1]) def readSettings(self): """Read the settings using QSettings. Entries have / replaced with set of characters self.sepchars This is because it greatly simplifies the logic as QSettings has special meaning for / The only issues are that the key may be larger than 255 characters We should probably check for this """ s = qt4.QSettings(self.domain, self.product) for key in s.childKeys(): val = s.value(key) realkey = key.replace(self.sepchars, '/') try: self.database[realkey] = eval(val) except: print('Error interpreting item "%s" in ' 'settings file' % realkey, file=sys.stderr) # set any defaults which haven't been set for key in defaultValues: if key not in self.database: self.database[key] = defaultValues[key] def writeSettings(self): """Write the settings using QSettings. This is called by the mainwindow on close """ s = qt4.QSettings(self.domain, self.product) # write each entry, keeping track of which ones haven't been written cleankeys = [] for key in self.database: cleankey = key.replace('/', self.sepchars) cleankeys.append(cleankey) s.setValue(cleankey, repr(self.database[key])) # now remove all the values which have been removed for key in list(s.childKeys()): if key not in cleankeys: s.remove(key) def get(self, key, defaultval=None): """Return key if it is in database, else defaultval.""" return self.database.get(key, defaultval) def __getitem__(self, key): """Get the item from the database.""" return self.database[key] def __setitem__(self, key, value): """Set the value in the database.""" self.database[key] = value def __delitem__(self, key): """Remove the key from the database.""" del self.database[key] def __contains__(self, key): """Is the key in the database.""" return key in self.database # create the SettingDB singleton settingdb = _SettingDB() # a normal dict for non-persistent settings # (e.g. disable safe mode) transient_settings = {} def updateUILocale(): """Update locale to one given in preferences.""" global uilocale if settingdb['ui_english']: uilocale = qt4.QLocale.c() else: uilocale = qt4.QLocale.system() uilocale.setNumberOptions(qt4.QLocale.OmitGroupSeparator) qt4.QLocale.setDefault(uilocale) def ui_floattostring(f): """Convert float to string with more precision.""" if not N.isfinite(f): if N.isnan(f): return 'nan' if f < 0: return '-inf' return 'inf' elif 1e-4 <= abs(f) <= 1e5 or f == 0: s = '%.14g' % f # strip excess zeros to right if s.find('.') >= 0: s = s.rstrip('0').rstrip('.') else: s = '%.14e' % f # split into mantissa/exponent and strip extra zeros, etc mant, expon = s.split('e') mant = mant.rstrip('0').rstrip('.') expon = int(expon) s = '%se%i' % (mant, expon) # make decimal point correct for local s = s.replace('.', uilocale.decimalPoint()) return s def ui_stringtofloat(s): """Convert string to float, allowing for decimal point in different locale.""" s = s.replace(uilocale.decimalPoint(), '.') return float(s) updateUILocale() veusz-1.21.1/veusz/windows/0000775000175000017500000000000012376130063014001 5ustar jssjssveusz-1.21.1/veusz/windows/plotwindow.py0000644000175000017500000013402712327177747016606 0ustar jssjss# plotwindow.py # the main window for showing plots # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import sys import traceback from ..compat import crange from .. import qtall as qt4 import numpy as N from .. import setting from ..dialogs import exceptiondialog from .. import document from .. import utils from .. import widgets def _(text, disambiguation=None, context='PlotWindow'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class PickerCrosshairItem( qt4.QGraphicsPathItem ): """The picker cross widget: it moves from point to point and curve to curve with the arrow keys, and hides itself when it looses focus""" def __init__(self, parent=None): path = qt4.QPainterPath() path.addRect(-4, -4, 8, 8) path.addRect(-5, -5, 10, 10) path.moveTo(-8, 0) path.lineTo(8, 0) path.moveTo(0, -8) path.lineTo(0, 8) qt4.QGraphicsPathItem.__init__(self, path, parent) self.setBrush(qt4.QBrush(qt4.Qt.black)) self.setFlags(self.flags() | qt4.QGraphicsItem.ItemIsFocusable) def paint(self, painter, option, widget): """Override this to enforce the global antialiasing setting""" aa = setting.settingdb['plot_antialias'] painter.save() painter.setRenderHint(qt4.QPainter.Antialiasing, aa) qt4.QGraphicsPathItem.paint(self, painter, option, widget) painter.restore() def focusOutEvent(self, event): qt4.QGraphicsPathItem.focusOutEvent(self, event) self.hide() class RenderControl(qt4.QObject): """Object for rendering plots in a separate thread.""" signalRenderFinished = qt4.pyqtSignal( int, qt4.QImage, document.PaintHelper) def __init__(self, plotwindow): """Start up numthreads rendering threads.""" qt4.QObject.__init__(self) self.sem = qt4.QSemaphore() self.mutex = qt4.QMutex() self.threads = [] self.exit = False self.latestjobs = [] self.latestaddedjob = -1 self.latestdrawnjob = -1 self.plotwindow = plotwindow self.updateNumberThreads() def updateNumberThreads(self, num=None): """Changes the number of rendering threads.""" if num is None: if qt4.QFontDatabase.supportsThreadedFontRendering(): # use number of threads in preference num = setting.settingdb['plot_numthreads'] else: # disable threads num = 0 if self.threads: # delete old ones self.exit = True self.sem.release(len(self.threads)) for t in self.threads: t.wait() del self.threads[:] self.exit = False # start new ones for i in crange(num): t = RenderThread(self) t.start() self.threads.append(t) def exitThreads(self): """Exit threads started.""" self.updateNumberThreads(num=0) def processNextJob(self): """Take a job from the queue and process it. emits renderfinished(jobid, img, painthelper) when done, if job has not been superseded """ self.mutex.lock() jobid, helper = self.latestjobs[-1] del self.latestjobs[-1] lastadded = self.latestaddedjob self.mutex.unlock() # don't process jobs which have been superseded if lastadded == jobid: img = qt4.QImage(helper.pagesize[0], helper.pagesize[1], qt4.QImage.Format_ARGB32_Premultiplied) img.fill( setting.settingdb.color('page').rgb() ) painter = qt4.QPainter(img) aa = self.plotwindow.antialias painter.setRenderHint(qt4.QPainter.Antialiasing, aa) painter.setRenderHint(qt4.QPainter.TextAntialiasing, aa) helper.renderToPainter(painter) painter.end() self.mutex.lock() # just throw away result if it older than the latest one if jobid > self.latestdrawnjob: self.signalRenderFinished.emit(jobid, img, helper) self.latestdrawnjob = jobid self.mutex.unlock() # tell any listeners that a job has been processed self.plotwindow.sigQueueChange.emit(-1) def addJob(self, helper): """Process drawing job in PaintHelper given.""" # indicate that there is a new item to be processed to listeners self.plotwindow.sigQueueChange.emit(1) # add the job to the queue self.mutex.lock() self.latestaddedjob += 1 self.latestjobs.append( (self.latestaddedjob, helper) ) self.mutex.unlock() if self.threads: # tell a thread to process job self.sem.release(1) else: # process job in current thread if multithreading disabled self.processNextJob() class RenderThread( qt4.QThread ): """A thread for processing rendering jobs. This is controlled by a RenderControl object """ def __init__(self, rendercontrol): qt4.QThread.__init__(self) self.rc = rendercontrol def run(self): """Repeat forever until told to exit. If it aquires 1 resource from the semaphore it will process the next job. """ while True: # wait until we can aquire the resources self.rc.sem.acquire(1) if self.rc.exit: break try: self.rc.processNextJob() except Exception: sys.stderr.write(_("Error in rendering thread\n")) traceback.print_exc(file=sys.stderr) class PlotWindow( qt4.QGraphicsView ): """Class to show the plot(s) in a scrollable window.""" # emitted when new item on plot queue sigQueueChange = qt4.pyqtSignal(int) # on drawing a page sigUpdatePage = qt4.pyqtSignal(int) # point picked on plot sigPointPicked = qt4.pyqtSignal(object) # picker enabled sigPickerEnabled = qt4.pyqtSignal(bool) # axis values update from moving mouse sigAxisValuesFromMouse = qt4.pyqtSignal(dict) # gives widget clicked sigWidgetClicked = qt4.pyqtSignal(object) # how often the document can update updateintervals = ( (0, _('Disable')), (-1, _('On document change')), (100, _('Every 0.1s')), (250, _('Every 0.25s')), (500, _('Every 0.5s')), (1000, _('Every 1s')), (2000, _('Every 2s')), (5000, _('Every 5s')), (10000, _('Every 10s')), ) def __init__(self, document, parent, menu=None): """Initialise the window. menu gives a menu to add any menu items to """ qt4.QGraphicsView.__init__(self, parent) self.setBackgroundRole(qt4.QPalette.Dark) self.scene = qt4.QGraphicsScene() self.setScene(self.scene) # this graphics scene item is the actual graph pixmap = qt4.QPixmap(1, 1) self.dpi = (pixmap.logicalDpiX(), pixmap.logicalDpiY()) self.pixmapitem = self.scene.addPixmap(pixmap) # whether full screen mode self.isfullscreen = False # set to be parent's actions self.vzactions = None # for controlling plot elements g = self.controlgraphgroup = qt4.QGraphicsItemGroup() g.setHandlesChildEvents(False) self.scene.addItem(g) # zoom rectangle for zooming into graph (not shown normally) self.zoomrect = self.scene.addRect( 0, 0, 100, 100, qt4.QPen(qt4.Qt.DotLine) ) self.zoomrect.setZValue(2.) self.zoomrect.hide() # picker graphicsitem for marking the picked point self.pickeritem = PickerCrosshairItem() self.scene.addItem(self.pickeritem) self.pickeritem.setZValue(2.) self.pickeritem.hide() # all the widgets that picker key-navigation might cycle through self.pickerwidgets = [] # the picker state self.pickerinfo = widgets.PickInfo() # set up so if document is modified we are notified self.document = document self.docchangeset = -100 self.oldpagenumber = -1 self.document.signalModified.connect(self.slotDocModified) # state of last plot from painthelper self.painthelper = None self.lastwidgetsselected = [] self.oldzoom = -1. self.zoomfactor = 1. self.pagenumber = 0 self.ignoreclick = False # for rendering plots in separate threads self.rendercontrol = RenderControl(self) self.rendercontrol.signalRenderFinished.connect( self.slotRenderFinished) # mode for clicking self.clickmode = 'select' self.currentclickmode = None # wheel zooming/scrolling accumulator self.sumwheeldelta = 0 # set up redrawing timer self.timer = qt4.QTimer(self) self.timer.timeout.connect(self.checkPlotUpdate) # for drag scrolling self.grabpos = None self.scrolltimer = qt4.QTimer(self) self.scrolltimer.setSingleShot(True) # for turning clicking into scrolling after a period self.scrolltimer.timeout.connect(self.slotBecomeScrollClick) # get plot view updating policy # -1: update on document changes # 0: never update automatically # >0: check for updates every x ms self.interval = setting.settingdb['plot_updatepolicy'] # if using a time-based document update checking, start timer if self.interval > 0: self.timer.start(self.interval) # load antialias settings self.antialias = setting.settingdb['plot_antialias'] # allow window to get focus, to allow context menu self.setFocusPolicy(qt4.Qt.StrongFocus) # get mouse move events if mouse is not pressed self.setMouseTracking(True) # create toolbar in main window (urgh) self.createToolbar(parent, menu) def hideEvent(self, event): """Window closing, so exit rendering threads.""" self.rendercontrol.exitThreads() qt4.QGraphicsView.hideEvent(self, event) def sizeHint(self): """Return size hint for window.""" p = self.pixmapitem.pixmap() if p.width() <= 1 and p.height() <= 1: # if the document has been uninitialized, get the doc size return qt4.QSize(*self.document.docSize()) return p.size() def showToolbar(self, show=True): """Show or hide toolbar""" self.viewtoolbar.setVisible(show) def createToolbar(self, parent, menu=None): """Make a view toolbar, and optionally update menu.""" self.viewtoolbar = qt4.QToolBar(_("View toolbar - Veusz"), parent) self.viewtoolbar.setObjectName('veuszviewtoolbar') iconsize = setting.settingdb['toolbar_size'] self.viewtoolbar.setIconSize(qt4.QSize(iconsize, iconsize)) self.viewtoolbar.hide() if parent: parent.addToolBar(qt4.Qt.TopToolBarArea, self.viewtoolbar) if parent and hasattr(parent, 'vzactions'): # share actions with parent if possible # as plot windows can be isolated from mainwindows, we need this self.vzactions = actions = parent.vzactions else: self.vzactions = actions = {} a = utils.makeAction actions.update({ 'view.zoomin': a(self, _('Zoom into the plot'), _('Zoom &In'), self.slotViewZoomIn, icon='kde-zoom-in', key='Ctrl++'), 'view.zoomout': a(self, _('Zoom out of the plot'), _('Zoom &Out'), self.slotViewZoomOut, icon='kde-zoom-out', key='Ctrl+-'), 'view.zoom11': a(self, _('Restore plot to natural size'), _('Zoom 1:1'), self.slotViewZoom11, icon='kde-zoom-1-veuszedit', key='Ctrl+1'), 'view.zoomwidth': a(self, _('Zoom plot to show whole width'), _('Zoom to width'), self.slotViewZoomWidth, icon='kde-zoom-width-veuszedit'), 'view.zoomheight': a(self, _('Zoom plot to show whole height'), _('Zoom to height'), self.slotViewZoomHeight, icon='kde-zoom-height-veuszedit'), 'view.zoompage': a(self, _('Zoom plot to show whole page'), _('Zoom to page'), self.slotViewZoomPage, icon='kde-zoom-page-veuszedit'), 'view.zoommenu': a(self, _('Zoom functions menu'), _('Zoom'), self.doZoomMenuButton, icon='kde-zoom-veuszedit'), 'view.prevpage': a(self, _('Move to the previous page'), _('&Previous page'), self.slotViewPreviousPage, icon='kde-go-previous', key='Ctrl+PgUp'), 'view.nextpage': a(self, _('Move to the next page'), _('&Next page'), self.slotViewNextPage, icon='kde-go-next', key='Ctrl+PgDown'), 'view.select': a(self, _('Select items from the graph or scroll'), _('Select items or scroll'), None, icon='kde-mouse-pointer'), 'view.pick': a(self, _('Read data points on the graph'), _('Read data points'), None, icon='veusz-pick-data'), 'view.zoomgraph': a(self, _('Zoom into graph'), _('Zoom graph'), None, icon='veusz-zoom-graph'), 'view.fullscreen': a(self, _('View plot full screen'), _('Full screen'), self.slotFullScreen, icon='veusz-view-fullscreen', key='Ctrl+F11'), }) if menu: # only construct menu if required menuitems = [ ('view', '', [ 'view.zoomin', 'view.zoomout', 'view.zoom11', 'view.zoomwidth', 'view.zoomheight', 'view.zoompage', '', 'view.prevpage', 'view.nextpage', 'view.fullscreen', '', 'view.select', 'view.pick', 'view.zoomgraph', ]), ] utils.constructMenus(menu, {'view': menu}, menuitems, actions) # populate menu on zoom menu toolbar icon zoommenu = qt4.QMenu(self) zoomag = qt4.QActionGroup(self) for act in ('view.zoomin', 'view.zoomout', 'view.zoom11', 'view.zoomwidth', 'view.zoomheight', 'view.zoompage'): a = actions[act] zoommenu.addAction(a) zoomag.addAction(a) a.vzname = act actions['view.zoommenu'].setMenu(zoommenu) zoomag.triggered.connect(self.zoomActionTriggered) lastzoom = setting.settingdb.get('view_defaultzoom', 'view.zoompage') self.updateZoomMenuButton(actions[lastzoom]) # add items to toolbar utils.addToolbarActions(self.viewtoolbar, actions, ('view.prevpage', 'view.nextpage', 'view.fullscreen', 'view.select', 'view.pick', 'view.zoomgraph', 'view.zoommenu')) # define action group for various different selection models grp = self.selectactiongrp = qt4.QActionGroup(self) grp.setExclusive(True) for a in ('view.select', 'view.pick', 'view.zoomgraph'): actions[a].setActionGroup(grp) actions[a].setCheckable(True) actions['view.select'].setChecked(True) grp.triggered.connect(self.slotSelectMode) return self.viewtoolbar def zoomActionTriggered(self, action): """Keep track of the last zoom action selected.""" setting.settingdb['view_defaultzoom'] = action.vzname self.updateZoomMenuButton(action) def updateZoomMenuButton(self, action): """Make zoom button call default zoom action and change icon.""" menuact = self.vzactions['view.zoommenu'] setting.settingdb['view_defaultzoom'] = action.vzname menuact.setIcon( action.icon() ) def doZoomMenuButton(self): """Select previous zoom option when clicking on zoom menu.""" act = self.vzactions[setting.settingdb['view_defaultzoom']] act.trigger() def doZoomRect(self, endpos): """Take the zoom rectangle drawn by the user and do the zooming. endpos is a QPoint end point This is pretty messy - first we have to work out the graph associated to the first point Then we have to iterate over each of the plotters, identify their axes, and change the range of the axes to match the screen region selected. """ # safety net if self.grabpos is None or endpos is None: return # get points corresponding to corners of rectangle pt1 = self.grabpos pt2 = endpos # work out whether it's worthwhile to zoom: only zoom if there # are >=5 pixels movement if abs((pt2-pt1).x()) < 10 or abs((pt2-pt1).y()) < 10: return # try to work out in which widget the first point is in widget = self.painthelper.pointInWidgetBounds( pt1.x(), pt1.y(), widgets.Graph) if widget is None: return # convert points on plotter to points on axis for each axis # we also add a neighbouring pixel for the rounding calculation xpts = N.array( [pt1.x(), pt2.x(), pt1.x()+1, pt2.x()-1] ) ypts = N.array( [pt1.y(), pt2.y(), pt2.y()+1, pt2.y()-1] ) # build up operation list to do zoom operations = [] axes = {} # iterate over children, to look for plotters for c in [i for i in widget.children if isinstance(i, widgets.GenericPlotter)]: # get axes associated with plotter caxes = c.parent.getAxes( (c.settings.xAxis, c.settings.yAxis) ) for a in caxes: if a: axes[a] = True # iterate over each axis, and update the ranges for axis in axes: s = axis.settings if s.direction == 'horizontal': p = xpts else: p = ypts # convert points on plotter to axis coordinates # FIXME: Need To Trap Conversion Errors! try: r = axis.plotterToGraphCoords( self.painthelper.widgetBounds(axis), p) except KeyError: continue # invert if min and max are inverted if r[1] < r[0]: r[1], r[0] = r[0], r[1] r[3], r[2] = r[2], r[3] # build up operations to change axis if s.min != r[0]: operations.append( document.OperationSettingSet( s.get('min'), utils.round2delt(r[0], r[2])) ) if s.max != r[1]: operations.append( document.OperationSettingSet( s.get('max'), utils.round2delt(r[1], r[3])) ) # finally change the axes self.document.applyOperation( document.OperationMultiple(operations,descr=_('zoom axes')) ) def axesForPoint(self, mousepos): """Find all the axes which contain the given mouse position""" if self.painthelper is None: return [] pos = self.mapToScene(mousepos) px, py = pos.x(), pos.y() axes = [] for widget, bounds in self.painthelper.widgetBoundsIterator( widgettype=widgets.Axis): # if widget is axis, and point lies within bounds if ( px>=bounds[0] and px<=bounds[2] and py>=bounds[1] and py<=bounds[3] ): # convert correct pointer position if widget.settings.direction == 'horizontal': val = px else: val = py coords=widget.plotterToGraphCoords(bounds, N.array([val])) axes.append( (widget, coords[0]) ) return axes def emitPicked(self, pickinfo): """Report that a new point has been picked""" self.pickerinfo = pickinfo self.pickeritem.setPos(pickinfo.screenpos[0], pickinfo.screenpos[1]) self.sigPointPicked.emit(pickinfo) def doPick(self, mousepos): """Find the point on any plot-like widget closest to the cursor""" self.pickerwidgets = [] pickinfo = widgets.PickInfo() pos = self.mapToScene(mousepos) for w, bounds in self.painthelper.widgetBoundsIterator(): try: # ask the widget for its (visually) closest point to the cursor info = w.pickPoint(pos.x(), pos.y(), bounds) # this is a pickable widget, so remember it for future key navigation self.pickerwidgets.append(w) if info.distance < pickinfo.distance: # and remember the overall closest pickinfo = info except AttributeError: # ignore widgets that don't support axes or picking continue if not pickinfo: self.pickeritem.hide() return self.emitPicked(pickinfo) def slotBecomeScrollClick(self): """If the click is still down when this timer is reached then we turn the click into a scrolling click.""" if self.currentclickmode == 'select': qt4.QApplication.setOverrideCursor(qt4.QCursor(qt4.Qt.SizeAllCursor)) self.currentclickmode = 'scroll' def mousePressEvent(self, event): """Allow user to drag window around.""" qt4.QGraphicsView.mousePressEvent(self, event) # work out whether user is clicking on a control point # we have to ignore the item group which seems to be above # its constituents items = self.items(event.pos()) if len(items) > 0 and isinstance(items[0], qt4.QGraphicsItemGroup): del items[0] self.ignoreclick = ( len(items)==0 or items[0] is not self.pixmapitem or self.painthelper is None ) if event.button() == qt4.Qt.LeftButton and not self.ignoreclick: # need to copy position, otherwise it gets reused! self.winpos = qt4.QPoint(event.pos()) self.grabpos = self.mapToScene(self.winpos) if self.clickmode == 'select': # we set this to true unless the timer runs out (400ms), # then it becomes a scroll click # scroll clicks drag the window around, and selecting clicks # select widgets! self.scrolltimer.start(400) elif self.clickmode == 'pick': self.pickeritem.show() self.pickeritem.setFocus(qt4.Qt.MouseFocusReason) self.doPick(event.pos()) elif self.clickmode == 'scroll': qt4.QApplication.setOverrideCursor( qt4.QCursor(qt4.Qt.SizeAllCursor)) elif self.clickmode == 'graphzoom': self.zoomrect.setRect(self.grabpos.x(), self.grabpos.y(), 0, 0) self.zoomrect.show() #self.label.drawRect(self.grabpos, self.grabpos) # record what mode we were clicked in self.currentclickmode = self.clickmode def mouseMoveEvent(self, event): """Scroll window by how much the mouse has moved since last time.""" qt4.QGraphicsView.mouseMoveEvent(self, event) if self.currentclickmode == 'scroll': event.accept() # move scroll bars by amount pos = event.pos() dx = self.winpos.x()-pos.x() scrollx = self.horizontalScrollBar() scrollx.setValue( scrollx.value() + dx ) dy = self.winpos.y()-pos.y() scrolly = self.verticalScrollBar() scrolly.setValue( scrolly.value() + dy ) # need to copy point self.winpos = qt4.QPoint(event.pos()) elif self.currentclickmode == 'graphzoom' and self.grabpos is not None: pos = self.mapToScene(event.pos()) r = self.zoomrect.rect() self.zoomrect.setRect( r.x(), r.y(), pos.x()-r.x(), pos.y()-r.y() ) elif self.clickmode == 'select' or self.clickmode == 'pick': # find axes which map to this position axes = self.axesForPoint(event.pos()) vals = dict([ (a[0].name, a[1]) for a in axes ]) self.sigAxisValuesFromMouse.emit(vals) if self.currentclickmode == 'pick': # drag the picker around self.doPick(event.pos()) def mouseReleaseEvent(self, event): """If the mouse button is released, check whether the mouse clicked on a widget, and emit a sigWidgetClicked(widget).""" qt4.QGraphicsView.mouseReleaseEvent(self, event) if event.button() == qt4.Qt.LeftButton and not self.ignoreclick: event.accept() self.scrolltimer.stop() if self.currentclickmode == 'select': # work out where the mouse clicked and choose widget pos = self.mapToScene(event.pos()) self.locateClickWidget(pos.x(), pos.y()) elif self.currentclickmode == 'scroll': # return the cursor to normal after scrolling self.clickmode = 'select' self.currentclickmode = None qt4.QApplication.restoreOverrideCursor() elif self.currentclickmode == 'graphzoom': self.zoomrect.hide() self.doZoomRect(self.mapToScene(event.pos())) self.grabpos = None elif self.currentclickmode == 'viewgetclick': self.clickmode = 'select' elif self.currentclickmode == 'pick': self.currentclickmode = None def keyPressEvent(self, event): """Keypad motion moves the picker if it has focus""" if self.pickeritem.hasFocus(): k = event.key() if k == qt4.Qt.Key_Left or k == qt4.Qt.Key_Right: # navigate to the previous or next point on the curve event.accept() dir = 'right' if k == qt4.Qt.Key_Right else 'left' ix = self.pickerinfo.index pickinfo = self.pickerinfo.widget.pickIndex( ix, dir, self.painthelper.widgetBounds( self.pickerinfo.widget)) if pickinfo: # more points visible in this direction self.emitPicked(pickinfo) return elif k == qt4.Qt.Key_Up or k == qt4.Qt.Key_Down: # navigate to the next plot up or down on the screen event.accept() p = self.pickeritem.pos() oldw = self.pickerinfo.widget pickinfo = widgets.PickInfo() dist = float('inf') for w in self.pickerwidgets: if w == oldw: continue # ask the widgets to pick their point which is closest horizontally # to the last (screen) x value picked pi = w.pickPoint(self.pickerinfo.screenpos[0], p.y(), self.painthelper.widgetBounds(w), distance='horizontal') if not pi: continue dy = p.y() - pi.screenpos[1] # take the new point which is closest vertically to the current # one and either above or below it as appropriate if abs(dy) < dist and ( (k == qt4.Qt.Key_Up and dy > 0) or (k == qt4.Qt.Key_Down and dy < 0) ): pickinfo = pi dist = abs(dy) if pickinfo: oldx = self.pickerinfo.screenpos[0] self.emitPicked(pickinfo) # restore the previous x-position, so that vertical navigation # stays repeatable pickinfo.screenpos = (oldx, pickinfo.screenpos[1]) return # handle up-stream qt4.QGraphicsView.keyPressEvent(self, event) def wheelEvent(self, event): """For zooming in or moving.""" if event.modifiers() & qt4.Qt.ControlModifier: self.sumwheeldelta += event.delta() while self.sumwheeldelta <= -120: self.slotViewZoomOut() self.sumwheeldelta += 120 while self.sumwheeldelta >= 120: self.slotViewZoomIn() self.sumwheeldelta -= 120 elif event.modifiers() & qt4.Qt.ShiftModifier: self.sumwheeldelta += event.delta() while self.sumwheeldelta <= -120: # scroll left self.sumwheeldelta += 120 scrollx = self.horizontalScrollBar() scrollx.setValue(scrollx.value() + 120) while self.sumwheeldelta >= 120: # scroll right scrollx = self.horizontalScrollBar() scrollx.setValue(scrollx.value() - 120) self.sumwheeldelta -= 120 else: qt4.QGraphicsView.wheelEvent(self, event) def locateClickWidget(self, x, y): """Work out which widget was clicked, and if necessary send a sigWidgetClicked(widget) signal.""" if self.document.getNumberPages() == 0: return widget = self.painthelper.identifyWidgetAtPoint( x, y, antialias=self.antialias) if widget is None: # select page if nothing clicked widget = self.document.basewidget.getPage(self.pagenumber) # tell connected objects that widget was clicked if widget is not None: self.sigWidgetClicked.emit(widget) def setPageNumber(self, pageno): """Move the the selected page.""" # we don't need to do anything if (self.pagenumber == pageno and self.document.changeset == self.docchangeset): return # keep within bounds pageno = min(pageno, self.document.getNumberPages()-1) pageno = max(0, pageno) self.pagenumber = pageno if self.pagenumber != self.oldpagenumber and self.interval != 0: self.checkPlotUpdate() def getPageNumber(self): """Get the the selected page.""" return self.pagenumber @qt4.pyqtSlot(int) def slotDocModified(self, ismodified): """Update plot on document being modified.""" # only update if doc is modified and the update policy is set # to update on document updates if ismodified and self.interval == -1: self.checkPlotUpdate() def checkPlotUpdate(self): """Check whether plot needs updating.""" # print >>sys.stderr, "checking update" # no threads, so can't get interrupted here # draw data into background pixmap if modified if ( self.zoomfactor != self.oldzoom or self.document.changeset != self.docchangeset or self.pagenumber != self.oldpagenumber ): # print >>sys.stderr, "updating" self.pickeritem.hide() self.pagenumber = min( self.document.getNumberPages() - 1, self.pagenumber ) self.oldpagenumber = self.pagenumber if self.pagenumber >= 0: size = self.document.pageSize( self.pagenumber, scaling=self.zoomfactor) # draw the data into the buffer # errors cause an exception window to pop up try: phelper = document.PaintHelper( size, scaling=self.zoomfactor, dpi=self.dpi) self.document.paintTo(phelper, self.pagenumber) except Exception: # stop updates this time round and show exception dialog d = exceptiondialog.ExceptionDialog(sys.exc_info(), self) self.oldzoom = self.zoomfactor self.docchangeset = self.document.changeset d.exec_() self.painthelper = phelper self.rendercontrol.addJob(phelper) else: self.painthelper = None self.pagenumber = 0 size = self.document.docSize() pixmap = qt4.QPixmap(*size) pixmap.fill( setting.settingdb.color('page') ) self.setSceneRect(0, 0, *size) self.pixmapitem.setPixmap(pixmap) self.sigUpdatePage.emit(self.pagenumber) self.updatePageToolbar() self.updateControlGraphs(self.lastwidgetsselected) self.oldzoom = self.zoomfactor self.docchangeset = self.document.changeset def slotRenderFinished(self, jobid, img, helper): """Update image on display if rendering (usually in other thread) finished.""" bufferpixmap = qt4.QPixmap.fromImage(img) self.setSceneRect(0, 0, bufferpixmap.width(), bufferpixmap.height()) self.pixmapitem.setPixmap(bufferpixmap) def updatePlotSettings(self): """Update plot window settings from settings.""" self.setTimeout(setting.settingdb['plot_updatepolicy']) self.antialias = setting.settingdb['plot_antialias'] self.rendercontrol.updateNumberThreads() self.actionForceUpdate() def contextMenuEvent(self, event): """Show context menu.""" menu = qt4.QMenu(self) # add some useful entries menu.addAction( self.vzactions['view.zoommenu'] ) menu.addSeparator() menu.addAction( self.vzactions['view.prevpage'] ) menu.addAction( self.vzactions['view.nextpage'] ) menu.addSeparator() # force an update now menu item menu.addAction(_('Force update'), self.actionForceUpdate) if self.isfullscreen: menu.addAction(_('Close full screen'), self.slotFullScreen) else: menu.addAction( self.vzactions['view.fullscreen'] ) # Update policy submenu submenu = menu.addMenu(_('Updates')) intgrp = qt4.QActionGroup(self) # bind interval options to actions for intv, text in self.updateintervals: act = intgrp.addAction(text) act.setCheckable(True) def setfn(interval): return lambda checked: self.actionSetTimeout(interval, checked) act.triggered.connect(setfn(intv)) if intv == self.interval: act.setChecked(True) submenu.addAction(act) # antialias menu.addSeparator() act = menu.addAction(_('Antialias'), self.actionAntialias) act.setCheckable(True) act.setChecked(self.antialias) menu.exec_(qt4.QCursor.pos()) def actionForceUpdate(self): """Force an update for the graph.""" self.docchangeset = -100 self.checkPlotUpdate() def slotFullScreen(self): """Show window full screen or not.""" if not self.isfullscreen: self._fullscreenwindow = FullScreenPlotWindow( self.document, self.pagenumber) else: # cheesy way of closing full screen window p = self while p.parent() is not None: p = p.parent() p.close() def setTimeout(self, interval): """Change timer setting without changing save value.""" self.interval = interval if interval <= 0: # stop updates if self.timer.isActive(): self.timer.stop() else: # change interval to one selected self.timer.setInterval(interval) # start timer if it was stopped if not self.timer.isActive(): self.timer.start() def actionSetTimeout(self, interval, checked): """Called by setting the interval.""" self.setTimeout(interval) # remember changes for next time setting.settingdb['plot_updatepolicy'] = self.interval def actionAntialias(self): """Toggle antialias.""" self.antialias = not self.antialias setting.settingdb['plot_antialias'] = self.antialias self.actionForceUpdate() def setZoomFactor(self, zoomfactor): """Set the zoom factor of the window.""" self.zoomfactor = float(zoomfactor) self.checkPlotUpdate() def slotViewZoomIn(self): """Zoom into the plot.""" self.setZoomFactor(self.zoomfactor * N.sqrt(2.)) def slotViewZoomOut(self): """Zoom out of the plot.""" self.setZoomFactor(self.zoomfactor / N.sqrt(2.)) def slotViewZoomWidth(self): """Make the zoom factor so that the plot fills the whole width.""" # need to take account of scroll bars when deciding size viewportsize = self.maximumViewportSize() aspectwin = viewportsize.width() / viewportsize.height() r = self.pixmapitem.boundingRect() aspectplot = r.width() / r.height() width = viewportsize.width() if aspectwin > aspectplot: # take account of scroll bar width -= self.verticalScrollBar().width() mult = width / r.width() self.setZoomFactor(self.zoomfactor * mult) def slotViewZoomHeight(self): """Make the zoom factor so that the plot fills the whole width.""" # need to take account of scroll bars when deciding size viewportsize = self.maximumViewportSize() aspectwin = viewportsize.width() / viewportsize.height() r = self.pixmapitem.boundingRect() aspectplot = r.width() / r.height() height = viewportsize.height() if aspectwin < aspectplot: # take account of scroll bar height -= self.horizontalScrollBar().height() mult = height / r.height() self.setZoomFactor(self.zoomfactor * mult) def slotViewZoomPage(self): """Make the zoom factor correct to show the whole page.""" viewportsize = self.maximumViewportSize() r = self.pixmapitem.boundingRect() if r.width() != 0 and r.height() != 0: multw = viewportsize.width() / r.width() multh = viewportsize.height() / r.height() self.setZoomFactor(self.zoomfactor * min(multw, multh)) def slotViewZoom11(self): """Restore the zoom to 1:1""" self.setZoomFactor(1.) def slotViewPreviousPage(self): """View the previous page.""" self.setPageNumber( self.pagenumber - 1 ) def slotViewNextPage(self): """View the next page.""" self.setPageNumber( self.pagenumber + 1 ) def updatePageToolbar(self): """Update page number when the plot window says so.""" # disable previous and next page actions if self.vzactions is not None: np = self.document.getNumberPages() self.vzactions['view.prevpage'].setEnabled(self.pagenumber != 0) self.vzactions['view.nextpage'].setEnabled(self.pagenumber < np-1) def slotSelectMode(self, action): """Called when the selection mode has changed.""" modecnvt = { self.vzactions['view.select'] : 'select', self.vzactions['view.pick'] : 'pick', self.vzactions['view.zoomgraph'] : 'graphzoom' } # close the current picker self.pickeritem.hide() self.sigPickerEnabled.emit(False) # convert action into clicking mode self.clickmode = modecnvt[action] if self.clickmode == 'select': self.pixmapitem.unsetCursor() #self.label.setCursor(qt4.Qt.ArrowCursor) elif self.clickmode == 'graphzoom': self.pixmapitem.unsetCursor() #self.label.setCursor(qt4.Qt.CrossCursor) elif self.clickmode == 'pick': self.pixmapitem.setCursor(qt4.Qt.CrossCursor) self.sigPickerEnabled.emit(True) def getClick(self): """Return a click point from the graph.""" # wait for click from user qt4.QApplication.setOverrideCursor(qt4.QCursor(qt4.Qt.CrossCursor)) oldmode = self.clickmode self.clickmode = 'viewgetclick' while self.clickmode == 'viewgetclick': qt4.qApp.processEvents() self.clickmode = oldmode qt4.QApplication.restoreOverrideCursor() # take clicked point and convert to coords of scrollview pt = self.grabpos # try to work out in which widget the first point is in widget = self.painthelper.pointInWidgetBounds( pt.x(), pt.y(), widgets.Graph) if widget is None: return [] # convert points on plotter to points on axis for each axis xpts = N.array( [pt.x()] ) ypts = N.array( [pt.y()] ) axesretn = [] # iterate over children, to look for plotters for c in [i for i in widget.children if isinstance(i, widgets.GenericPlotter)]: # get axes associated with plotter axes = c.parent.getAxes( (c.settings.xAxis, c.settings.yAxis) ) # iterate over each, and update the ranges for axis in [a for a in axes if a is not None]: s = axis.settings if s.direction == 'horizontal': p = xpts else: p = ypts # convert point on plotter to axis coordinate # FIXME: Need To Trap Conversion Errors! r = axis.plotterToGraphCoords( self.painthelper.widgetBounds(axis), p) axesretn.append( (axis.path, r[0]) ) return axesretn def selectedWidgets(self, widgets): """Update control items on screen associated with widget. Called when widgets have been selected in the tree edit window """ self.updateControlGraphs(widgets) self.lastwidgetsselected = widgets def updateControlGraphs(self, widgets): """Add control graphs for the widgets given.""" cgg = self.controlgraphgroup # delete old items for c in cgg.childItems(): cgg.removeFromGroup(c) self.scene.removeItem(c) # add each item to the group if self.painthelper: for widget in widgets: cgis = self.painthelper.getControlGraph(widget) if cgis: for control in cgis: graphitem = control.createGraphicsItem() cgg.addToGroup(graphitem) class FullScreenPlotWindow(qt4.QScrollArea): """Window for showing plot in full-screen mode.""" def __init__(self, document, pagenumber): qt4.QScrollArea.__init__(self) self.setFrameShape(qt4.QFrame.NoFrame) self.setWidgetResizable(True) # window which shows plot self.document = document pw = self.plotwin = PlotWindow(document, None) pw.isfullscreen = True pw.pagenumber = pagenumber self.setWidget(pw) pw.setFocus() self.showFullScreen() self.toolbar = qt4.QToolBar(_("Full screen toolbar"), self) self.toolbar.addAction(utils.getIcon("kde-window-close"), _("Close"), self.close) for a in ('view.zoom11', 'view.zoomin', 'view.zoomout', 'view.zoomwidth', 'view.zoomheight', 'view.zoompage', 'view.prevpage', 'view.nextpage'): self.toolbar.addAction( pw.vzactions[a] ) self.toolbar.show() def resizeEvent(self, event): """Make zoom fit screen.""" qt4.QScrollArea.resizeEvent(self, event) # size graph to fill screen pagesize = self.document.pageSize(self.plotwin.pagenumber, dpi=self.plotwin.dpi) screensize = self.plotwin.size() aspectw = screensize.width() / pagesize[0] aspecth = screensize.height() / pagesize[1] self.plotwin.zoomfactor = min(aspectw, aspecth) self.plotwin.checkPlotUpdate() def keyPressEvent(self, event): k = event.key() if k == qt4.Qt.Key_Escape: event.accept() self.close() return qt4.QScrollArea.keyPressEvent(self, event) veusz-1.21.1/veusz/windows/widgettree.py0000644000175000017500000002675112327177747016547 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Contains a model and view for handling a tree of widgets.""" from __future__ import division from .. import qtall as qt4 from .. import utils from .. import document def _(text, disambiguation=None, context="WidgetTree"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class WidgetTreeModel(qt4.QAbstractItemModel): """A model representing the widget tree structure. """ def __init__(self, document, parent=None): """Initialise using document.""" qt4.QAbstractItemModel.__init__(self, parent) self.document = document document.signalModified.connect(self.slotDocumentModified) self.document.sigWiped.connect(self.slotDocumentModified) # suspend signals to the view that the model has changed self.suspendmodified = False def slotDocumentModified(self): """The document has been changed.""" if not self.suspendmodified: # needs to be suspended within insert/delete row operations self.layoutChanged.emit() def columnCount(self, parent): """Return number of columns of data.""" return 2 def data(self, index, role): """Return data for the index given.""" # why do we get passed invalid indicies? :-) if not index.isValid(): return None column = index.column() obj = index.internalPointer() if role in (qt4.Qt.DisplayRole, qt4.Qt.EditRole): # return text for columns if column == 0: return obj.name elif column == 1: return obj.typename elif role == qt4.Qt.DecorationRole: # return icon for first column if column == 0: filename = 'button_%s' % obj.typename return utils.getIcon(filename) elif role == qt4.Qt.ToolTipRole: # provide tool tip showing description if obj.userdescription: return obj.userdescription elif role == qt4.Qt.TextColorRole: # show disabled looking text if object or any parent is hidden hidden = False p = obj while p is not None: if 'hide' in p.settings and p.settings.hide: hidden = True break p = p.parent # return brush for hidden widget text, based on disabled text if hidden: return qt4.QPalette().brush(qt4.QPalette.Disabled, qt4.QPalette.Text) # return nothing return None def setData(self, index, name, role): """User renames object. This renames the widget.""" widget = index.internalPointer() # check symbols in name if not utils.validateWidgetName(name): return False # check name not already used if widget.parent.hasChild(name): return False # actually rename the widget self.document.applyOperation( document.OperationWidgetRename(widget, name)) self.dataChanged.emit(index, index) return True def flags(self, index): """What we can do with the item.""" if not index.isValid(): return qt4.Qt.ItemIsEnabled flags = ( qt4.Qt.ItemIsEnabled | qt4.Qt.ItemIsSelectable | qt4.Qt.ItemIsDropEnabled ) if ( index.internalPointer() is not self.document.basewidget and index.column() == 0 ): # allow items other than root to be edited and dragged flags |= qt4.Qt.ItemIsEditable | qt4.Qt.ItemIsDragEnabled return flags def headerData(self, section, orientation, role): """Return the header of the tree.""" if orientation == qt4.Qt.Horizontal and role == qt4.Qt.DisplayRole: val = ('Name', 'Type')[section] return val return None def index(self, row, column, parent): """Construct an index for a child of parent.""" if parent.isValid(): # normal widget try: child = parent.internalPointer().children[row] except IndexError: return qt4.QModelIndex() else: # root widget child = self.document.basewidget return self.createIndex(row, column, child) def getWidgetIndex(self, widget): """Returns index for widget specified.""" return self.createIndex(widget.widgetSiblingIndex(), 0, widget) def parent(self, index): """Find the parent of the index given.""" parentobj = index.internalPointer().parent if parentobj is None: return qt4.QModelIndex() else: try: return self.createIndex(parentobj.widgetSiblingIndex(), 0, parentobj) except ValueError: return qt4.QModelIndex() def rowCount(self, index): """Return number of rows of children of index.""" if index.isValid(): return len(index.internalPointer().children) else: # always 1 root node return 1 def getSettings(self, index): """Return the settings for the index selected.""" obj = index.internalPointer() return obj.settings def getWidget(self, index): """Get associated widget for index selected.""" return index.internalPointer() def removeRows(self, row, count, parentindex): """Remove widgets from parent.""" if not parentindex.isValid(): return parent = self.getWidget(parentindex) self.suspendmodified = True self.beginRemoveRows(parentindex, row, row+count-1) # construct an operation for deleting the rows deleteops = [] for w in parent.children[row:row+count]: deleteops.append( document.OperationWidgetDelete(w) ) op = document.OperationMultiple(deleteops, descr=_("remove widget(s)")) self.document.applyOperation(op) self.endRemoveRows() self.suspendmodified = False return True def supportedDropActions(self): """Supported drag and drop actions.""" return qt4.Qt.MoveAction | qt4.Qt.CopyAction def mimeData(self, indexes): """Get mime data for indexes.""" widgets = [idx.internalPointer() for idx in indexes] return document.generateWidgetsMime(widgets) def mimeTypes(self): """Accepted mime types.""" return [document.widgetmime] def dropMimeData(self, mimedata, action, row, column, parentindex): """User drags and drops widget.""" if action == qt4.Qt.IgnoreAction: return True data = document.getWidgetMime(mimedata) if data is None: return False if parentindex.isValid(): parent = self.getWidget(parentindex) else: parent = self.document.basewidget # check parent supports child if not document.isMimeDropable(parent, data): return False # work out where row will be pasted startrow = row if row == -1: startrow = len(parent.children) # need to tell qt that these rows are being inserted, so that the # right number of rows are removed afterwards self.suspendmodified = True self.beginInsertRows(parentindex, startrow, startrow+document.getMimeWidgetCount(data)-1) op = document.OperationWidgetPaste(parent, data, index=startrow) self.document.applyOperation(op) self.endInsertRows() self.suspendmodified = False return True class WidgetTreeView(qt4.QTreeView): """A model view for viewing the widgets.""" def __init__(self, model, *args): qt4.QTreeView.__init__(self, *args) self.setModel(model) self.expandAll() # stretch header hdr = self.header() hdr.setStretchLastSection(False) hdr.setResizeMode(0, qt4.QHeaderView.Stretch) hdr.setResizeMode(1, qt4.QHeaderView.Custom) # setup drag and drop self.setSelectionMode(qt4.QAbstractItemView.ExtendedSelection) self.setDragEnabled(True) self.viewport().setAcceptDrops(True) self.setDropIndicatorShown(True) def testModifier(self, e): """Look for keyboard modifier for copy or move.""" if e.keyboardModifiers() & qt4.Qt.ControlModifier: e.setDropAction(qt4.Qt.CopyAction) else: e.setDropAction(qt4.Qt.MoveAction) def handleInternalMove(self, event): """Handle a move inside treeview.""" # make sure qt doesn't handle this event.setDropAction(qt4.Qt.IgnoreAction) event.ignore() if not self.viewport().rect().contains(event.pos()): return # get widget at event position index = self.indexAt(event.pos()) if not index.isValid(): index = self.rootIndex() # adjust according to drop indicator position row = -1 posn = self.dropIndicatorPosition() if posn == qt4.QAbstractItemView.AboveItem: row = index.row() index = index.parent() elif posn == qt4.QAbstractItemView.BelowItem: row = index.row() + 1 index = index.parent() if index.isValid(): parent = self.model().getWidget(index) data = document.getWidgetMime(event.mimeData()) if document.isMimeDropable(parent, data): # move the widget! parentpath = parent.path widgetpaths = document.getMimeWidgetPaths(data) ops = [] r = row for path in widgetpaths: ops.append( document.OperationWidgetMove(path, parentpath, r) ) if r >= 0: r += 1 self.model().document.applyOperation( document.OperationMultiple(ops, descr='move')) event.ignore() def dropEvent(self, e): """When an object is dropped on the view.""" self.testModifier(e) if e.source() is self and e.dropAction() == qt4.Qt.MoveAction: self.handleInternalMove(e) qt4.QTreeView.dropEvent(self, e) def dragMoveEvent(self, e): """Make items move by default and copy if Ctrl is held down.""" self.testModifier(e) qt4.QTreeView.dragMoveEvent(self, e) veusz-1.21.1/veusz/windows/consolewindow.py0000644000175000017500000002571212327177747017272 0ustar jssjss# consolewindow.py # a python-like qt console # Copyright (C) 2003 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import codeop import traceback import sys from ..compat import cstr from .. import qtall as qt4 from .. import document from .. import utils from .. import setting # TODO - command line completion def _(text, disambiguation=None, context='ConsoleWindow'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class _Writer(object): """ Class to behave like an output stream. Pipes input back to the specified function.""" def __init__(self, function): """ Set the function output is sent to.""" self.function = function def write(self, text): """ Send text to the output function.""" self.function(text) def flush(self): """ Does nothing as yet.""" pass class _CommandEdit(qt4.QLineEdit): """ A special class to allow entering of the command line. emits sigEnter if the return key is pressed, and returns command The edit control has a history (press up and down keys to access) """ sigEnter = qt4.pyqtSignal(cstr) def __init__(self, *args): qt4.QLineEdit.__init__(self, *args) self.history = [] self.history_posn = 0 self.entered_text = '' self.returnPressed.connect(self.slotReturnPressed) self.setToolTip(_("Input a python expression here and press enter")) def slotReturnPressed(self): """ Called if the return key is pressed in the edit control.""" # retrieve the text command = self.text() self.setText("") # keep the command for history self.history.append( command ) self.history_posn = len(self.history) self.entered_text = '' # tell the console we have a command self.sigEnter.emit(command) historykeys = (qt4.Qt.Key_Up, qt4.Qt.Key_Down) def keyPressEvent(self, key): """ Overridden to handle history. """ qt4.QLineEdit.keyPressEvent(self, key) code = key.key() # check whether one of the "history keys" has been pressed if code in _CommandEdit.historykeys: # look for the next or previous history item which our current text # is a prefix of if self.isModified(): text = self.text() self.history_posn = len(self.history) else: text = self.entered_text if code == qt4.Qt.Key_Up: step = -1 elif code == qt4.Qt.Key_Down: step = 1 newpos = self.history_posn + step while True: if newpos >= len(self.history): break if newpos < 0: return if self.history[newpos].startswith(text): break newpos += step if newpos >= len(self.history): # go back to whatever the user had typed in self.history_posn = len(self.history) self.setText(self.entered_text) return # found a relevant history item self.history_posn = newpos # user has modified text since last set if self.isModified(): self.entered_text = text # replace the text in the control text = self.history[ self.history_posn ] self.setText(text) introtext=_(u'''Welcome to Veusz %s --- a scientific plotting application.
    Copyright \u00a9 2003-2014 Jeremy Sanders <jeremy@jeremysanders.net> and contributors.
    Veusz comes with ABSOLUTELY NO WARRANTY. Veusz is Free Software, and you are
    welcome to redistribute it under certain conditions. Enter "GPL()" for details.
    This window is a Python command line console and acts as a calculator.
    ''') % utils.version() class ConsoleWindow(qt4.QDockWidget): """ A python-like qt console.""" def __init__(self, thedocument, *args): qt4.QDockWidget.__init__(self, *args) self.setWindowTitle(_("Console - Veusz")) self.setObjectName("veuszconsolewindow") # arrange sub-widgets in a vbox self.vbox = qt4.QWidget() self.setWidget(self.vbox) vlayout = qt4.QVBoxLayout(self.vbox) vlayout.setMargin( vlayout.margin()//4 ) vlayout.setSpacing( vlayout.spacing()//4 ) # start an interpreter instance to the document self.interpreter = document.CommandInterpreter(thedocument) self.document = thedocument # output from the interpreter goes to self.output_stdxxx self.con_stdout = _Writer(self.output_stdout) self.con_stderr = _Writer(self.output_stderr) self.interpreter.setOutputs(self.con_stdout, self.con_stderr) self.stdoutbuffer = "" self.stderrbuffer = "" # (mostly) hidden notification self._hiddennotify = qt4.QLabel() vlayout.addWidget(self._hiddennotify) self._hiddennotify.hide() # the output from the console goes here self._outputdisplay = qt4.QTextEdit() self._outputdisplay.setReadOnly(True) self._outputdisplay.insertHtml( introtext ) vlayout.addWidget(self._outputdisplay) self._hbox = qt4.QWidget() hlayout = qt4.QHBoxLayout(self._hbox) hlayout.setMargin(0) vlayout.addWidget(self._hbox) self._prompt = qt4.QLabel(">>>") hlayout.addWidget(self._prompt) # where commands are typed in self._inputedit = _CommandEdit() hlayout.addWidget(self._inputedit) self._inputedit.setFocus() # keep track of multiple line commands self.command_build = '' # get called if enter is pressed in the input control self._inputedit.sigEnter.connect(self.slotEnter) # called if document logs something thedocument.sigLog.connect(self.slotDocumentLog) def _makeTextFormat(self, cursor, color): fmt = cursor.charFormat() if color is not None: brush = qt4.QBrush(color) fmt.setForeground(brush) else: # use the default foreground color fmt.clearForeground() return fmt def appendOutput(self, text, style): """Add text to the tail of the error log, with a specified style""" if style == 'error': color = setting.settingdb.color('error') elif style == 'command': color = setting.settingdb.color('command') else: color = None cursor = self._outputdisplay.textCursor() cursor.movePosition(qt4.QTextCursor.End) cursor.insertText(text, self._makeTextFormat(cursor, color)) self._outputdisplay.setTextCursor(cursor) self._outputdisplay.ensureCursorVisible() def runFunction(self, func): """Execute the function within the console window, trapping exceptions.""" # preserve output streams temp_stdout = sys.stdout temp_stderr = sys.stderr sys.stdout = _Writer(self.output_stdout) sys.stderr = _Writer(self.output_stderr) # catch any exceptions, printing problems to stderr self.document.suspendUpdates() try: func() except: # print out the backtrace to stderr i = sys.exc_info() backtrace = traceback.format_exception( *i ) for l in backtrace: sys.stderr.write(l) self.document.enableUpdates() # return output streams sys.stdout = temp_stdout sys.stderr = temp_stderr def checkVisible(self): """If this window is hidden, show it, then hide it again in a few seconds.""" if self.isHidden(): self._hiddennotify.setText(_("This window will shortly disappear. " "You can bring it back by selecting " "View, Windows, Console Window on the " "menu.")) qt4.QTimer.singleShot(5000, self.hideConsole) self.show() self._hiddennotify.show() def hideConsole(self): """Hide window and notification widget.""" self._hiddennotify.hide() self.hide() def output_stdout(self, text): """ Write text in stdout font to the log.""" self.checkVisible() self.appendOutput(text, 'normal') def output_stderr(self, text): """ Write text in stderr font to the log.""" self.checkVisible() self.appendOutput(text, 'error') def insertTextInOutput(self, text): """ Inserts the text into the log.""" self.appendOutput(text, 'normal') def slotEnter(self, command): """ Called if the return key is pressed in the edit control.""" newc = self.command_build + '\n' + command # check whether command can be compiled # c set to None if incomplete try: c = codeop.compile_command(newc) except Exception: # we want errors to be caught by self.interpreter.run below c = 1 # which prompt? prompt = '>>>' if self.command_build != '': prompt = '...' # output the command in the log pane self.appendOutput('%s %s\n' % (prompt, command), 'command') # are we ready to run this? if c is None or (len(command) != 0 and len(self.command_build) != 0 and (command[0] == ' ' or command[0] == '\t')): # build up the expression self.command_build = newc # modify the prompt self._prompt.setText( '...' ) else: # actually execute the command self.interpreter.run(newc) self.command_build = '' # modify the prompt self._prompt.setText( '>>>' ) def slotDocumentLog(self, text): """Output information if the document logs something.""" self.output_stderr(text + '\n') veusz-1.21.1/veusz/windows/datanavigator.py0000664000175000017500000000311112237406466017204 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2011 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division from .. import qtall as qt4 from ..qtwidgets.datasetbrowser import DatasetBrowser def _(text, disambiguation=None, context="DataNavigator"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class DataNavigatorWindow(qt4.QDockWidget): """A dock window containing a dataset browsing widget.""" def __init__(self, thedocument, mainwin, *args): qt4.QDockWidget.__init__(self, *args) self.setWindowTitle(_("Data - Veusz")) self.setObjectName("veuszdatawindow") self.nav = DatasetBrowser(thedocument, mainwin, self) self.setWidget(self.nav) veusz-1.21.1/veusz/windows/treeeditwindow.py0000664000175000017500000014303112376130006017407 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### """Window to edit the document using a tree, widget properties and formatting properties.""" from __future__ import division from ..compat import crange, citems from .. import qtall as qt4 from .. import widgets from .. import utils from .. import document from .. import setting from .widgettree import WidgetTreeModel, WidgetTreeView def _(text, disambiguation=None, context='TreeEditWindow'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class SettingsProxy(object): """Object to handle communication between widget/settings or sets of widgets/settings.""" def childProxyList(self): """Return a list settings and setting variables proxified.""" def settingsProxyList(self): """Return list of SettingsProxy objects for sub Settings.""" def settingList(self): """Return list of Setting objects.""" def actionsList(self): """Return list of Action objects.""" def onSettingChanged(self, control, setting, val): """Called when a setting has been modified.""" def onAction(self, action, console): """Called if action pressed. Console window is given.""" def name(self): """Return name of Settings.""" def pixmap(self): """Return pixmap for Settings.""" def usertext(self): """Return text for user.""" def setnsmode(self): """Return setnsmode of Settings.""" def multivalued(self, name): """Is setting with name multivalued?""" return False def resetToDefault(self, name): """Reset setting to default.""" class SettingsProxySingle(SettingsProxy): """A proxy wrapping settings for a single widget.""" def __init__(self, document, settings, actions=None): """Initialise settings proxy. settings is the widget settings, actions is its actions.""" self.document = document self.settings = settings self.actions = actions def childProxyList(self): """Return a list settings and setting variables proxified.""" retn = [] s = self.settings for n in s.getNames(): o = s.get(n) if isinstance(o, setting.Settings): retn.append( SettingsProxySingle(self.document, o) ) else: retn.append(o) return retn def settingsProxyList(self): """Return list of SettingsProxy objects.""" return [ SettingsProxySingle(self.document, s) for s in self.settings.getSettingsList() ] def settingList(self): """Return list of Setting objects.""" return self.settings.getSettingList() def actionsList(self): """Return list of actions.""" return self.actions def onSettingChanged(self, control, setting, val): """Change setting in document.""" if setting.val != val: self.document.applyOperation( document.OperationSettingSet(setting, val)) def onAction(self, action, console): """Run action on console.""" console.runFunction(action.function) def name(self): """Return name.""" return self.settings.name def pixmap(self): """Return pixmap.""" return self.settings.pixmap def usertext(self): """Return text for user.""" return self.settings.usertext def setnsmode(self): """Return setnsmode of Settings.""" return self.settings.setnsmode def resetToDefault(self, name): """Reset setting to default.""" setn = self.settings.get(name) self.document.applyOperation( document.OperationSettingSet(setn, setn.default)) class SettingsProxyMulti(SettingsProxy): """A proxy wrapping settings for multiple widgets.""" def __init__(self, document, widgets, _root=''): """Initialise settings proxy. widgets is a list of widgets to proxy for.""" self.document = document self.widgets = widgets self._root = _root self._settingsatlevel = self._getSettingsAtLevel() self._cachesettings = self._cachesetting = self._cachechild = None def _getSettingsAtLevel(self): """Return settings of widgets at level given.""" if self._root: levels = self._root.split('/') else: levels = [] setns = [] for w in self.widgets: s = w.settings for lev in levels: s = s.get(lev) setns.append(s) return setns def _objList(self, filterclasses): """Return a list of objects with the type in filterclasses.""" setns = self._settingsatlevel # get list of names with appropriate class names = [] for n in setns[0].getNames(): o = setns[0].get(n) for c in filterclasses: if isinstance(o, c): names.append(n) break sset = set(names) for s in setns[1:]: sset &= set(s.getNames()) names = [n for n in names if n in sset] proxylist = [] for n in names: o = setns[0].get(n) if isinstance(o, setting.Settings): # construct new proxy settings (adding on name of root) newroot = n if self._root: newroot = self._root + '/' + newroot v = SettingsProxyMulti(self.document, self.widgets, _root=newroot) else: # use setting from first settings as template v = o proxylist.append(v) return proxylist def childProxyList(self): """Make a list of proxy settings.""" if self._cachechild is None: self._cachechild = self._objList( (setting.Settings, setting.Setting) ) return self._cachechild def settingsProxyList(self): """Get list of settings proxy.""" if self._cachesettings is None: self._cachesettings = self._objList( (setting.Settings,) ) return self._cachesettings def settingList(self): """Set list of common Setting objects for each widget.""" if self._cachesetting is None: self._cachesetting = self._objList( (setting.Setting,) ) return self._cachesetting def actionsList(self): """Get list of common actions.""" anames = None for widget in self.widgets: a = set([a.name for a in widget.actions]) if anames is None: anames = a else: anames &= a actions = [a for a in self.widgets[0].actions if a.name in anames] return actions def onSettingChanged(self, control, setting, val): """Change setting in document.""" # construct list of operations to change each setting ops = [] sname = setting.name if self._root: sname = self._root + '/' + sname for w in self.widgets: s = self.document.resolveFullSettingPath(w.path + '/' + sname) if s.val != val: ops.append(document.OperationSettingSet(s, val)) # apply all operations if ops: self.document.applyOperation( document.OperationMultiple(ops, descr=_('change settings'))) def onAction(self, action, console): """Run actions with same name.""" aname = action.name for w in self.widgets: for a in w.actions: if a.name == aname: console.runFunction(a.function) def name(self): return self._settingsatlevel[0].name def pixmap(self): """Return pixmap.""" return self._settingsatlevel[0].pixmap def usertext(self): """Return text for user.""" return self._settingsatlevel[0].usertext def setnsmode(self): """Return setnsmode.""" return self._settingsatlevel[0].setnsmode def multivalued(self, name): """Is setting multivalued?""" slist = [s.get(name) for s in self._settingsatlevel] first = slist[0].get() for s in slist[1:]: if s.get() != first: return True return False def resetToDefault(self, name): """Reset settings to default.""" ops = [] for s in self._settingsatlevel: setn = s.get(name) ops.append(document.OperationSettingSet(setn, setn.default)) self.document.applyOperation( document.OperationMultiple(ops, descr=_("reset to default"))) class PropertyList(qt4.QWidget): """Edit the widget properties using a set of controls.""" def __init__(self, document, showformatsettings=True, *args): qt4.QWidget.__init__(self, *args) self.document = document self.showformatsettings = showformatsettings self.layout = qt4.QGridLayout(self) self.layout.setSpacing( self.layout.spacing()//2 ) self.layout.setMargin(4) self.childlist = [] self.setncntrls = {} # map setting name to controls def getConsole(self): """Find console window. This is horrible: HACK.""" win = self.parent() while not hasattr(win, 'console'): win = win.parent() return win.console def _addActions(self, setnsproxy, row): """Add a list of actions.""" for action in setnsproxy.actionsList(): text = action.name if action.usertext: text = action.usertext lab = qt4.QLabel(text) self.layout.addWidget(lab, row, 0) self.childlist.append(lab) button = qt4.QPushButton(text) button.setToolTip(action.descr) button.clicked[()].connect( lambda a=action: setnsproxy.onAction(a, self.getConsole())) self.layout.addWidget(button, row, 1) self.childlist.append(button) row += 1 return row def _addControl(self, setnsproxy, setn, row): """Add a control for a setting.""" cntrl = setn.makeControl(None) if cntrl: lab = SettingLabel(self.document, setn, setnsproxy) self.layout.addWidget(lab, row, 0) self.childlist.append(lab) cntrl.sigSettingChanged.connect(setnsproxy.onSettingChanged) self.layout.addWidget(cntrl, row, 1) self.childlist.append(cntrl) self.setncntrls[setn.name] = (lab, cntrl) row += 1 return row def _addGroupedSettingsControl(self, grpdsetting, row): """Add a control for a set of grouped settings.""" slist = grpdsetting.settingList() # make first widget with expandable button # this is a label with a + button by this side setnlab = SettingLabel(self.document, slist[0], grpdsetting) expandbutton = qt4.QPushButton("+", checkable=True, flat=True, maximumWidth=16) l = qt4.QHBoxLayout(spacing=0) l.setContentsMargins(0,0,0,0) l.addWidget( expandbutton ) l.addWidget( setnlab ) lw = qt4.QWidget() lw.setLayout(l) self.layout.addWidget(lw, row, 0) self.childlist.append(lw) # make main control cntrl = slist[0].makeControl(None) cntrl.sigSettingChanged.connect(grpdsetting.onSettingChanged) self.layout.addWidget(cntrl, row, 1) self.childlist.append(cntrl) row += 1 # set of controls for remaining settings l = qt4.QGridLayout() grp_row = 0 for setn in slist[1:]: cntrl = setn.makeControl(None) if cntrl: lab = SettingLabel(self.document, setn, grpdsetting) l.addWidget(lab, grp_row, 0) cntrl.sigSettingChanged.connect(grpdsetting.onSettingChanged) l.addWidget(cntrl, grp_row, 1) grp_row += 1 grpwidget = qt4.QFrame( frameShape = qt4.QFrame.Panel, frameShadow = qt4.QFrame.Raised, visible=False ) grpwidget.setLayout(l) def ontoggle(checked): """Toggle button text and make grp visible/invisible.""" expandbutton.setText( ("+","-")[checked] ) grpwidget.setVisible( checked ) expandbutton.toggled.connect(ontoggle) # add group to standard layout self.layout.addWidget(grpwidget, row, 0, 1, -1) self.childlist.append(grpwidget) row += 1 return row def updateProperties(self, setnsproxy, title=None, showformatting=True, onlyformatting=False): """Update the list of controls with new ones for the SettingsProxy.""" # keep a reference to keep it alive self._setnsproxy = setnsproxy # delete all child widgets self.setUpdatesEnabled(False) while len(self.childlist) > 0: c = self.childlist.pop() self.layout.removeWidget(c) c.deleteLater() del c if setnsproxy is None: self.setUpdatesEnabled(True) return row = 0 self.setncntrls = {} self.layout.setEnabled(False) # add a title if requested if title is not None: lab = qt4.QLabel(title[0], frameShape=qt4.QFrame.Panel, frameShadow=qt4.QFrame.Sunken, toolTip=title[1]) self.layout.addWidget(lab, row, 0, 1, -1) row += 1 # add actions if parent is widget if setnsproxy.actionsList() and not showformatting: row = self._addActions(setnsproxy, row) if setnsproxy.settingsProxyList() and self.showformatsettings: # if we have subsettings, use tabs tabbed = TabbedFormatting(self.document, setnsproxy) self.layout.addWidget(tabbed, row, 1, 1, 2) row += 1 self.childlist.append(tabbed) else: # else add settings proper as a list for setn in setnsproxy.childProxyList(): # add setting # only add if formatting setting and formatting allowed # and not formatting and not formatting not allowed if ( isinstance(setn, setting.Setting) and ( (setn.formatting and (showformatting or onlyformatting)) or (not setn.formatting and not onlyformatting)) and not setn.hidden ): row = self._addControl(setnsproxy, setn, row) elif ( isinstance(setn, SettingsProxy) and setn.setnsmode() == 'groupedsetting' and not onlyformatting ): row = self._addGroupedSettingsControl(setn, row) # add empty widget to take rest of space w = qt4.QWidget( sizePolicy=qt4.QSizePolicy( qt4.QSizePolicy.Maximum, qt4.QSizePolicy.MinimumExpanding) ) self.layout.addWidget(w, row, 0) self.childlist.append(w) self.setUpdatesEnabled(True) self.layout.setEnabled(True) def showHideSettings(self, setnshow, setnhide): """Show or hide controls for settings.""" for vis, setns in ( (True, setnshow), (False, setnhide) ): for setn in setns: if setn in self.setncntrls: for cntrl in self.setncntrls[setn]: cntrl.setVisible(vis) class TabbedFormatting(qt4.QTabWidget): """Class to have tabbed set of settings.""" def __init__(self, document, setnsproxy, shownames=False): qt4.QTabWidget.__init__(self) self.document = document if setnsproxy is None: return # get list of settings self.setnsproxy = setnsproxy setnslist = setnsproxy.settingsProxyList() # add formatting settings if necessary numformat = len( [setn for setn in setnsproxy.settingList() if setn.formatting] ) if numformat > 0: # add on a formatting tab setnslist.insert(0, setnsproxy) self.currentChanged.connect(self.slotCurrentChanged) # subsettings for tabs self.tabsubsetns = [] # collected titles and tooltips for tabs self.tabtitles = [] self.tabtooltips = [] # tabs which have been initialized self.tabinit = set() # add tab for each subsettings for subset in setnslist: if subset.setnsmode() not in ('formatting', 'widgetsettings'): continue self.tabsubsetns.append(subset) # details of tab if subset is setnsproxy: # main tab formatting, so this is special pixmap = 'settings_main' tabname = title = _('Main') tooltip = _('Main formatting') else: # others if hasattr(subset, 'pixmap'): pixmap = subset.pixmap() else: pixmap = None tabname = subset.name() tooltip = title = subset.usertext() # hide name in tab if not shownames: tabname = '' self.tabtitles.append(title) self.tabtooltips.append(tooltip) # create tab indx = self.addTab(qt4.QWidget(), utils.getIcon(pixmap), tabname) self.setTabToolTip(indx, tooltip) def slotCurrentChanged(self, tab): """Lazy loading of tab when displayed.""" if tab in self.tabinit: # already initialized return self.tabinit.add(tab) # settings to show subsetn = self.tabsubsetns[tab] # whether these are the main settings mainsettings = subsetn is self.setnsproxy # add this property list to the scroll widget for tab plist = PropertyList(self.document, showformatsettings=not mainsettings) plist.updateProperties(subsetn, title=(self.tabtitles[tab], self.tabtooltips[tab]), onlyformatting=mainsettings) # create scrollable area scroll = qt4.QScrollArea() scroll.setWidgetResizable(True) scroll.setWidget(plist) # layout for tab widget layout = qt4.QVBoxLayout() layout.setMargin(2) layout.addWidget(scroll) # finally use layout containing items for tab self.widget(tab).setLayout(layout) class FormatDock(qt4.QDockWidget): """A window for formatting the current widget. Provides tabbed formatting properties """ def __init__(self, document, treeedit, *args): qt4.QDockWidget.__init__(self, *args) self.setWindowTitle(_("Formatting - Veusz")) self.setObjectName("veuszformattingdock") self.document = document self.tabwidget = None # update our view when the tree edit window selection changes treeedit.widgetsSelected.connect(self.selectedWidgets) def selectedWidgets(self, widgets, setnsproxy): """Created tabbed widgets for formatting for each subsettings.""" # get current tab (so we can set it afterwards) if self.tabwidget: tab = self.tabwidget.currentIndex() else: tab = 0 # delete old tabwidget if self.tabwidget: self.tabwidget.deleteLater() self.tabwidget = None self.tabwidget = TabbedFormatting(self.document, setnsproxy) self.setWidget(self.tabwidget) # wrap tab from zero to max number tab = max( min(self.tabwidget.count()-1, tab), 0 ) self.tabwidget.setCurrentIndex(tab) class PropertiesDock(qt4.QDockWidget): """A window for editing properties for widgets.""" def __init__(self, document, treeedit, *args): qt4.QDockWidget.__init__(self, *args) self.setWindowTitle(_("Properties - Veusz")) self.setObjectName("veuszpropertiesdock") self.document = document # update our view when the tree edit window selection changes treeedit.widgetsSelected.connect(self.slotWidgetsSelected) # construct scrollable area self.scroll = qt4.QScrollArea() self.scroll.setWidgetResizable(True) self.setWidget(self.scroll) # construct properties list in scrollable area self.proplist = PropertyList(document, showformatsettings=False) self.scroll.setWidget(self.proplist) def slotWidgetsSelected(self, widgets, setnsproxy): """Update properties when selected widgets change.""" self.proplist.updateProperties(setnsproxy, showformatting=False) class TreeEditDock(qt4.QDockWidget): """A dock window presenting widgets as a tree.""" widgetsSelected = qt4.pyqtSignal(list, object) sigPageChanged = qt4.pyqtSignal(int) def __init__(self, document, parentwin): """Initialise dock given document and parent widget.""" qt4.QDockWidget.__init__(self, parentwin) self.parentwin = parentwin self.setWindowTitle(_("Editing - Veusz")) self.setObjectName("veuszeditingwindow") self.selwidgets = [] self.document = document self.document.sigWiped.connect(self.slotDocumentWiped) # construct tree self.treemodel = WidgetTreeModel(document) self.treeview = WidgetTreeView(self.treemodel) # receive change in selection self.treeview.selectionModel().selectionChanged.connect( self.slotTreeItemsSelected) # set tree as main widget self.setWidget(self.treeview) # toolbar to create widgets self.addtoolbar = qt4.QToolBar(_("Insert toolbar - Veusz"), parentwin) # note wrong description!: backwards compatibility self.addtoolbar.setObjectName("veuszeditingtoolbar") # toolbar for editting widgets self.edittoolbar = qt4.QToolBar(_("Edit toolbar - Veusz"), parentwin) self.edittoolbar.setObjectName("veuszedittoolbar") self._constructToolbarMenu() parentwin.addToolBarBreak(qt4.Qt.TopToolBarArea) parentwin.addToolBar(qt4.Qt.TopToolBarArea, self.addtoolbar) parentwin.addToolBar(qt4.Qt.TopToolBarArea, self.edittoolbar) # this sets various things up self.selectWidget(document.basewidget) # update paste button when clipboard changes qt4.QApplication.clipboard().dataChanged.connect( self.updatePasteButton) self.updatePasteButton() def slotDocumentWiped(self): """If the document is wiped, reselect root widget.""" self.selectWidget(self.document.basewidget) def slotTreeItemsSelected(self, current, previous): """New item selected in tree. This updates the list of properties """ # get selected widgets self.selwidgets = swidget = [ self.treemodel.getWidget(idx) for idx in self.treeview.selectionModel().selectedRows() ] if len(swidget) == 0: setnsproxy = None elif len(swidget) == 1: setnsproxy = SettingsProxySingle(self.document, swidget[0].settings, actions=swidget[0].actions) else: setnsproxy = SettingsProxyMulti(self.document, swidget) self._enableCorrectButtons() self._checkPageChange() self.widgetsSelected.emit(swidget, setnsproxy) def contextMenuEvent(self, event): """Bring up context menu.""" # no widgets selected if not self.selwidgets: return m = qt4.QMenu(self) # selection m.addMenu(self.parentwin.menus['edit.select']) m.addSeparator() # actions on widget(s) for act in ('edit.cut', 'edit.copy', 'edit.paste', 'edit.moveup', 'edit.movedown', 'edit.delete', 'edit.rename'): m.addAction(self.vzactions[act]) # allow show or hides of selected widget anyhide = False anyshow = False for w in self.selwidgets: if 'hide' in w.settings: if w.settings.hide: anyshow = True else: anyhide = True for (enabled, menutext, showhide) in ( (anyhide, 'Hide', True), (anyshow, 'Show', False) ): if enabled: m.addSeparator() act = qt4.QAction(menutext, self) def trigfn(showorhide): return lambda: self.slotWidgetHideShow( self.selwidgets, showorhide) act.triggered.connect(trigfn(showhide)) m.addAction(act) m.exec_(self.mapToGlobal(event.pos())) event.accept() def _checkPageChange(self): """Check to see whether page has changed.""" w = None if self.selwidgets: w = self.selwidgets[0] while w is not None and not isinstance(w, widgets.Page): w = w.parent if w is not None: # have page, so check what number we are in basewidget children try: i = self.document.basewidget.children.index(w) self.sigPageChanged.emit(i) except ValueError: pass def _enableCorrectButtons(self): """Make sure the create graph buttons are correctly enabled.""" selw = None if self.selwidgets: selw = self.selwidgets[0] # has to be visible if is to be enabled (yuck) nonorth = self.vzactions['add.nonorthpoint'].setVisible(True) # check whether each button can have this widget # (or a parent) as parent for wc, action in citems(self.addslots): w = selw while w is not None and not wc.willAllowParent(w): w = w.parent self.vzactions['add.%s' % wc.typename].setEnabled(w is not None) self.vzactions['add.axismenu'].setEnabled( self.vzactions['add.axis'].isEnabled()) # exclusive widgets nonorth = self.vzactions['add.nonorthpoint'].isEnabled() self.vzactions['add.nonorthpoint'].setVisible(nonorth) self.vzactions['add.xy'].setVisible(not nonorth) self.vzactions['add.nonorthfunc'].setVisible(nonorth) self.vzactions['add.function'].setVisible(not nonorth) # certain actions shouldn't work on root isnotroot = not any([isinstance(w, widgets.Root) for w in self.selwidgets]) for act in ('edit.cut', 'edit.copy', 'edit.delete', 'edit.moveup', 'edit.movedown', 'edit.rename'): self.vzactions[act].setEnabled(isnotroot) self.updatePasteButton() def _constructToolbarMenu(self): """Add items to edit/add graph toolbar and menu.""" iconsize = setting.settingdb['toolbar_size'] self.addtoolbar.setIconSize( qt4.QSize(iconsize, iconsize) ) self.edittoolbar.setIconSize( qt4.QSize(iconsize, iconsize) ) self.addslots = {} self.vzactions = actions = self.parentwin.vzactions for widgettype in ('page', 'grid', 'graph', 'axis', 'axis-broken', 'axis-function', 'xy', 'bar', 'fit', 'function', 'boxplot', 'image', 'contour', 'vectorfield', 'key', 'label', 'colorbar', 'rect', 'ellipse', 'imagefile', 'line', 'polygon', 'polar', 'ternary', 'nonorthpoint', 'nonorthfunc'): wc = document.thefactory.getWidgetClass(widgettype) def slotfn(klass=wc): return lambda: self.slotMakeWidgetButton(klass) slot = slotfn(wc) self.addslots[wc] = slot actionname = 'add.' + widgettype actions[actionname] = utils.makeAction( self, wc.description, _('Add %s') % widgettype, slot, icon='button_%s' % widgettype) a = utils.makeAction actions.update({ 'edit.cut': a(self, _('Cut the selected widget'), _('Cu&t'), self.slotWidgetCut, icon='veusz-edit-cut', key='Ctrl+X'), 'edit.copy': a(self, _('Copy the selected widget'), _('&Copy'), self.slotWidgetCopy, icon='kde-edit-copy', key='Ctrl+C'), 'edit.paste': a(self, _('Paste widget from the clipboard'), _('&Paste'), self.slotWidgetPaste, icon='kde-edit-paste', key='Ctrl+V'), 'edit.moveup': a(self, _('Move the selected widget up'), _('Move &up'), lambda: self.slotWidgetMove(-1), icon='kde-go-up'), 'edit.movedown': a(self, _('Move the selected widget down'), _('Move d&own'), lambda: self.slotWidgetMove(1), icon='kde-go-down'), 'edit.delete': a(self, _('Remove the selected widget'), _('&Delete'), self.slotWidgetDelete, icon='kde-edit-delete'), 'edit.rename': a(self, _('Renames the selected widget'), _('&Rename'), self.slotWidgetRename, icon='kde-edit-rename'), 'add.shapemenu': a(self, _('Add a shape to the plot'), _('Shape'), self.slotShowShapeMenu, icon='veusz-shape-menu'), 'add.axismenu': a(self, _('Add an axis to the plot'), _('Axis'), None, icon='button_axis'), }) # list of widget-generating actions for menu and toolbar widgetactions = ( 'add.page', 'add.grid', 'add.graph', 'add.axismenu', 'add.xy', 'add.nonorthpoint', 'add.bar', 'add.fit', 'add.function', 'add.nonorthfunc', 'add.boxplot', 'add.image', 'add.contour', 'add.vectorfield', 'add.key', 'add.label', 'add.colorbar', 'add.polar', 'add.ternary', 'add.shapemenu', ) # separate menus for adding shapes and axis types shapemenu = qt4.QMenu() shapemenu.addActions( [actions[act] for act in ( 'add.rect', 'add.ellipse', 'add.line', 'add.imagefile', 'add.polygon', )]) actions['add.shapemenu'].setMenu(shapemenu) axismenu = qt4.QMenu() axismenu.addActions( [actions[act] for act in ( 'add.axis', 'add.axis-broken', 'add.axis-function', )]) actions['add.axismenu'].setMenu(axismenu) actions['add.axismenu'].triggered.connect(actions['add.axis'].trigger) menuitems = ( ('insert', '', widgetactions), ('edit', '', ( 'edit.cut', 'edit.copy', 'edit.paste', 'edit.moveup', 'edit.movedown', 'edit.delete', 'edit.rename' )), ) utils.constructMenus( self.parentwin.menuBar(), self.parentwin.menus, menuitems, actions ) # add actions to toolbar to create widgets utils.addToolbarActions(self.addtoolbar, actions, widgetactions) # add action to toolbar for editing utils.addToolbarActions(self.edittoolbar, actions, ('edit.cut', 'edit.copy', 'edit.paste', 'edit.moveup', 'edit.movedown', 'edit.delete', 'edit.rename')) self.parentwin.menus['edit.select'].aboutToShow.connect( self.updateSelectMenu) def slotMakeWidgetButton(self, wc): """User clicks button to make widget.""" self.makeWidget(wc.typename) def slotShowShapeMenu(self): a = self.vzactions['add.shapemenu'] a.menu().popup( qt4.QCursor.pos() ) def makeWidget(self, widgettype, autoadd=True, name=None): """Called when an add widget button is clicked. widgettype is the type of widget autoadd specifies whether to add default children if name is set this name is used if possible (ie no other children have it) """ # if no widget selected, bomb out if not self.selwidgets: return parent = document.getSuitableParent(widgettype, self.selwidgets[0]) assert parent is not None if name in parent.childnames: name = None # make the new widget and update the document w = self.document.applyOperation( document.OperationWidgetAdd(parent, widgettype, autoadd=autoadd, name=name) ) # select the widget self.selectWidget(w) def slotWidgetCut(self): """Cut the selected widget""" self.slotWidgetCopy() self.slotWidgetDelete() def slotWidgetCopy(self): """Copy selected widget to the clipboard.""" if self.selwidgets: mimedata = document.generateWidgetsMime(self.selwidgets) clipboard = qt4.QApplication.clipboard() clipboard.setMimeData(mimedata) def updatePasteButton(self): """Is the data on the clipboard a valid paste at the currently selected widget? If so, enable paste button""" data = document.getClipboardWidgetMime() if len(self.selwidgets) == 0: show = False else: show = document.isWidgetMimePastable(self.selwidgets[0], data) self.vzactions['edit.paste'].setEnabled(show) def doInitialWidgetSelect(self): """Select a sensible initial widget.""" w = self.document.basewidget for i in crange(2): try: c = w.children[0] except IndexError: break if c: w = c self.selectWidget(w) def slotWidgetPaste(self): """Paste something from the clipboard""" data = document.getClipboardWidgetMime() if data: op = document.OperationWidgetPaste(self.selwidgets[0], data) widgets = self.document.applyOperation(op) if widgets: self.selectWidget(widgets[0]) def slotWidgetDelete(self): """Delete the widget selected.""" widgets = self.selwidgets # if no item selected, leave if not widgets: return # get list of widgets in order widgetlist = [] self.document.basewidget.buildFlatWidgetList(widgetlist) # find indices of widgets to be deleted - find one to select after indexes = [widgetlist.index(w) for w in widgets] if -1 in indexes: raise RuntimeError("Invalid widget in list of selected widgets") minindex = min(indexes) # delete selected widget self.document.applyOperation( document.OperationWidgetsDelete(widgets)) # rebuild list widgetlist = [] self.document.basewidget.buildFlatWidgetList(widgetlist) # find next to select if minindex < len(widgetlist): nextwidget = widgetlist[minindex] else: nextwidget = widgetlist[-1] # select the next widget (we have to select root first!) self.selectWidget(self.document.basewidget) self.selectWidget(nextwidget) def slotWidgetRename(self): """Allows the user to rename the selected widget.""" selected = self.treeview.selectedIndexes() if len(selected) != 0: self.treeview.edit(selected[0]) def selectWidget(self, widget): """Select the associated listviewitem for the widget w in the listview.""" index = self.treemodel.getWidgetIndex(widget) if index is not None: self.treeview.scrollTo(index) self.treeview.selectionModel().select( index, qt4.QItemSelectionModel.Clear | qt4.QItemSelectionModel.Current | qt4.QItemSelectionModel.Rows | qt4.QItemSelectionModel.Select ) def slotWidgetMove(self, direction): """Move the selected widget up/down in the hierarchy. a is the action (unused) direction is -1 for 'up' and +1 for 'down' """ if not self.selwidgets: return # widget to move w = self.selwidgets[0] # actually move the widget self.document.applyOperation( document.OperationWidgetMoveUpDown(w, direction) ) # re-highlight moved widget self.selectWidget(w) def slotWidgetHideShow(self, widgets, hideshow): """Hide or show selected widgets. hideshow is True for hiding, False for showing """ ops = [ document.OperationSettingSet(w.settings.get('hide'), hideshow) for w in widgets ] descr = ('show', 'hide')[hideshow] self.document.applyOperation( document.OperationMultiple(ops, descr=descr)) def checkWidgetSelected(self): """Check widget is selected.""" if len(self.treeview.selectionModel().selectedRows()) == 0: self.selectWidget(self.document.basewidget) def _selectWidgetsTypeAndOrName(self, wtype, wname): """Select widgets with type or name given. Give None if you don't care for either.""" def selectwidget(path, w): """Select widget if of type or name given.""" if ( (wtype is None or w.typename == wtype) and (wname is None or w.name == wname) ): idx = self.treemodel.getWidgetIndex(w) self.treeview.selectionModel().select( idx, qt4.QItemSelectionModel.Select | qt4.QItemSelectionModel.Rows) self.document.walkNodes(selectwidget, nodetypes=('widget',)) def _selectWidgetSiblings(self, w, wtype): """Select siblings of widget given with type.""" if w.parent is None: return for c in w.parent.children: if c is not w and c.typename == wtype: idx = self.treemodel.getWidgetIndex(c) self.treeview.selectionModel().select( idx, qt4.QItemSelectionModel.Select | qt4.QItemSelectionModel.Rows) def updateSelectMenu(self): """Update edit.select menu.""" menu = self.parentwin.menus['edit.select'] menu.clear() if len(self.selwidgets) == 0: return wtype = self.selwidgets[0].typename name = self.selwidgets[0].name menu.addAction( _("All '%s' widgets") % wtype, lambda: self._selectWidgetsTypeAndOrName(wtype, None)) menu.addAction( _("Siblings of '%s' with type '%s'") % (name, wtype), lambda: self._selectWidgetSiblings(self.selwidgets[0], wtype)) menu.addAction( _("All '%s' widgets called '%s'") % (wtype, name), lambda: self._selectWidgetsTypeAndOrName(wtype, name)) menu.addAction( _("All widgets called '%s'") % name, lambda: self._selectWidgetsTypeAndOrName(None, name)) class SettingLabel(qt4.QWidget): """A label to describe a setting. This widget shows the name, a tooltip description, and gives access to the context menu """ # this is emitted when widget is clicked signalClicked = qt4.pyqtSignal(qt4.QPoint) def __init__(self, document, setting, setnsproxy): """Initialise button, passing document, setting, and parent widget.""" qt4.QWidget.__init__(self) self.setFocusPolicy(qt4.Qt.StrongFocus) self.document = document document.signalModified.connect(self.slotDocModified) self.setting = setting self.setnsproxy = setnsproxy self.layout = qt4.QHBoxLayout(self) self.layout.setMargin(2) if setting.usertext: text = setting.usertext else: text = setting.name self.labelicon = qt4.QLabel(text) self.layout.addWidget(self.labelicon) self.iconlabel = qt4.QLabel() self.layout.addWidget(self.iconlabel) self.signalClicked.connect(self.settingMenu) self.infocus = False self.inmouse = False self.inmenu = False # initialise settings self.slotDocModified(True) def mouseReleaseEvent(self, event): """Emit signalClicked(pos) on mouse release.""" self.signalClicked.emit( self.mapToGlobal(event.pos()) ) return qt4.QWidget.mouseReleaseEvent(self, event) def keyReleaseEvent(self, event): """Emit signalClicked(pos) on key release.""" if event.key() == qt4.Qt.Key_Space: self.signalClicked.emit( self.mapToGlobal(self.iconlabel.pos()) ) event.accept() else: return qt4.QWidget.keyReleaseEvent(self, event) # Mark as a qt slot. This fixes a bug where you get C/C++ object # deleted messages when the document emits signalModified but this # widget has been deleted. This can be reproduced by dragging a # widget between two windows, then undoing. @qt4.pyqtSlot(int) def slotDocModified(self, ismodified): """If the document has been modified.""" # update pixmap (e.g. link added/removed) self.updateHighlight() # update tooltip tooltip = self.setting.descr if self.setting.isReference(): paths = self.setting.getReference().getPaths() tooltip += _('\nLinked to: %s') % ', '.join(paths) self.setToolTip(tooltip) # if not default, make label bold f = qt4.QFont(self.labelicon.font()) multivalued = self.setnsproxy.multivalued(self.setting.name) f.setBold( (not self.setting.isDefault()) or multivalued ) f.setItalic( multivalued ) self.labelicon.setFont(f) def updateHighlight(self): """Show drop down arrow if item has focus.""" if self.inmouse or self.infocus or self.inmenu: pixmap = 'downarrow.png' else: if self.setting.isReference() and not self.setting.isDefault(): pixmap = 'link.png' else: pixmap = 'downarrow_blank.png' self.iconlabel.setPixmap(utils.getPixmap(pixmap)) def enterEvent(self, event): """Focus on mouse enter.""" self.inmouse = True self.updateHighlight() return qt4.QWidget.enterEvent(self, event) def leaveEvent(self, event): """Clear focus on mouse leaving.""" self.inmouse = False self.updateHighlight() return qt4.QWidget.leaveEvent(self, event) def focusInEvent(self, event): """Focus if widgets gets focus.""" self.infocus = True self.updateHighlight() return qt4.QWidget.focusInEvent(self, event) def focusOutEvent(self, event): """Lose focus if widget loses focus.""" self.infocus = False self.updateHighlight() return qt4.QWidget.focusOutEvent(self, event) def addCopyToWidgets(self, menu): """Make a menu with list of other widgets in it.""" def getWidgetsOfType(widget, widgettype, widgets=[]): """Recursively build up a list of widgets of the type given.""" for w in widget.children: if w.typename == widgettype: widgets.append(w) getWidgetsOfType(w, widgettype, widgets) # get list of widget paths to copy setting to # this is all widgets of same type widgets = [] setwidget = self.setting.getWidget() if setwidget is None: return getWidgetsOfType(self.document.basewidget, setwidget.typename, widgets) widgets = [w.path for w in widgets if w != setwidget] widgets.sort() # chop off widget part of setting path # this is so we can add on a different widget path # note setpath needs to include Settings part of path too setpath = self.setting.path wpath = self.setting.getWidget().path setpath = setpath[len(wpath):] # includes / def modifyfn(widget): def modify(): """Modify the setting for the widget given.""" wpath = widget + setpath self.document.applyOperation( document.OperationSettingSet(wpath, self.setting.get())) return modify for widget in widgets: action = menu.addAction(widget) action.triggered.connect(modifyfn(widget)) @qt4.pyqtSlot(qt4.QPoint) def settingMenu(self, pos): """Pop up menu for each setting.""" # forces settings to be updated self.parentWidget().setFocus() # get it back straight away self.setFocus() # get widget, with its type and name widget = self.setting.parent while widget is not None and not isinstance(widget, widgets.Widget): widget = widget.parent if widget is None: return self._clickwidget = widget wtype = widget.typename name = widget.name popup = qt4.QMenu(self) popup.addAction(_('Reset to default'), self.actionResetDefault) copyto = popup.addMenu(_('Copy to')) copyto.addAction(_("all '%s' widgets") % wtype, self.actionCopyTypedWidgets) copyto.addAction(_("'%s' siblings") % wtype, self.actionCopyTypedSiblings) copyto.addAction(_("'%s' widgets called '%s'") % (wtype, name), self.actionCopyTypedNamedWidgets) copyto.addSeparator() self.addCopyToWidgets(copyto) popup.addAction(_('Use as default style'), self.actionSetStyleSheet) # special actions for references if self.setting.isReference(): popup.addSeparator() popup.addAction(_('Unlink setting'), self.actionUnlinkSetting) self.inmenu = True self.updateHighlight() popup.exec_(pos) self.inmenu = False self.updateHighlight() def actionResetDefault(self): """Reset setting to default.""" self.setnsproxy.resetToDefault(self.setting.name) def actionCopyTypedWidgets(self): """Copy setting to widgets of same type.""" self.document.applyOperation( document.OperationSettingPropagate(self.setting) ) def actionCopyTypedSiblings(self): """Copy setting to siblings of the same type.""" self.document.applyOperation( document.OperationSettingPropagate(self.setting, root=self._clickwidget.parent, maxlevels=1) ) def actionCopyTypedNamedWidgets(self): """Copy setting to widgets with the same name and type.""" self.document.applyOperation( document.OperationSettingPropagate(self.setting, widgetname= self._clickwidget.name) ) def actionUnlinkSetting(self): """Unlink the setting if it is a reference.""" self.document.applyOperation( document.OperationSettingSet(self.setting, self.setting.get()) ) def actionSetStyleSheet(self): """Use the setting as the default in the stylesheet.""" # get name of stylesheet setting sslink = self.setting.getStylesheetLink() # apply operation to change it self.document.applyOperation( document.OperationMultiple( [ document.OperationSettingSet(sslink, self.setting.get()), document.OperationSettingSet(self.setting, self.setting.default) ], descr=_("make default style")) ) veusz-1.21.1/veusz/windows/tutorial.py0000644000175000017500000007072212327177747016244 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2011 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### from __future__ import division import os.path from .. import qtall as qt4 from .. import utils from .. import setting def _(text, disambiguation=None, context="Tutorial"): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) class TutorialStep(qt4.QObject): nextStep = qt4.pyqtSignal() def __init__(self, text, mainwin, nextstep=None, flash=None, disablenext=False, closestep=False, nextonsetting=None, nextonselected=None): """ nextstep is class next TutorialStep class to use If flash is set, flash widget disablenext: wait until nextStep is emitted before going to next slide closestep: add a close button nextonsetting: (setnpath, lambda val: ok) - check setting to go to next slide nextonselected: go to next if widget with name is selected """ qt4.QObject.__init__(self) self.text = text self.nextstep = nextstep self.flash = flash self.disablenext = disablenext self.closestep = closestep self.mainwin = mainwin self.nextonsetting = nextonsetting if nextonsetting is not None: mainwin.document.signalModified.connect(self.slotNextSetting) self.nextonselected = nextonselected if nextonselected is not None: mainwin.treeedit.widgetsSelected.connect(self.slotWidgetsSelected) def slotNextSetting(self, *args): """Check setting to emit next.""" try: setn = self.mainwin.document.basewidget.prefLookup( self.nextonsetting[0]).get() if self.nextonsetting[1](setn): self.nextStep.emit() except ValueError: pass def slotWidgetsSelected(self, widgets, *args): """Go to next page if widget selected.""" if len(widgets) == 1 and widgets[0].name == self.nextonselected: self.nextStep.emit() ########################## ## Introduction to widgets class StepIntro(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Welcome to Veusz!

    This tutorial aims to get you working with Veusz as quickly as possible.

    You can close this tutorial at any time using the close button to the top-right of this panel. The tutorial can be replayed in the help menu.

    Press Next to go to the next step

    '''), mainwin, nextstep=StepWidgets1) class StepWidgets1(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Widgets

    Plots in Veusz are constructed from widgets. Different types of widgets are used to make different parts of a plot. For example, there are widgets for axes, for a graph, for plotting data and for plotting functions.

    There are also special widgets. The grid widget arranges graphs inside it in a grid arrangement.

    '''), mainwin, nextstep=StepWidgets2) class StepWidgets2(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Widget can often be placed inside each other. For instance, a graph widget is placed in a page widget or a grid widget. Plotting widgets are placed in graph widget.

    You can have multiple widgets of different types. For example, you can have several graphs on the page, optionally arranged in a grid. Several plotting widgets and axis widgets can be put in a graph.

    '''), mainwin, nextstep=StepWidgetWin) class StepWidgetWin(TutorialStep): def __init__(self, mainwin): t = mainwin.treeedit TutorialStep.__init__( self, _('''

    Widget editing

    The flashing window is the Editing window, which shows the widgets currently in the plot in a hierarchical tree. Each widget has a name (the left column) and a type (the right column).

    Press Next to continue.

    '''), mainwin, nextstep=StepWidgetWinExpand, flash=t) class StepWidgetWinExpand(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    The graph widget is the currently selected widget.

    Expand the graph widget - click the arrow or plus to its left in the editing window - and select the x axis widget.

    '''), mainwin, disablenext=True, nextonselected='x', nextstep=StepPropertiesWin) class StepPropertiesWin(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Widget properties

    This window shows the properties of the currently selected widget, the x axis widget of the graph.

    Enter a new label for the widget, by clicking in the text edit box to the right of "Label", typing some text and press the Enter key.

    '''), mainwin, flash = mainwin.propdock, disablenext = True, nextonsetting = ('/page1/graph1/x/label', lambda val: val != ''), nextstep = StepPropertiesWin2) class StepPropertiesWin2(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Notice that the x axis label of your plot has now been updated. Veusz supports LaTeX style formatting for labels, so you could include superscripts, subscripts and fractions.

    Other important axis properties include the minimum, maximum values of the axis and whether the axis is logarithmic.

    Click Next to continue.

    '''), mainwin, nextstep=WidgetAdd) class WidgetAdd(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Adding widgets

    The flashing Add Widget toolbar and the Insert menu add widgets to the document. New widgets are inserted in the currently selected widget, if possible, or its parents.

    Hold your mouse pointer over one of the toolbar buttons to see a description of a widget type.

    Press Next to continue.

    '''), mainwin, flash=mainwin.treeedit.addtoolbar, nextstep=FunctionAdd ) class FunctionAdd(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Add a function

    We will now add a function plotting widget to the current graph.

    Click on the flashing icon, or go to the Insert menu and choosing "Add function".

    '''), mainwin, flash=mainwin.treeedit.addtoolbar.widgetForAction( mainwin.vzactions['add.function']), disablenext=True, nextonsetting = ('/page1/graph1/function1/function', lambda val: val != ''), nextstep=FunctionSet) class FunctionSet(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    You have now added a function widget to the graph widget. By default function widgets plot y=x.

    Go to the Function property and change the function to be x**2, plotting x squared.

    (Veusz uses Python syntax for its functions, so the power operator is **, rather than ^)

    '''), mainwin, nextonsetting = ('/page1/graph1/function1/function', lambda val: val.strip() == 'x**2'), disablenext = True, nextstep=FunctionFormatting) class FunctionFormatting(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Formatting

    Widgets have a number of formatting options. The Formatting window (flashing) shows the options for the currently selected widget, here the function widget.

    Press Next to continue

    '''), mainwin, flash=mainwin.formatdock, nextstep=FunctionFormatLine) class FunctionFormatLine(TutorialStep): def __init__(self, mainwin): tb = mainwin.formatdock.tabwidget.tabBar() label = qt4.QLabel(" ", tb) tb.setTabButton(1, qt4.QTabBar.LeftSide, label) TutorialStep.__init__( self, _('''

    Different types of formatting properties are grouped under separate tables. The options for drawing the function line are grouped under the flashing Line tab (%s).

    Click on the Line tab to continue.

    ''') % utils.pixmapAsHtml(utils.getPixmap('settings_plotline.png')), mainwin, flash=label, disablenext=True, nextstep=FunctionLineFormatting) tb.currentChanged[int].connect(self.slotCurrentChanged) def slotCurrentChanged(self, idx): if idx == 1: self.nextStep.emit() class FunctionLineFormatting(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Veusz lets you choose a line style, thickness and color for the function line.

    Choose a new line color for the line.

    '''), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/function1/Line/color', lambda val: val.strip() != 'black'), nextstep=DataStart) ########### ## Datasets class DataStart(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Datasets

    Many widgets in Veusz plot datasets. Datasets can be imported from files, entered manually or created from existing datasets using operations or expressions.

    Imported data can be linked to an external file or embedded in the document.

    Press Next to continue

    '''), mainwin, nextstep=DataImport) class DataImport(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Importing data

    Let us start by importing data.

    Click the flashing Data Import icon, or choose "Import..." From the Data menu.

    '''), mainwin, flash=mainwin.datatoolbar.widgetForAction( mainwin.vzactions['data.import']), disablenext=True, nextstep=DataImportDialog) # make sure we have the default delimiters for k in ( 'importdialog_csvdelimitercombo_HistoryCombo', 'importdialog_csvtextdelimitercombo_HistoryCombo' ): if k in setting.settingdb: del setting.settingdb[k] mainwin.dialogShown.connect(self.slotDialogShown) def slotDialogShown(self, dialog): """Called when a dialog is opened in the main window.""" from ..dialogs.importdialog import ImportDialog if isinstance(dialog, ImportDialog): # make life easy by sticking in filename dialog.filenameedit.setText( os.path.join(utils.exampleDirectory, 'tutorialdata.csv')) # and choosing tab dialog.guessImportTab() # get rid of existing values self.nextStep.emit() class DataImportDialog(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    This is the data import dialog. In this tutorial, we have selected an example CSV (comma separated value) file for you, but you would normally browse to find your data file.

    This example file defines three datasets, alpha, beta and gamma, entered as columns in the CSV file.

    Press Next to continue

    '''), mainwin, nextstep=DataImportDialog2) class DataImportDialog2(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Veusz will try to guess the datatype - numeric, text or date - from the data in the file or you can specify it manually.

    Several different data formats are supported in Veusz and plugins can be defined to import any data format. The Link option links data to the original file.

    Click the Import button in the dialog.

    '''), mainwin, nextstep=DataImportDialog3, disablenext=True) mainwin.document.signalModified.connect(self.slotDocModified) def slotDocModified(self): if 'alpha' in self.mainwin.document.data: self.nextStep.emit() class DataImportDialog3(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Notice how Veusz has loaded the three different datasets from the file. You could carry on importing new datasets from the Import dialog box or reopen it later.

    Close the Import dialog box.

    '''), mainwin, disablenext=True, nextstep=DataImportDialog4) self.timer = qt4.QTimer() self.timer.timeout.connect(self.slotTimeout) self.timer.start(200) def slotTimeout(self): from ..dialogs.importdialog import ImportDialog closed = True for dialog in self.mainwin.dialogs: if isinstance(dialog, ImportDialog): closed = False if closed: # move forward if no import dialog open self.nextStep.emit() class DataImportDialog4(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    The Data viewing window (flashing) shows the currently loaded datasets in the document.

    Hover your mouse over datasets to get information about them. You can see datasets in more detail in the Data Edit dialog box.

    Click Next to continue

    '''), mainwin, flash=mainwin.datadock, nextstep=AddXYPlotter) ############## ## XY plotting class AddXYPlotter(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Plotting data

    The point plotting widget plots datasets loaded in Veusz.

    The flashing icon adds a point plotting (xy) widget. Click on this, or go to the Add menu and choose "Add xy".

    '''), mainwin, flash=mainwin.treeedit.addtoolbar.widgetForAction( mainwin.vzactions['add.xy']), disablenext=True, nextonsetting = ('/page1/graph1/xy1/xData', lambda val: val != ''), nextstep=SetXY_X) class SetXY_X(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    The datasets to be plotted are in the widget's properties.

    Change the "X data" setting to be the alpha dataset. You can choose this from the drop down menu or type it.

    '''), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/xy1/xData', lambda val: val == 'alpha'), nextstep=SetXY_Y) class SetXY_Y(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Change the "Y data" setting to be the beta dataset.

    '''), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/xy1/yData', lambda val: val == 'beta'), nextstep=SetXYLine) class SetXYLine(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Veusz has now plotted the data on the graph. You can manipulate how the data are shown using the formatting settings.

    Make sure that the line Formatting tab (%s) for the widget is selected.

    Click on the check box next to the Hide option at the bottom, to hide the line plotted between the data points.

    ''') % utils.pixmapAsHtml(utils.getPixmap('settings_plotline.png')), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/xy1/PlotLine/hide', lambda val: val), nextstep=SetXYFill) class SetXYFill(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Now we will change the point color.

    Click on the "Marker fill (%s)" formatting tab. Change the fill color of the plotted data.

    ''') % utils.pixmapAsHtml(utils.getPixmap('settings_plotmarkerfill.png')), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/xy1/MarkerFill/color', lambda val: val != 'black'), nextstep=AddXY2nd) class AddXY2nd(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Adding a second dataset

    We will now plot dataset alpha against gamma on the same graph.

    Add a second point plotting (xy) widget using the flashing icon, or go to the Add menu and choose "Add xy".

    '''), mainwin, flash=mainwin.treeedit.addtoolbar.widgetForAction( mainwin.vzactions['add.xy']), disablenext=True, nextonsetting = ('/page1/graph1/xy2/xData', lambda val: val != ''), nextstep=AddXY2nd_2) class AddXY2nd_2(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Change the "X data" setting to be the alpha dataset.

    '''), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/xy2/xData', lambda val: val == 'alpha'), nextstep=AddXY2nd_3) class AddXY2nd_3(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Next, change the "Y data" setting to be the gamma dataset.

    '''), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/xy2/yData', lambda val: val == 'gamma'), nextstep=AddXY2nd_4) class AddXY2nd_4(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    We can fill regions under plots using the Fill Below Formatting tab (%s).

    Go to this tab, and unselect the "Hide edge fill" option.

    ''') % utils.pixmapAsHtml(utils.getPixmap('settings_plotfillbelow.png')), mainwin, disablenext=True, nextonsetting = ('/page1/graph1/xy2/FillBelow/hide', lambda val: not val), nextstep=File1) class File1(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Saving

    The document can be saved under the File menu, choosing "Save as...", or by clicking on the Save icon (flashing).

    Veusz documents are simple text files which can be easily modified outside the program.

    Click Next to continue

    '''), mainwin, flash=mainwin.maintoolbar.widgetForAction( mainwin.vzactions['file.save']), nextstep=File2) class File2(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Exporting

    The document can be exported in scalable (EPS, PDF, SVG and EMF) or bitmap formats.

    The "Export..." command under the File menu exports the selected page. Alternatively, click on the Export icon (flashing).

    Click Next to continue

    '''), mainwin, flash=mainwin.maintoolbar.widgetForAction( mainwin.vzactions['file.export']), nextstep=Cut1, ) class Cut1(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Cut and paste

    Widgets can be cut and pasted to manipulate the document.

    Select the "graph1" widget in the Editing window.

    '''), mainwin, disablenext=True, nextonselected='graph1', nextstep=Cut2) class Cut2(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Now click the Cut icon (flashing) or choose "Cut" from the Edit menu.

    This copies the currently selected widget to the clipboard and deletes it from the document.

    '''), mainwin, disablenext=True, flash=mainwin.treeedit.edittoolbar.widgetForAction( mainwin.vzactions['edit.cut']), nextstep=AddGrid) mainwin.document.signalModified.connect(self.slotCheckDelete) def slotCheckDelete(self, *args): d = self.mainwin.document try: d.resolve(d.basewidget, '/page1/graph1') except ValueError: # success! self.nextStep.emit() class AddGrid(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Adding a grid

    Now we will add a grid widget to paste the graph back into.

    Click on the flashing Grid widget icon, or choose "Add grid" from the Insert menu.

    '''), mainwin, flash=mainwin.treeedit.addtoolbar.widgetForAction( mainwin.vzactions['add.grid']), disablenext=True, nextonsetting = ('/page1/grid1/rows', lambda val: val != ''), nextstep=Paste1) class Paste1(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Now click the Paste icon (flashing) or choose "Paste" from the Edit menu.

    This pastes back the widget from the clipboard.

    '''), mainwin, disablenext=True, flash=mainwin.treeedit.edittoolbar.widgetForAction( mainwin.vzactions['edit.paste']), nextonsetting = ('/page1/grid1/graph1/leftMargin', lambda val: val != ''), nextstep=Paste2) class Paste2(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    For a second time, click the Paste icon (flashing) or choose "Paste" from the Edit menu.

    This adds a second copy of the original graph to the grid.

    '''), mainwin, disablenext=True, flash=mainwin.treeedit.edittoolbar.widgetForAction( mainwin.vzactions['edit.paste']), nextonsetting = ('/page1/grid1/graph2/leftMargin', lambda val: val != ''), nextstep=Paste3) class Paste3(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    Having the graphs side-by-side looks a bit messy. We would like to change the graphs to be arranged in rows.

    Navigate to the grid1 widget properties. Change the number of columns to 1.

    '''), mainwin, disablenext=True, nextonsetting = ('/page1/grid1/columns', lambda val: val == 1), nextstep=Paste4) class Paste4(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    We could now adjust the margins of the graphs and the grid.

    Axes can also be shared by the graphs of the grid by moving them into the grid widget. This shares the same axis scale for graphs.

    Click Next to continue

    '''), mainwin, nextstep=EndStep) class EndStep(TutorialStep): def __init__(self, mainwin): TutorialStep.__init__( self, _('''

    The End

    Thank you for working through this Veusz tutorial. We hope you enjoy using Veusz!

    Please send comments, bug reports and suggestions to the developers via the mailing list.

    You can try this tutorial again from the Help menu.

    '''), mainwin, closestep=True, disablenext=True) class TutorialDock(qt4.QDockWidget): '''A dock tutorial window.''' def __init__(self, document, mainwin, *args): qt4.QDockWidget.__init__(self, *args) self.setAttribute(qt4.Qt.WA_DeleteOnClose) self.setMinimumHeight(300) self.setWindowTitle('Tutorial - Veusz') self.setObjectName('veusztutorialwindow') self.setStyleSheet('background: lightyellow;') self.document = document self.mainwin = mainwin self.layout = l = qt4.QVBoxLayout() txtdoc = qt4.QTextDocument(self) txtdoc.setDefaultStyleSheet( "p.usercmd { color: blue; } " "h1 { font-size: x-large;} " "code { color: green;} " ) self.textedit = qt4.QTextEdit(readOnly=True) self.textedit.setDocument(txtdoc) l.addWidget(self.textedit) self.buttonbox = qt4.QDialogButtonBox() self.nextb = self.buttonbox.addButton( 'Next', qt4.QDialogButtonBox.ActionRole) self.nextb.clicked.connect(self.slotNext) l.addWidget(self.buttonbox) # have to use a separate widget as dialog already has layout self.widget = qt4.QWidget() self.widget.setLayout(l) self.setWidget(self.widget) # timer for controlling flashing self.flashtimer = qt4.QTimer(self) self.flashtimer.timeout.connect(self.slotFlashTimeout) self.flash = self.oldflash = None self.flashon = False self.flashct = 0 self.flashtimer.start(500) self.changeStep(StepIntro) def ensureShowFlashWidgets(self): '''Ensure we can see the widgets flashing.''' w = self.flash while w is not None: w.show() w = w.parent() def changeStep(self, stepklass): '''Apply the next step.''' # this is the current text self.step = stepklass(self.mainwin) # listen to step for next step self.step.nextStep.connect(self.slotNext) # update text self.textedit.setHtml(self.step.text) # handle requests for flashing self.flashct = 20 self.flashon = True self.flash = self.step.flash if self.flash is not None: self.ensureShowFlashWidgets() # enable/disable next button self.nextb.setEnabled(not self.step.disablenext) # add a close button if requested if self.step.closestep: closeb = self.buttonbox.addButton( 'Close', qt4.QDialogButtonBox.ActionRole) closeb.clicked.connect(self.close) # work around C/C++ object deleted @qt4.pyqtSlot() def slotFlashTimeout(self): '''Handle flashing of UI components.''' # because we're flashing random UI components, the C++ object # might be deleted, so we have to check before doing things to # it: hence the qt4.isdeleted if ( self.flash is not self.oldflash and self.oldflash is not None and not qt4.isdeleted(self.oldflash) ): # clear any flashing on previous widget self.oldflash.setStyleSheet('') self.oldflash = None if self.flash is not None and not qt4.isdeleted(self.flash): # set flash state and toggle variable if self.flashon: self.flash.setStyleSheet('background: yellow;') else: self.flash.setStyleSheet('') self.flashon = not self.flashon self.oldflash = self.flash # stop flashing after N iterations self.flashct -= 1 if self.flashct == 0: self.flash = None def slotNext(self): """Move to the next page of the tutorial.""" nextstepklass = self.step.nextstep if nextstepklass is not None: self.changeStep( nextstepklass ) veusz-1.21.1/veusz/windows/simplewindow.py0000664000175000017500000000513212271770356017106 0ustar jssjss# Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division from .. import qtall as qt4 from .. import document from .. import dataimport from . import plotwindow """ A simple window class for wrapping a plotwindow """ class SimpleWindow(qt4.QMainWindow): """ The main window class for the application.""" def __init__(self, title, doc=None): qt4.QMainWindow.__init__(self) self.setWindowTitle(title) self.document = doc if not doc: self.document = document.Document() self.plot = plotwindow.PlotWindow(self.document, self) self.toolbar = None self.setCentralWidget( self.plot ) def enableToolbar(self, enable=True): """Enable or disable the zoom toolbar in this window.""" if self.toolbar is None and enable: self.toolbar = self.plot.createToolbar(self, None) self.toolbar.show() if self.toolbar is not None and not enable: self.toolbar.close() self.toolbar = None def setZoom(self, zoom): """Zoom(zoom) Set the plot zoom level: This is a number to for the zoom from 1:1 or 'page': zoom to page 'width': zoom to fit width 'height': zoom to fit height """ if zoom == 'page': self.plot.slotViewZoomPage() elif zoom == 'width': self.plot.slotViewZoomWidth() elif zoom == 'height': self.plot.slotViewZoomHeight() else: self.plot.setZoomFactor(zoom) def setAntiAliasing(self, ison): """AntiAliasing(ison) Switches on or off anti aliasing in the plot.""" self.plot.antialias = ison self.plot.actionForceUpdate() veusz-1.21.1/veusz/windows/__init__.py0000664000175000017500000000165712237406466016134 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Veusz windows module.""" veusz-1.21.1/veusz/windows/mainwindow.py0000644000175000017500000014030112327177747016544 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2003 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Implements the main window of the application.""" from __future__ import division, print_function import os.path import sys import glob import re try: import h5py except ImportError: h5py = None from ..compat import cstr, cstrerror from .. import qtall as qt4 from .. import document from .. import utils from ..utils import vzdbus from .. import setting from .. import plugins from . import consolewindow from . import plotwindow from . import treeeditwindow from .datanavigator import DataNavigatorWindow from ..dialogs.aboutdialog import AboutDialog from ..dialogs.reloaddata import ReloadData from ..dialogs.datacreate import DataCreateDialog from ..dialogs.datacreate2d import DataCreate2DDialog from ..dialogs.preferences import PreferencesDialog from ..dialogs.errorloading import ErrorLoadingDialog from ..dialogs.capturedialog import CaptureDialog from ..dialogs.stylesheet import StylesheetDialog from ..dialogs.custom import CustomDialog from ..dialogs.safetyimport import SafetyImportDialog from ..dialogs.histodata import HistoDataDialog from ..dialogs.plugin import handlePlugin from ..dialogs import importdialog from ..dialogs import dataeditdialog def _(text, disambiguation=None, context='MainWindow'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) # shortcut to this setdb = setting.settingdb class DBusWinInterface(vzdbus.Object): """Simple DBus interface to window for triggering actions.""" interface = 'org.veusz.actions' def __init__(self, actions, index): prefix = '/Windows/%i/Actions' % index vzdbus.Object.__init__(self, vzdbus.sessionbus, prefix) self.actions = actions @vzdbus.method(dbus_interface=interface, out_signature='as') def GetActions(self): """Get list of actions which can be activated.""" return sorted(self.actions) @vzdbus.method(dbus_interface=interface, in_signature='s') def TriggerAction(self, action): """Activate action given.""" self.actions[action].trigger() class MainWindow(qt4.QMainWindow): """ The main window class for the application.""" # this is emitted when a dialog is opened by the main window dialogShown = qt4.pyqtSignal(qt4.QWidget) # emitted when a document is opened documentOpened = qt4.pyqtSignal() windows = [] @classmethod def CreateWindow(cls, filename=None): """Window factory function. If filename is given then that file is loaded into the window. Returns window created """ # create the window, and optionally load a saved file win = cls() win.show() if filename: # load document win.openFileInWindow(filename) else: win.setupDefaultDoc() # try to select first graph of first page win.treeedit.doInitialWidgetSelect() cls.windows.append(win) # check if tutorial wanted if not setting.settingdb['ask_tutorial']: win.askTutorial() # don't ask again setting.settingdb['ask_tutorial'] = True return win def __init__(self, *args): qt4.QMainWindow.__init__(self, *args) self.setAcceptDrops(True) # icon and different size variations self.setWindowIcon( utils.getIcon('veusz') ) # master documenent self.document = document.Document() # filename for document and update titlebar self.filename = '' self.updateTitlebar() # keep a list of references to dialogs self.dialogs = [] # construct menus and toolbars self._defineMenus() # make plot window self.plot = plotwindow.PlotWindow(self.document, self, menu = self.menus['view']) self.setCentralWidget(self.plot) self.plot.showToolbar() # likewise with the tree-editing window self.treeedit = treeeditwindow.TreeEditDock(self.document, self) self.addDockWidget(qt4.Qt.LeftDockWidgetArea, self.treeedit) self.propdock = treeeditwindow.PropertiesDock(self.document, self.treeedit, self) self.addDockWidget(qt4.Qt.LeftDockWidgetArea, self.propdock) self.formatdock = treeeditwindow.FormatDock(self.document, self.treeedit, self) self.addDockWidget(qt4.Qt.LeftDockWidgetArea, self.formatdock) self.datadock = DataNavigatorWindow(self.document, self, self) self.addDockWidget(qt4.Qt.RightDockWidgetArea, self.datadock) # make the console window a dock self.console = consolewindow.ConsoleWindow(self.document, self) self.console.hide() self.interpreter = self.console.interpreter self.addDockWidget(qt4.Qt.BottomDockWidgetArea, self.console) # assemble the statusbar statusbar = self.statusbar = qt4.QStatusBar(self) self.setStatusBar(statusbar) self.updateStatusbar(_('Ready')) # a label for the picker readout self.pickerlabel = qt4.QLabel(statusbar) self._setPickerFont(self.pickerlabel) statusbar.addPermanentWidget(self.pickerlabel) self.pickerlabel.hide() # plot queue - how many plots are currently being drawn self.plotqueuecount = 0 self.plot.sigQueueChange.connect(self.plotQueueChanged) self.plotqueuelabel = qt4.QLabel() self.plotqueuelabel.setToolTip(_("Number of rendering jobs remaining")) statusbar.addWidget(self.plotqueuelabel) self.plotqueuelabel.show() # a label for the cursor position readout self.axisvalueslabel = qt4.QLabel(statusbar) statusbar.addPermanentWidget(self.axisvalueslabel) self.axisvalueslabel.show() self.slotUpdateAxisValues(None) # a label for the page number readout self.pagelabel = qt4.QLabel(statusbar) statusbar.addPermanentWidget(self.pagelabel) self.pagelabel.show() # working directory - use previous one self.dirname = setdb.get('dirname', qt4.QDir.homePath()) self.dirname_export = setdb.get('dirname_export', self.dirname) if setdb['dirname_usecwd']: self.dirname = self.dirname_export = os.getcwd() # connect plot signals to main window self.plot.sigUpdatePage.connect(self.slotUpdatePage) self.plot.sigAxisValuesFromMouse.connect(self.slotUpdateAxisValues) self.plot.sigPickerEnabled.connect(self.slotPickerEnabled) self.plot.sigPointPicked.connect(self.slotUpdatePickerLabel) # disable save if already saved self.document.signalModified.connect(self.slotModifiedDoc) # if the treeeditwindow changes the page, change the plot window self.treeedit.sigPageChanged.connect(self.plot.setPageNumber) # if a widget in the plot window is clicked by the user self.plot.sigWidgetClicked.connect(self.treeedit.selectWidget) self.treeedit.widgetsSelected.connect(self.plot.selectedWidgets) # enable/disable undo/redo self.menus['edit'].aboutToShow.connect(self.slotAboutToShowEdit) #Get the list of recently opened files self.populateRecentFiles() self.setupWindowGeometry() self.defineViewWindowMenu() # if document requests it, ask whether an allowed import self.document.sigAllowedImports.connect(self.slotAllowedImportsDoc) # add on dbus interface self.dbusdocinterface = document.DBusInterface(self.document) self.dbuswininterface = DBusWinInterface( self.vzactions, self.dbusdocinterface.index) # has the document already been setup self.documentsetup = False def updateStatusbar(self, text): '''Display text for a set period.''' self.statusBar().showMessage(text, 2000) def dragEnterEvent(self, event): """Check whether event is valid to be dropped.""" if (event.provides("text/uri-list") and self._getVeuszDropFiles(event)): event.acceptProposedAction() def dropEvent(self, event): """Respond to a drop event on the current window""" if event.provides("text/uri-list"): files = self._getVeuszDropFiles(event) if files: if self.document.isBlank(): self.openFileInWindow(files[0]) else: self.CreateWindow(files[0]) for filename in files[1:]: self.CreateWindow(filename) def _getVeuszDropFiles(self, event): """Return a list of veusz files from a drag/drop event containing a text/uri-list""" mime = event.mimeData() if not mime.hasUrls(): return [] else: # get list of vsz files dropped urls = [u.path() for u in mime.urls()] urls = [u for u in urls if os.path.splitext(u)[1] == '.vsz'] return urls def setupDefaultDoc(self): """Setup default document.""" if not self.documentsetup: # add page and default graph self.document.makeDefaultDoc() # load defaults if set self.loadDefaultStylesheet() self.loadDefaultCustomDefinitions() # done setup self.documentsetup = True def loadDefaultStylesheet(self): """Loads the default stylesheet for the new document.""" filename = setdb['stylesheet_default'] if filename: try: self.document.applyOperation( document.OperationLoadStyleSheet(filename) ) except EnvironmentError as e: qt4.QMessageBox.warning( self, _("Error - Veusz"), _("Unable to load default stylesheet '%s'\n\n%s") % (filename, cstrerror(e))) else: # reset any modified flag self.document.setModified(False) self.document.changeset = 0 def loadDefaultCustomDefinitions(self): """Loads the custom definitions for the new document.""" filename = setdb['custom_default'] if filename: try: self.document.applyOperation( document.OperationLoadCustom(filename) ) except EnvironmentError as e: qt4.QMessageBox.warning( self, _("Error - Veusz"), _("Unable to load custom definitions '%s'\n\n%s") % (filename, cstrerror(e))) else: # reset any modified flag self.document.setModified(False) self.document.changeset = 0 def slotAboutToShowEdit(self): """Enable/disable undo/redo menu items.""" # enable distable, and add appropriate text to describe # the operation being undone/redone canundo = self.document.canUndo() undotext = _('Undo') if canundo: undotext = "%s %s" % (undotext, self.document.historyundo[-1].descr) self.vzactions['edit.undo'].setText(undotext) self.vzactions['edit.undo'].setEnabled(canundo) canredo = self.document.canRedo() redotext = _('Redo') if canredo: redotext = "%s %s" % (redotext, self.document.historyredo[-1].descr) self.vzactions['edit.redo'].setText(redotext) self.vzactions['edit.redo'].setEnabled(canredo) def slotEditUndo(self): """Undo the previous operation""" if self.document.canUndo(): self.document.undoOperation() self.treeedit.checkWidgetSelected() def slotEditRedo(self): """Redo the previous operation""" if self.document.canRedo(): self.document.redoOperation() def slotEditPreferences(self): dialog = PreferencesDialog(self) dialog.exec_() def slotEditStylesheet(self): dialog = StylesheetDialog(self, self.document) self.showDialog(dialog) return dialog def slotEditCustom(self): dialog = CustomDialog(self, self.document) self.showDialog(dialog) return dialog def definePlugins(self, pluginlist, actions, menuname): """Create menu items and actions for plugins. pluginlist: list of plugin classes actions: dict of actions to add new actions to menuname: string giving prefix for new menu entries (inside actions) """ menu = [] for pluginkls in pluginlist: def loaddialog(pluginkls=pluginkls): """Load plugin dialog""" handlePlugin(self, self.document, pluginkls) actname = menuname + '.' + '.'.join(pluginkls.menu) text = pluginkls.menu[-1] if pluginkls.has_parameters: text += '...' actions[actname] = utils.makeAction( self, pluginkls.description_short, text, loaddialog) # build up menu from tuple of names menulook = menu namebuild = [menuname] for cmpt in pluginkls.menu[:-1]: namebuild.append(cmpt) name = '.'.join(namebuild) for c in menulook: if c[0] == name: menulook = c[2] break else: menulook.append( [name, cmpt, []] ) menulook = menulook[-1][2] menulook.append(actname) return menu def _defineMenus(self): """Initialise the menus and toolbar.""" # these are actions for main menu toolbars and menus a = utils.makeAction self.vzactions = { 'file.new': a(self, _('New document'), _('&New'), self.slotFileNew, icon='kde-document-new', key='Ctrl+N'), 'file.open': a(self, _('Open a document'), _('&Open...'), self.slotFileOpen, icon='kde-document-open', key='Ctrl+O'), 'file.save': a(self, _('Save the document'), _('&Save'), self.slotFileSave, icon='kde-document-save', key='Ctrl+S'), 'file.saveas': a(self, _('Save the current graph under a new name'), _('Save &As...'), self.slotFileSaveAs, icon='kde-document-save-as'), 'file.print': a(self, _('Print the document'), _('&Print...'), self.slotFilePrint, icon='kde-document-print', key='Ctrl+P'), 'file.export': a(self, _('Export the current page'), _('&Export...'), self.slotFileExport, icon='kde-document-export'), 'file.close': a(self, _('Close current window'), _('Close Window'), self.slotFileClose, icon='kde-window-close', key='Ctrl+W'), 'file.quit': a(self, _('Exit the program'), _('&Quit'), self.slotFileQuit, icon='kde-application-exit', key='Ctrl+Q'), 'edit.undo': a(self, _('Undo the previous operation'), _('Undo'), self.slotEditUndo, icon='kde-edit-undo', key='Ctrl+Z'), 'edit.redo': a(self, _('Redo the previous operation'), _('Redo'), self.slotEditRedo, icon='kde-edit-redo', key='Ctrl+Shift+Z'), 'edit.prefs': a(self, _('Edit preferences'), _('Preferences...'), self.slotEditPreferences, icon='veusz-edit-prefs'), 'edit.custom': a(self, _('Edit custom functions and constants'), _('Custom definitions...'), self.slotEditCustom, icon='veusz-edit-custom'), 'edit.stylesheet': a(self, _('Edit stylesheet to change default widget settings'), _('Default styles...'), self.slotEditStylesheet, icon='settings_stylesheet'), 'view.edit': a(self, _('Show or hide edit window'), _('Edit window'), None, checkable=True), 'view.props': a(self, _('Show or hide property window'), _('Properties window'), None, checkable=True), 'view.format': a(self, _('Show or hide formatting window'), _('Formatting window'), None, checkable=True), 'view.console': a(self, _('Show or hide console window'), _('Console window'), None, checkable=True), 'view.datanav': a(self, _('Show or hide data navigator window'), _('Data navigator window'), None, checkable=True), 'view.maintool': a(self, _('Show or hide main toolbar'), _('Main toolbar'), None, checkable=True), 'view.datatool': a(self, _('Show or hide data toolbar'), _('Data toolbar'), None, checkable=True), 'view.viewtool': a(self, _('Show or hide view toolbar'), _('View toolbar'), None, checkable=True), 'view.edittool': a(self, _('Show or hide editing toolbar'), _('Editing toolbar'), None, checkable=True), 'view.addtool': a(self, _('Show or hide insert toolbar'), _('Insert toolbar'), None, checkable=True), 'data.import': a(self, _('Import data into Veusz'), _('&Import...'), self.slotDataImport, icon='kde-vzdata-import'), 'data.edit': a(self, _('Edit and enter new datasets'), _('&Editor...'), self.slotDataEdit, icon='kde-edit-veuszedit'), 'data.create': a(self, _('Create new datasets using ranges, parametrically or as functions of existing datasets'), _('&Create...'), self.slotDataCreate, icon='kde-dataset-new-veuszedit'), 'data.create2d': a(self, _('Create new 2D datasets from existing datasets, or as a function of x and y'), _('Create &2D...'), self.slotDataCreate2D, icon='kde-dataset2d-new-veuszedit'), 'data.capture': a(self, _('Capture remote data'), _('Ca&pture...'), self.slotDataCapture, icon='veusz-capture-data'), 'data.histogram': a(self, _('Histogram data'), _('&Histogram...'), self.slotDataHistogram, icon='button_bar'), 'data.reload': a(self, _('Reload linked datasets'), _('&Reload'), self.slotDataReload, icon='kde-view-refresh'), 'help.home': a(self, _('Go to the Veusz home page on the internet'), _('Home page'), self.slotHelpHomepage), 'help.project': a(self, _('Go to the Veusz project page on the internet'), _('GNA Project page'), self.slotHelpProjectPage), 'help.bug': a(self, _('Report a bug on the internet'), _('Suggestions and bugs'), self.slotHelpBug), 'help.tutorial': a(self, _('An interactive Veusz tutorial'), _('Tutorial'), self.slotHelpTutorial), 'help.about': a(self, _('Displays information about the program'), _('About...'), self.slotHelpAbout, icon='veusz') } # create main toolbar tb = self.maintoolbar = qt4.QToolBar(_("Main toolbar - Veusz"), self) iconsize = setdb['toolbar_size'] tb.setIconSize(qt4.QSize(iconsize, iconsize)) tb.setObjectName('veuszmaintoolbar') self.addToolBar(qt4.Qt.TopToolBarArea, tb) utils.addToolbarActions(tb, self.vzactions, ('file.new', 'file.open', 'file.save', 'file.print', 'file.export')) # data toolbar tb = self.datatoolbar = qt4.QToolBar(_("Data toolbar - Veusz"), self) tb.setIconSize(qt4.QSize(iconsize, iconsize)) tb.setObjectName('veuszdatatoolbar') self.addToolBar(qt4.Qt.TopToolBarArea, tb) utils.addToolbarActions(tb, self.vzactions, ('data.import', 'data.edit', 'data.create', 'data.capture', 'data.reload')) # menu structure filemenu = [ 'file.new', 'file.open', ['file.filerecent', _('Open &Recent'), []], '', 'file.save', 'file.saveas', '', 'file.print', 'file.export', '', 'file.close', 'file.quit' ] editmenu = [ 'edit.undo', 'edit.redo', '', ['edit.select', _('&Select'), []], '', 'edit.prefs', 'edit.stylesheet', 'edit.custom', '' ] viewwindowsmenu = [ 'view.edit', 'view.props', 'view.format', 'view.console', 'view.datanav', '', 'view.maintool', 'view.viewtool', 'view.addtool', 'view.edittool' ] viewmenu = [ ['view.viewwindows', _('&Windows'), viewwindowsmenu], '' ] insertmenu = [ ] # load dataset plugins and create menu datapluginsmenu = self.definePlugins( plugins.datasetpluginregistry, self.vzactions, 'data.ops' ) datamenu = [ ['data.ops', _('&Operations'), datapluginsmenu], 'data.import', 'data.edit', 'data.create', 'data.create2d', 'data.capture', 'data.histogram', 'data.reload', ] helpmenu = [ 'help.home', 'help.project', 'help.bug', '', 'help.tutorial', '', ['help.examples', _('&Example documents'), []], '', 'help.about' ] # load tools plugins and create menu toolsmenu = self.definePlugins( plugins.toolspluginregistry, self.vzactions, 'tools' ) menus = [ ['file', _('&File'), filemenu], ['edit', _('&Edit'), editmenu], ['view', _('&View'), viewmenu], ['insert', _('&Insert'), insertmenu], ['data', _('&Data'), datamenu], ['tools', _('&Tools'), toolsmenu], ['help', _('&Help'), helpmenu], ] self.menus = {} utils.constructMenus(self.menuBar(), self.menus, menus, self.vzactions) self.populateExamplesMenu() def _setPickerFont(self, label): f = label.font() f.setBold(True) f.setPointSizeF(f.pointSizeF() * 1.2) label.setFont(f) def populateExamplesMenu(self): """Add examples to help menu.""" examples = glob.glob(os.path.join(utils.exampleDirectory, '*.vsz')) menu = self.menus["help.examples"] for ex in sorted(examples): name = os.path.splitext(os.path.basename(ex))[0] def _openexample(ex=ex): MainWindow.CreateWindow(ex) a = menu.addAction(name, _openexample) a.setStatusTip(_("Open %s example document") % name) def defineViewWindowMenu(self): """Setup View -> Window menu.""" def viewHideWindow(window): """Toggle window visibility.""" w = window def f(): w.setVisible(not w.isVisible()) return f # set whether windows are visible and connect up to toggle windows self.viewwinfns = [] for win, act in ((self.treeedit, 'view.edit'), (self.propdock, 'view.props'), (self.formatdock, 'view.format'), (self.console, 'view.console'), (self.datadock, 'view.datanav'), (self.maintoolbar, 'view.maintool'), (self.datatoolbar, 'view.datatool'), (self.treeedit.edittoolbar, 'view.edittool'), (self.treeedit.addtoolbar, 'view.addtool'), (self.plot.viewtoolbar, 'view.viewtool')): a = self.vzactions[act] fn = viewHideWindow(win) self.viewwinfns.append( (win, a, fn) ) a.triggered.connect(fn) # needs to update state every time menu is shown self.menus['view.viewwindows'].aboutToShow.connect( self.slotAboutToShowViewWindow) def slotAboutToShowViewWindow(self): """Enable/disable View->Window item check boxes.""" for win, act, fn in self.viewwinfns: act.setChecked(not win.isHidden()) def showDialog(self, dialog): """Show dialog given.""" dialog.dialogFinished.connect(self.deleteDialog) self.dialogs.append(dialog) dialog.show() self.dialogShown.emit(dialog) def deleteDialog(self, dialog): """Remove dialog from list of dialogs.""" try: idx = self.dialogs.index(dialog) del self.dialogs[idx] except ValueError: pass def slotDataImport(self): """Display the import data dialog.""" dialog = importdialog.ImportDialog(self, self.document) self.showDialog(dialog) return dialog def slotDataEdit(self, editdataset=None): """Edit existing datasets. If editdataset is set to a dataset name, edit this dataset """ dialog = dataeditdialog.DataEditDialog(self, self.document) self.showDialog(dialog) if editdataset is not None: dialog.selectDataset(editdataset) return dialog def slotDataCreate(self): """Create new datasets.""" dialog = DataCreateDialog(self, self.document) self.showDialog(dialog) return dialog def slotDataCreate2D(self): """Create new datasets.""" dialog = DataCreate2DDialog(self, self.document) self.showDialog(dialog) return dialog def slotDataCapture(self): """Capture remote data.""" dialog = CaptureDialog(self.document, self) self.showDialog(dialog) return dialog def slotDataHistogram(self): """Histogram data.""" dialog = HistoDataDialog(self, self.document) self.showDialog(dialog) return dialog def slotDataReload(self): """Reload linked datasets.""" dialog = ReloadData(self.document, self) self.showDialog(dialog) return dialog def slotHelpHomepage(self): """Go to the veusz homepage.""" qt4.QDesktopServices.openUrl(qt4.QUrl('http://home.gna.org/veusz/')) def slotHelpProjectPage(self): """Go to the veusz project page.""" qt4.QDesktopServices.openUrl(qt4.QUrl('http://gna.org/projects/veusz/')) def slotHelpBug(self): """Go to the veusz bug page.""" qt4.QDesktopServices.openUrl( qt4.QUrl('https://gna.org/bugs/?group=veusz') ) def askTutorial(self): """Ask if tutorial wanted.""" retn = qt4.QMessageBox.question( self, _("Veusz Tutorial"), _("Veusz includes a tutorial to help get you started.\n" "Would you like to start the tutorial now?\n" "If not, you can access it later through the Help menu."), qt4.QMessageBox.Yes | qt4.QMessageBox.No ) if retn == qt4.QMessageBox.Yes: self.slotHelpTutorial() def slotHelpTutorial(self): """Show a Veusz tutorial.""" if self.document.isBlank(): # run the tutorial from .tutorial import TutorialDock tutdock = TutorialDock(self.document, self, self) self.addDockWidget(qt4.Qt.RightDockWidgetArea, tutdock) tutdock.show() else: # open up a blank window for tutorial win = self.CreateWindow() win.slotHelpTutorial() def slotHelpAbout(self): """Show about dialog.""" AboutDialog(self).exec_() def queryOverwrite(self): """Do you want to overwrite the current document. Returns qt4.QMessageBox.(Yes,No,Cancel).""" # include filename in mesage box if we can filetext = '' if self.filename: filetext = " '%s'" % os.path.basename(self.filename) # show message box mb = qt4.QMessageBox(_("Save file?"), _("Document%s was modified. Save first?") % filetext, qt4.QMessageBox.Warning, qt4.QMessageBox.Yes | qt4.QMessageBox.Default, qt4.QMessageBox.No, qt4.QMessageBox.Cancel | qt4.QMessageBox.Escape, self) mb.setButtonText(qt4.QMessageBox.Yes, _("&Save")) mb.setButtonText(qt4.QMessageBox.No, _("&Discard")) mb.setButtonText(qt4.QMessageBox.Cancel, _("&Cancel")) return mb.exec_() def closeEvent(self, event): """Before closing, check whether we need to save first.""" # if the document has been modified then query user for saving if self.document.isModified(): v = self.queryOverwrite() if v == qt4.QMessageBox.Cancel: event.ignore() return elif v == qt4.QMessageBox.Yes: self.slotFileSave() # store working directory setdb['dirname'] = self.dirname setdb['dirname_export'] = self.dirname_export # store the current geometry in the settings database geometry = ( self.x(), self.y(), self.width(), self.height() ) setdb['geometry_mainwindow'] = geometry # store docked windows data = str(self.saveState()) setdb['geometry_mainwindowstate'] = data # save current setting db setdb.writeSettings() event.accept() def setupWindowGeometry(self): """Restoring window geometry if possible.""" # count number of main windows shown nummain = 0 for w in qt4.qApp.topLevelWidgets(): if isinstance(w, qt4.QMainWindow): nummain += 1 # if we can restore the geometry, do so if 'geometry_mainwindow' in setdb: geometry = setdb['geometry_mainwindow'] self.resize( qt4.QSize(geometry[2], geometry[3]) ) if nummain <= 1: geomrect = qt4.QApplication.desktop().availableGeometry() newpos = qt4.QPoint(geometry[0], geometry[1]) if geomrect.contains(newpos): self.move(newpos) # restore docked window geometry if 'geometry_mainwindowstate' in setdb: b = qt4.QByteArray(setdb['geometry_mainwindowstate']) self.restoreState(b) def slotFileNew(self): """New file.""" self.CreateWindow() def slotFileSave(self): """Save file.""" if self.filename == '': self.slotFileSaveAs() else: # show busy cursor qt4.QApplication.setOverrideCursor( qt4.QCursor(qt4.Qt.WaitCursor) ) try: ext = os.path.splitext(self.filename)[1] mode = 'hdf5' if ext == '.vszh5' else 'vsz' self.document.save(self.filename, mode) self.updateStatusbar(_("Saved to %s") % self.filename) except EnvironmentError as e: qt4.QApplication.restoreOverrideCursor() qt4.QMessageBox.critical( self, _("Error - Veusz"), _("Unable to save document as '%s'\n\n%s") % (self.filename, cstrerror(e))) else: # restore the cursor qt4.QApplication.restoreOverrideCursor() def updateTitlebar(self): """Put the filename into the title bar.""" if self.filename == '': self.setWindowTitle(_('Untitled - Veusz')) else: self.setWindowTitle( _("%s - Veusz") % os.path.basename(self.filename) ) def plotQueueChanged(self, incr): self.plotqueuecount += incr text = u'•' * self.plotqueuecount self.plotqueuelabel.setText(text) def fileSaveDialog(self, filters, dialogtitle): """A generic file save dialog for exporting / saving. filters: list of filters """ fd = qt4.QFileDialog(self, dialogtitle) fd.setDirectory(self.dirname) fd.setFileMode(qt4.QFileDialog.AnyFile) fd.setAcceptMode(qt4.QFileDialog.AcceptSave) fd.setNameFilters(filters) # selected filetype is saved under a key constructed here filetype_re = re.compile(r'.*\(\*\.([a-z0-9]+)\)') filtertypes = [filetype_re.match(f).group(1) for f in filters] filterkey = '_'.join(['filterdefault'] + filtertypes) if filterkey in setting.settingdb: filter = setting.settingdb[filterkey] if filter in filters: fd.selectNameFilter(filter) # okay was selected (and is okay to overwrite if it exists) if fd.exec_() == qt4.QDialog.Accepted: # save directory for next time self.dirname = fd.directory().absolutePath() # update the edit box filename = fd.selectedFiles()[0] filetype = filetype_re.match(fd.selectedNameFilter()).group(1) if os.path.splitext(filename)[1][1:] != filetype: filename += '.' + filetype setting.settingdb[filterkey] = fd.selectedNameFilter() return filename return None def fileOpenDialog(self, filters, dialogtitle): """Display an open dialog and return a filename. filters: list of filters in format "Filetype (*.vsz)" """ fd = qt4.QFileDialog(self, dialogtitle) fd.setDirectory(self.dirname) fd.setFileMode( qt4.QFileDialog.ExistingFile ) fd.setAcceptMode( qt4.QFileDialog.AcceptOpen ) fd.setNameFilters(filters) # if the user chooses a file if fd.exec_() == qt4.QDialog.Accepted: # save directory for next time self.dirname = fd.directory().absolutePath() filename = fd.selectedFiles()[0] try: with open(filename): pass except EnvironmentError as e: qt4.QMessageBox.critical( self, _("Error - Veusz"), _("Unable to open '%s'\n\n%s") % (filename, cstrerror(e))) return None return filename return None def slotFileSaveAs(self): """Save As file.""" filters = [_('Veusz document files (*.vsz)')] if h5py is not None: filters += [_('Veusz HDF5 document files (*.vszh5)')] filename = self.fileSaveDialog(filters, _('Save as')) if filename: self.filename = filename self.updateTitlebar() self.slotFileSave() def openFile(self, filename): """Select whether to load the file in the current window or in a blank window and calls the appropriate loader""" if self.document.isBlank(): # If the file is new and there are no modifications, # reuse the current window self.openFileInWindow(filename) else: # create a new window self.CreateWindow(filename) def loadDocument(self, filename): """Load a Veusz document. Return True if loaded ok """ class _unsafeCmdMsgBox(qt4.QMessageBox): """Show document is unsafe.""" def __init__(self, window): qt4.QMessageBox.__init__( self, _("Unsafe code in document"), _("The document '%s' contains potentially unsafe code " "which may damage your computer or data. Please check " "that the file comes from a trusted source.") % filename, qt4.QMessageBox.Warning, qt4.QMessageBox.Yes, qt4.QMessageBox.No | qt4.QMessageBox.Default, qt4.QMessageBox.NoButton, window) self.setButtonText(qt4.QMessageBox.Yes, _("C&ontinue anyway")) self.setButtonText(qt4.QMessageBox.No, _("&Stop loading")) def _callbackunsafe(): qt4.QApplication.restoreOverrideCursor() v = _unsafeCmdMsgBox(self).exec_() qt4.QApplication.setOverrideCursor( qt4.QCursor(qt4.Qt.WaitCursor) ) return v == qt4.QMessageBox.Yes # save stdout and stderr, then redirect to console stdout, stderr = sys.stdout, sys.stderr sys.stdout = self.console.con_stdout sys.stderr = self.console.con_stderr qt4.QApplication.setOverrideCursor( qt4.QCursor(qt4.Qt.WaitCursor) ) try: # get loading mode ext = os.path.splitext(filename)[1].lower() if ext in ('.vsz', '.py'): mode = 'vsz' elif ext in ('.h5', '.hdf5', '.he5', '.vszh5'): mode = 'hdf5' else: raise document.LoadError( _("Did not recognise file type '%s'") % ext) # do the actual loading self.document.load( filename, mode=mode, callbackunsafe=_callbackunsafe) except document.LoadError as e: qt4.QApplication.restoreOverrideCursor() if e.backtrace: d = ErrorLoadingDialog(self, filename, cstr(e), e.backtrace) d.exec_() else: qt4.QMessageBox.critical( self, _("Error opening %s - Veusz") % filename, cstr(e)) return False qt4.QApplication.restoreOverrideCursor() # need to remember to restore stdout, stderr sys.stdout, sys.stderr = stdout, stderr self.documentsetup = True return True def openFileInWindow(self, filename): """Actually do the work of loading a new document. """ ok = self.loadDocument(filename) if not ok: return # remember file for recent list self.addRecentFile(filename) # let the main window know self.filename = filename self.updateTitlebar() self.updateStatusbar(_("Opened %s") % filename) # use current directory of file if not using cwd mode if not setdb['dirname_usecwd']: self.dirname = os.path.dirname( os.path.abspath(filename) ) self.dirname_export = self.dirname # notify cmpts which need notification that doc has finished opening self.documentOpened.emit() def addRecentFile(self, filename): """Add a file to the recent files list.""" recent = setdb['main_recentfiles'] filename = os.path.abspath(filename) if filename in recent: del recent[recent.index(filename)] recent.insert(0, filename) setdb['main_recentfiles'] = recent[:10] self.populateRecentFiles() def slotFileOpen(self): """Open an existing file in a new window.""" filters = ['*.vsz'] if h5py is not None: filters.append('*.vszh5') filename = self.fileOpenDialog( [_('Veusz document files (%s)') % ' '.join(filters)], _('Open')) if filename: self.openFile(filename) def populateRecentFiles(self): """Populate the recently opened files menu with a list of recently opened files""" menu = self.menus["file.filerecent"] menu.clear() newMenuItems = [] if setdb['main_recentfiles']: files = [f for f in setdb['main_recentfiles'] if os.path.isfile(f)] self._openRecentFunctions = [] # add each recent file to menu for i, path in enumerate(files): def fileOpener(filename=path): self.openFile(filename) self._openRecentFunctions.append(fileOpener) newMenuItems.append(('filerecent%i' % i, _('Open File %s') % path, os.path.basename(path), 'file.filerecent', fileOpener, '', False, '')) menu.setEnabled(True) self.recentFileActions = utils.populateMenuToolbars( newMenuItems, self.maintoolbar, self.menus) else: menu.setEnabled(False) def slotFileExport(self): """Export the graph.""" # check there is a page if self.document.getNumberPages() == 0: qt4.QMessageBox.warning(self, _("Error - Veusz"), _("No pages to export")) return # File types we can export to in the form ([extensions], Name) fd = qt4.QFileDialog(self, _('Export page')) fd.setDirectory( self.dirname_export ) fd.setFileMode( qt4.QFileDialog.AnyFile ) fd.setAcceptMode( qt4.QFileDialog.AcceptSave ) # Create a mapping between a format string and extensions filtertoext = {} # convert extensions to filter exttofilter = {} filters = [] # a list of extensions which are allowed validextns = [] formats = document.Export.formats for extns, name in formats: extensions = " ".join(["*." + item for item in extns]) # join eveything together to make a filter string filterstr = '%s (%s)' % (name, extensions) filtertoext[filterstr] = extns for e in extns: exttofilter[e] = filterstr filters.append(filterstr) validextns += extns fd.setNameFilters(filters) # restore last format if possible try: filt = setdb['export_lastformat'] fd.selectNameFilter(filt) extn = formats[filters.index(filt)][0][0] except (KeyError, IndexError, ValueError): extn = 'pdf' fd.selectNameFilter( exttofilter[extn] ) if self.filename: # try to convert current filename to export name filename = os.path.basename(self.filename) filename = os.path.splitext(filename)[0] + '.' + extn fd.selectFile(filename) if fd.exec_() == qt4.QDialog.Accepted: # save directory for next time self.dirname_export = fd.directory().absolutePath() filterused = str(fd.selectedFilter()) setdb['export_lastformat'] = filterused chosenextns = filtertoext[filterused] # show busy cursor qt4.QApplication.setOverrideCursor( qt4.QCursor(qt4.Qt.WaitCursor) ) filename = fd.selectedFiles()[0] # Add a default extension if one isn't supplied # this is the extension without the dot ext = os.path.splitext(filename)[1][1:] if (ext not in validextns) and (ext not in chosenextns): filename += "." + chosenextns[0] export = document.Export( self.document, filename, self.plot.getPageNumber(), bitmapdpi=setdb['export_DPI'], pdfdpi=setdb['export_DPI_PDF'], antialias=setdb['export_antialias'], color=setdb['export_color'], quality=setdb['export_quality'], backcolor=setdb['export_background'], svgtextastext=setdb['export_SVG_text_as_text'], ) try: export.export() except (RuntimeError, EnvironmentError) as e: if isinstance(e, EnvironmentError): msg = cstrerror(e) else: msg = cstr(e) qt4.QApplication.restoreOverrideCursor() qt4.QMessageBox.critical( self, _("Error - Veusz"), _("Error exporting to file '%s'\n\n%s") % (filename, msg)) else: qt4.QApplication.restoreOverrideCursor() def slotFilePrint(self): """Print the document.""" document.printDialog(self, self.document, filename=self.filename) def slotModifiedDoc(self, ismodified): """Disable certain actions if document is not modified.""" # enable/disable file, save menu item self.vzactions['file.save'].setEnabled(ismodified) def slotFileClose(self): """File close window chosen.""" self.close() def slotFileQuit(self): """File quit chosen.""" qt4.qApp.closeAllWindows() def slotUpdatePage(self, number): """Update page number when the plot window says so.""" np = self.document.getNumberPages() if np == 0: self.pagelabel.setText(_("No pages")) else: self.pagelabel.setText(_("Page %i/%i") % (number+1, np)) def slotUpdateAxisValues(self, values): """Update the position where the mouse is relative to the axes.""" if values: # construct comma separated text representing axis values valitems = [ '%s=%#.4g' % (name, values[name]) for name in sorted(values) ] self.axisvalueslabel.setText(', '.join(valitems)) else: self.axisvalueslabel.setText(_('No position')) def slotPickerEnabled(self, enabled): if enabled: self.pickerlabel.setText(_('No point selected')) self.pickerlabel.show() else: self.pickerlabel.hide() def slotUpdatePickerLabel(self, info): """Display the picked point""" xv, yv = info.coords xn, yn = info.labels xt, yt = info.displaytype ix = str(info.index) if ix: ix = '[' + ix + ']' # format values for display def fmt(val, dtype): if dtype == 'date': return utils.dateFloatToString(val) elif dtype == 'numeric': return '%0.5g' % val elif dtype == 'text': return val else: raise RuntimeError xtext = fmt(xv, xt) ytext = fmt(yv, yt) t = '%s: %s%s = %s, %s%s = %s' % ( info.widget.name, xn, ix, xtext, yn, ix, ytext) self.pickerlabel.setText(t) if setdb['picker_to_console']: self.console.appendOutput(t + "\n", 'error') if setdb['picker_to_clipboard']: clipboard = qt4.QApplication.clipboard() if clipboard.mimeData().hasText(): clipboard.setText(clipboard.text()+"\n"+t) else: qt4.QApplication.clipboard().setText(t) def slotAllowedImportsDoc(self, module, names): """Are allowed imports?""" d = SafetyImportDialog(self, module, names) d.exec_() veusz-1.21.1/veusz/plugins/0000775000175000017500000000000012376130063013770 5ustar jssjssveusz-1.21.1/veusz/plugins/datasetplugin.py0000664000175000017500000020661412376130006017214 0ustar jssjss# -*- coding: utf-8 -*- # Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Plugins for creating datasets.""" from __future__ import division import numpy as N from . import field from ..compat import czip, citems, cstr from .. import utils try: from ..helpers import qtloops except ImportError: pass from .. import qtall as qt4 def _(text, disambiguation=None, context='DatasetPlugin'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) # add an instance of your class to this list to be registered datasetpluginregistry = [] class DatasetPluginException(RuntimeError): """Raise this to report an error. """ pass def numpyCopyOrNone(data): """If data is None return None Otherwise return a numpy array corresponding to data.""" if data is None: return None return N.array(data, dtype=N.float64) # these classes are returned from dataset plugins class Dataset1D(object): """1D dataset for ImportPlugin or DatasetPlugin.""" def __init__(self, name, data=[], serr=None, perr=None, nerr=None): """1D dataset name: name of dataset data: data in dataset: list of floats or numpy 1D array serr: (optional) symmetric errors on data: list or numpy array perr: (optional) positive errors on data: list or numpy array nerr: (optional) negative errors on data: list or numpy array If errors are returned for data give serr or nerr and perr. nerr should be negative values if used. perr should be positive values if used. """ self.name = name self.update(data=data, serr=serr, perr=perr, nerr=nerr) def update(self, data=[], serr=None, perr=None, nerr=None): """Update values to those given.""" self.data = numpyCopyOrNone(data) self.serr = numpyCopyOrNone(serr) self.perr = numpyCopyOrNone(perr) self.nerr = numpyCopyOrNone(nerr) def _null(self): """Empty data contents.""" self.data = N.array([]) self.serr = self.perr = self.nerr = None def _makeVeuszDataset(self, manager): """Make a Veusz dataset from the plugin dataset.""" # need to do the import here as otherwise we get a loop from .. import document return document.Dataset1DPlugin(manager, self) class Dataset2D(object): """2D dataset for ImportPlugin or DatasetPlugin.""" def __init__(self, name, data=[[]], rangex=None, rangey=None, xedge=None, yedge=None, xcent=None, ycent=None): """2D dataset. name: name of dataset data: 2D numpy array of values or list of lists of floats rangex: optional tuple with X range of data (min, max) rangey: optional tuple with Y range of data (min, max) xedge: x values for grid (instead of rangex) yedge: y values for grid (instead of rangey) xcent: x values for pixel centres (instead of rangex) ycent: y values for pixel centres (instead of rangey) """ self.name = name self.update(data=data, rangex=rangex, rangey=rangey, xedge=xedge, yedge=yedge, xcent=xcent, ycent=ycent) def update(self, data=[[]], rangex=None, rangey=None, xedge=None, yedge=None, xcent=None, ycent=None): self.data = N.array(data, dtype=N.float64) self.rangex = rangex self.rangey = rangey self.xedge = xedge self.yedge = yedge self.xcent = xcent self.ycent = ycent def _null(self): """Empty data contents.""" self.data = N.array([[]]) self.rangex = self.rangey = (0, 1) self.xedge = self.yedge = self.xcent = self.ycent = None def _makeVeuszDataset(self, manager): """Make a Veusz dataset from the plugin dataset.""" from .. import document return document.Dataset2DPlugin(manager, self) class DatasetDateTime(object): """Date-time dataset for ImportPlugin or DatasetPlugin.""" def __init__(self, name, data=[]): """A date dataset name: name of dataset data: list of datetime objects """ self.name = name self.update(data=data) def update(self, data=[]): self.data = N.array(data) @staticmethod def datetimeToFloat(datetimeval): """Return a python datetime object to the required float type.""" return utils.datetimeToFloat(datetimeval) @staticmethod def dateStringToFloat(text): """Try to convert an iso or local date time to the float type.""" return utils.dateStringToDate(text) @staticmethod def floatToDateTime(val): """Convert float format datetime to Python datetime.""" return utils.floatToDateTime(val) def _null(self): """Empty data contents.""" self.data = N.array([]) def _makeVeuszDataset(self, manager): """Make a Veusz dataset from the plugin dataset.""" from .. import document return document.DatasetDateTimePlugin(manager, self) class DatasetText(object): """Text dataset for ImportPlugin or DatasetPlugin.""" def __init__(self, name, data=[]): """A text dataset name: name of dataset data: data in dataset: list of strings """ self.name = name self.update(data=data) def update(self, data=[]): self.data = list(data) def _null(self): """Empty data contents.""" self.data = [] def _makeVeuszDataset(self, manager): """Make a Veusz dataset from the plugin dataset.""" from .. import document return document.DatasetTextPlugin(manager, self) class Constant(object): """Dataset to return to set a Veusz constant after import. This is only useful in an ImportPlugin, not a DatasetPlugin """ def __init__(self, name, val): """Map string value val to name. Convert float vals to strings first!""" self.name = name self.val = val class Function(object): """Dataset to return to set a Veusz function after import.""" def __init__(self, name, val): """Map string value val to name. name is "funcname(param,...)", val is a text expression of param. This is only useful in an ImportPlugin, not a DatasetPlugin """ self.name = name self.val = val # class to pass to plugin to give parameters class DatasetPluginHelper(object): """Helpers to get existing datasets for plugins.""" def __init__(self, doc): """Construct helper object to pass to DatasetPlugins.""" self._doc = doc @property def datasets1d(self): """Return list of existing 1D numeric datasets""" return [name for name, ds in citems(self._doc.data) if (ds.dimensions == 1 and ds.datatype == 'numeric')] @property def datasets2d(self): """Return list of existing 2D numeric datasets""" return [name for name, ds in citems(self._doc.data) if (ds.dimensions == 2 and ds.datatype == 'numeric')] @property def datasetstext(self): """Return list of existing 1D text datasets""" return [name for name, ds in citems(self._doc.data) if (ds.dimensions == 1 and ds.datatype == 'text')] @property def datasetsdatetime(self): """Return list of existing date-time datesets""" from .. import document return [name for name, ds in citems(self._doc.data) if isinstance(ds, document.DatasetDateTime)] @property def locale(self): """Return Qt locale.""" return self._doc.locale def evaluateExpression(self, expr, part='data'): """Return results of evaluating a 1D dataset expression. part is 'data', 'serr', 'perr' or 'nerr' - these are the dataset parts which are evaluated by the expression Returns None if expression could not be evaluated. """ ds = self._doc.evalDatasetExpression(expr, part=part) if ds is not None: return ds.data else: return None def getDataset(self, name, dimensions=1): """Return numerical dataset object for name given. Please make sure that dataset data are not modified. name: name of dataset dimensions: number of dimensions dataset requires name not found: raise a DatasetPluginException dimensions not right: raise a DatasetPluginException """ from .. import document try: ds = self._doc.data[name] except KeyError: raise DatasetPluginException(_("Unknown dataset '%s'") % name) if ds.dimensions != dimensions: raise DatasetPluginException( _("Dataset '%s' does not have %i dimensions") % ( name, dimensions)) if ds.datatype != 'numeric': raise DatasetPluginException( _("Dataset '%s' is not a numerical dataset") % name) if isinstance(ds, document.DatasetDateTime): return DatasetDateTime(name, data=ds.data) elif ds.dimensions == 1: return Dataset1D(name, data=ds.data, serr=ds.serr, perr=ds.perr, nerr=ds.nerr) elif ds.dimensions == 2: return Dataset2D(name, ds.data, xrange=ds.xrange, yrange=ds.yrange, xedge=ds.xedge, yedge=ds.yedge, xcent=ds.xcent, ycent=ds.ycent) else: raise RuntimeError("Invalid number of dimensions in dataset") def getDatasets(self, names, dimensions=1): """Get a list of numerical datasets (of the dimension given).""" return [ self.getDataset(n, dimensions=dimensions) for n in names ] def getTextDataset(self, name): """Return a text dataset with name given. Do not modify this dataset. name not found: raise a DatasetPluginException """ try: ds = self._doc.data[name] except KeyError: raise DatasetPluginException(_("Unknown dataset '%s'") % name) if ds.datatype == 'text': return DatasetText(name, ds.data) raise DatasetPluginException(_("Dataset '%s' is not a text datset") % name) # internal object to synchronise datasets created by a plugin class DatasetPluginManager(object): """Manage datasets generated by plugin.""" def __init__(self, plugin, doc, fields): """Construct manager object. plugin - instance of plugin class doc - document instance fields - fields to pass to plugin """ self.plugin = plugin self.document = doc self.helper = DatasetPluginHelper(doc) self.fields = dict(fields) self.changeset = -1 self.fixMissingFields() self.setupDatasets() def fixMissingFields(self): """If fields are missing, use defaults.""" for pluginfield in self.plugin.fields: if pluginfield.name not in self.fields: self.fields[pluginfield.name] = pluginfield.default def setupDatasets(self): """Do initial construction of datasets.""" self.datasetnames = [] self.datasets = [] self.veuszdatasets = [] self.datasets = self.plugin.getDatasets(self.fields) for ds in self.datasets: self.datasetnames.append(ds.name) veuszds = ds._makeVeuszDataset(self) veuszds.document = self.document self.veuszdatasets.append(veuszds) def nullDatasets(self): """Clear out contents of datasets.""" for ds in self.datasets: ds._null() def saveToFile(self, fileobj): """Save command to load in plugin and parameters.""" args = [ repr(self.plugin.name), repr(self.fields) ] # look for renamed or deleted datasets names = {} for ds, dsname in czip( self.veuszdatasets, self.datasetnames ): try: currentname = self.document.datasetName(ds) except ValueError: # deleted currentname = None if currentname != dsname: names[dsname] = currentname if names: args.append( "datasetnames="+repr(names) ) fileobj.write( 'DatasetPlugin(%s)\n' % (', '.join(args)) ) def update(self, raiseerrors=False): """Update created datasets. if raiseerrors is True, raise an exception if there is an exeception when updating the dataset """ if self.document.changeset == self.changeset: return self.changeset = self.document.changeset # run the plugin with its parameters try: self.plugin.updateDatasets(self.fields, self.helper) except DatasetPluginException as ex: # this is for immediate notification if raiseerrors: raise # otherwise if there's an error, then log and null outputs self.document.log( cstr(ex) ) self.nullDatasets() class DatasetPlugin(object): """Base class for defining dataset plugins.""" # the plugin will get inserted into the menu in a hierarchy based on # the elements of this tuple menu = ('Base plugin',) name = 'Base plugin' author = '' description_short = '' description_full = '' # if the plugin takes no parameters, set this to False has_parameters = True def __init__(self): """Override this to declare a list of input fields if required.""" self.fields = [] def getDatasets(self, fields): """Override this to return a list of (empty) Dataset1D, Dataset2D and DatasetText objects to provide the initial names and type of datasets. These should be saved for updating in updateDatasets. fields: dict of results to the field objects given in self.fields raise a DatasetPluginException if there is a problem with fields """ return [] def updateDatasets(self, fields, helper): """Override this to update the dataset objects provided by this plugin. fields: dict of field results (also provided to setup) helper: DatasetPluginHelper object, to get other datasets in document raise a DatasetPluginException if there is a problem """ class _OneOutputDatasetPlugin(DatasetPlugin): """Simplify plugins which create one output with field ds_out.""" def getDatasets(self, fields): """Returns single output dataset (self.dsout).""" if fields['ds_out'] == '': raise DatasetPluginException(_('Invalid output dataset name')) self.dsout = Dataset1D(fields['ds_out']) return [self.dsout] def errorBarType(ds): """Return type of error bars in list of datasets. 'none', 'symmetric', 'asymmetric' """ symerr = False for d in ds: if d.serr is not None: symerr = True elif d.perr is not None or d.nerr is not None: return 'asymmetric' if symerr: return 'symmetric' return 'none' def combineAddedErrors(inds, length): """Combine error bars from list of input dataset, adding errors squared (suitable for adding/subtracting).""" errortype = errorBarType(inds) serr = perr = nerr = None if errortype == 'symmetric': serr = N.zeros(length, dtype=N.float64) elif errortype == 'asymmetric': perr = N.zeros(length, dtype=N.float64) nerr = N.zeros(length, dtype=N.float64) for d in inds: f = N.isfinite(d.data) if errortype == 'symmetric' and d.serr is not None: serr[f] += d.serr[f]**2 elif errortype == 'asymmetric': if d.serr is not None: v = (d.serr[f])**2 perr[f] += v nerr[f] += v if d.perr is not None: perr[f] += (d.perr[f])**2 if d.nerr is not None: nerr[f] += (d.nerr[f])**2 if serr is not None: serr = N.sqrt(serr) if perr is not None: perr = N.sqrt(perr) if nerr is not None: nerr = -N.sqrt(nerr) return serr, perr, nerr def combineMultipliedErrors(inds, length, data): """Combine error bars from list of input dataset, adding fractional errors squared (suitable for multipling/dividing).""" errortype = errorBarType(inds) serr = perr = nerr = None if errortype == 'symmetric': serr = N.zeros(length, dtype=N.float64) elif errortype == 'asymmetric': perr = N.zeros(length, dtype=N.float64) nerr = N.zeros(length, dtype=N.float64) for d in inds: f = N.isfinite(d.data) if len(f) > length: f = f[:length] if errortype == 'symmetric' and d.serr is not None: serr[f] += (d.serr[f]/d.data[f])**2 elif errortype == 'asymmetric': if d.serr is not None: v = (d.serr[f]/d.data[f])**2 perr[f] += v nerr[f] += v if d.perr is not None: perr[f] += (d.perr[f]/d.data[f])**2 if d.nerr is not None: nerr[f] += (d.nerr[f]/d.data[f])**2 if serr is not None: serr = N.abs(N.sqrt(serr) * data) if perr is not None: perr = N.abs(N.sqrt(perr) * data) if nerr is not None: nerr = -N.abs(N.sqrt(nerr) * data) return serr, perr, nerr ########################################################################### ## Real plugins are below class MultiplyDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to scale a dataset.""" menu = (_('Multiply'), _('By constant'),) name = 'Multiply' description_short = _('Multiply dataset by a constant') description_full = _('Multiply a dataset by a factor. ' 'Error bars are also scaled.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldFloat('factor', _('Factor'), default=1.), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do scaling of dataset.""" ds_in = helper.getDataset(fields['ds_in']) f = fields['factor'] data, serr, perr, nerr = ds_in.data, ds_in.serr, ds_in.perr, ds_in.nerr data = data * f if serr is not None: serr = serr * f if perr is not None: perr = perr * f if nerr is not None: nerr = nerr * f self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class AddDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to add a constant to a dataset.""" menu = (_('Add'), _('Constant'),) name = 'Add' description_short = _('Add a constant to a dataset') description_full = _('Add a dataset by adding a value. ' 'Error bars remain the same.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldFloat('value', _('Add value'), default=0.), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do shifting of dataset.""" ds_in = helper.getDataset(fields['ds_in']) self.dsout.update(data = ds_in.data + fields['value'], serr=ds_in.serr, perr=ds_in.perr, nerr=ds_in.nerr) class ConcatenateDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to concatenate datasets.""" menu = (_('Join'), _('Concatenate'),) name = 'Concatenate' description_short = _('Concatenate datasets') description_full = _('Concatenate datasets into single dataset.\n' 'Error bars are merged.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDatasetMulti('ds_in', _('Input datasets')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do concatenation of dataset.""" dsin = helper.getDatasets(fields['ds_in']) if len(dsin) == 0: raise DatasetPluginException(_('Requires one or more input datasets')) # concatenate main data dstack = N.hstack([d.data for d in dsin]) sstack = pstack = nstack = None # what sort of error bars do we need? errortype = errorBarType(dsin) if errortype == 'symmetric': # symmetric and not asymmetric error bars sstack = [] for d in dsin: if d.serr is not None: sstack.append(d.serr) else: sstack.append(N.zeros(d.data.shape, dtype=N.float64)) sstack = N.hstack(sstack) elif errortype == 'asymmetric': # asymmetric error bars pstack = [] nstack = [] for d in dsin: p = n = N.zeros(d.data.shape, dtype=N.float64) if d.serr is not None: p, n = d.serr, -d.serr else: if d.perr is not None: p = d.perr if d.nerr is not None: n = d.nerr pstack.append(p) nstack.append(n) pstack = N.hstack(pstack) nstack = N.hstack(nstack) self.dsout.update(data=dstack, serr=sstack, perr=pstack, nerr=nstack) class InterleaveDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to interleave datasets.""" menu = (_('Join'), _('Element by element'),) name = 'Interleave' description_short = _('Join datasets, interleaving element by element') description_full = _('Join datasets, interleaving element by element.\n' 'Error bars are merged.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDatasetMulti('ds_in', _('Input datasets')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do concatenation of dataset.""" dsin = helper.getDatasets(fields['ds_in']) if len(dsin) == 0: raise DatasetPluginException(_('Requires one or more input datasets')) maxlength = max( [len(d.data) for d in dsin] ) def interleave(datasets): """This is complex to account for different length datasets.""" # stick in columns ds = [ N.hstack( (d, N.zeros(maxlength-len(d))) ) for d in datasets ] # which elements are valid good = [ N.hstack( (N.ones(len(d), dtype=N.bool), N.zeros(maxlength-len(d), dtype=N.bool)) ) for d in datasets ] intl = N.column_stack(ds).reshape(maxlength*len(datasets)) goodintl = N.column_stack(good).reshape(maxlength*len(datasets)) return intl[goodintl] # do interleaving data = interleave([d.data for d in dsin]) # interleave error bars errortype = errorBarType(dsin) serr = perr = nerr = None if errortype == 'symmetric': slist = [] for ds in dsin: if ds.serr is None: slist.append(N.zeros_like(ds.data)) else: slist.append(ds.serr) serr = interleave(slist) elif errortype == 'asymmetric': plist = [] nlist = [] for ds in dsin: if ds.serr is not None: plist.append(ds.serr) nlist.append(-ds.serr) else: if ds.perr is not None: plist.append(ds.perr) else: plist.append(N.zeros_like(ds.data)) if ds.nerr is not None: nlist.append(ds.nerr) else: nlist.append(N.zeros_like(ds.data)) perr = interleave(plist) nerr = interleave(nlist) # finally update self.dsout.update(data=data, serr=serr, nerr=nerr, perr=perr) class ChopDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to chop datasets.""" menu = (_('Split'), _('Chop'),) name = 'Chop' description_short = _('Chop dataset part into new dataset') description_full = _('Chop out a section of a dataset. Give starting ' 'index of data and number of datapoints to take.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldInt('start', _('Starting index (from 1)'), default=1), field.FieldInt('num', _('Maximum number of datapoints'), default=1), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do chopping of dataset.""" ds_in = helper.getDataset(fields['ds_in']) start = fields['start'] num = fields['num'] data, serr, perr, nerr = ds_in.data, ds_in.serr, ds_in.perr, ds_in.nerr # chop the data data = data[start-1:start-1+num] if serr is not None: serr = serr[start-1:start-1+num] if perr is not None: perr = perr[start-1:start-1+num] if nerr is not None: nerr = nerr[start-1:start-1+num] self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class PartsDatasetPlugin(DatasetPlugin): """Dataset plugin to split datasets into parts.""" menu = (_('Split'), _('Parts'),) name = 'Parts' description_short = _('Split dataset into equal-size parts') description_full = _('Split dataset into equal-size parts. ' 'The parts will differ in size if the dataset ' 'cannot be split equally.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldDatasetMulti('ds_out', _('Output datasets')), ] def getDatasets(self, fields): """Get output datasets.""" self.dsout = [] for d in fields['ds_out']: if d.strip() != '': self.dsout.append( Dataset1D(d.strip()) ) if len(self.dsout) == 0: raise DatasetPluginException(_('Needs at least one output dataset')) return self.dsout def updateDatasets(self, fields, helper): """Do chopping of dataset.""" ds_in = helper.getDataset(fields['ds_in']) data, serr, perr, nerr = ds_in.data, ds_in.serr, ds_in.perr, ds_in.nerr plen = len(data) / len(self.dsout) for i, ds in enumerate(self.dsout): minv, maxv = int(plen*i), int(plen*(i+1)) pserr = pperr = pnerr = None pdata = data[minv:maxv] if serr is not None: pserr = serr[minv:maxv] if perr is not None: pperr = perr[minv:maxv] if nerr is not None: pnerr = nerr[minv:maxv] ds.update(data=pdata, serr=pserr, perr=pperr, nerr=pnerr) class ThinDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to thin datasets.""" menu = (_('Split'), _('Thin'),) name = 'Thin' description_short = _('Select data points at intervals from dataset') description_full = _('Select data points at intervals from dataset ' 'to create new dataset') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldInt('start', _('Starting index (from 1)'), default=1), field.FieldInt('interval', _('Interval between data points'), default=1), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do thinning of dataset.""" ds_in = helper.getDataset(fields['ds_in']) start = fields['start'] interval = fields['interval'] data, serr, perr, nerr = ds_in.data, ds_in.serr, ds_in.perr, ds_in.nerr data = data[start-1::interval] if serr is not None: serr = serr[start-1::interval] if perr is not None: perr = perr[start-1::interval] if nerr is not None: nerr = nerr[start-1::interval] self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class MeanDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to mean datasets together.""" menu = (_('Compute'), _('Mean of datasets'),) name = 'Mean' description_short = _('Compute mean of datasets') description_full = _('Compute mean of multiple datasets to create ' 'a single dataset.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDatasetMulti('ds_in', _('Input datasets')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Compute means of dataset.""" inds = helper.getDatasets(fields['ds_in']) if len(inds) == 0: raise DatasetPluginException(_('Requires one or more input datasets')) maxlength = max( [len(d.data) for d in inds] ) # mean data (only use finite values) tot = N.zeros(maxlength, dtype=N.float64) num = N.zeros(maxlength, dtype=N.int) for d in inds: f = N.isfinite(d.data) tot[f] += d.data[f] num[f] += 1 data = tot / num def averageError(errtype, fallback=None): """Get average for an error value.""" tot = N.zeros(maxlength, dtype=N.float64) num = N.zeros(maxlength, dtype=N.int) for d in inds: vals = getattr(d, errtype) if vals is None and fallback: vals = getattr(d, fallback) # add values if not missing if vals is not None: f = N.isfinite(vals) tot[f] += (vals[f]) ** 2 num[f] += 1 else: # treat as zero errors if missing errors num[:len(d.data)] += 1 return N.sqrt(tot) / num # do error bar handling serr = perr = nerr = None errortype = errorBarType(inds) if errortype == 'symmetric': serr = averageError('serr') elif errortype == 'asymmetric': perr = averageError('perr', fallback='serr') nerr = -averageError('nerr', fallback='serr') self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class AddDatasetsPlugin(_OneOutputDatasetPlugin): """Dataset plugin to mean datasets together.""" menu = (_('Add'), _('Datasets'),) name = 'Add Datasets' description_short = _('Add two or more datasets together') description_full = _('Add datasets together to make a single dataset. ' 'Error bars are combined.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDatasetMulti('ds_in', _('Input datasets')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Compute means of dataset.""" inds = helper.getDatasets(fields['ds_in']) if len(inds) == 0: raise DatasetPluginException(_('Requires one or more input datasets')) maxlength = max( [len(d.data) for d in inds] ) # add data where finite data = N.zeros(maxlength, dtype=N.float64) anyfinite = N.zeros(maxlength, dtype=N.bool) for d in inds: f = N.isfinite(d.data) data[f] += d.data[f] anyfinite[f] = True data[N.logical_not(anyfinite)] = N.nan # handle error bars serr, perr, nerr = combineAddedErrors(inds, maxlength) # update output dataset self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class SubtractDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to subtract two datasets.""" menu = (_('Subtract'), _('Datasets'),) name = 'Subtract Datasets' description_short = _('Subtract two datasets') description_full = _('Subtract two datasets. ' 'Combined error bars are also calculated.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in1', _('Input dataset 1')), field.FieldDataset('ds_in2', _('Input dataset 2')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do scaling of dataset.""" dsin1 = helper.getDataset(fields['ds_in1']) dsin2 = helper.getDataset(fields['ds_in2']) minlength = min( len(dsin1.data), len(dsin2.data) ) data = dsin1.data[:minlength] - dsin2.data[:minlength] # computing error bars is non trivial! serr = perr = nerr = None errortype = errorBarType([dsin1, dsin2]) if errortype == 'symmetric': serr1 = serr2 = 0 if dsin1.serr is not None: serr1 = dsin1.serr[:minlength] if dsin2.serr is not None: serr2 = dsin2.serr[:minlength] serr = N.sqrt(serr1**2 + serr2**2) elif errortype == 'asymmetric': perr1 = perr2 = nerr1 = nerr2 = 0 if dsin1.serr is not None: perr1 = nerr1 = dsin1.serr[:minlength] else: if dsin1.perr is not None: perr1 = dsin1.perr[:minlength] if dsin1.nerr is not None: nerr1 = dsin1.nerr[:minlength] if dsin2.serr is not None: perr2 = nerr2 = dsin2.serr[:minlength] else: if dsin2.perr is not None: perr2 = dsin2.perr[:minlength] if dsin2.nerr is not None: nerr2 = dsin2.nerr[:minlength] perr = N.sqrt(perr1**2 + nerr2**2) nerr = -N.sqrt(nerr1**2 + perr2**2) self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class SubtractMeanDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to subtract mean from dataset.""" menu = (_('Subtract'), _('Mean'),) name = 'Subtract Mean' description_short = _('Subtract mean from dataset') description_full = _('Subtract mean from dataset,' ' optionally dividing by standard deviation.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset 1')), field.FieldBool('divstddev', _('Divide by standard deviation')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do scaling of dataset.""" dsin = helper.getDataset(fields['ds_in']) vals = dsin.data mean = vals[N.isfinite(vals)].mean() vals = vals - mean if fields['divstddev']: vals /= vals[N.isfinite(vals)].std() self.dsout.update( data=vals, serr=dsin.serr, perr=dsin.perr, nerr=dsin.nerr) class SubtractMinimumDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to subtract minimum from dataset.""" menu = (_('Subtract'), _('Minimum'),) name = 'Subtract Minimum' description_short = _('Subtract minimum from dataset') description_full = _('Subtract the minimum value from a dataset') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset 1')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Do scaling of dataset.""" dsin = helper.getDataset(fields['ds_in']) vals = dsin.data minval = vals[N.isfinite(vals)].min() vals = vals - minval self.dsout.update( data=vals, serr=dsin.serr, perr=dsin.perr, nerr=dsin.nerr) class MultiplyDatasetsPlugin(_OneOutputDatasetPlugin): """Dataset plugin to multiply two or more datasets.""" menu = (_('Multiply'), _('Datasets'),) name = 'Multiply Datasets' description_short = _('Multiply two or more datasets') description_full = _('Multiply two or more datasets. ' 'Combined error bars are also calculated.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDatasetMulti('ds_in', _('Input datasets')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Multiply the datasets.""" names = fields['ds_in'] inds = [ helper.getDataset(d) for d in names ] maxlength = max( [d.data.shape[0] for d in inds] ) # output data and where data is finite data = N.ones(maxlength, dtype=N.float64) anyfinite = N.zeros(maxlength, dtype=N.bool) for d in inds: f = N.isfinite(d.data) anyfinite[f] = True data[f] *= d.data[f] # where always NaN, make NaN data[N.logical_not(anyfinite)] = N.nan # get error bars serr, perr, nerr = combineMultipliedErrors(inds, maxlength, data) self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class DivideDatasetsPlugin(_OneOutputDatasetPlugin): """Dataset plugin to divide two datasets.""" menu = (_('Divide'), _('Datasets'),) name = 'Divide Datasets' description_short = _('Compute ratio or fractional difference' ' between two datasets') description_full = _('Divide or compute fractional difference' ' between two datasets') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in1', _('Input dataset 1')), field.FieldDataset('ds_in2', _('Input dataset 2')), field.FieldBool('frac', _('Compute fractional difference'), default=False), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): """Compute ratio.""" inds1 = helper.getDataset( fields['ds_in1'] ) inds2 = helper.getDataset( fields['ds_in2'] ) length = min( len(inds1.data), len(inds2.data) ) # compute ratio data = inds1.data[:length] / inds2.data[:length] # get error bars serr, perr, nerr = combineMultipliedErrors([inds1, inds2], length, data) # convert to fractional difference (if reqd) if fields['frac']: data -= 1 self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class DivideMaxPlugin(_OneOutputDatasetPlugin): """Plugin to divide by maximum of dataset.""" menu = (_('Divide'), _('By maximum'),) name = 'Divide Maximum' description_short = description_full = _('Divide dataset by its maximum') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): inds = helper.getDataset( fields['ds_in'] ) maxval = N.nanmax( inds.data ) # divide data data = inds.data / maxval # divide error bars serr = perr = nerr = None if inds.serr: serr = inds.serr / maxval if inds.perr: perr = inds.perr / maxval if inds.nerr: nerr = inds.nerr / maxval self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class DivideNormalizePlugin(_OneOutputDatasetPlugin): """Plugin to normalize dataset.""" menu = (_('Divide'), _('Normalize'),) name = 'Normalize' description_short = description_full = _( 'Divide dataset by its sum of values') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldDataset('ds_out', _('Output dataset name')), ] def updateDatasets(self, fields, helper): inds = helper.getDataset( fields['ds_in'] ) tot = N.nansum( inds.data ) # divide data data = inds.data / tot # divide error bars serr = perr = nerr = None if inds.serr: serr = inds.serr / tot if inds.perr: perr = inds.perr / tot if inds.nerr: nerr = inds.nerr / tot self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class ExtremesDatasetPlugin(DatasetPlugin): """Dataset plugin to get extremes of dataset.""" menu = (_('Compute'), _('Dataset extremes'),) name = 'Extremes' description_short = _('Compute extreme values of input datasets') description_full = _('Compute extreme values of input datasets. Creates ' 'minimum and maximum datasets.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDatasetMulti('ds_in', _('Input datasets')), field.FieldBool('errorbars', _('Include error bars')), field.FieldDataset('ds_min', _('Output minimum dataset (optional)')), field.FieldDataset('ds_max', _('Output maximum dataset (optional)')), field.FieldDataset('ds_errorbar', _('Output range as error bars ' 'in dataset (optional)')), ] def getDatasets(self, fields): """Returns output dataset.""" dsout = [] self.dsmin = self.dsmax = self.dserror = None if fields['ds_min'] != '': self.dsmin = Dataset1D(fields['ds_min']) dsout.append(self.dsmin) if fields['ds_max'] != '': self.dsmax = Dataset1D(fields['ds_max']) dsout.append(self.dsmax) if fields['ds_errorbar'] != '': self.dserror = Dataset1D(fields['ds_errorbar']) dsout.append(self.dserror) if not dsout: raise DatasetPluginException(_('Provide at least one output dataset')) return dsout def updateDatasets(self, fields, helper): """Compute extremes of datasets.""" names = fields['ds_in'] inds = [ helper.getDataset(d) for d in names ] maxlength = max( [d.data.shape[0] for d in inds] ) minvals = N.zeros(maxlength, dtype=N.float64) + 1e100 maxvals = N.zeros(maxlength, dtype=N.float64) - 1e100 anyfinite = N.zeros(maxlength, dtype=N.bool) for d in inds: f = N.isfinite(d.data) anyfinite[f] = True v = d.data if fields['errorbars']: if d.serr is not None: v = v - d.serr elif d.nerr is not None: v = v + d.nerr minvals[f] = N.min( (minvals[f], v[f]), axis=0 ) v = d.data if fields['errorbars']: if d.serr is not None: v = v + d.serr elif d.perr is not None: v = v + d.perr maxvals[f] = N.max( (maxvals[f], v[f]), axis=0 ) minvals[N.logical_not(anyfinite)] = N.nan maxvals[N.logical_not(anyfinite)] = N.nan if self.dsmin is not None: self.dsmin.update(data=minvals) if self.dsmax is not None: self.dsmax.update(data=maxvals) if self.dserror is not None: # compute mean and look at differences from it tot = N.zeros(maxlength, dtype=N.float64) num = N.zeros(maxlength, dtype=N.int) for d in inds: f = N.isfinite(d.data) tot[f] += d.data[f] num[f] += 1 mean = tot / num self.dserror.update(data=mean, nerr=minvals-mean, perr=maxvals-mean) class CumulativePlugin(_OneOutputDatasetPlugin): """Compute cumulative values.""" menu = (_('Compute'), _('Cumulative value'),) name = 'Cumulative' description_short = _('Compute the cumulative value of a dataset') description_full = _('Compute the cumulative value of a dataset. ' ' Error bars are combined.\n' 'Default behaviour is to accumulate from start.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldBool('fromend', _('Compute cumulative value from end')), field.FieldDataset('ds_out', _('Output dataset')), ] def updateDatasets(self, fields, helper): """Do accumulation.""" ds_in = helper.getDataset(fields['ds_in']) fromend = fields['fromend'] def cumsum(v): """Compute cumulative, handing nans and reverse.""" v = N.array(v) if fromend: v = v[::-1] v[ N.logical_not(N.isfinite(v)) ] = 0. c = N.cumsum(v) if fromend: c = c[::-1] return c # compute cumulative values data, serr, perr, nerr = ds_in.data, ds_in.serr, ds_in.perr, ds_in.nerr data = cumsum(data) if serr is not None: serr = N.sqrt( cumsum(serr**2) ) if perr is not None: perr = N.sqrt( cumsum(perr**2) ) if nerr is not None: nerr = -N.sqrt( cumsum(nerr**2) ) self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class DemultiplexPlugin(DatasetPlugin): """Dataset plugin to split a dataset into multiple datasets, element-by-element.""" menu = (_('Split'), _('Element by element'),) name = 'Demultiplex' description_short = _('Split dataset into multiple datasets element-by-element') description_full = _('Split dataset into multiple datasets on an ' 'element-by-element basis.\n' 'e.g. 1, 2, 3, 4, 5, 6 could be converted to ' '1, 3, 5 and 2, 4, 6.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldDatasetMulti('ds_out', _('Output datasets')), ] def getDatasets(self, fields): """Returns demuxed output datasets.""" names = [n.strip() for n in fields['ds_out'] if n.strip() != ''] if len(names) == 0: raise DatasetPluginException(_('Requires at least one output dataset')) self.ds_out = [ Dataset1D(n) for n in names ] return self.ds_out def updateDatasets(self, fields, helper): """Compute means of dataset.""" ds_in = helper.getDataset( fields['ds_in'] ) num = len(self.ds_out) for i, ds in enumerate(self.ds_out): data = ds_in.data[i::num] serr = nerr = perr = None if ds_in.serr is not None: serr = ds_in.serr[i::num] if ds_in.perr is not None: perr = ds_in.perr[i::num] if ds_in.nerr is not None: nerr = ds_in.nerr[i::num] ds.update(data=data, serr=serr, perr=perr, nerr=nerr) class PolarToCartesianPlugin(DatasetPlugin): """Convert from r,theta to x,y coordinates.""" menu = (_('Convert'), _('Polar to Cartesian'),) name = 'PolarToCartesian' description_short = _('Convert r,theta coordinates to x,y coordinates') description_full = _('Convert r,theta coordinates to x,y coordinates.\n' 'Error bars are ignored.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('r_in', _('Input dataset (r)')), field.FieldDataset('theta_in', _('Input dataset (theta)')), field.FieldCombo('units', _('Angular units'), items=('radians', 'degrees'), editable=False), field.FieldDataset('x_out', _('Output dataset (x)')), field.FieldDataset('y_out', _('Output dataset (y)')), ] def getDatasets(self, fields): """Returns x and y output datasets.""" if fields['x_out'] == '': raise DatasetPluginException(_('Invalid output x dataset name')) if fields['y_out'] == '': raise DatasetPluginException(_('Invalid output y dataset name')) self.x_out = Dataset1D(fields['x_out']) self.y_out = Dataset1D(fields['y_out']) return [self.x_out, self.y_out] def updateDatasets(self, fields, helper): """Compute means of dataset.""" ds_r = helper.getDataset( fields['r_in'] ).data ds_theta = helper.getDataset( fields['theta_in'] ).data if fields['units'] == 'degrees': # convert to radians ds_theta = ds_theta * (N.pi / 180.) x = ds_r * N.cos(ds_theta) y = ds_r * N.sin(ds_theta) self.x_out.update(data=x) self.y_out.update(data=y) class FilterDatasetPlugin(_OneOutputDatasetPlugin): """Dataset plugin to filter a dataset using an expression.""" menu = (_('Filter'), _('Expression'),) name = 'FilterExpression' description_short = _('Filter a dataset using an expression') description_full = _('Filter a dataset using an expression, ' 'e.g. "x>10" or "(x>1) & (y<2)"') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldText('filter', _('Filter expression')), field.FieldBool('replacenan', _('Replace excluded points by NaN\n' '(indicate missing points)'), default=False), field.FieldDataset('ds_out', _('Output dataset')), ] def updateDatasets(self, fields, helper): """Do shifting of dataset.""" ds_in = helper.getDataset(fields['ds_in']) filt = helper.evaluateExpression(fields['filter']) data, serr, perr, nerr = ds_in.data, ds_in.serr, ds_in.perr, ds_in.nerr if filt is None: # select nothing filt = N.zeros(data.shape, dtype=N.bool) else: # filter must have int/bool type filt = N.array(filt, dtype=N.bool) try: if fields['replacenan']: # replace bad points with nan data = data.copy() data[N.logical_not(filt)] = N.nan else: # just select good points data = data[filt] if serr is not None: serr = serr[filt] if perr is not None: perr = perr[filt] if nerr is not None: nerr = nerr[filt] except (ValueError, IndexError) as e: raise DatasetPluginException(_("Error filtering dataset: '%s')") % cstr(e)) self.dsout.update(data=data, serr=serr, perr=perr, nerr=nerr) class MovingAveragePlugin(_OneOutputDatasetPlugin): """Compute moving average for dataset.""" menu = (_('Filtering'), _('Moving Average'),) name = 'MovingAverage' description_short = _('Compute moving average for regularly spaced data') description_full = _('Compute moving average for regularly spaced data.' 'Average is computed either\nside of each data point ' 'by number of points given.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldInt('width', _('Points either side of point to average'), default=1, minval=0), field.FieldBool('weighterrors', _('Weight by error bars'), default=True), field.FieldDataset('ds_out', _('Output dataset')), ] def updateDatasets(self, fields, helper): """Do shifting of dataset.""" ds_in = helper.getDataset(fields['ds_in']) weights = None if fields['weighterrors']: if ds_in.serr is not None: weights = 1. / ds_in.serr**2 elif ds_in.perr is not None and ds_in.nerr is not None: weights = 1. / ( (ds_in.perr**2+ds_in.nerr**2)/2. ) width = fields['width'] data = qtloops.rollingAverage(ds_in.data, weights, width) self.dsout.update(data=data) class LinearInterpolatePlugin(_OneOutputDatasetPlugin): """Do linear interpolation of data.""" menu = (_('Filtering'), _('Linear interpolation'),) name = 'LinearInterpolation' description_short = _('Linear interpolation of x,y data') description_full = _("Compute linear interpolation of x,y data.\n" "Given datasets for y = f(x), compute y' = f(x'), " "using linear interpolation.\n" "Assumes x dataset increases in value.") def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_x', _('Input dataset x')), field.FieldDataset('ds_y', _('Input dataset y')), field.FieldDataset('ds_xprime', _("Input dataset x'")), field.FieldBool('edgenan', _('Use nan for values outside x range')), field.FieldDataset('ds_out', _("Output dataset y'")), ] def updateDatasets(self, fields, helper): """Do shifting of dataset.""" ds_x = helper.getDataset(fields['ds_x']).data ds_y = helper.getDataset(fields['ds_y']).data ds_xprime = helper.getDataset(fields['ds_xprime']).data minlenin = min( len(ds_x), len(ds_y) ) pad = None if fields['edgenan']: pad = N.nan interpol = N.interp(ds_xprime, ds_x[:minlenin], ds_y[:minlenin], left=pad, right=pad) self.dsout.update(data=interpol) class ReBinXYPlugin(DatasetPlugin): """Bin-up data by factor given.""" menu = (_('Filtering'), _('Bin X,Y')) name = 'RebinXY' description_short = 'Bin every N datapoints' description_full = ('Given dataset Y (and optionally X), for every N ' 'datapoints calculate the binned value. For ' 'dataset Y this is the sum or mean of every N ' 'datapoints. For X this is the midpoint of the ' 'datapoints (using error bars to give the range.') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_y', _('Input dataset Y')), field.FieldDataset('ds_x', _('Input dataset X (optional)')), field.FieldInt('binsize', _('Bin size (N)'), minval=1, default=2), field.FieldCombo('mode', _('Mode of binning'), items=('sum', 'average'), editable=False), field.FieldDataset('ds_yout', _("Output Y'")), field.FieldDataset('ds_xout', _("Output X' (optional)")), ] def getDatasets(self, fields): """Return output datasets""" if fields['ds_yout'] == '': raise DatasetPluginException(_('Invalid output Y dataset name')) if fields['ds_x'] != '' and fields['ds_xout'] == '': raise DatasetPluginException(_('Invalid output X dataset name')) self.dssout = out = [ Dataset1D(fields['ds_yout']) ] if fields['ds_xout'] != '': out.append(Dataset1D(fields['ds_xout'])) return out def updateDatasets(self, fields, helper): """Do binning.""" binsize = fields['binsize'] average = fields['mode'] == 'average' def binerr(err): """Compute binned error.""" if err is None: return None err2 = qtloops.binData(err**2, binsize, False) cts = qtloops.binData(N.ones(err.shape), binsize, False) return N.sqrt(err2) / cts # bin up data and calculate errors (if any) dsy = helper.getDataset(fields['ds_y']) binydata = qtloops.binData(dsy.data, binsize, average) binyserr = binerr(dsy.serr) binyperr = binerr(dsy.perr) binynerr = binerr(dsy.nerr) self.dssout[0].update(data=binydata, serr=binyserr, perr=binyperr, nerr=binynerr) if len(self.dssout) == 2: # x datasets dsx = helper.getDataset(fields['ds_x']) # Calculate ranges between adjacent binned points. This # is horribly messy - we have to account for the fact # there might not be error bars and calculate the midpoint # to the previous/next point. minvals = N.array(dsx.data) if dsx.serr is not None: minvals -= dsx.serr elif dsx.nerr is not None: minvals += dsx.nerr else: minvals = 0.5*(dsx.data[1:] + dsx.data[:-1]) if len(dsx.data) > 2: # assume +ve error bar on last point is as big as its -ve error minvals = N.insert(minvals, 0, dsx.data[0] - 0.5*( dsx.data[1] - dsx.data[0])) elif len(dsx.data) != 0: # no previous point so we assume 0 error minvals = N.insert(minvals, 0, dsx.data[0]) maxvals = N.array(dsx.data) if dsx.serr is not None: maxvals += dsx.serr elif dsx.perr is not None: maxvals += dsx.perr else: maxvals = 0.5*(dsx.data[1:] + dsx.data[:-1]) if len(dsx.data) > 2: maxvals = N.append(maxvals, dsx.data[-1] + 0.5*( dsx.data[-1] - dsx.data[-2])) elif len(dsx.data) != 0: maxvals = N.append(maxvals, dsx.data[-1]) minbin = minvals[::binsize] maxbin = maxvals[binsize-1::binsize] if len(minbin) > len(maxbin): # not an even number of bin size maxbin = N.append(maxbin, maxvals[-1]) self.dssout[1].update(data=0.5*(minbin+maxbin), serr=0.5*(maxbin-minbin)) class SortPlugin(_OneOutputDatasetPlugin): """Sort a dataset.""" menu = (_('Compute'), _('Sorted'),) name = 'Sort' description_short = description_full = _('Sort a dataset') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldDataset('ds_sort', _('Sort by (optional)')), field.FieldBool('reverse', _('Reverse')), field.FieldDataset('ds_out', _('Output dataset')), ] def updateDatasets(self, fields, helper): """Do sorting of dataset.""" ds_sort = ds = helper.getDataset(fields['ds_in']) if fields['ds_sort'].strip(): ds_sort = helper.getDataset(fields['ds_sort']) idxs = N.argsort(ds_sort.data) if fields['reverse']: idxs = idxs[::-1] out = { 'data': ds.data[idxs] } if ds.serr is not None: out['serr'] = ds.serr[idxs] if ds.perr is not None: out['perr'] = ds.perr[idxs] if ds.nerr is not None: out['nerr'] = ds.nerr[idxs] self.dsout.update(**out) class SortTextPlugin(_OneOutputDatasetPlugin): """Sort a text dataset.""" menu = (_('Compute'), _('Sorted Text'),) name = 'Sort Text' description_short = description_full = _('Sort a text dataset') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset'), datatype='text'), field.FieldDataset('ds_sort', _('Sort by (optional)')), field.FieldBool('reverse', _('Reverse')), field.FieldDataset('ds_out', _('Output dataset'), datatype='text'), ] def getDatasets(self, fields): """Returns single output dataset (self.dsout).""" if fields['ds_out'] == '': raise DatasetPluginException(_('Invalid output dataset name')) self.dsout = DatasetText(fields['ds_out']) return [self.dsout] def updateDatasets(self, fields, helper): """Do sorting of dataset.""" ds = helper.getTextDataset(fields['ds_in']).data if fields['ds_sort'].strip(): ds_sort = helper.getDataset(fields['ds_sort']) idxs = N.argsort(ds_sort.data) dout = [] for i in idxs: dout.append(ds[i]) else: dout = list(ds) dout.sort() if fields['reverse']: dout = dout[::1] self.dsout.update(dout) class Histogram2D(DatasetPlugin): """Compute 2D histogram for two 1D dataset. Algorithm: Count up values in boxes. Sort. Compute probability working downwards. """ menu = (_('Compute'), _('2D histogram'),) name = 'Histogram 2D' description_short = _('Compute 2D histogram.') description_full = _('Given two 1D datasets, compute a 2D histogram. ' 'Can optionally compute a probability distribution.') def __init__(self): """Input fields.""" self.fields = [ field.FieldDataset('ds_inx', _('Input dataset x')), field.FieldFloatOrAuto('minx', _('Minimum value for dataset x')), field.FieldFloatOrAuto('maxx', _('Maximum value for dataset x')), field.FieldInt('binsx', _('Number of bins for dataset x'), default=10, minval=2), field.FieldDataset('ds_iny', _('Input dataset y')), field.FieldFloatOrAuto('miny', _('Minimum value for dataset y')), field.FieldFloatOrAuto('maxy', _('Maximum value for dataset y')), field.FieldInt('binsy', _('Number of bins for dataset y'), default=10, minval=2), field.FieldCombo('mode', _('Mode'), items=('Count', 'Fraction', 'CumulativeProbability', 'CumulativeProbabilityInverse'), default='Count', editable=False), field.FieldDataset('ds_out', _('Output 2D dataset'), dims=2), ] def probabilityCalculator(self, histo): """Convert an image of counts to a cumulative probability distribution. """ # get sorted pixel values pixvals = N.ravel(histo) sortpix = N.sort(pixvals) # cumulative sum of values probs = N.cumsum(sortpix) probs = probs * (1./probs[-1]) # values in pixvals which are unique unique = N.concatenate( (sortpix[:-1] != sortpix[1:], [True]) ) # now we have the pixel values and probabilities pixaxis = sortpix[unique] probaxis = probs[unique] # use linear interpolation to map the pixvals -> cumulative probability probvals = N.interp(pixvals, pixaxis, probaxis) probvals = probvals.reshape(histo.shape) return probvals def updateDatasets(self, fields, helper): """Calculate values of output dataset.""" dsy = helper.getDataset(fields['ds_iny']).data dsx = helper.getDataset(fields['ds_inx']).data # use range of data or specified parameters miny = fields['miny'] if miny == 'Auto': miny = N.nanmin(dsy) maxy = fields['maxy'] if maxy == 'Auto': maxy = N.nanmax(dsy) minx = fields['minx'] if minx == 'Auto': minx = N.nanmin(dsx) maxx = fields['maxx'] if maxx == 'Auto': maxx = N.nanmax(dsx) # compute counts in each bin histo, xedge, yedge = N.histogram2d( dsy, dsx, bins=[fields['binsy'], fields['binsx']], range=[[miny,maxy], [minx,maxx]], normed=False) m = fields['mode'] if m == 'Count': out = histo elif m == 'Fraction': out = histo * (1./N.sum(histo)) elif m == 'CumulativeProbability': out = self.probabilityCalculator(histo) elif m == 'CumulativeProbabilityInverse': out = 1. - self.probabilityCalculator(histo) # update output dataset self.dsout.update(out, rangey=(miny, maxy), rangex=(minx, maxx)) def getDatasets(self, fields): """Returns single output dataset (self.dsout).""" if fields['ds_out'] == '': raise DatasetPluginException(_('Invalid output dataset name')) self.dsout = Dataset2D(fields['ds_out']) return [self.dsout] class ConvertNumbersToText(DatasetPlugin): """Convert a set of numbers to text.""" menu = (_('Convert'), _('Numbers to Text'),) name = 'NumbersToText' description_short = _('Convert numeric dataset to text') description_full = _('Given a 1D numeric dataset, create a text dataset ' 'by applying formatting. Format string is in standard ' 'Veusz-extended C formatting, e.g.\n' ' "%Vg" - general,' ' "%Ve" - scientific,' ' "%VE" - engineering suffix,' ' "%.2f" - two decimal places and' ' "%e" - C-style scientific') def __init__(self): """Define fields.""" self.fields = [ field.FieldDataset('ds_in', _('Input dataset')), field.FieldText('format', _('Format'), default='%Vg'), field.FieldDataset('ds_out', _('Output dataset name')), ] def getDatasets(self, fields): if fields['ds_out'] == '': raise DatasetPluginException(_('Invalid output dataset name')) self.dsout = DatasetText(fields['ds_out']) return [self.dsout] def updateDatasets(self, fields, helper): """Convert dataset.""" ds_in = helper.getDataset(fields['ds_in']) f = fields['format'] data = [ utils.formatNumber(n, f, locale=helper.locale) for n in ds_in.data ] self.dsout.update(data=data) datasetpluginregistry += [ AddDatasetPlugin, AddDatasetsPlugin, SubtractDatasetPlugin, SubtractMeanDatasetPlugin, SubtractMinimumDatasetPlugin, MultiplyDatasetPlugin, MultiplyDatasetsPlugin, DivideDatasetsPlugin, DivideMaxPlugin, DivideNormalizePlugin, MeanDatasetPlugin, ExtremesDatasetPlugin, CumulativePlugin, ConcatenateDatasetPlugin, InterleaveDatasetPlugin, ChopDatasetPlugin, PartsDatasetPlugin, DemultiplexPlugin, ThinDatasetPlugin, PolarToCartesianPlugin, ConvertNumbersToText, FilterDatasetPlugin, MovingAveragePlugin, LinearInterpolatePlugin, ReBinXYPlugin, SortPlugin, SortTextPlugin, Histogram2D, ] veusz-1.21.1/veusz/plugins/importplugin.py0000664000175000017500000005560112237406466017113 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Import plugin base class and helpers.""" from __future__ import division import os.path import numpy as N from ..compat import crange, cstr, cstrerror from .. import utils from .. import qtall as qt4 from . import field from . import datasetplugin def _(text, disambiguation=None, context='ImportPlugin'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) # add an instance of your class to this list to get it registered importpluginregistry = [] class ImportPluginParams(object): """Parameters to plugin are passed in this object.""" def __init__(self, filename, encoding, field_results): self.filename = filename self.encoding = encoding self.field_results = field_results def openFileWithEncoding(self): """Helper to open filename but respecting encoding.""" return utils.openEncoding(self.filename, self.encoding) class ImportPluginException(RuntimeError): """An exception to return errors about importing or previewing data.""" class ImportPlugin(object): """Define a plugin to read data in a particular format. Override doImport and optionally getPreview to define a new plugin. Register the class by adding it to the importpluginregistry list. Of promote_tab is set to some text, put the plugin on its own tab in the import dialog using that text as the tab name. """ name = 'Import plugin' author = '' description = '' # if set to some text, use this plugin on its own tab promote_tab = None # set these to get focus if a file is selected with these extensions # include the dot in the extension names file_extensions = set() def __init__(self): """Override this to declare a list of input fields if required.""" # a list of Field objects to display self.fields = [] def getPreview(self, params): """Get data to show in a text box to show a preview. params is a ImportPluginParams object. Returns (text, okaytoimport) """ f = params.openFileWithEncoding() return f.read(4096), True def doImport(self, params): """Actually import data params is a ImportPluginParams object. Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects """ return [] ################################################################# class ImportPluginExample(ImportPlugin): """An example plugin for reading a set of unformatted numbers from a file.""" name = "Example plugin" author = "Jeremy Sanders" description = _("Reads a list of numbers in a text file") def __init__(self): self.fields = [ field.FieldText("name", descr=_("Dataset name"), default="name"), field.FieldBool("invert", descr=_("invert values")), field.FieldFloat("mult", descr=_("Multiplication factor"), default=1), field.FieldInt("skip", descr=_("Skip N lines"), default=0, minval=0), field.FieldCombo("subtract", items=("0", "1", "2"), editable=False, default="0") ] def doImport(self, params): """Actually import data params is a ImportPluginParams object. Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects """ try: f = params.openFileWithEncoding() data = [] mult = params.field_results["mult"] sub = float(params.field_results["subtract"]) if params.field_results["invert"]: mult *= -1 for i in crange(params.field_results["skip"]): f.readline() for line in f: data += [float(x)*mult-sub for x in line.split()] return [datasetplugin.Dataset1D(params.field_results["name"], data), datasetplugin.Constant("testconst", "42"), datasetplugin.Function("testfunc(x)", "testconst*x**2")] except Exception as e: raise ImportPluginException(cstr(e)) class ImportPluginDateTime(ImportPlugin): """An example plugin for reading a set of iso date-times from a file.""" name = "Example plugin for date/times" author = "Jeremy Sanders" description = _("Reads a list of ISO date times in a text file") def __init__(self): self.fields = [ field.FieldText("name", descr=_("Dataset name"), default="name"), ] def doImport(self, params): """Actually import data params is a ImportPluginParams object. Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects """ f = params.openFileWithEncoding() data = [] for line in f: data.append( datasetplugin.DatasetDateTime. dateStringToFloat(line.strip()) ) return [ datasetplugin.DatasetDateTime(params.field_results["name"], data) ] #importpluginregistry.append( ImportPluginDateTime ) class QdpFile(object): """Handle reading of a Qdp file.""" def __init__(self, colnames): self.colmodes = {} self.skipmode = 'none' self.retndata = [] # store read in data here self.data = [] # index of max vector self.dataindex = 1 self.colnames = colnames # list of data groups for 2d objects self.datagroup2d = [] # axis ranges for 2d objects self.axis2d = [None, None] def handleRead(self, p): """Handle read command.""" try: mode = {'t': 'terr', 's': 'serr'}[p[1][:1]] except (IndexError, KeyError): raise ImportPluginException(_("read command takes terr/serr")) try: cols = [int(x) for x in p[2:]] except ValueError: raise ImportPluginException(_("read command takes list of columns separated by spaces")) for c in cols: self.colmodes[c] = mode def handleSkip(self, p): """Handle skip command.""" try: self.skipmode = {'o': 'off', 's': 'single', 'd': 'double'}[p[1][:1]] except (IndexError, KeyError): raise ImportPluginException(_("skip command takes single/double/off")) def handleNO(self, p, lastp): """Handle no command, meaning no data.""" if self.skipmode == 'none': self.addNans( len(p) ) elif self.skipmode == 'single': self.pushData() del self.data[:] self.dataindex += 1 elif self.skipmode == 'double': if lastp[0] == 'no': self.pushData() del self.data[:] self.dataindex += 1 else: self.addNans( len(p) ) def addNans(self, num): """Add a blank set of data to output.""" col = 0 ds = 0 while col < num or ds < len(self.data): if ds >= len(self.data): self.data.append([]) m = self.colmodes.get(ds+1) if m == 'serr': self.data[ds].append( (N.nan, N.nan) ) col += 2 elif m == 'terr': self.data[ds].append( (N.nan, N.nan, N.nan) ) col += 3 else: self.data[ds].append( N.nan ) col += 1 ds += 1 def pushData2D(self): """Handle 2D data groups.""" for num, r1, c1, r2, c2 in self.datagroup2d: arr = [] for c in crange(c1-1,c2-1+1): arr.append( self.data[c][r1-1:r2-1+1] ) # make data as "used" self.data[c] = None arr = N.array(arr) if num-1 < len(self.colnames): name = self.colnames[num-1] else: name = 'vec2d%i' % num rangex = rangey = None if self.axis2d[0] is not None: minval, pixsize = self.axis2d[0] rangex = (minval - pixsize*0.5, minval+(arr.shape[1]-0.5)*pixsize ) if self.axis2d[1] is not None: minval, pixsize = self.axis2d[1] rangey = (minval - pixsize*0.5, minval+(arr.shape[0]-0.5)*pixsize ) ds = datasetplugin.Dataset2D(name, data=arr, rangex=rangex, rangey=rangey) self.retndata.append(ds) def pushData(self): """Add data to output array. """ for i in crange(len(self.data)): if self.data[i] is None: continue # get dataset name if i < len(self.colnames): name = self.colnames[i] else: name = 'vec%i' % (i+1) if self.skipmode == 'single' or self.skipmode == 'double': name = name + '_' + str(self.dataindex) # convert data a = N.array(self.data[i]) if len(a.shape) == 1: # no error bars ds = datasetplugin.Dataset1D(name, data=a) elif a.shape[1] == 2: # serr ds = datasetplugin.Dataset1D(name, data=a[:,0], serr=a[:,1]) elif a.shape[1] == 3: # perr/nerr p = N.where(a[:,1] < a[:,2], a[:,2], a[:,1]) n = N.where(a[:,1] < a[:,2], a[:,1], a[:,2]) ds = datasetplugin.Dataset1D(name, data=a[:,0], perr=p, nerr=n) else: raise RuntimeError self.retndata.append(ds) def handleDataGroup(self, p): """Handle data groups.""" if len(p) == 3: # we don't support the renaming thing pass elif len(p) == 6: # 2d data try: pint = [int(x) for x in p[1:]] except ValueError: raise ImportPluginException(_("invalid 2d datagroup command")) self.datagroup2d.append(pint) def handleAxis(self, p): """Axis command gives range of axes (used for 2d).""" try: minval, maxval = float(p[2]), float(p[3]) except ValueError: raise ImportPluginException(_("invalid axis range")) self.axis2d[ p[0][0] == 'y' ] = (minval, maxval) def handleNum(self, p): """Handle set of numbers.""" nums = [] try: for n in p: if n.lower() == 'no': nums.append(N.nan) else: nums.append(float(n)) except ValueError: raise ImportPluginException(_("Cannot convert '%s' to numbers") % (' '.join(p))) col = 0 ds = 0 while col < len(nums): if ds >= len(self.data): self.data.append([]) m = self.colmodes.get(ds+1) if m == 'serr': self.data[ds].append( (nums[col], nums[col+1]) ) col += 2 elif m == 'terr': self.data[ds].append( (nums[col], nums[col+1], nums[col+2]) ) col += 3 else: self.data[ds].append( nums[col] ) col += 1 ds += 1 def importFile(self, fileobj, dirname): """Read data from file object. dirname is the directory in which the file is located """ contline = None lastp = [] for line in fileobj: # strip comments if line.find("!") >= 0: line = line[:line.find("!")] if line[:1] == '@': # read another file fname = os.path.join(dirname, line[1:].strip()) try: newf = open(fname) self.importFile(newf, dirname) except EnvironmentError: pass continue p = [x.lower() for x in line.split()] if contline: # add on previous continuation if existed p = contline + p contline = None if len(p) > 0 and p[-1][-1] == '-': # continuation p[-1] = p[-1][:-1] contline = p continue if len(p) == 0: # nothing continue v0 = p[0] if v0[0] in '0123456789-.': self.handleNum(p) elif v0 == 'no': self.handleNO(p, lastp) elif v0 == 'read': self.handleRead(p) elif v0[:2] == 'sk': self.handleSkip(p) elif v0[:2] == 'dg': self.handleDataGroup(p) elif v0[:1] == 'x' or v0[:2] == 'ya': self.handleAxis(p) else: # skip everything else (for now) pass lastp = p class ImportPluginQdp(ImportPlugin): """An example plugin for reading data from QDP files.""" name = "QDP import" author = "Jeremy Sanders" description = _("Reads datasets from QDP files") file_extensions = set(['.qdp']) def __init__(self): self.fields = [ field.FieldTextMulti("names", descr=_("Vector name list "), default=['']), ] def doImport(self, params): """Actually import data params is a ImportPluginParams object. Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects """ names = [x.strip() for x in params.field_results["names"] if x.strip()] f = params.openFileWithEncoding() rqdp = QdpFile(names) rqdp.importFile(f, os.path.dirname(params.filename)) rqdp.pushData2D() rqdp.pushData() f.close() return rqdp.retndata def cnvtImportNumpyArray(name, val, errorsin2d=True): """Convert a numpy array to plugin returns.""" try: val.shape except AttributeError: raise ImportPluginException(_("Not the correct format file")) try: val + 0. val = val.astype(N.float64) except TypeError: raise ImportPluginException(_("Unsupported array type")) if val.ndim == 1: return datasetplugin.Dataset1D(name, val) elif val.ndim == 2: if errorsin2d and val.shape[1] in (2, 3): # return 1d array if val.shape[1] == 2: # use as symmetric errors return datasetplugin.Dataset1D(name, val[:,0], serr=val[:,1]) else: # asymmetric errors # unclear on ordering here... return datasetplugin.Dataset1D(name, val[:,0], perr=val[:,1], nerr=val[:,2]) else: return datasetplugin.Dataset2D(name, val) else: raise ImportPluginException(_("Unsupported dataset shape")) class ImportPluginNpy(ImportPlugin): """For reading single datasets from NPY numpy saved files.""" name = "Numpy NPY import" author = "Jeremy Sanders" description = _("Reads a 1D/2D numeric dataset from a Numpy NPY file") file_extensions = set(['.npy']) def __init__(self): self.fields = [ field.FieldText("name", descr=_("Dataset name"), default=''), field.FieldBool("errorsin2d", descr=_("Treat 2 and 3 column 2D arrays as\n" "data with error bars"), default=True), ] def getPreview(self, params): """Get data to show in a text box to show a preview. params is a ImportPluginParams object. Returns (text, okaytoimport) """ try: retn = N.load(params.filename) except Exception: return _("Cannot read file"), False try: text = _('Array shape: %s\n') % str(retn.shape) text += _('Array datatype: %s (%s)\n') % (retn.dtype.str, str(retn.dtype)) text += str(retn) return text, True except AttributeError: return _("Not an NPY file"), False def doImport(self, params): """Actually import data. """ name = params.field_results["name"].strip() if not name: raise ImportPluginException(_("Please provide a name for the dataset")) try: retn = N.load(params.filename) except Exception as e: raise ImportPluginException(_("Error while reading file: %s") % cstr(e)) return [ cnvtImportNumpyArray( name, retn, errorsin2d=params.field_results["errorsin2d"]) ] class ImportPluginNpz(ImportPlugin): """For reading single datasets from NPY numpy saved files.""" name = "Numpy NPZ import" author = "Jeremy Sanders" description = _("Reads datasets from a Numpy NPZ file.") file_extensions = set(['.npz']) def __init__(self): self.fields = [ field.FieldBool("errorsin2d", descr=_("Treat 2 and 3 column 2D arrays as\n" "data with error bars"), default=True), ] def getPreview(self, params): """Get data to show in a text box to show a preview. params is a ImportPluginParams object. Returns (text, okaytoimport) """ try: retn = N.load(params.filename) except Exception: return _("Cannot read file"), False # npz files should define this attribute try: retn.files except AttributeError: return _("Not an NPZ file"), False text = [] for f in sorted(retn.files): a = retn[f] text.append(_('Name: %s') % f) text.append(_(' Shape: %s') % str(a.shape)) text.append(_(' Datatype: %s (%s)') % (a.dtype.str, str(a.dtype))) text.append('') return '\n'.join(text), True def doImport(self, params): """Actually import data. """ try: retn = N.load(params.filename) except Exception as e: raise ImportPluginException(_("Error while reading file: %s") % cstr(e)) try: retn.files except AttributeError: raise ImportPluginException(_("File is not in NPZ format")) # convert each of the imported arrays out = [] for f in sorted(retn.files): out.append( cnvtImportNumpyArray( f, retn[f], errorsin2d=params.field_results["errorsin2d"]) ) return out class ImportPluginBinary(ImportPlugin): name = "Binary import" author = "Jeremy Sanders" description = _("Reads numerical binary files.") file_extensions = set(['.bin']) def __init__(self): self.fields = [ field.FieldText("name", descr=_("Dataset name"), default=""), field.FieldCombo("datatype", descr=_("Data type"), items = ("float32", "float64", "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64"), default="float64", editable=False), field.FieldCombo("endian", descr=_("Endian (byte order)"), items = ("little", "big"), editable=False), field.FieldInt("offset", descr=_("Offset (bytes)"), default=0, minval=0), field.FieldInt("length", descr=_("Length (values)"), default=-1) ] def getNumpyDataType(self, params): """Convert params to numpy datatype.""" t = N.dtype(str(params.field_results["datatype"])) return t.newbyteorder( {"little": "<", "big": ">"} [ params.field_results["endian"]] ) def getPreview(self, params): """Preview of data files.""" try: f = open(params.filename, "rb") data = f.read() f.close() except EnvironmentError as e: return _("Cannot read file (%s)") % cstrerror(e), False text = [_('File length: %i bytes') % len(data)] def filtchr(c): """Filtered character to ascii range.""" if ord(c) <= 32 or ord(c) > 127: return '.' else: return c # do a hex dump (like in CP/M) for i in crange(0, min(65536, len(data)), 16): hdr = '%04X ' % i subset = data[i:i+16] hexdata = ('%02X '*len(subset)) % tuple([ord(x) for x in subset]) chrdata = ''.join([filtchr(c) for c in subset]) text.append(hdr+hexdata + ' ' + chrdata) return '\n'.join(text), True def doImport(self, params): """Import the data.""" name = params.field_results["name"].strip() if not name: raise ImportPluginException(_("Please provide a name for the dataset")) try: f = open(params.filename, "rb") f.seek( params.field_results["offset"] ) retn = f.read() f.close() except EnvironmentError as e: raise ImportPluginException(_("Error while reading file '%s'\n\n%s") % (params.filename, cstrerror(e))) try: data = N.fromstring(retn, dtype=self.getNumpyDataType(params), count=params.field_results["length"]) except ValueError as e: raise ImportPluginException(_("Error converting data for file '%s'\n\n%s") % (params.filename, cstr(e))) data = data.astype(N.float64) return [ datasetplugin.Dataset1D(name, data) ] importpluginregistry += [ ImportPluginNpy, ImportPluginNpz, ImportPluginQdp, ImportPluginBinary, ImportPluginExample, ] veusz-1.21.1/veusz/plugins/field.py0000644000175000017500000003437612327177747015460 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Data entry fields for plugins.""" from __future__ import division from .. import qtall as qt4 from .. import utils from .. import setting class Field(object): """A class to represent an input field on the dialog or command line.""" def __init__(self, name, descr=None, default=None): """name: name of field descr: description to show to user default: default value.""" self.name = name if descr: self.descr = descr else: self.descr = name self.default = default def makeControl(self, doc, currentwidget): """Create a set of controls for field.""" return None def setControlVal(self, controls, val): """Update control's value to val.""" pass def getControlResults(self, cntrls): """Get result from created contrls.""" return None class FieldText(Field): """Text entry on the dialog.""" def makeControl(self, doc, currentwidget): l = qt4.QLabel(self.descr) e = qt4.QLineEdit() if self.default: e.setText(self.default) return (l, e) def setControlVal(self, controls, val): controls[1].setText(val) def getControlResults(self, cntrls): return cntrls[1].text() class FieldCombo(Field): """Drop-down combobox on dialog.""" def __init__(self, name, descr=None, default=None, items=(), editable=True): """name: name of field descr: description to show to user default: default value items: items in drop-down box editable: whether user can enter their own value.""" Field.__init__(self, name, descr=descr, default=default) self.items = items self.editable = editable def makeControl(self, doc, currentwidget): l = qt4.QLabel(self.descr) c = qt4.QComboBox() c.addItems(self.items) c.setEditable(bool(self.editable)) if self.default: self.setControlVal((l, c), self.default) return (l, c) def setControlVal(self, controls, val): """Update value to val.""" if self.editable: controls[1].setEditText(val) else: controls[1].setCurrentIndex(controls[1].findText(val)) def getControlResults(self, cntrls): return cntrls[1].currentText() class _WidgetCombo(qt4.QComboBox): """Combo box for selecting widgets.""" def __init__(self, doc, widgettypes, default): """doc: Veusz document widgettypes: set of allowed widgettypes or empty for all default: default path.""" qt4.QComboBox.__init__(self) self.doc = doc self.widgettypes = widgettypes self.default = default self.updateWidgets() doc.signalModified.connect(self.updateWidgets) def _iterateWidgets(self, comboitems, paths, widget, level): """Walk widget tree recursively. Adds name onto a list of strings (comboitems) Adds path to widget onto list of paths (paths) """ if not self.widgettypes or widget.typename in self.widgettypes: comboitems.append(' '*level + widget.name) paths.append(widget.path) for w in widget.children: self._iterateWidgets(comboitems, paths, w, level+1) @qt4.pyqtSlot() def updateWidgets(self): """Update combo with new widgets.""" self.paths = [] # veusz widget paths of items comboitems = [] # names of items (with tree spacing) self._iterateWidgets(comboitems, self.paths, self.doc.basewidget, 0) if self.count() == 0: # first time around add default to get it selected, yuck :-( try: idx = self.paths.index(self.default) self.addItem( comboitems[idx] ) except ValueError: pass utils.populateCombo(self, comboitems) def getWidgetPath(self): """Get path of selected widget.""" return self.paths[self.currentIndex()] class FieldWidget(Field): """Drop-down combobox for selecting widgets.""" def __init__(self, name, descr=None, default='/', widgettypes=set()): """name: name of field descr: description to show to user default: default value - set to '' to get current widget.""" Field.__init__(self, name, descr=descr, default=default) self.widgettypes = widgettypes def makeControl(self, doc, currentwidget): default = self.default if default == '': default = currentwidget l = qt4.QLabel(self.descr) c = _WidgetCombo(doc, self.widgettypes, default) return (l, c) def setControlVal(self, controls, val): controls[1].setCurrentIndex(controls[1].findText(val)) def getControlResults(self, cntrls): return cntrls[1].getWidgetPath() class _FieldSetting(Field): """Field using a setting internally to avoid code duplication. Designed to be subclassed.""" def __init__(self, settingkls, name, descr=None, default='', setnparams = {}): Field.__init__(self, name, descr=descr, default=default) self.default = default self.setn = settingkls(name, default, **setnparams) def makeControl(self, doc, currentwidget): """Use setting makeControl method to make control.""" self.setn.parent = self # setting looks to parent for document self.setn.set(self.default) self.document = doc l = qt4.QLabel(self.descr) c = self.setn.makeControl(None) def updateval(cntrl, setn, val): setn.set(val) # if control changes setting, update setting c.sigSettingChanged.connect(updateval) return (l, c) def setControlVal(self, cntrls, val): self.setn.set(val) def getDocument(self): """This is used by settings to get their document.""" return self.document def getControlResults(self, cntrls): """Get result from setting.""" return self.setn.get() class FieldBool(_FieldSetting): """A true/false value using a check box.""" def __init__(self, name, descr=None, default=False): _FieldSetting.__init__(self, setting.Bool, name, descr=descr, default=default) class FieldInt(_FieldSetting): """An integer number field.""" def __init__(self, name, descr=None, default=0, minval=-9999999, maxval=9999999): """name: name of field descr: description to show to user default: default value. minval and maxval: minimum and maximum integers """ _FieldSetting.__init__(self, setting.Int, name, descr=descr, default=default, setnparams={'minval': minval, 'maxval': maxval}) class FieldFloat(_FieldSetting): """A floating point number field.""" def __init__(self, name, descr=None, default=None, minval=-1e99, maxval=1e99): """name: name of field descr: description to show to user default: default value. minval and maxval: minimum and maximum values """ _FieldSetting.__init__(self, setting.Float, name, descr=descr, default=default, setnparams={'minval': minval, 'maxval': maxval}) class FieldFloatOrAuto(_FieldSetting): """A floating point value or the text 'Auto'.""" def __init__(self, name, descr=None, default='Auto'): """name: name of field descr: description to show to user default: default value. """ _FieldSetting.__init__(self, setting.FloatOrAuto, name, descr=descr, default=default) class FieldColor(_FieldSetting): """Field for selecting a color - returns #rrggbb string.""" def __init__(self, name, descr=None, default='black'): _FieldSetting.__init__(self, setting.Color, name, descr=descr, default=default) class FieldFillStyle(_FieldSetting): """Field for selecting fill styles - returns a string.""" def __init__(self, name, descr=None, default='solid'): _FieldSetting.__init__(self, setting.FillStyle, name, descr=descr, default=default) class FieldLineStyle(_FieldSetting): """Field for selecting line styles - returns a string.""" def __init__(self, name, descr=None, default='solid'): _FieldSetting.__init__(self, setting.LineStyle, name, descr=descr, default=default) class FieldMarker(_FieldSetting): """Field for selecting a marker type. Returns a string """ def __init__(self, name, descr=None, default='circle'): _FieldSetting.__init__(self, setting.Marker, name, descr=descr, default=default) class FieldArrow(_FieldSetting): """Field for selecting an arrow type. Returns a string """ def __init__(self, name, descr=None, default='none'): _FieldSetting.__init__(self, setting.Arrow, name, descr=descr, default=default) class FieldErrorStyle(_FieldSetting): """Field for selecting an error bar style Returns a string """ def __init__(self, name, descr=None, default='bar'): _FieldSetting.__init__(self, setting.ErrorStyle, name, descr=descr, default=default) class FieldDistance(_FieldSetting): """Field for selecting a veusz-style distance, e.g. '1pt'. Returns a string """ def __init__(self, name, descr=None, default='1pt'): _FieldSetting.__init__(self, setting.Distance, name, descr=descr, default=default) class FieldFloatList(_FieldSetting): """Field for entering multiple numbers, separated by commas or spaces Returns a list/tuple of floats """ def __init__(self, name, descr=None, default=()): _FieldSetting.__init__(self, setting.FloatList, name, descr=descr, default=default) class FieldDataset(_FieldSetting): """Field for selecting a datset. Returns a string. Note that the validity of dataset names is not checked Note that a blank string may result """ def __init__(self, name, descr=None, default='', dims=1, datatype='numeric'): """name: name of field descr: description to show to user default: default value (ignored currently) dims: dimensions of dataset to show datatype: type of data: numeric or text """ _FieldSetting.__init__(self, setting.Dataset, name, descr=descr, default=default, setnparams={'dimensions': dims, 'datatype': datatype}) class FieldTextMulti(_FieldSetting): """Field for entering multiple lines of text. Returns a tuple/list of strings. """ def __init__(self, name, descr=None, default=('')): _FieldSetting.__init__(self, setting.Strings, name, descr=descr, default=default) class FieldDatasetMulti(_FieldSetting): """Field for entering multiple datasets. Returns a tuple/list of strings. """ def __init__(self, name, descr=None, default=(''), dims=1, datatype='numeric'): """dims is number of dimensions of datasets to show in drop-down list. datatype is 'numeric' or 'text' """ _FieldSetting.__init__(self, setting.Datasets, name, descr=descr, default=default, setnparams={'dimensions': dims, 'datatype': datatype}) class FieldLineMulti(_FieldSetting): """A field for holding a set of lines. Consists of tuples [('dotted', '1pt', 'color', , False), ...] These are style, width, color, and hide or style, widget, color, transparency, hide This is compatible with the contour widget line style """ def __init__(self, name, descr=None, default=(('solid', '1pt', 'black', False),) ): _FieldSetting.__init__(self, setting.LineSet, name, descr=descr, default=default) class FieldFillMulti(_FieldSetting): """A field for holding a set of fills. Consists of tuples [('solid', 'color', , False), ...] These are color, fill style, and hide or color, fill style, transparency and hide This is compatible with the contour widget line style """ def __init__(self, name, descr=None, default=()): _FieldSetting.__init__(self, setting.FillSet, name, descr=descr, default=default) class FieldFontFamily(_FieldSetting): """A field for holding a font family. Returns a string. """ def __init__(self, name, descr=None, default=None): """Default None selects the default font.""" if default is None: default = setting.Text.defaultfamily _FieldSetting.__init__(self, setting.FontFamily, name, descr=descr, default=default) class FieldFilename(_FieldSetting): """Select a filename with a browse button.""" def __init__(self, name, descr=None, default=''): _FieldSetting.__init__(self, setting.Filename, name, descr=descr, default=default) veusz-1.21.1/veusz/plugins/votable.py0000664000175000017500000000723012376130006015775 0ustar jssjss# Copyright (C) 2012 Science and Technology Facilities Council. # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division, print_function from ..compat import CStringIO, curlrequest from .importplugin import ImportPlugin, importpluginregistry from .datasetplugin import Dataset1D, DatasetText try: import astropy.version if [int(x) for x in astropy.version.version.split('.')] >= [0, 2]: from astropy.io.votable.table import parse else: from astropy.io.vo.table import parse except ImportError: print('VO table import: astropy module not available') else: class ImportPluginVoTable(ImportPlugin): name = 'VO table import' author = 'Graham Bell' description = 'Reads datasets from VO tables' def _load_votable(self, params): if 'url' in params.field_results: buff = CStringIO(curlrequest.urlopen( params.field_results['url']).read()) return parse(buff, filename=params.filename) else: return parse(params.filename) def doImport(self, params): result = [] votable = self._load_votable(params) for table in votable.iter_tables(): for field in table.fields: fieldname = field.name if field.datatype in ['float', 'double', 'short', 'int', 'unsignedByte']: result.append(Dataset1D(fieldname, table.array[fieldname])) elif field.datatype in ['char', 'string', 'unicodeChar']: result.append(DatasetText(fieldname, table.array[fieldname])) elif field.datatype in ['floatComplex', 'doubleComplex']: print(('VO table import: skipping complex field ' + fieldname)) elif field.datatype in ['boolean', 'bit']: print(('VO table import: skipping boolean field ' + fieldname)) else: print(('VO table import: unknown data type ' + field.datatype + ' for field ' + fieldname)) return result def getPreview(self, params): try: votable = self._load_votable(params) except: return ('', False) summary = [] for table in votable.iter_tables(): summary.append(table.name + ':') for field in table.fields: summary.append(' ' + field.name + ' (' + field.datatype +')') return ('\n'.join(summary), True) importpluginregistry += [ImportPluginVoTable] veusz-1.21.1/veusz/plugins/toolsplugin.py0000664000175000017500000004655712237406466016753 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## """Plugins for general operations.""" from __future__ import division import random import re import fnmatch from ..compat import cbasestr from .. import qtall as qt4 from .. import utils from . import field def _(text, disambiguation=None, context='ToolsPlugin'): """Translate text.""" return qt4.QCoreApplication.translate(context, text, disambiguation) # add an instance of your class to this list to be registered toolspluginregistry = [] class ToolsPluginException(RuntimeError): """Raise this to report an error doing what was requested. """ pass class ToolsPlugin(object): # the plugin will get inserted into the menu in a hierarchy based on # the elements of this tuple menu = (_('Base plugin'),) name = 'Base plugin' author = '' description_short = '' description_full = '' # if the plugin takes no parameters, set this to False has_parameters = True def __init__(self): """Override this to declare a list of input fields if required.""" self.fields = [] def apply(self, commandinterface, fieldresults): """Override this option to do the work of the plugin. * commandinterface is an instance of the embedding interface, which also contains the Root widget node object * fieldresults is a dict containing the values of the fields plus 'currentwidget' which is the path to the current widget * Raise an ToolsPluginException(str) to report a problem to the user """ ######################################################################### class ColorsRandomize(ToolsPlugin): """Randomize the colors used in plotting.""" menu = (_('Colors'), _('Randomize')) name = 'Randomize colors' description_short = _('Randomize the colors used in plotting') description_full = _('Randomize the colors used in plotting markers, lines or error bars. Random colors in hue, saturation and luminosity (HSV) are chosen between the two colors given.') def __init__(self): """Construct plugin.""" self.fields = [ field.FieldWidget("widget", descr=_("Start from widget"), default="/"), field.FieldBool("randxy", descr=_("Randomize xy plotters"), default=True), field.FieldBool("randfunc", descr=_("Randomize function plotters"), default=True), field.FieldColor('color1', descr=_("Start of color range"), default='#404040'), field.FieldColor('color2', descr=_("End of color range"), default='#ff0004'), ] def getRandomColor(self, col1, col2): """Return RGB name for a random color.""" H1, H2 = col1.hue(), col2.hue() S1, S2 = col1.saturation(), col2.saturation() V1, V2 = col1.value(), col2.value() def rand(a, b): if a > b: return random.randint(b, a) return random.randint(a, b) col = qt4.QColor.fromHsv(rand(H1, H2), rand(S1, S2), rand(V1, V2)) return str(col.name()) def apply(self, ifc, fields): """Do the randomizing.""" fromwidget = ifc.Root.fromPath(fields['widget']) col1 = qt4.QColor(fields['color1']) col2 = qt4.QColor(fields['color2']) if fields['randxy']: for node in fromwidget.WalkWidgets(widgettype='xy'): col = self.getRandomColor(col1, col2) node.PlotLine.color.val = col node.MarkerFill.color.val = col node.ErrorBarLine.color.val = col if fields['randfunc']: for node in fromwidget.WalkWidgets(widgettype='function'): node.Line.color.val = self.getRandomColor(col1, col2) class ColorsSequence(ToolsPlugin): """Color plotters in sequence.""" menu = (_('Colors'), _('Sequence')) name = 'Create color sequence' description_short = _('Make widgets use sequence of colors') description_full = _('Give new colors to each widget in a sequence between the two colors given.') def __init__(self): """Construct plugin.""" self.fields = [ field.FieldWidget("widget", descr=_("Start from widget"), default="/"), field.FieldBool("randxy", descr=_("Color xy plotters"), default=True), field.FieldBool("randfunc", descr=_("Color function plotters"), default=True), field.FieldColor('color1', descr=_("Start of color range"), default='#ff0000'), field.FieldColor('color2', descr=_("End of color range"), default='#4000ff'), ] def apply(self, ifc, fields): """Do the randomizing.""" fromwidget = ifc.Root.fromPath(fields['widget']) col1 = qt4.QColor(fields['color1']) col2 = qt4.QColor(fields['color2']) H1, H2 = col1.hue(), col2.hue() S1, S2 = col1.saturation(), col2.saturation() V1, V2 = col1.value(), col2.value() # add up total number of widgets numwidgets = ( len( list(fromwidget.WalkWidgets(widgettype='xy')) ) + len( list(fromwidget.WalkWidgets(widgettype='function')) ) ) def colatidx(i): """Get color in range 0...numwidgets-1.""" div = max(numwidgets-1, 1) H = i * (H2-H1) / div + H1 S = i * (S2-S1) / div + S1 V = i * (V2-V1) / div + V1 return str(qt4.QColor.fromHsv(H, S, V).name()) idx = 0 for node in fromwidget.WalkWidgets(): t = node.widgettype if fields['randxy'] and t == 'xy': col = colatidx(idx) idx += 1 node.PlotLine.color.val = col node.MarkerFill.color.val = col node.ErrorBarLine.color.val = col if fields['randfunc'] and t == 'function': node.Line.color.val = colatidx(idx) idx += 1 class ColorsReplace(ToolsPlugin): """Replace one color by another.""" menu = (_('Colors'), _('Replace')) name = 'Replace colors' description_short = _('Search and replace colors') description_full = _('Searches for a color and replaces it with a different color') def __init__(self): """Construct plugin.""" self.fields = [ field.FieldWidget("widget", descr=_("Start from widget"), default="/"), field.FieldBool("follow", descr=_("Change references and defaults"), default=True), field.FieldColor('color1', descr=_("Color to change"), default='black'), field.FieldColor('color2', descr=_("Replacement color"), default='red'), ] def apply(self, ifc, fields): """Do the color search and replace.""" fromcol = qt4.QColor(fields['color1']) def walkNodes(node): """Walk nodes, changing values.""" if node.type == 'setting' and node.settingtype == 'color': # only follow references if requested if node.isreference: if fields['follow']: node = node.resolveReference() else: return # evaluate into qcolor to make sure is a true match if qt4.QColor(node.val) == fromcol: node.val = fields['color2'] else: for c in node.children: walkNodes(c) fromwidget = ifc.Root.fromPath(fields['widget']) walkNodes(fromwidget) class ColorsSwap(ToolsPlugin): """Swap colors used in plotting.""" menu = (_('Colors'), _('Swap')) name = 'Swap colors' description_short = _('Swap two colors') description_full = _('Swaps two colors in the plot') def __init__(self): """Construct plugin.""" self.fields = [ field.FieldWidget("widget", descr=_("Start from widget"), default="/"), field.FieldBool("follow", descr=_("Change references and defaults"), default=True), field.FieldColor('color1', descr=_("First color"), default='black'), field.FieldColor('color2', descr=_("Second color"), default='red'), ] def apply(self, ifc, fields): """Do the color search and replace.""" col1 = qt4.QColor(fields['color1']) col2 = qt4.QColor(fields['color2']) def walkNodes(node): """Walk nodes, changing values.""" if node.type == 'setting' and node.settingtype == 'color': # only follow references if requested if node.isreference: if fields['follow']: node = node.resolveReference() else: return # evaluate into qcolor to make sure is a true match if qt4.QColor(node.val) == col1: node.val = fields['color2'] elif qt4.QColor(node.val) == col2: node.val = fields['color1'] else: for c in node.children: walkNodes(c) fromwidget = ifc.Root.fromPath(fields['widget']) walkNodes(fromwidget) class TextReplace(ToolsPlugin): """Randomize the colors used in plotting.""" menu = (_('General'), _('Replace text')) name = 'Replace text' description_short = _('Search and replace text in settings') description_full = _('Searches for text in a setting and replaces it') def __init__(self): """Construct plugin.""" self.fields = [ field.FieldWidget("widget", descr=_("Start from widget"), default="/"), field.FieldBool("follow", descr=_("Change references and defaults"), default=True), field.FieldBool("onlystr", descr=_("Change only textual data"), default=False), field.FieldText('text1', descr=_("Text to change"), default=''), field.FieldText('text2', descr=_("Replacement text"), default=''), ] def apply(self, ifc, fields): """Do the search and replace.""" def walkNodes(node): """Walk nodes, changing values.""" if node.type == 'setting': # only follow references if requested if node.isreference: if fields['follow']: node = node.resolveReference() else: return val = node.val # try to change if a string, and not only strings or type is string if isinstance(val, cbasestr) and (not fields['onlystr'] or node.settingtype == 'str'): # update text if it changes val2 = val.replace(fields['text1'], fields['text2']) if val != val2: try: node.val = val2 except utils.InvalidType: pass else: for c in node.children: walkNodes(c) fromwidget = ifc.Root.fromPath(fields['widget']) walkNodes(fromwidget) class WidgetsClone(ToolsPlugin): """Take a widget and children and clone them.""" menu = (_('Widgets'), _('Clone for datasets')) name = 'Clone widgets for datasets' description_short = _('Clones a widget and its children for datasets') description_full = _('Take a widget and its children and clone it, plotting different sets of data in each clone.\nHint: Use a "*" in the name of a replacement dataset to match multiple datasets, e.g. x_*') def __init__(self): """Construct plugin.""" self.fields = [ field.FieldWidget("widget", descr=_("Clone widget"), default=""), field.FieldDataset('ds1', descr=_("Dataset 1 to change"), default=''), field.FieldDatasetMulti('ds1repl', descr=_("Replacement(s) for dataset 1")), field.FieldDataset('ds2', descr=_("Dataset 2 to change (optional)"), default=''), field.FieldDatasetMulti('ds2repl', descr=_("Replacement(s) for dataset 2")), field.FieldBool("names", descr=_("Build new names from datasets"), default=True), ] def apply(self, ifc, fields): """Do the cloning.""" def expanddatasets(dslist): """Expand * and ? in dataset names.""" datasets = [] for ds in dslist: if ds.find('*') == -1 and ds.find('?') == -1: datasets.append(ds) else: dlist = fnmatch.filter(ifc.GetDatasets(), ds) dlist.sort() datasets += dlist return datasets def chainpairs(dslist1, dslist2): """Return pairs of datasets, repeating if necessary.""" if not dslist1: dslist1 = [''] if not dslist2: dslist2 = [''] end1 = end2 = False idx1 = idx2 = 0 while True: if idx1 >= len(ds1repl): idx1 = 0 end1 = True if idx2 >= len(ds2repl): idx2 = 0 end2 = True if end1 and end2: break yield dslist1[idx1], dslist2[idx2] idx1 += 1 idx2 += 1 def walkNodes(node, dsname, dsrepl): """Walk nodes, changing datasets.""" if node.type == 'setting': if node.settingtype in ( 'dataset', 'dataset-or-floatlist', 'dataset-or-str'): # handle single datasets if node.val == dsname: node.val = dsrepl elif node.settingtype == 'dataset-multi': # settings with multiple datasets out = list(node.val) for i, v in enumerate(out): if v == dsname: out[i] = dsrepl if tuple(out) != node.val: node.val = out else: for c in node.children: walkNodes(c, dsname, dsrepl) # get names of replacement datasets ds1repl = expanddatasets(fields['ds1repl']) ds2repl = expanddatasets(fields['ds2repl']) # make copies of widget and children for each pair of datasets widget = ifc.Root.fromPath(fields['widget']) for ds1r, ds2r in chainpairs(ds1repl, ds2repl): # construct a name newname = None if fields['names']: newname = widget.name if ds1r: newname += ' ' + ds1r if ds2r: newname += ' ' + ds2r # make the new widget (and children) newwidget = widget.Clone(widget.parent, newname=newname) # do replacement of datasets if fields['ds1']: walkNodes(newwidget, fields['ds1'], ds1r) if fields['ds2']: walkNodes(newwidget, fields['ds2'], ds2r) class FontSize(ToolsPlugin): """Increase or decrease the font size.""" def __init__(self, dirn): """Construct plugin. dirn == 1: increase sizes dirn == -1: decrease sizes """ self.dirn = dirn self.fields = [ field.FieldWidget("widget", descr=_("Start from widget"), default="/"), field.FieldBool("follow", descr=_("Change references and defaults"), default=True), field.FieldFloat("delta", descr=_("Change by value"), default=2), ] def apply(self, ifc, fields): """Do the search and replace.""" pt_re = re.compile(r'^([\d.]+)[ ]*pt$') delta = fields['delta'] changed = set() def walkNodes(node): """Walk nodes, changing values.""" if node.type == 'setting': if node.name == 'size': # find size setting with sibling font (improve this) if not hasattr(node.parent, 'font'): return # only follow references if requested if node.isreference: if fields['follow']: node = node.resolveReference() else: return # avoid doing things more than once p = node.path if p in changed: return changed.add(p) # change point size if requested m = pt_re.match(node.val) if m: pt = float(m.group(1)) + delta*self.dirn if pt < 0: pt = 0.1 node.val = '%gpt' % pt else: for c in node.children: walkNodes(c) fromwidget = ifc.Root.fromPath(fields['widget']) walkNodes(fromwidget) class FontSizeIncrease(FontSize): menu = (_('General'), _('Increase font sizes')) name = 'Increase font sizes' description_short = _('Increase font sizes') description_full = _('Increase font sizes by number of points given') def __init__(self): FontSize.__init__(self, 1) class FontSizeDecrease(FontSize): menu = (_('General'), _('Decrease font sizes')) name = 'Decrease font sizes' description_short = _('Decrease font sizes') description_full = _('Decrease font sizes by number of points given') def __init__(self): FontSize.__init__(self, -1) toolspluginregistry += [ ColorsRandomize, ColorsSequence, ColorsReplace, ColorsSwap, TextReplace, WidgetsClone, FontSizeIncrease, FontSizeDecrease, ] veusz-1.21.1/veusz/plugins/__init__.py0000664000175000017500000000245012237406466016113 0ustar jssjss# Copyright (C) 2010 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from .field import * from .datasetplugin import * from .importplugin import * from .toolsplugin import * from .votable import * # backward compatibility ImportDataset1D = Dataset1D ImportDataset2D = Dataset2D ImportDatasetText = DatasetText ImportField = Field ImportFieldCheck = FieldBool ImportFieldText = FieldText ImportFieldFloat = FieldFloat ImportFieldInt = FieldInt ImportFieldCombo = FieldCombo veusz-1.21.1/veusz/embed_remote.py0000664000175000017500000002426612376130006015317 0ustar jssjss# Copyright (C) 2008 Jeremy S. Sanders # Email: Jeremy Sanders # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## from __future__ import division import sys import struct import socket from .compat import citems, pickle from .windows.simplewindow import SimpleWindow from . import document from . import setting from . import qtall as qt4 """Program to be run by embedding interface to run Veusz commands.""" # embed.py module checks this is the same as its version number API_VERSION = 2 class EmbeddedClient(object): """An object for each instance of embedded window with document.""" def __init__(self, title, doc=None, hidden=False): """Construct window with title given.""" self.window = SimpleWindow(title, doc=doc) if not hidden: self.window.show() self.document = self.window.document self.plot = self.window.plot # use time based checking by default self.plot.setTimeout(250) self.ci = document.CommandInterpreter(self.document) self.ci.addCommand('Close', self.cmdClose) self.ci.addCommand('Zoom', self.cmdZoom) self.ci.addCommand('EnableToolbar', self.cmdEnableToolbar) self.ci.addCommand('ForceUpdate', self.cmdForceUpdate) self.ci.addCommand('GetClick', self.cmdGetClick) self.ci.addCommand('ResizeWindow', self.cmdResizeWindow) self.ci.addCommand('SetUpdateInterval', self.cmdSetUpdateInterval) self.ci.addCommand('MoveToPage', self.cmdMoveToPage) self.ci.addCommand('IsClosed', self.cmdIsClosed) self.ci.addCommand('SetAntiAliasing', self.cmdSetAntiAliasing) self.ci.addCommand('_apiVersion', self.cmd_apiVersion) setting.transient_settings['unsafe_mode'] = True self.document.sigLog.connect(self.logEmitted) def logEmitted(self, msg): """Write anything logged to stderr.""" sys.stderr.write(msg + '\n') def cmdClose(self): """Close() Close this window.""" self.window.close() self.document = None self.window = None self.plot = None self.ci = None def cmdIsClosed(self): """IsClosed() Return whether window is still open.""" return not self.window.isVisible() def cmd_apiVersion(self): """Get internal API version.""" return API_VERSION def cmdZoom(self, zoom): """Zoom(zoom) Set the plot zoom level: This is a number to for the zoom from 1:1 or 'page': zoom to page 'width': zoom to fit width 'height': zoom to fit height """ self.window.setZoom(zoom) def cmdSetAntiAliasing(self, ison): """SetAntiAliasing(zoom) Enables or disables anti aliasing. """ self.window.setAntiAliasing(ison) def cmdEnableToolbar(self, enable=True): """EnableToolbar(enable=True) Enable the toolbar in this plotwindow. if enable is False, disable it. """ self.window.enableToolbar(enable) def cmdForceUpdate(self): """ForceUpdate() Forces an update of the plot window. """ self.plot.actionForceUpdate() def cmdGetClick(self): """GetClick() Return a clicked point. The user can click a point on the graph This returns a list of tuples containing items for each axis in the clicked region: (axisname, valonaxis) where axisname is the full name of an axis valonaxis is value clicked along the axis [] is returned if no axes span the clicked region """ return self.plot.getClick() def cmdResizeWindow(self, width, height): """ResizeWindow(width, height) Resize the window to be width x height pixels.""" self.window.resize(width, height) def cmdSetUpdateInterval(self, interval): """SetUpdateInterval(interval) Set graph update interval. interval is in milliseconds (ms) set to zero to disable updates set to -1 to update when document changes default interval is 250ms """ self.plot.setTimeout(interval) def cmdMoveToPage(self, pagenum): """MoveToPage(pagenum) Tell window to show specified pagenumber (starting from 1). """ self.plot.setPageNumber(pagenum-1) class EmbedApplication(qt4.QApplication): """Application to run remote end of embed connection. Commands are sent over stdin, with responses sent to stdout """ # lengths of lengths sent to application cmdlenlen = struct.calcsize('