pax_global_header00006660000000000000000000000064117545676360014536gustar00rootroot0000000000000052 comment=79841cca2a70aa1cc4c7e27602751b882948c3ed gaphas-0.7.2/000077500000000000000000000000001175456763600130075ustar00rootroot00000000000000gaphas-0.7.2/COPYING000066400000000000000000000622451175456763600140530ustar00rootroot00000000000000 GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, 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 library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, 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 companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, 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 library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete 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 distribute a copy of this License along with the Library. 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 Library or any portion of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, 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 Library, 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 Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you 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. If distribution of 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 satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. 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. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library 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. 9. 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 Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library 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. 11. 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 Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library 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 Library. 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. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library 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. 13. The Free Software Foundation may publish revised and/or new versions of the Library 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 Library 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 Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, 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 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. 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 LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. 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 library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA. Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! gaphas-0.7.2/NEWS000066400000000000000000000036571175456763600135210ustar00rootroot000000000000000.7.2 ----- - Fix bug in calculating bounding box for rotated text - Few minor updates 0.7.1 ----- - Views no longer lose reference to canvas on unrealize() - bug fix in log message 0.7.0 ----- - Painters are bound to a specific view, like tools - Introduced aspects for finding items and handles - New feature: Guides, for aligning elements - Free hand drawing style 0.6.0 ----- - Handlers are no longer called recursively for observed methods/properties. - removed enable_dispatching() and disable_dispatching() calls. - Made aspect code simpler. - Moved disconnect code from tool to aspect, as stated in Aspect's docstring. - Fixed issues in connections. - Lot's of fixes and testing has been done on the undo mechanism. 0.5.0 ----- - Split tools in tools and aspects, separating the _what_ from the _how_. For this, a dependency to the simplegeneric module is introduced. - Renamed VariablePoint to Position. - Handle is no longer inheriting from VariablePoint/Position. - Handle connections are no longer registered on the handle, but are maintained in the Canvas instance. This makes for much easier querying (e.g. which elements are attached to some element). Added a Table class to support this functionality. - Added a timeout property on the @async decorator. The method is invoked the amunt of milliseconds after the first invocation. 0.4.1 ------ - Call Item._set_canvas after matrix update - Verify if value changed before marking variable as dirty. 0.4.0 ------ - allow to define connectable parts of item's (ports feature) - implemented default connection tool (thanks to ports feature) - line segment tool implemented (code taken from gaphor) - implemented Item.constraint method to simplify item's constraint creation - The canvas (-view) is no longer tied to the (0, 0) position. Scrolling can be done quite fluidly with the new PanTool implementation. - API changes - use positions instead of "x, y" pairs in all method calls gaphas-0.7.2/README.rst000066400000000000000000000212111175456763600144730ustar00rootroot00000000000000Gaphor's Canvas =============== This module contains a new canvas implementation for Gaphor. Homepage: http://github.com/amolenaar/gaphas Issue tracker: http://gaphor.lighthouseapp.com Mailing list: gaphor-dev@googlegroups.com The basic idea is: - Items (canvas items) can be added to a Canvas. - The canvas maintains the tree structure (parent-child relationships between items). - A constraint solver is used to maintain item constraints and inter-item constraints. - The item (and user) should not be bothered with things like bounding-box calculations. - Very modular: e.g. handle support could be swapped in and swapped out. - Rendering using Cairo. Gaphas is released under the terms of the GNU Library General Public License (LGPL). See the COPYING file for details. .. contents:: How it Works ============ The Canvas class (from canvas.py) acts as a container for Item's (from item.py). The item's parent/child relationships are maintained here (not in the Item!). An Item can have a set of Handle's (from connector.py) which can be used to manipulate the item (although this is not necessary). Each item has it's own coordinate system (a (0, 0) point). Item.matrix is the transformation relative to the parent item of the Item, as defined in the Canvas. Handles can connect to Ports. A Port is a location (line or point) where a handle is allowed to connect on another item. The process of connecting depends on the case at hand, but most often involves the creation of some sort of constraint between the Handle and the item it's connecting to (see doc/ports.txt). The Canvas also contains a constraint Solver (from solver.py) that can be used to solve mathematical dependencies between items (such as Handles that should be aligned). The constraint solver is also a handy tool to keep constraint in the item true (e.g. make sure a box maintains it's rectangular shape). View (from view.py) is used to visualize a canvas. On a View, a Tool (from tool.py) can be applied, which will handle user input (button presses, key presses, etc.). Painters (from painter.py) are used to do the actual drawing. This way it should be easy do draw to other media than the screen, such as a printer or PDF document. Updating item state ------------------- If an items needs updating, it sets out an update request on the Canvas (Canvas.request_update()). The canvas performs an update by calling: 1. Item.pre_update(context) for each item marked for update 2. Updating Canvas-to-Item matrices, for fast transformation of coordinates from the canvas' to the items' coordinate system. The c2i matrix is stored on the Item as Item._matrix_c2i. 3. Solve constraints. 4. Normalize items by setting the coordinates of the first handle to (0, 0). 5. Updating Canvas-to-Item matrices for items that have been changed by normalization, just to be on the save side. 6. Item.post_update(context) for each item marked for update, including items that have been marked during constraint solving. The idea is to do as much updating as possible in the {pre|post}_update() methods, since they are called when the application is not handling user input. The context contains: :cairo: a CairoContext, this can be used to calculate the dimensions of text for example NOTE: updating is done from the canvas, items should not update sub-items. After an update, the Item should be ready to be drawn. Constraint solving ------------------ A word about the constraint solver seems in place. It is one of the big features of this library after all. The Solver is able to solve constraints. Constraints can be applied to items (Variables owned by the item actually). Element items, for example, uses constraints to maintain their recangular shape. Constraints can be created *between* items (for example a line that connects to a box). Constraints that apply to one item are pretty straight forward, as all variables live in the same coordinate system (of the item). The variables (in most cases a Handle's x and y coordinate) can simply be put in a constraint. When two items are connected to each other and constraints are created, a problem shows up: variables live in separate coordinate systems. To overcome this problem a Projection (from solver.py) has been defined. With a Projection instance, a variable can be "projected" on another coordinate system. In this case, where two items are connecting to each other, the Canvas' coordinate system is used. Drawing ------- Drawing is done by the View. All items marked for redraw (e.i. the items that had been updated) will be drawn in the order in which they reside in the Canvas (first root item, then it's children; second root item, etc.) The view context passed to the Items draw() method has the following properties: :view: the view we're drawing to :cairo: the CairoContext to draw to :selected: True if the item is actually selected in the view :focused: True if the item has the focus :hovered: True if the mouse pointer if over the item. Only the top-most item is marked as hovered. :dropzone: The item is marked as drop zone. This happens then an item is dragged over the item and (if dropped) will become a child of this item. :draw_all: True if everything drawable on the item should be drawn (e.g. when calculating the bounding boxes). The View automatically calculates the bounding box for the item, based on the items drawn in the draw(context) function (this is only done when really necessary, e.g. after an update of the item). The bounding box is in viewport coordinates. The actual drawing is done by Painters (painter.py). A series of Painters have been defined: one for handles, one for items, etc. Tools ----- Behaviour is added to the canvas(-view) by tools. Tools can be chained together in order to provide more complex behaviour. To make it easy a DefaultTool has been defined: a ToolChain instance with the tools added that are listed in the following sections. ToolChain The ToolChain does not do anything by itself. It delegates to a set of tools and keeps track of which tool has grabbed the focus. This happens most of the time when the uses presses a mouse button. The tool requests a grab() and all upcoming events (e.g. motion or button release events) are directly sent to the focused tool. HoverTool A small and simple tool that does nothing more than making the item under the mouse button the "hovered item". When such an item is drawn, its context.hovered_item flag will be set to True. HandleTool The HandleTool is used to deal with handles. Handles can be dragged around. Clicking on a handle automatically makes the underlying item the focused item. ItemTool The item tool takes care of selecting items and dragging items around. TextEditTool This is a demo-tool, featuring a text-edit pop-up. RubberbandTool The last tool in line is the rubber band tool. It's invoked when the mouse button is pressed on a section of the view where no items or handles are present. It allows the user to select items using a selection box (rubber band). Interaction ----------- Interaction with the canvas view (visual component) is handled by tools. Although the default tools do a fair amount of work, in most cases you'll see that especially the way items connect with each other is not the way you want it. That's okay. HandleTool provides some hooks (connect, disconnect and glue) to implement custom connection behaviour (in fact, the default implementation doesn't do any connecting at all!). One of the problems you'll face is what to do when an item is removed from the canvas and there are other items (lines) connected to. This problem can be overcome by providing a disconnect handler to the handle instance ones it is connected. A callable object (e.g. function) can be assigned to the handle. It is called at the moment the item it's connected to is removed from the canvas. Undo ==== Gaphas has a simple build-in system for registering changes in it's classes and notifying the application. This code resides in state.py. There is also a "reverter" framework in place. This "framework" is notified when objects change their state and will figure out the reverse operation that has to be applied in order to undo the operation. See state.txt and undo.txt for details and usage examples. Guidelines ========== Documentation should be in UK English. Following the `Python coding guidelines`_ indentation should be 4 spaces (no tabs), function and method names should be ``lowercase_with_underscore()``. We're using two white lines as separator between methods, as it makes method boundries more clear. .. _Python coding guidelines: http://www.python.org/dev/peps/pep-0008/ gaphas-0.7.2/demo.py000077500000000000000000000274151175456763600143210ustar00rootroot00000000000000#!/usr/bin/env python """ A simple demo app. It sports a small canvas and some trivial operations: - Add a line/box - Zoom in/out - Split a line segment - Delete focused item - Record state changes - Play back state changes (= undo !) With visual updates - Exports to SVG and PNG """ __version__ = "$Revision$" # $HeadURL$ try: import pygtk except ImportError: pass else: pygtk.require('2.0') import math import gtk import cairo from gaphas import Canvas, GtkView, View from gaphas.examples import Box, PortoBox, Text, FatLine, Circle from gaphas.item import Line, NW, SE from gaphas.tool import PlacementTool, HandleTool from gaphas.segment import Segment import gaphas.guide from gaphas.painter import PainterChain, ItemPainter, HandlePainter, FocusedItemPainter, ToolPainter, BoundingBoxPainter from gaphas import state from gaphas.util import text_extents, text_underline from gaphas.freehand import FreeHandPainter from gaphas import painter #painter.DEBUG_DRAW_BOUNDING_BOX = True # Ensure data gets picked well: import gaphas.picklers # Global undo list undo_list = [] def undo_handler(event): global undo_list undo_list.append(event) def factory(view, cls): """ Simple canvas item factory. """ def wrapper(): item = cls() view.canvas.add(item) return item return wrapper class MyBox(Box): """Box with an example connection protocol. """ class MyLine(Line): """Line with experimental connection protocol. """ def __init__(self): super(MyLine, self).__init__() self.fuzziness = 2 def draw_head(self, context): cr = context.cairo cr.move_to(0, 0) cr.line_to(10, 10) cr.stroke() # Start point for the line to the next handle cr.move_to(0, 0) def draw_tail(self, context): cr = context.cairo cr.line_to(0, 0) cr.line_to(10, 10) cr.stroke() class MyText(Text): """ Text with experimental connection protocol. """ def draw(self, context): Text.draw(self, context) cr = context.cairo w, h = text_extents(cr, self.text, multiline=self.multiline) cr.rectangle(0, 0, w, h) cr.set_source_rgba(.3, .3, 1., .6) cr.stroke() class UnderlineText(Text): def draw(self, context): cr = context.cairo text_underline(cr, 0, 0, "Some text(y)") def create_window(canvas, title, zoom=1.0): view = GtkView() view.painter = PainterChain(). \ append(FreeHandPainter(ItemPainter())). \ append(HandlePainter()). \ append(FocusedItemPainter()). \ append(ToolPainter()) view.bounding_box_painter = FreeHandPainter(BoundingBoxPainter()) w = gtk.Window() w.set_title(title) h = gtk.HBox() w.add(h) # VBox contains buttons that can be used to manipulate the canvas: v = gtk.VBox() v.set_property('border-width', 3) v.set_property('spacing', 2) f = gtk.Frame() f.set_property('border-width', 1) f.add(v) h.pack_start(f, expand=False) v.add(gtk.Label('Item placement:')) b = gtk.Button('Add box') def on_clicked(button, view): #view.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSSHAIR)) view.tool.grab(PlacementTool(view, factory(view, MyBox), HandleTool(), 2)) b.connect('clicked', on_clicked, view) v.add(b) b = gtk.Button('Add line') def on_clicked(button): view.tool.grab(PlacementTool(view, factory(view, MyLine), HandleTool(), 1)) b.connect('clicked', on_clicked) v.add(b) v.add(gtk.Label('Zooming:')) b = gtk.Button('Zoom in') def on_clicked(button): view.zoom(1.2) b.connect('clicked', on_clicked) v.add(b) b = gtk.Button('Zoom out') def on_clicked(button): view.zoom(1/1.2) b.connect('clicked', on_clicked) v.add(b) v.add(gtk.Label('Misc:')) b = gtk.Button('Split line') def on_clicked(button): if isinstance(view.focused_item, Line): segment = Segment(view.focused_item, view) segment.split_segment(0) view.queue_draw_item(view.focused_item) b.connect('clicked', on_clicked) v.add(b) b = gtk.Button('Delete focused') def on_clicked(button): if view.focused_item: canvas.remove(view.focused_item) #print 'items:', canvas.get_all_items() b.connect('clicked', on_clicked) v.add(b) v.add(gtk.Label('State:')) b = gtk.ToggleButton('Record') def on_toggled(button): global undo_list if button.get_active(): print 'start recording' del undo_list[:] state.subscribers.add(undo_handler) else: print 'stop recording' state.subscribers.remove(undo_handler) b.connect('toggled', on_toggled) v.add(b) b = gtk.Button('Play back') def on_clicked(self): global undo_list apply_me = list(undo_list) del undo_list[:] print 'Actions on the undo stack:', len(apply_me) apply_me.reverse() saveapply = state.saveapply for event in apply_me: print 'Undo: invoking', event saveapply(*event) print 'New undo stack size:', len(undo_list) # Visualize each event: #while gtk.events_pending(): # gtk.main_iteration() b.connect('clicked', on_clicked) v.add(b) v.add(gtk.Label('Export:')) b = gtk.Button('Write demo.png') def on_clicked(button): svgview = View(view.canvas) svgview.painter = ItemPainter() # Update bounding boxes with a temporaly CairoContext # (used for stuff like calculating font metrics) tmpsurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) tmpcr = cairo.Context(tmpsurface) svgview.update_bounding_box(tmpcr) tmpcr.show_page() tmpsurface.flush() w, h = svgview.bounding_box.width, svgview.bounding_box.height surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(w), int(h)) cr = cairo.Context(surface) svgview.matrix.translate(-svgview.bounding_box.x, -svgview.bounding_box.y) cr.save() svgview.paint(cr) cr.restore() cr.show_page() surface.write_to_png('demo.png') b.connect('clicked', on_clicked) v.add(b) b = gtk.Button('Write demo.svg') def on_clicked(button): svgview = View(view.canvas) svgview.painter = ItemPainter() # Update bounding boxes with a temporaly CairoContext # (used for stuff like calculating font metrics) tmpsurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) tmpcr = cairo.Context(tmpsurface) svgview.update_bounding_box(tmpcr) tmpcr.show_page() tmpsurface.flush() w, h = svgview.bounding_box.width, svgview.bounding_box.height surface = cairo.SVGSurface('demo.svg', w, h) cr = cairo.Context(surface) svgview.matrix.translate(-svgview.bounding_box.x, -svgview.bounding_box.y) svgview.paint(cr) cr.show_page() surface.flush() surface.finish() b.connect('clicked', on_clicked) v.add(b) b = gtk.Button('Dump QTree') def on_clicked(button, li): view._qtree.dump() b.connect('clicked', on_clicked, [0]) v.add(b) b = gtk.Button('Pickle (save)') def on_clicked(button, li): f = open('demo.pickled', 'w') try: import cPickle as pickle pickle.dump(view.canvas, f) finally: f.close() b.connect('clicked', on_clicked, [0]) v.add(b) b = gtk.Button('Unpickle (load)') def on_clicked(button, li): f = open('demo.pickled', 'r') try: import cPickle as pickle canvas = pickle.load(f) canvas.update_now() finally: f.close() create_window(canvas, 'Unpickled diagram') b.connect('clicked', on_clicked, [0]) v.add(b) b = gtk.Button('Unpickle (in place)') def on_clicked(button, li): f = open('demo.pickled', 'r') try: import cPickle as pickle canvas = pickle.load(f) finally: f.close() #[i.request_update() for i in canvas.get_all_items()] canvas.update_now() view.canvas = canvas b.connect('clicked', on_clicked, [0]) v.add(b) b = gtk.Button('Reattach (in place)') def on_clicked(button, li): view.canvas = None view.canvas = canvas b.connect('clicked', on_clicked, [0]) v.add(b) # Add the actual View: t = gtk.Table(2,2) h.add(t) w.connect('destroy', gtk.main_quit) view.canvas = canvas view.zoom(zoom) view.set_size_request(150, 120) hs = gtk.HScrollbar(view.hadjustment) vs = gtk.VScrollbar(view.vadjustment) t.attach(view, 0, 1, 0, 1) t.attach(hs, 0, 1, 1, 2, xoptions=gtk.FILL, yoptions=gtk.FILL) t.attach(vs, 1, 2, 0, 1, xoptions=gtk.FILL, yoptions=gtk.FILL) w.show_all() def handle_changed(view, item, what): print what, 'changed: ', item view.connect('focus-changed', handle_changed, 'focus') view.connect('hover-changed', handle_changed, 'hover') view.connect('selection-changed', handle_changed, 'selection') def create_canvas(c=None): if not c: c = Canvas() b=MyBox() b.min_width = 20 b.min_height = 30 print 'box', b b.matrix=(1.0, 0.0, 0.0, 1, 20,20) b.width = b.height = 40 c.add(b) bb=Box() print 'box', bb bb.matrix=(1.0, 0.0, 0.0, 1, 10,10) c.add(bb, parent=b) fl = FatLine() fl.height = 50 fl.matrix.translate(100, 100) c.add(fl) circle = Circle() h1, h2 = circle.handles() circle.radius = 20 circle.matrix.translate(50, 100) c.add(circle) # AJM: extra boxes: bb = Box() print 'box', bb bb.matrix.rotate(math.pi/4.) c.add(bb, parent=b) # for i in xrange(10): # bb=Box() # print 'box', bb # bb.matrix.rotate(math.pi/4.0 * i / 10.0) # c.add(bb, parent=b) b = PortoBox(60, 60) b.min_width = 40 b.min_height = 50 b.matrix.translate(55, 55) c.add(b) t = UnderlineText() t.matrix.translate(70,30) c.add(t) t = MyText('Single line') t.matrix.translate(70,70) c.add(t) l = MyLine() c.add(l) l.handles()[1].pos = (30, 30) segment = Segment(l, view=None) segment.split_segment(0, 3) l.matrix.translate(30, 60) l.orthogonal = True off_y = 0 for align_x in (-1, 0, 1): for align_y in (-1, 0, 1): t=MyText('Aligned text %d/%d' % (align_x, align_y), align_x=align_x, align_y=align_y) t.matrix.translate(120, 200 + off_y) off_y += 30 c.add(t) t=MyText('Multiple\nlines', multiline = True) t.matrix.translate(70,100) c.add(t) return c def main(): ## ## State handling (a.k.a. undo handlers) ## # First, activate the revert handler: state.observers.add(state.revert_handler) def print_handler(event): print 'event:', event c=Canvas() create_window(c, 'View created before') create_canvas(c) #state.subscribers.add(print_handler) ## ## Start the main application ## create_window(c, 'View created after') gtk.main() if __name__ == '__main__': import sys if '-p' in sys.argv: print 'Profiling...' import hotshot, hotshot.stats prof = hotshot.Profile('demo-gaphas.prof') prof.runcall(main) prof.close() stats = hotshot.stats.load('demo-gaphas.prof') stats.strip_dirs() stats.sort_stats('time', 'calls') stats.print_stats(20) else: main() # vim: sw=4:et: gaphas-0.7.2/demo_profile.py000066400000000000000000000011721175456763600160260ustar00rootroot00000000000000#!/usr/bin/env python from demo import * if __name__ == '__main__': try: import cProfile import pstats cProfile.run('main()', 'demo-gaphas.prof') p = pstats.Stats('demo-gaphas.prof') p.strip_dirs().sort_stats('time').print_stats(40) except ImportError, ex: import hotshot, hotshot.stats import gc prof = hotshot.Profile('demo-gaphas.prof') prof.runcall(main) prof.close() stats = hotshot.stats.load('demo-gaphas.prof') stats.strip_dirs() stats.sort_stats('time', 'calls') stats.print_stats(20) # vim: sw=4:et: gaphas-0.7.2/doc/000077500000000000000000000000001175456763600135545ustar00rootroot00000000000000gaphas-0.7.2/doc/api.txt000066400000000000000000000023001175456763600150610ustar00rootroot00000000000000############# API reference ############# This part describes the API of Gaphas. The API can be separated into three parts. First of all there's the model (canvas and items). Then there's the view (view) and controllers (tools). Some more generic stuff is also described here. Canvas and items ---------------- .. toctree:: :maxdepth: 2 api/canvas api/items api/connectors api/solver api/constraints api/utils View and tools -------------- Everything related to displaying the canvas and interacting with it. .. toctree:: :maxdepth: 2 api/view api/gtkview api/painters Interacting with the canvas is done through tools. Tools tell _what_ has to be done (like moving). To make an element move aspects are defined. Aspects tell _how_ the behaviour has to be performed. .. toctree:: :maxdepth: 2 api/tools api/aspects Extended behaviour ~~~~~~~~~~~~~~~~~~ By importing the following modules, extra behaviour is added to the default view behaviour. .. toctree:: :maxdepth: 2 api/segment api/guide Miscellaneous ------------- .. toctree:: :maxdepth: 2 api/tree api/matrix api/table api/quadtree api/geometry api/decorators gaphas-0.7.2/doc/api/000077500000000000000000000000001175456763600143255ustar00rootroot00000000000000gaphas-0.7.2/doc/api/aspects.txt000066400000000000000000000011671175456763600165350ustar00rootroot00000000000000####### Aspects ####### TODO: Explain aspects :mod: `gaphor.aspect` --------------------- .. module:: gaphas.aspect .. autoclass:: Finder Find elements on a canvas .. autoclass:: Selection Perform selection of elements .. autoclass:: InMotion Move items around .. autoclass:: HandleSelection Select a handle .. autoclass:: HandleInMotion Perform handle motion .. autoclass:: Connector Connect and disconnect a handle .. autoclass:: ConnectionSink Perform behaviour for elements a handle is connected to. ... autoclass:: PaintFocused Special aspect that can perform paint behaviour for the `FocusedItemPainter`. gaphas-0.7.2/doc/api/canvas.txt000066400000000000000000000002751175456763600163450ustar00rootroot00000000000000###### Canvas ###### This part describes the API of Gaphas. :mod: `gaphor.canvas` --------------------- .. module:: gaphas.canvas .. autoclass:: Canvas :members: :undoc-members: gaphas-0.7.2/doc/api/connectors.txt000066400000000000000000000007101175456763600172410ustar00rootroot00000000000000################# Handles and ports ################# This part describes the API of Gaphas. :mod: `gaphor.connector` ------------------------ .. module:: gaphas.connector .. autoclass:: Position :members: :undoc-members: .. autoclass:: Handle :members: :undoc-members: .. autoclass:: Port :members: :undoc-members: .. autoclass:: PointPort :members: :undoc-members: .. autoclass:: LinePort :members: :undoc-members: gaphas-0.7.2/doc/api/constraints.txt000066400000000000000000000004021175456763600174310ustar00rootroot00000000000000################# Constraints ################# The default constraint classes provided by Gaphas :mod: `gaphor.constraint` --------------------- .. module:: gaphas.constraint .. autoclass:: Constraint :members: :undoc-members: TODO: And the rest gaphas-0.7.2/doc/api/decorators.txt000066400000000000000000000003021175456763600172260ustar00rootroot00000000000000########## Decorators ########## :mod: `gaphor.decorators` --------------------- .. module:: gaphas.decorators .. autoclass:: async .. autofunction:: nonrecursive .. autoclass:: recursive gaphas-0.7.2/doc/api/geometry.txt000066400000000000000000000001301175456763600167130ustar00rootroot00000000000000############################ Lines, points and rectangles ############################ gaphas-0.7.2/doc/api/gtkview.txt000066400000000000000000000003031175456763600165420ustar00rootroot00000000000000######### GTK+ View ######### This part describes the API of Gaphas. :mod: `gaphor.view` --------------------- .. module:: gaphas.view .. autoclass:: GtkView :members: :undoc-members: gaphas-0.7.2/doc/api/guide.txt000066400000000000000000000015611175456763600161660ustar00rootroot00000000000000####################### Alignment helper: Guide ####################### By importing this module Guide behaviour will be added to all views. Guides will help you align items next to each other. The guide module consists of a few aspects, triggered when items are moved, as well as a painter, so guides will be drawn. :mod: `gaphor.guide` -------------------- .. module:: gaphas.guide .. autoclass:: ItemGuide :members: :undoc-members: .. autoclass:: ElementGuide :members: :undoc-members: .. autoclass:: LineGuide :members: :undoc-members: .. autoclass:: Guides :members: :undoc-members: .. autoclass:: GuideMixin :members: :undoc-members: .. autoclass:: GuidedItemInMotion :members: :undoc-members: .. autoclass:: GuidedItemHandleInMotion :members: :undoc-members: .. autoclass:: GuidePainter :members: :undoc-members: gaphas-0.7.2/doc/api/items.txt000066400000000000000000000004551175456763600162130ustar00rootroot00000000000000################# Items ################# Items are put on a Canvas. :mod: `gaphor.item` --------------------- .. module:: gaphas.item .. autoclass:: Item :members: :undoc-members: .. autoclass:: Element :members: :undoc-members: .. autoclass:: Line :members: :undoc-members: gaphas-0.7.2/doc/api/matrix.txt000066400000000000000000000005621175456763600163750ustar00rootroot00000000000000###### Matrix ###### The Matrix class is used to define transformations on an item, relative to the parent Item. This is basically the same implementation as cairo.Matrix, only notifications are sent on state changes (see the state module). :mod: `gaphor.matrix` --------------------- .. module:: gaphas.matrix .. autoclass:: Matrix :members: :undoc-members: gaphas-0.7.2/doc/api/painters.txt000066400000000000000000000003311175456763600167100ustar00rootroot00000000000000######## Painters ######## This part describes the API of Gaphas. :mod: `gaphor.painter` --------------------- .. module:: gaphas.painter .. autoclass:: Painter :members: :undoc-members: TODO: Add the rest gaphas-0.7.2/doc/api/quadtree.txt000066400000000000000000000003111175456763600166730ustar00rootroot00000000000000######## Quadtree ######## This part describes the API of Gaphas. :mod: `gaphor.quadtree` --------------------- .. module:: gaphas.quadtree .. autoclass:: Quadtree :members: :undoc-members: gaphas-0.7.2/doc/api/segment.txt000066400000000000000000000007541175456763600165360ustar00rootroot00000000000000################# Line segmentation ################# This part describes the API of Gaphas. :mod: `gaphor.segment` ---------------------- .. module:: gaphas.segment .. autoclass:: Segment :members: :undoc-members: .. autoclass:: LineSegment :members: :undoc-members: .. autoclass:: SegmentHandleFinder :members: :undoc-members: .. autoclass:: SegmentHandleSelection :members: :undoc-members: .. autoclass:: LineSegmentPainter :members: :undoc-members: gaphas-0.7.2/doc/api/solver.txt000066400000000000000000000002751175456763600164040ustar00rootroot00000000000000###### Solver ###### This part describes the API of Gaphas. :mod: `gaphor.solver` --------------------- .. module:: gaphas.solver .. autoclass:: Solver :members: :undoc-members: gaphas-0.7.2/doc/api/table.txt000066400000000000000000000003251175456763600161550ustar00rootroot00000000000000############### Table structure ############### This part describes the API of Gaphas. :mod: `gaphor.table` --------------------- .. module:: gaphas.table .. autoclass:: Table :members: :undoc-members: gaphas-0.7.2/doc/api/tools.txt000066400000000000000000000014111175456763600162230ustar00rootroot00000000000000##### Tools ##### This part describes the API of Gaphas. :mod: `gaphor.tool` --------------------- .. module:: gaphas.tool .. autoclass:: Tool :members: :undoc-members: .. autoclass:: ToolChain :members: :undoc-members: .. autoclass:: ItemTool :members: :undoc-members: .. autoclass:: HandleTool :members: :undoc-members: .. autoclass:: ConnectHandleTool :members: :undoc-members: .. autoclass:: HoverTool :members: :undoc-members: .. autoclass:: RubberbandTool :members: :undoc-members: .. autoclass:: PanTool :members: :undoc-members: .. autoclass:: ZoomTool :members: :undoc-members: .. autoclass:: PlacementTool :members: :undoc-members: .. autoclass:: TextEditTool :members: :undoc-members: gaphas-0.7.2/doc/api/tree.txt000066400000000000000000000003171175456763600160260ustar00rootroot00000000000000############## Tree structure ############## This part describes the API of Gaphas. :mod: `gaphor.tree` --------------------- .. module:: gaphas.tree .. autoclass:: Tree :members: :undoc-members: gaphas-0.7.2/doc/api/utils.txt000066400000000000000000000006361175456763600162330ustar00rootroot00000000000000######################### Various utility functions ######################### This part describes the API of Gaphas. :mod: `gaphor.util` --------------------- .. module:: gaphas.util .. autofunction:: text_extents .. autofunction:: text_center .. autofunction:: text_align .. autofunction:: text_multiline .. autofunction:: text_underline .. autofunction:: text_set_font .. autofunction:: path_ellipse gaphas-0.7.2/doc/api/view.txt000066400000000000000000000005571175456763600160470ustar00rootroot00000000000000#### View #### The View is the base class for viewing related operations. A good example is the ``GtkView``, which provides a GTK+ widget for viewing (and editing) the canvas. Views are also used for rendering to images (for example SVG or PNG). :mod: `gaphor.view` ------------------- .. module:: gaphas.view .. autoclass:: View :members: :undoc-members: gaphas-0.7.2/doc/comparison.txt000066400000000000000000000017011175456763600164660ustar00rootroot00000000000000 Gaphas vs jHotDraw ------------------ Gaphas || jHotDraw Item | Figure Canvas | Drawing Tool | Tool Painter | Painter View | DrawingView jHotDraw let's you enable one tool at a time. In Gaphas everything should be handled from within one tool chain. Tool chains can be switched, e.g. for specific actions such as item placement. Everything else (item, handle manupilation, zooming, selection) is handled without the need to select specific tools. Items (Figures in jHotDraw) keep track of their own child items. In Gaphas, the Canvas object maintains the hierarchical order of items. Advantage is that addition of items is never unnoticed. Also iterating the nodes in the tree structure is a bit faster. In jHotDraw, connections of items (figures) are maintained within the special connection figure. Gaphas maintains connections between items as constraints and uses a constraint solver (one per canvas) to ensure the constraints remain true. gaphas-0.7.2/doc/conf.py000066400000000000000000000004061175456763600150530ustar00rootroot00000000000000extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo'] project = 'Gaphas' version = '0.5' release = version source_suffix = '.txt' master_doc = 'contents' #html_theme = 'traditional' unused_docs = 'constraints.txt index.txt comparison.txt' gaphas-0.7.2/doc/constraints.txt000066400000000000000000000037761175456763600167010ustar00rootroot00000000000000Constraints =========== Introduction ------------ There are problems related to canvas items, which can be solved in `declarative way `_ allowing for simpler and less error prone implementation of canvas item. For example, if an item should be a rectangle, then it could be declared that - bottom-right vertex should be below and on the right side of top-left vertex - two top rectangle vertices should be always at the same y-axis - two left rectangle vertices should be always at the same x-axis - ... Above rules are constraints, which need to be applied to a rectangular item. The rules can be satisfied (constraints can be solved) using `constraint solver `_. Gaphas implements its own constraint solver (`gaphas.solver` module). Items can be constrained using APIs defined in `Canvas` and `Item` classes. Constraints API --------------- The `Canvas` class constraints API supports adding a constraint to constraint solver. Instance of a constraint has to be created and then added using `Canvas.add_constraint` method. For example, it allows to declare that two variables should be equal. The `Item` class constraint API is more abstract, it allows to constraint positions, i.e. - positions of two item handles should be on the same x-axis - position should be always on a line If this API does not provide some constraint declaration, then one can fallback to `Canvas` class constraint API. Examples -------- Item API ^^^^^^^^ Canvas API ^^^^^^^^^^ Further Reading --------------- Theory and examples related to constraint solving - http://en.wikipedia.org/wiki/Declarative_programming - http://en.wikipedia.org/wiki/Constraint_satisfaction_problem - http://norvig.com/sudoku.html There are other projects providing constraint solvers - http://adaptagrams.sourceforge.net/ - http://minion.sourceforge.net/ - http://labix.org/python-constraint - http://www.cs.washington.edu/research/constraints/cassowary/ gaphas-0.7.2/doc/contents.txt000066400000000000000000000025761175456763600161640ustar00rootroot00000000000000Gaphas Documentation ==================== Gaphas is Gaphor's diagram drawing widget. Gaphas has been built with some extensibility in mind. It can be used for many drawing purposes, including vector drawing applications, diagram drawing tools and we even have a geographical map demo in our repository. The basic idea is: - Items (canvas items) can be added to a Canvas. - A Canvas can be visualized by one or more Views. - The canvas maintains the tree structure (parent-child relationships between items). - A constraint solver is used to maintain item constraints and inter-item constraints. - The item (and user) should not be bothered with things like bounding-box calculations. - Very modular: e.g. handle support could be swapped in and swapped out. - Rendering using Cairo_. This implies the diagrams can be exported in a number of formats, including PNG and SVG. Gaphas is released under the terms of the GNU Library General Public License (LGPL). Gaphas has been build using `setuptools` and can be installed as Python Egg. * Git repository: http://gitgub.com/amolenaar/gaphas * Python Package index (PyPI): http://cheeseshop.python.org/pypi/gaphas Table of Contents ================= .. toctree:: :maxdepth: 2 diagram tools ports state undo quadtree solver api * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _Cairo: http://cairographics.org gaphas-0.7.2/doc/diagram.txt000066400000000000000000000025141175456763600157230ustar00rootroot00000000000000Class diagram ============= This class diagram describes the basic layout of Gaphas. .. image:: gaphas-canvas.png :width: 700 One-oh-one: :doc:`api/canvas` The main canvas class (container for Items) :doc:`api/items` Objects placed on a Canvas. Items can draw themselves, but not act on user events :doc:`api/solver` A constraint solver. Nice to have when you want to connect items together in a generic way. :doc:`api/view` Base class that renders content (`paint()`). The view is responsible for the calculation of bounding boxes. This information is stored in a quadtree_ data structure for fast access. :doc:`api/gtkview` A view to be used in GTK+ applications. This view class is interactive. Interaction with users is handled by Tools. :doc:`api/painters` Painters are the workers when it comes to painting items. :doc:`api/tools` Tools are used to handle user events (such as mouse movement and button presses). :doc:`api/aspects` Tools do not modify the items directly. They use aspects (not the AOP kind) as intermediate step. Since aspects are defined as generic functions, the behaviour for each diagram item type can be slightly different. Tools ----- Several tools_ are used to make the overall user experience complete. .. image:: tools.png :width: 532 .. _quadtree: quadtree.html .. _tools: tools.html gaphas-0.7.2/doc/gaphas-canvas.png000066400000000000000000003247201175456763600170060ustar00rootroot00000000000000‰PNG  IHDR¬¶c:ŽbKGDÿÿÿ ½§“ IDATxœìw˜$U¹ÿ?›ÃìÌβ˲°dPÀ€YÁœ®zÅ„YL`ÅœsÎW/(¢‚3ÆŸ¯á" —°ì²afgvfóüþøž÷VuMuuuOuWu÷ûyžzN¥>}º»ºêœïy8Žã8Žã8Žã8Žã8Žã8Žã8Žã8Žã8Žã8Žã8Žã8Žã8Žã8ŽÓÌ*»Žã8Nn–G{›€aÙì)±]Žã8Žã8Žã8…â‚•ã8Nµ˜ Œ„©;wŽŽöÎx W3Ö7$¶·¶ã8Žã8Žã8Žã̬Çé$ós€'À0Žmv;ñ°oYm&Ãúh86Ο?êœ ¯Ù l ÇÆ€]á=L¨Ù\àgKcYC- å^‰íå)ÇöÔ©o#ðô9íõËÃz+lnî ¬o±ÇqÇqÇqœÂqÁÊqœNò=àø²‘° Zcaß$xm§¾¸¶ ‰>ibÔ^Ôž²Ø܈„©«k¿× q)ÙL°’ë+Rö- ¯ ðÁÚê8Žã8Žã8ŽÓæ–ÝÇqúŠƒBù`àJ$´, ûÑ=i>²L‰?‹‘ø3 û†Âö‚p|0Ž „zæ†z ï3Xˆ„šøù‹ÃÙnwͲ¹ßmå¦ÄöÆ”cëHÖ {»ß†&_÷àíDß©ã8Žã8Žã8N%pÁÊqœN²(¯%rÇÛXR[â,æ…eIØ7ŒD­EHäª'®Í']ŒÚDóÂS§1‹±V,ÁÇqÇqÇqÚ† VŽãtŠÀJäJWµxIã±õªµ­L†ra©­pÇqÇqÇI0»ì8ŽÓ7Ê›Q<(§|L¨Ì<ËqÇqÇq§Ã¸…•ã8«›Jm…gG(ç—Ú Çi?‹Qâ•D±ë,±ÂæPn Ê ê8Žã8Žã”Œ VŽãtŠýByk©­p★Õ@æYŽS-f!ñÉ–å(YÂÊÄþ±}‹Skªeð;à[Àwé/÷`ÇqÇqœÊá‚•ã8b[(Ýš§:X «<ƒy§9¾<؉„Áx9ìJ)Íâ'YŽ"1%YŽ ÷ÚdYE…e]o‹Pƒ°>„,B.ªñõÁpÞr"!ªÙÈ‚j=Q²Ë4º,”û Ëg¾T#1„ã8Žã8Nßá‚•ã8bC(W”Ú 'Ž»¶‡€gǶ—uøý7§”iÂÖéBX¼4ál HKг‰HKÃúb$F™0µ ó·õ"™nG÷” HPº‰Q‹í›ÈQïà1À3€g…}o^]`ÛÇqÇqœœ¸`å8N§¸-”«Jm…g4”C¥¶¢÷xP(ß|‰<óbå zþ&K³øI–K‘EQ²FîqÉÒ¢N eYL†e‰G“èúÛÖ· ´Idq_ çm$¢ö´¡ÛóÃòsà`uÞÇqÇqÇÉ VŽãt s«Y^j+¦“åB5«c­(³°ZPj+zoEÖM›3ÎmIÁjéÂÖ0éBX¼4ál HãH\šD"ÒhXŸ@b” S›‘°­wk¯J¢Ÿã8Žã8N_á‚•ã8bŠß³Y™ì,·9ÿ‡¹þ|Ø?¶]*µ}¦X «…¥¶¢w)+žTÒ%Ðiûî†3ÏrÇqÇqÇqz‚µh_E·À+©/0L×?DÁ±aà«DnN?&ʆ˜çxÙì…>Û¦²Òcü}¯O(»!NËŽ~ÃÊnˆã8Žã8N¿âVŽãt’ÛXµŠ(¦U·p8ðkàÔØ¾ÏÏ> Ü œœ <,çñ²ñ ëíÁ\Iw—Ú g&˜…•»:N},_½xzVf¹§•IÒ¬E-‹jœ ‹.Îv¦']°ì¬q,Èúx[xß´uÇq§C¸`å8N'éæL€—Sìùq¡Œ‹X¹ 6:^6ã¡(µ½Ç’PæÉLçT¸Ú€»,÷NÇ)ƒ…À¾aY…’¬Dnó+Ãö>a=M`êuÒ„,K*1“uÇq' VŽãt’u¡Ü§ÔV´Æ:¦g&›B3¼O ²VŠn¯“À"`1.°ÅœPVA”tZc7²àB¢•ÇsŠâ3À“‘•ñZ`= ò¿>±o-ºÛÁ0pp(pp(Bnëyc·íB‰Fˆ,”ÒÊQôüÌ[Æ1Ë«´ÏLŒb™Xã,bzœÆùLŸ¨± çá¢ð¾áõ¶>Y^¶Ãúr‰`#-¬ÿ…6pÇé)\°r§“Ä­zóç/ ëû'Ç ë¥FÇ«ÀÔ1Ÿ VE±8”“™g9UgÀ.Ã+§8N ežx†“D"ÖíHØZöÝF$v­£ÖUm¢ÒD©Ch,¶l ï¹6¼Ï-á½nå-á=×3]`êuL@K YõÖ¡ûHžõÅa٫ŶÜØâkÇq*‰ VŽãt‹1Xj+Šãd$ò<uîCC£Ññ*0Š:ÖC¸KBQØ ÿŽÌ³œª³ ô{E`wÊgAlý~ÈÚxäz·‰XûÄÖˆD¦FŒ¢gÌ`xmÒú(Îð/àŸHàøgØþ7ÀüYPŸ=èÞÐ;.^-AÂWžõ†×߬Çé1\°r§“Œ†2ͼ¿lî’q¬^Ç xiXZ9^LTYy–Ó ‹BY%aÒ8x'p4²Þ¸xj©-ª.6h÷ÀëNQ˜kÚ(ð§ç ñi%²l}_"±Ëö-%z¶nGÂEš(õ/Üb°ªl ˆF'&X„¬·«Ø·rÇ™.X9ŽÓI,ÇPæYN'± UWºûN«–Mê1ÀЀõ4äÚòÜR[Tm œ;~ºv¡øM§ŸBƒæ³‘›ë À½Â±o…×ì¶?οgŽzOC±pv!+çÌôCH¯ÅÜsÊÇÜ…ÛqoØ\þk.VõÞ·r§gqÁÊqœNâªjqþ²¢9Å`n?U b$ ]ÛØÞ¼xðKƒí‰±ã‡#K¼O Îï ûÏE×Ññaû©(cÕ¥9ê}?ðWàDà T+€³[X9EcñÇ2Ïrœæp +Çqzw t§“ôZÐõnç°PÞPj+z³¢è6pð^j3TÝ ø^X_¼‰SoE‚'ÀÏuÇSKP€è·æ¬÷Zà(à!HàúYaŸfæ˜`åVNQ˜õ¥»`;Eâ‚•ã8=‹[X9ŽÓIܪZÊ›xÍ<àéÀQÀß~áN(@ùzàmd©_ʼqj:Å¥è7¿cl_<¡À'Ðïû àua_|bk²ÈÚ¶­±ø6ð0à%áœssÖûHdeµøÊ¢YÌ%py©­pz ³¾¬Z|;§»±¾• VŽãô.X9ŽÓI\°ª‡†òŸ9Î= ø p3pŠƒôöÌWôK€—GqšÞ \ ügÊùñ˜OUã}¡ü ð2d-uQìøê Oh²îs‘õbàBäfš§ÞO K´KPÖÂCš|ßv²)”naåE³A×'faå}+ÇqÇqœ°¼ÞVvC¾‰~gÔ9>x2ðs$*L…ådI³¤Îëz…'#áe*cù>‘kh†{ŠÈ:§j<ørI%r÷xj÷Zdõ4E§j ‰tÔÙž…,õ¦Ðµ'«Þ ëÝn$œV)èúcP[^vCœžáñèšúaÙ qzŠ£ëêwe7ÄqÇq§›YŒ:U[ËnˆÈf x@bÿAÈÇR£O¡øcg÷îdKâpd…”%TÅ— ä&¸YãTY°ròs ú-ÿXvCœžá¦»Ìö I‘$\ŸÜù¦ôwGßï_Ënˆã8Žã8N·³u¬<éC~ûwî…ÜóŠpSZƒ~‹+ÛãQlª¸5ÕåÈu¬bc,Þ‰¨¼bU|¹x~Xÿ7N·sú-¯it¢ãää$tM}©ì†”@š`•¶ÏižCÐwù¯²â8ŽS4>`t§ÓlAÙ†ˆbÄôËÈ”Vf¦~€ï=è;lfÙʹÀ¾(€ö‹ÛÖþ¡ÞmÀ€ïWÀ]‘ 3ÌG™ç"!kvhëô»ÎCî‚ Ø¶(¼v Ã6Æ– aÙXgÙÓø«-„ÇŸAÖU­r8pNX÷çk÷c.¯[2ÏrœüxÐõ‹ówç°n †Ñ½øñèYókäf|kìuׇå(‘ãQLÁÏoè@Û«†g t§gñµã8f~(¯Anf£¨ó¾ ·¡€´ca},lo Çãë[Ãú(²Š):Uø<$Ì ‡r b‘@ߎŸgû– O3a¹˜mFŸw¯°,V„¥ÌeÍžïH_<3,e³Å<» Xn ë·! ±uèó ý&¶ ¡ïhiÊþø¹Ëˆ’E± Ð~{Áõ:ã€PÞTj+œ^ƒ®G< øJäñúØþÏ¢gϧñS åÃbçœ <ø\8÷6àµÀ‡è¯É0pÁÊqœÆ+ÇqÊbï°‰ 1“HØ1g2±>‚fsMÌ0i ˜†Âv=˦VØL$:%ˬc#Ôâæ áj9‘ˆÕhYN¶ˆ6…:¿;‘X¸ ƒön ûÆuÖ(²‚²ÀÙ[ÂkÇC»'b¯m^[Lp[^gÙ',w«ÓÞ*3Úø§ÂìÊ[3Ïrœü؄͎R[Q ¾‰«Ñ°n<.”§Æö€&v†í5À€çG¢‚û«è?Áj7z6 ¾ËX¹Íqz³˜7¬è8mÃ+Çq:Í-ÀÈ”-ŒÌÕlˆÈul=m}!‘¨´0œ;Ö—"׳D.vE`¢ÍH(ÍÚk 4ñíøy¶o”Hxj»‘O³V<³/¢¬B—ÏFâ,4¨úðyÊ8½€€„ýÂú*àÀp|Êâ¿ÓXlߖľ´sA³ôGΰ½»‘(÷|PÚí¸`å[X5f ÝGŸ@tEäB‘ ³;¶mƒæÙín`E%êG¹`å  >ôºï,"²*·¾õPXH¬/ ë‹Ñ碰4ê[Û$¥1‚þ·ÛQàëˆÜy¯G±>wÍèS:} VŽãtšE¡¼ Í’É,j®¶¾ÈíËö›%Ñ(‘À4N­ÀQ´‹aU°ØW 1í4÷ã‰ÀKG ˜V/@A×?\2ʈå³.,íÎ~ôKdÅõjàíDËfø- ¦üe /›©J9v=ðFŒýIÈêê¡hFòEÈ…ðL$ÐôâŒ÷-( ð™ÀéÀꜷ ¹¾›¨ójÏUwì~\°rŠf0”ýè²5+eß«Ãg =s^š£ž»ÄÖ«’(¤,,fbÚ3Ý©ÅîíOC¢Ì!a94¶¾ ¸W›Û±‡(iÐdëñDDe°Za9.p]™rþ|ô]ÿ–(^©ãLÃ+Çq:Ï0W‹ý1§Îñw# «ýP°Û;¢ÔâÏî‰Äœ»£Ùð^å7ÈMðTàÔ~ƒÌׯJ¼ÆY¿¾³Y@üYb} µ³¾c¨S¾Íön ÛívQuÁÊ)šy¡Ü™y–ã4g ÌÅ'ü5õô/GbÊpØ$7Ï¡¶? D޲Öû}bkð”|áÕ¸ØêÔÁ+Çq:Çð¨Ö¹¬sü§H”º6l_ ¼x+ðä2ø™v6°"ì>:T@3°g†í©”óÍÄ¿®ï¦‹MÉízë‹Sêk…1"k3‘°eö'bÇì<;fÛ#‰ce ÝBZÃ8íÁƒ®;í«|,AßÑ6²³In ‹Ó~lòÉ+'¬Çé$n]UŸùHFƒùÅÔv<“1`ú ³Æ.—fa5¯ÎñzÙ·#WÀ¯7¨¿×X œ”ã<»Æï‹:Ã[Ñàt+MÆcÛ–©ÐÎ% þoq$LXi— í"`/j§ø’ul&ñ7¶eÐÜœX'ú …Ŷ—¡ÿÉb¢ŒGõD×V%ú_y†@§H|ÂÆiÃ*f]uS©­pâ¸`ådâ‚•ã8¤W:ë–îw)š­[L4kgëajÖÉAw|@75/ŠMh°½&”7‡åÔQ³8 EYº8ba¬4a§LŒ´ªæ.g®–éÒ¬…¶'ÚPO|Š»14‹‰pq±)¹]o}"¥¾V°ÿ•‰½ñÿ\Úÿq€Úÿª¥ðNþ wtŠÄ'mœvá1¬ò±(}2¢:¸`ådâ‚•ã8Ä:ë(]õõa¹.,7¡ ”EcÓA¢ë’°¤m/‰íKÛN ;SvYݘSu»µc;!JFà1Lœ"ñ ëN»p—À|˜K [ÏV¬œL\°r§“˜…Õà4àp”}î𰬎 K‘Ä1›d±„Ò¶ÇcûÒ¶Ó‚mW a¹¢ÎñH¬h‡µX?cÏÕ¢-wQ¬ædc–lþ};EâA×vá.ùØ7”.XU¬œL\°r§“˜…Õ(ð½”ã‹‘pwSK¦ ^Bm ðYHL2AʬžâU7LÆb¨Ô ºþAà¹è7¹ø*ðæ´«Û¥»üt7.X9í Wâ8:ÕÃ]óaÖ³E[µ;­ã‚•“‰ VŽãt’Fõ ê[9ų•ȵ,)°¼¨ZÁ],{ƒå¡t—@§H<èút†IO1\gÝ„™Í±rŠhr*«Ü^_¯ìf\°Ê‡]7EǃÌËpµñEÓöµëýÚù^­â×®“‰ VŽãtï¬W ›at‘¥8,¨r·~úáPŽ”Ú §×ð ëb5ð,à9ÀÝJnKœ¤€eÙY•»ˆbQf•;‰âpÆË"ØÊ%™g9e VÎtÜÂÊÉÄ+Çq:‰»CT‹-hFk).°8ų€(âR4@XÛ· Ø,±Â(Ê<8Vgß»Ý%Ð)š9hr` ]WÛÑu½5¬ k~]÷Û‰b+nûì1ö™ëû¶X]UÈÞY;K4Q2BzBˆ‘”õQ¢d!K3J˘ۨ´dscõÚ߬,;ARÀ²¬ÁÊíD÷ÅëB]7v°ÝÝH«9(1N=NÞìƒúißNEéÊ®}!ð `-ðtà’ðÚÃï‡ÏÑ–aà3ÀãQ²–_/¡³Y]°r2qÁÊqœNâVÕbw(çdžå4ƒ™´w»E#±iib{8,ñã §ÕZ #hÀf±êv Aˆ âlÀgƒzËÔ¹ xMØöm¡V Ûƒ VNñ˜åËn¢ÙÞmxskUüÚN”Õv;‘¸fYmw“eÈ’j`ð àyhìq „ øë >kÑ$,+çÅÏÌ*çeiÍ* øVÎ#ºçÄcx6ÛBY¥ï³Šl å^%¶áHàöŒã·ïG1Z œ \@ûõð°ý à­À»àŸŽ¯?8G[> <ø4ŠWz p.𰜟¥ö {“e;ö¤N .X9ŽÓIܪZ˜¨â.Å1Ê—ÿE­uÀHlIn'÷MR ¥À^±ÒÖW$¶­\F1b“ Œ·„Ò,$lß8¨Yb…¥h@7XgŸ ðÌ]¯Õ]#v‰¸ž°Á)ŠE¡Ü€2á. Š!¸‰Xv/#3†Âú ‘Ø1Œþ‹ÐdÑ`/.~´soK³ [€²°­&ú¼q®Þü”â3©ec…lA¡h’–•ö7*ç¹V^ÞÁvw#U¬n^Ûþeâø*à½Ô¶ñND‚Õ:Ôǘƒ«ƒbç= OoC÷†ç5hËãByjlßèšÚÙàµE²Ý¿†pÁÊIà‚•ã8Ä-¬ª…Űš_j+z ÒƒZÁ‚iÂV|ÿj§‰ídÅM¼–Ø4šØ¶vÅ·K˜F×ì"«…eDÖ fé0L4ð ûÌòkêÏe\ ‹÷nlÓgpú»Ïšõ_»0·8»¦íGKŠ_ RêšK=‹ÍÈ’êfd1ò{à+xv¶4¶3ó~ÑU¡t «lª Xm~•qüHŒz>FísišLi‡tV޶L!«É'¹Ï¢ó“5[€ýн©“îˆNà‚•ã8Ä-¬ª…iM› wZÃfå|(»U¼®³ß·ůØg†íÙŽD´M±ÒÖ7$¶­ÜLµÿ£6Ø_ߦú‡Ñw°•ÎÎ0;½¹nÉÜD4yW8÷c ê{u86 üŒ(@³Ó9Ì=·j"ò[€d¿¸‡ÚⴆůrwÀÆXÅa™èðû§¹é%÷}),Æ[3ÎMn_‚®ohpþzV¼4¥]¤W}§ ¸`å8N'1 «ÓÐ mš°dñeÒ„¥N°¸sŽó¶P+dÝÊ›Èps8Þª…M3,D¯áPZ Þ%Dߥ­ ŠIr\x}§¾Û~À\Û‘ýËé Egü ð[Üv‘ËÀsÂúÑÀYÀýÂöt¹oXÎA‚ÕyÀ§ˆ«§!÷Fõ½¥9ßL[„ÅÅ—FBMј%kW¹NZîe}sp±ªpÁª96£þÐ2:/X9éTUÐw*€ VŽãt’[bëoláõñö;Q§ÞRo†ccD麷„ó,­·¥·ß‰âàìuZ½óаe:ÊZ Ë Ú¼ W·Æ–[¾›Ñ€`QX–Q+8 Y…™åØ"ÁÉDª<5³øû _ïD˜@¹¼ÔV´—NôGˆ²fÑl›~ât€Rx?:vÌÞ¯^`ìO¥ü^D”•ëwÔfZ2 ¼(¬O¹>Ú¼YD{ͧþ„‚Ñ\Îß݇6#‘¼Q}¾êünÏÕíäyââK§«xÐõ"Gí"J¶ðjàîh"bäôãp[ß½‡èþr G"ë 댢ïõ>À‘€:ø L¯,èó8­s÷Pz†À|l@™,WPÛ/uÊÃîÝž!Й† VŽãt’¯"‘åê K–;MXê뉲íd1D­µ_(@)‰÷Ç—#W CÚÑØÀ6$Æ„r}—ãDßåX8¶5› ÛWàAZ‹¤—Àf­(æPø$èç¡™6Ý ÇÂö­À±ÀÅa»‘…Õi±õq¢Áb=êe[úð|$&-¤Öqõ]#¾ <Ýg¾™³¾'GY§NB"Z?bÿ¹4¡f%²P;}÷/.‰½.¯8TÏý²è ësÐ`û~À=Q¾¡`É÷D׃µ©žõÝ;€×P{ ÏE"Ô›ï÷g$^½=Ç¿‚‹UU`úM§ÐóÜiL7ıê7\°rÇqœ’XˆÄª ÙèWAâÝÿ ÔÆ?¾œ‰¬7>€,ÐN^@äöóà^Àáhvp3·®rŠeµé¦³ÈÓ1GÙÿ‚‡•ÀOQpÝKÑõ`‚Ø—¡kl$QOœø±¬:_ \‹\M>„ú»Ñ@ÿû Ú]ïýƇv^B­˜›õNDÖFW„vÞ'ìOkSÖgúo$àOB%ÆkÑoø±ŒÏÿ,ø&ðú°>‹H¸XÖ_I­«XÖ÷wgäÚv-µY$³ê;0”ˈÜV{¼ÿ§zçýÇ@A¬ÿ;6RÉZ= IDAT‰Ä@´xVlûêØ¹›ˆÂqAúyèºúr¶}]×]Ï—Ÿ®sþµñ'ÑýLj_3Ç¿FÓåD¹`ú÷0Io2y|>ºü1ñ^Ny€®«59ÎíµþÂkµàÒ&_÷=ô_x‹œVA¿I³¿¥ã8Nnæ¢ÚÀ¡hæwž}Ìqœþc#êx5²²Ê3ÀÞ<;¶5¨>¥¦&”y¬:ÓßÍ É÷›L´óüر¬Ïw < šë½_ÖgºY@ûQk™ð>ôûÅÝÖ#ÏwaâÙUh°ÿаÿDàß¡mï ¿Àâ~•Ø—UßÂ{_‰Dð^¡Y‘'K°º=öúËbÇš‡~„,‘žM­ðórt]Q÷ÓÔo[žs’×H|ûz4iBhSB4¡¸ Yìš8œ¬7¾ýcàëÈr_dì”ËˀϟC¢hœ%À;‘¥l< ÌàíÀçQ¼Y€\£ODÿ7ªÖ +Æ/S+:Ç9}W'£ïÎ)—tŸÙJ'qêà1¬ªËaèFúäÎpYX® =`ç<4K| r 8u¼@¦}ˆÄ§v`âÑXX7k1z¨Ì'š,#{V=ñËq[ï,ñk ð޶Úqœnd#¬–3]°j6þÑn"± Ô)¿?­e6š…îes©}þgÕ9“ØGõÞ¯U²b4%iô=%'ìâ3»f×(Kàlà9h`ô,jãI9ÕÇ2ÕšPsr/údØ>ŠÖR¬~‰Üx¯í/:èz3¼‰â둽=vìLÔ·¼’l©“Â뾎D¹‹Ð½á×mh¯“Ÿ#CyMbÿ5ÕLgj^‚Âü¾m­kÛQÂŒsÑg|~XCbÜÛÐç:Åý‹ÇAµ sw?«¿ÊÉÄ«êòŸh6¢´Ô aåjÔ¹˜G$P­"<ÔA›DòzAš·„u Ò<‘ñšzÁa…¨c8‰fsÑŒ÷lôÀ˜E4hYφÂgJŠ_ɺæ…sæ„פÕe7ÙXœ‡L‰ÇqêÑÎLYƒê ‘°t6ðLjE™5(æÓŸÂ9ñçEViƒïä@?¬÷#ÑÎ cû³>à ²ÆÄŽkF|Xƒž™V×*¢L{‰WYVèE–2÷Ûß@®v¯@vNõI 5§_"Êèøò¹…&ùQŸ$ž ·™ ëy¬ ’ç$ݼâÛç]Å}3ÞLm€õd½¶}vX@BúýpªÀBi÷Pœ¾'äxí=€ß"±ý D÷Änc ð^äÎý $®> xpX>…¬„ÏAÉ5Lqkžjà‚•ãt)ïFæ_!7‘sPü‰D~¾ñep#šAû*ð~d&| к‚þŽ70„ĪPŒ­»  ¼@Ç¡‡Û hÆé娣ù&hxúž?™¬Øq'Á—Ñýây Îk%NÍJä–sUXÞ;v(‘En2`ùqÀ?Io”UgZ죢ÁQVÐõ¬÷G÷ÕËB{ãA׳>ÉÔÑ”lSÖgJ º~zlûkÔor ø¤=‡§`öN4¹âô.ŸFÿ‰“‰ÜRñQt¼®]rú’ëÐuušè¶¸NÍ.cH ]Ho°õç/¢ösþŘ›þ«´Ö9q,Ëå_ËnˆSM<†Uuù 8û24 h,DbËÑÈÊi êÀ߆Çjj'' ³ãóЬ¿ã8N=>\ÿ^C¹"w2¦MUÈŸ§‹¬¯N Ûgg¡ ¤Ç7@V(¯!_2‘k‘µU20ºÓýÌAƒ^³lC"çéH­‡ÇÍqŠf‘ÛóÕhl0Sþ…b^ý %§Ø'”«ÅíªØþ}ˆBlE“»“Ƚ™®ÃÎ!V¯Å­M;ߘ…&æ—'ʽCyŠy0µÖ¾ß$ÊöÙ+,A¿Ó>(TŒ•«Âbûö&úm¶ŧŒ¯¡ß³•õfx rß¼YÈ9N îX]V†r]bÿ6äšñ—Î6§ï±ßaŸÌ³'?èã°/Š™³uʶ¢‡ýhXßV¯§²Xl¥FYr¸Y1¯@ƒŸÕDbhr;Ÿ…&)>Dz,˜zܹR~ؾmfMv*Änd¹þd4©øP$D½Y¹Ÿ|é¡LèÌr¥uœf8˜h,W„X²x=¿áY3 µ1Là²ðÍò`-ÀFQk+ÙζPއõ-á5“±õ¢ãÔÍFã•H,\I$<ÅE¨ýhΛfúýÚñ6#p­Ž½Æq¦á‚UuYÊ[Km…cØï°o©­pº¥D³Ye+ ˆ:dcD‚ÖµâÖêpYGkKl{$œ3;×S·— ¡\‘yVûi·uÕrÒS©?‚(ŽWUˆò¨:ëÍæ†„¬cfð>'Þ‚¬¥eä²XŒñŒ½ƒa} Ó³÷.Bñ“ÙÑמ-(ㆰl ˆØ~Ûîe±}'²¸>‰W'£ü3ç6d)&Q\ 2ƒ®;½I;Ÿ1(Óêmè^ÊÛbû×Å’MÆ™]öå‰3k¯µs@÷½¹DñjãçÛýmºßMQ{OÚ€&Ž‘WÊDI@þˆ\Ý_Úfã­"±Lèè>lëÛÐDå¶ØþùHŒZM$J­ ëyã£ßg»Xy[XlßíD¿Íú>“ëñgO+ëƒDyÛî8ÓpÁªº˜0’´°rÊÁ~‡v<Øú¹(˜í“õÂz e˜4óq3õ¶`þ£H´‰Ÿk&âöÐO;·(V$–½Ñxyʾ½É/D ŽÃZÔiF²ô°F¸…?6:‡~V`½Ž¨Š`Õn6Ò8Ëa7b‚Â+ ªoYݼ Åß²ÝRô_„ D¢F/°•t!ËJ;ßßbÎUÈýóMH´: ’߆ÜHÏG.€–‘l4¥§?8Y_Ü YˬEIþÂ|¬AÂPžÿ‚ÅO»õŸaû¶  {gYš6"+[k»1ë&Ôÿ21ìÈâñ°pÞŠï{:r| ¬þ%\XŒþŸ Ñ}8KÀYÊ%aÝîÝÉûx‘ý¶=DáÍ¡4á).BÝJm6ÄFìDýÏFq[¡ëX4±ã‚•“Š VÕÅiw#¨›ˆb3ÚnÒßgRÀ¸&äf™TOÜÚÎÙLÿm¦ T³›|ßQ¢ÙǬ2ï5d²!Ô©@²%aÛÄ­°mçÚ¶;{íâP÷GpÁª´3K Ó~Úu?Æn…vߊ»›Ä³÷ÆÝ)’™|'‘ŸÍ/š!Ô?±81ñ˜1+ÛvÏ9¨‰úLj²Ì:Àd6XË“m¯“Œ!aê³ÈÊêåÈåèÉa±d?'À鿢äwŽ ËQ¡Ì  YkbËM(¹ÒMa{c¬¾5ç+QBŠ!šç;(vÕ--¼¶,Þü-¬¿ ÆGõq®AI6¾JíýÂ\ÏÑžq–e@·IǸ¬­ÛdäÂØþèþv Ñ}΄ªn‹SlÏ«õ9Î= V͈mNá‚U5YŠÔy³:qªÁm¨ã½/Ê~åäÇÌ¿OB³ÍÃè>ŒîCƒDæãf¾™+/E¢Mü\3·‡~Ú¹f*>S6$–ÛÑ8i%°>”EMT+rì(4ËØJ§ÖiŒuøWgžåTûÿÅ…)ÂüVà[ÈÅ0î2Ö-f]7ZÕc éB–•9.~™É¡9êŸDÏåµDVI˃µa_2žTQd{¾e­¼ ÐnBÕ7/¢þÝ(‘uñ8úýGˆ&Zl‚e3Q€ä­±óv¢¾âD¬>›qŠgºž-y<¦ÐH,9õGÒXƒ2ü=ƒoEn_†×ÊÕaÿ~ÔwI6qwjðq7ï}(©Dž ·‘ÐõãçV÷?@ÿ­‡}»Ã¾3Pàø´fÉÓ.÷ô=´Ïr©±ßÁ-¬œT\°ª&¦0çu-r:ƒÅã¨×qêc‚ÕMtî!n–IõÄ­!"QËb¬cº@Õ(æL7b‚¼1œæ°˜5û—ÚŠZòf | ð&êýšÙeR}t“íª"&ý\²–iÖÚ$ª|Y3ö›eîxXþÝÄk†ˆ„,µöEîøTØb¼ ¢ú!9ê]Od¹°ŽÈb¡™I9L¦VÐ|°çYè9d‚^»0«â-D.ó/Ç\ë f&ŽÅ3ǬþnÁžýyÊ…Ô´Ž¶n4vÚ…D©«…Ï5H º†üA¥ç Y!ëÀÄvÜZzAìµkQ–ñÿFkšÑÎO¢ÝhÙ2 ~æz?\¬¬nlðÚv VNs¸»´“‰ VÕÄÒÚ9}7>Hz9¨S¼¥üušÃÊEÆ–j„Y&9Ó±Ùwוּ›ùK½ë~œêýÍV÷G‹Ùu+ŠEqqÚÕIì™;ˆ~»S€/ŸîÙD=ßN£9Á¦ß±¤7ä8w1éb– û†eel¹KñMfµŸ-ëÕˆ±=Ⱥæ`à½ÈÄ&Nl2Å&X,fŽÝŸtf…¼$ì&š‰ÇÍ1Ñ%„¿“˜[kc¤ÇF2Ñ,ÉÙ÷ÏDŸÝÊzBTL!ÁÓÇc Ýü¸ž™[ôíõÝ„„§1¼xŠÅ”ä2àaÀSÑõwG¾e¸¼b†í,“]À;Q¼ª» ÿ÷vò%•1á0¯‹f§¸=Waú³8~¬h&ѵ;}‡/Cñ³ÈÛž<} ·°r2qÁªºŒe*B°:ÝØF7¦ ÐC¬¼ =ÌϘa=Shƪ™gQï} êý‹ÞrÙèeVN},‚?ÚÇÍèºßŸ™]÷IQ+n)5Ž‚È> ˆŸJ$¨‚b’Ì~™¨óDz!êô¿uN߃¸—‡zŽGƒ€sÐŒþNàÅÀ%¡žë…q~Ø×í‚• ¨ãUk„ìàèW£X0ÉïÝ)– $l5·Ì+.fYÖ­f&5¦¨Í@v{xïåèzx<K‘ï—Ç„º<@uklDߟ¡®S䛉tZãçèû}TÆ9y:gÉsââ×$rý ”çÇŽŸ8wG‹»Ò|×{¿oëw¥vÖõrjÝ÷£»gìÇ¡ß.-¦Ë;‘À·“è?dË(Ê<ÕK™þœtÞ¨ $ÚŽ]Û€¯ Ö]ðYáø×;ÚÒÎ3—(CZr9Å'K.wÜxdåxàih€ýHdÝy/GêP$JšÅY¯r:º¦N.»!]Èúî–fœ“÷ýadÑv µ.Ê! ¹¿R›€âÕÈuô¯À‡ï÷$"]ŽÆd[²êGKÿ„&žŽËùŒ¹D‚z£÷iôžiŸ#ï ßâ)9Úê8N…¸ýy- ®? ÁêÈØ¾x'j9 k¾ì;>\‡fmFó^±ã§Í:݈üÇã¯/É:HílÕI(ÆÄ.4ƒy:ÑLeR|jÔ®zïÝ §„:>3Ãzú•Ýèûó˜IÕÁÜ>¸n_Dßï û?‰:o—uä.>]§žF‚•Å0YH­µÆÆÄ±¸»Í1À¯Ñýôrjãª$ßïöX/§ÖšeµÿéÙô†åc©?AaÏÎqÔ)¿6œûeŠ ÎîTŸÈJñfjûW ¾Â^u^÷¨pÞÏÛßD§Oø º¦žÛèDgkÐww@Æ9y«¬‰£fÛDdq˜fa”|ïøvV½“Dã°û"ã¢:Ÿ!^÷³‰ÜP³Þ'þš¬÷Ìóþýÿ‘ã\Çq*ÄÏП·Y‹¢4&ÈÎj÷íð^ Ë­ ëŒ}Ÿ~”¨ÿèfõ6¢¼ «¨)ä‡ÿÌ”:Ϥvfè?Ãö+€óÂ9O޽&)Xeµ«Þ{·ÂÇC]¯™a=ýÈúº VãDY­ëñ÷)ä¿? Nl&Ѹ‰Bg!—ÃdŒ‚4·¼)4sŸÌºt‘û˜-o«SO£vÕ{ïV¨g-á4æ ôÝ­)»!N ëÑï²OÙ éQ^B$Ê×#OGîj4c êØÆ]û&Ñ=dñ‘Œ'?Ý:"ÓþW&Žm¢6Cí·­ïÜ÷ªKà±è·ë¤ë¼Ó]<ý¯,ÆhžL‡¢ë*O@ù"c¹”iÙœW°jæóÞÅÕkE|÷É:Φ‰$¿D×Ô#ÛPw¯ó'ôÝÝ'㜼V­VsEÑ€_¥¼_«‚U–Uv=ê}μï“õžy¾Ã+Ñoqçç:}H+)šÎ`þÃEX?\ŠâÜ1¶¯Ù4ÌÐÍ$-í#÷#£¿ÉYç:¦:üR韼.ìË Õ®"ñ á­cß]/X^ô&R,È<Ëi•[B¹z†õ¼M`ü8 eð1v£x-—§R+,†Ä¨ËP¶²øëÞü.Ô¹,qìL$:Y¼‰SP,ž«Âwù¾Y÷%ŸURÕ±v=Ë2Çù!MÂvž°6Ø+Ê2(+vÎ8ðAà/(ÐöJà§È‚âRjÃ'Ô«;NrZï} û.CÉ âœˆD‚+BL(ˆ'{°ûNV{“ÉV¢xsW…ºö·ç§™¶Ü'¼ßBtü%J«ÃRe¹Ë<ËIÞ{Eô̸à™èZ3~;ö,j3>®F‚ã뻥ԹƒÚÉ£8YõIï“õ9 ¿Ž§Kùê½¾€º,èúu(UéË©Íâô­ð^KšK`Ò²)¾ý”9ê…¨C–týÚŠ¬¦ŽÏ¨ÃØ„:/~Î{WÎv¤Õ™öÞ­pA¨ÿáNt¦ñôÝý¶ävÜuNw ëâ¯DnQy(ÊZ¯*ü}¦#˜…®ÿ7Ѹóыܙð_‹k7ʰxwôý^ÖÆ6•QçXj-Φ½™Œ:Åè·»¶ì†8=Çtm5 >ÓØ9ÛE¦‘•…¬¯n˨Ãxºq­EŸ}¦‚UÚ{·Â%¡þ{ΰž~ä‰è»;¿Ñ‰mæ*Ô‰ r•: ý/òÒ.Áª¬ FW Ït×çí…¾³kˆÜtÿ‡(æB¯³x/ºwÆ]•¯šñº•á¼uml[Ù‚D±d–‡õ^à@ôÛý»ì†8=ÇmÔwÅ.2vÎ$µ÷è¬ä itS²‡D$Îk%ÎO3m™D‡?&Ú”¬£] )òwÒÉÛjDžÃÝŽ%frÏ/Çé2^þ¼ÿ]vC@”)ƒÂiŽg ïî›%·cêØQçxžl™&X} ¹¡î¶Í"ÒÍa૨ƒ:ÜöKÔ•–%³“ü/Ù™HE™Ï&ˆ‚ _€Òˆ÷ršð8O@î"ÉŒ£ñåÔþ¶q¶…sÜí²»0±ñö²âôõ='™iìœf’'Ô{ÿnIö°æ«fbîdµe_Ô?¼šÈ=4­Žv%¤p˔ֹ}w‡Í°¬fÆúü{têâJfu)2†•3sZ‰a5x:ʸ±ªðuÖIKãï4¿CÔ5(¾ÐéÔ¶ésHŒù\b=sÑlª¹š>ů¸4lY~x'šÁ;7QÇáÈ’ðÔV?Ð ÙÊø¬ð r¾™Ž? N>X@˜;émAbâùÔšÀ§ñLä^ùz4ÛÇbdô‹5Z¯`¿[¿³Nç(:ŽU½Ø9I.^Ûn$˜­!Š1õ ¦n¯÷¾&ŽÅã¥%øyqâX2ÎMV{×PÛ§úznA­K`«ñwšiËYÀÛÑóýÃu¬Bɉœê`IÛ3ÏjÌ’™6¤Ï±ÿ«gwœ.äñHqþaÙ q€æÌU>@dú?|¦}M«<÷EßÁØf†‘k×%D¿çWcÇódË4 «ùhÖõ(ëµi¬Ç˜n‰³‡h\/Kf'ùYhÇcQÀÏ3P‡ÁÚk‚U?Å«Z€fUÖìrõ Ûßl’ §\,ëlYÖNïò}tm=©Áy3“|}<0ùU47q ñðgàL·@ª÷¾‡†}t=þº‘›mZÉ87Yíýoj¿? Š~rÏ{hØ0­ÅùÉÛ–“€ï†õ9¨óð:u< M’Í:<Ûo«¸uZ5¸#úþQvCÇiž¡?ðïÊnˆÃRô[dYWÍEÖ6?#C¦PÇí¥ô÷ ̨Fã¸õË‘D¿ÑŒ`²¾Ú‰²ªí¡6Ñ4Ø},Ê¢ùH”¾ØÜªÀý§D»^w K±³P–¹ç ¤ wGAn{ÙµíÑh€ÑŠP•\ÎCAp§ðYÃnÅ~KÇ)’/¢ëêNÌAY.4e»î$“=tíJHñot=؆º{wÙ¯6éû—²âT—FYŽœò°A? UÁÜÆÒâ¼(,Çf+°ž‰Ý÷;&ô gžÕ~n@fûW!†°nü¹šYÿjäþVs‘ùbäÐ|>{,3Ñþ(¥õ1”ßÙ‰3ÿÖïÛ?‰yY™Çõàt­¢Ž_r}KX¯ÙÊñ°¾%œ·£Îû,B®t‹Q‡r‰ŽƒèÙ5„fµ—"KµeÈ’i8ì ç †Ïµ$Ô³8Ô»(öVgQ<È]ÔÓ4w'cèºXJä¢ï83eS(÷Ê<ËÉâbàÝÈ­rCƒs«ÀrÔŸ¨—¡Í)‡¢\™añ缯äÔÅ«êbÜ´@’NgIƯšû/E4ÛáJduó5|€§*1¬~ƒbj¬F‚É7ÄŽ¿<”¯åwbûÒ0‘ê ô›Ç9¹•²$® ï_•ŽÑ3Û#(û÷…à*4Ø;”ñíÁ°Ü¡ ¶ìAÿ—HX2Q©ÛÙ…>[Xu'{Bé±>")2†ÕL'4—£øLI\ÞÛõ¾Eð¨²Ði_{'CÙ ÏÌNâq“ªƒ VNCºQ°º7 V¼‚(•ö ä¿ýZä7ç<ä^·=4nËFà+(KDœU¨ƒ:Nó72›ÍåuDãeÈßÝ0ÑÃY`˜¯ VÕÁ« §è%Dî_Û€¯#kªd@O³Þ¨g2Œ,B–¡kn)‘eˆY„̯7‹zÖ&›‘¥ÊVtíì@"ÀdhãHØ7–aß¶pÎhØ7FdõRTF›]á=—„¥, £ç68¾YÆÔ#‡hŠú¹Ç ùÒ:ÇËŽitºîžˆD´aäúw_t_< e=JcHÀ@×í”õ¡°n÷7³dZÖ‡ˆ®ã41Ӯ݉ÐF».ÇÐ5µ‰k£H\ØL亻›È-s ‰a㡞 ¢ëÞÞc+Š{òj-ÎZaº|ø%Þ!îVFÑõPNŠU ñɬ°–£?³­§e$EñYÌd)Q‰4×€aä&ÑŸwKhãî”óÏD®F#D?³æŠ[4l õ „÷-KùvÒo¢‹‘¥_që4Ë»ao&ºvÌ2Ä,BÌjÊ,BêY›,#ÌÃúR"+¯á°Ï®ëùaŸ‰kK‰,¶âV/ ²Œü.`»Ðr"¼‡½(樹{; IDAT×Íu^çt³âœîKg÷DñØN h?…„ü/~)fUv ìW_><5çë6"ëË/Ý«­æfî݉ ëîj㉠VN‘l ¥On7‡}_û£ D^ ¨nVØ›‰úà›cûGëD Í2õE^8Éeeum@Ù#o%š@^–³\šØî$+á=.¡ê‚Õ'€‡oCÙ×@ƒ‰fØŽþ´·æ<ÿɉmp/¡6¨²ñt$:Œ‘O©ÿBÎvê4—„*ÆD²¶Ùb4bL\1·(×ìt¦\…Òò :‰UY¿ávd˜qK¬\‡ ,Ö†u;67ǼÂV¼´d7­–†[3;u©º`u0šñ/s†s„t'ã¶6¾·ÅÐ(2KÑ""Éâh-IÙ¢¬ ñx[K“7‰Y™ED–G&ø˜¹(è&¿“Èâ"‘ƬÌ* ²E³äû4ÂêÜ <Y%= Å'zê €¬ÿ>‡b×ô Ûò™ü.`sQgl1úÆ‘[Ù)?S a1TÒ:÷[QÚðs€;#wÁç"—Á3À7¶½…åóKàhÒÝ/Cú/®óZµ]°êNlja©­pz¼V÷±StR°ÊK3mº?š˜nWÖÀ¢« ¨½ÇRÿâ´ûoý/ðtŸ_F”=xõaÍ!¾>Ö‡ëf0œ¿Mææa'2Šø'ð¯”e}«´I6'ÊNðtoúо§ÓeT]°²J/ÏägaBÝ9hpt}lYCºËáBäªv‡°Ž‚Ž|œ‹üÍG‰ÜÇÐÀ,®˜/Aй¹ªA¤ÊÛCÊ>jîx¹ØY)¢,û\Û€¯†åh$\=xpXnÎF–(7´¿ù•cӅܪd t"²«8W¡˜VoD.r/Noc»ªFÜMð£À‘Èªê ¢{CîØÝl ¥»:ER¤Kà8º? õcžŠ· ˯£ûÐ(ðl"/W#‹ÙIä½°õÇ.¯?>ãýâ"ZÜR*«-‡ P³Ñ$@œC[¢ûéKQ¬Ø÷¤´i%ê€ø/&Š{ðƒX½+/…÷Þ ¼ Å­Íú^Æ÷!O‹½S€Ÿ4Ù–û çÃ}‘ÅÌ_ÐçÓSê8?´{&‚•=_†2Ïr’ØøcŒü8ͯÃú’Äútíý ¸‰ô1]?`Iývº–‹`pÿF'ö(ãèó§-Ûk;Ú_7¢^½×˜ÅÑ:$zýøu¨ã\dô!4 ôJÔ‘x ðHô>ù{ÏÔº $`-C‚CÃrdÁñÀðžFú§¡`õ/NEç7‡¶~u’ÎD‡oßF¢_¢kè/À_‘xtº9nB’¬ï*¹dÍp,!ê°Øù»ÿ‡:&U‡ÛÍ¡ïäÔ²âüv=¶ì†ô(Aßo?X¢õ"?F¿ßqe7Äé9ÆÐµ•eA•'9É$êÊócÇ~˜qlÑ䑵!Ïû%ω{dµåüı¸e{Üêúh”¸¢Þû}“()Ò]©M‚t9êŸç!×vÐDª †³¾—I4ùœâqe›iËû€¡I7gÔ±3·*ñûTk‡¾·—݇{¢ß"™¤Ìqº†¿¡‹ø®e7¤$~>ÿ;7 ‹_#ëª=¤‹*“è{ûða$¢<eèwѤsQçmrC=¹AÝ uZN@ßmÞ`á÷E"âV¢ßå+…¶¸ûxúÞ^vCœÿã:ô›4Jà´ÆgÑ÷ûò²â´Ä·Ðï÷Œ²âô7 këÐÄþO"áår4áe럮SÏ$‘àBj­e7eûnžMäòZ„`Uïý6&ŽÅ㽃úµW¢Ï·HM¾ßíDßÉåÔZ°o¢6ÐF"·lçÕkçµqt⟯™¶ÌG“¤L´)YÇl²CŽäÁ«Çϰž~Ãúõç–݇ƒÑoñ¯ç9}LÕŒ~Zknj¥ÖÔ"׿Ñiù¿‘ÕÔMtoPó²ØE¶ß¶ UyƒZþ),¯ž< Å¿égì?ì&¿ÕaŸP®+µ½‹]ëUL˜á4Æ‚®{0c§h6 ±jŠYcœ[îÞ¦÷²zp²hÏË,4¸œK1cˆ¯Ïþ€úµ›2Î<.ê%8š[Ÿj¡-{ˆþ÷ÈjËr¢ ‰â®¦ÑJ;ãØó%MœsêcÉ4¶—Ú "Ѹìð0N…™]vÏÄÖØLSÚƒhš‘úòÓ¿‰V.Vu6ež543z,ûëTáNZUæ!+½C€»÷Cdžòdµw(p`8·¬€Î Ñ}ec©]صîßowâ±aœva1sö+ .³|&palÿïcÇž…!c5 ™ðzànaŸÅ±Êb aï›sëµåÂı¸°4H4)øâıd›.@1®Œ£m‹g{ûð²°w Ìú^²h¦-g!kòs‘§C½:Vá}ò°¾×¶Ì³œN`IµLäuœiTÝÂêÞhP7S“ÙnÅ„ºNfŠqêSu·ÊTÑÂj Š¥fñÔ‡uË‚idžˆ²Á „cÉ×¥In…1ôÀ!Ê„i.G‘à(Q2ËŒ9^7F”õÒl&ÊœiY9 ·®j?ýn!ÜíØä[X9Ecñ0WΰžÝÀ()ÏŠ9j¼ &¯A÷ gÇŽ}=ÃæÅØ;ÅSº’úA×_b7mDn…q딬¶œ†‚®ŸŠ„²øëÞˆ2,¯O©3Ù¦SÐí‹ÂñY¯_ˆÆ &~Šâšžˆž‡¯~Cö÷’EÞ¶œ>Ã×Ñ÷{²fûuJ÷EÚL°ûTÕÇsUà |B©lBýÒ½ð~©ãtDþ7•ÝçÿbQ9­ñôý Xl'#sú™Š±ƒÈZéîÀÃP'ùE(æÛQñÛh¶õR”˜`3ÍÛÏ»ì@Ý¢8rÿ‹:ç? åÅ(À Èr’ÚÑ–zË(2¿¾9lÇÅ:Åb·{”ݧ%üùë´‹w£kë3¬§JÞe·åX$Pug3ó¤Rgà±[ámè{{oÙ qø;ú=Ž,»!N5qE¾Úd¹:N7aV&K‘Ù¿±Z´`‹f]‘Èúiš}‰—31ﱉXf¹4†„;¶%,¶=šòÚx[[a}–ad©5€>óôÍ å"K°ùáüyáõ ‰Ò(["yDé•2ÝÂÍg²Ú‡}×>ƒÛ¸å‚Ó.ì¾»OæYN3\Œ„ÀÔR¯*Ë‘kæEe7¤OeV|1§sX캽Jm…SY¼#VmzÕ%Ð\ªl±ôÊæ¶dƒÿ-ÈÌ|„ò­›ÆQ[ñh+X «¡tÏwG–'w K+Œ!ÁhSF™\¥znÆvMeœ-‚!$b½x?pK›ß¯ŸiÅ%ЄEÃGcµià!º'™éѽÓiûOdžå8ÍS”`ÕŽ¾árŸ)É#+`'ÛÒ,*»M°‘bÚkAâg’ ±L‘õøÁ<òñïw<𺓉 VÕ¦7Ô<,D‘!¢Ø>&Þ S+>%·—†×-‰½¦öÅò#Šå“%ríN¼Æ¬w²^³‡t1ÃǪ5lÐ>™°Û>Ø7l›uÔÆ…š ]òÎ@sØï`¢ÈÍõNtfLR°ZˆÒ7”Xlß~{Ù†þ7#L³¶¶x¬ŸD0»·,È<Ëqšç¶P®Ê<«6Ò¾ì„NñØø Ù¤@ýNž,Ït8,õÖ“‹›~Ÿ[H}+úïßÖ×£>Øú°”=1_&–¡}YæYNßReÁêÞ(0âöOås€;ÝäìÆ¿ÙåMÍ: ìTìå±rï°,O¦øëÄ\ªl±•Y˜P1„‚V£œÝÈö.¸=i$E.Qáiê[¡^–À]ÀUaq:Çþ¡tÁª=, [‹©<ÖÉàø4ß°8pqÆPÇÚÜHѽsaXŠ4±ßÚSOè2—Ù±ØöHl}<±]%kÕ•ÀáÀBù¶°¿¬LžNïâ.ŽS.6yþ)à?ˆÄ¦eD‚ÓLnìBÏä;„¥Ñ¹ë€µh¬·â‹yÜÛ×éI[³òž‹¾Ãd˜Š¡p¬Ñ9sÐwmçÜ+Ôï.N*U¬VwDA’û•CC¹?ѳ‰”ûµh ‘¥VÐÚo¾ 4¶ÅöGƒjŧäöhxÝxì5­0›(–Oü˜%rÍI¼Æ¬w cv£MSúÝìº5¶ ÁözàõóLRXJw l{гë`” týwÊrcXÖR¬ÓBtfº˜5Ðıà€Pç|Ô¡,²SYOì2«Ê4±k‚(¶\ò;Kfì´˜nÆP8~ ¦G.Éõ¬ó¦½wœ¼TÙÂÊé.l²Ú…õæø[(R›Ñ2ŽÅ3 K½õäbÇv géj$Nï‡þó«ÂúJ4¶[–ÕD}³¼XXŒä²)±½‡ZÉbžf Miç´;kîoÚ\¿Ó¥TY°2ø*ÍÀvš X3E){ã7»}©½é-ËÔiêü$rYy{X6&Ž å¿löYbÝÞ÷KŠ\¿E–™ùîw¶¹ˆº•Z¹¸…U{Ù <8}Çk ÕI¶…e¦qÑž|9¬oEÍ–&…®ôŒmÇ…¯%‰m{m2@l®Ë ¡¼øS™rz’q$º.Aÿ¡¢Â>Œ0=Æ]Qü8ö4 ðëX<4=Ã:»³Èu×åæø0KXöd›â–LEL M×…%‹¹HÔ²±Ý 4inËãÐØõ¡]¶([v'0+ïdx–xˆ•]9ÎÙ¾ëø9ëèÌøÎéBª,XÙlgÙ©rËÄÜЮ~Øàܽ‰nvû"e<)Jm âS7°‹H #¬ïOWÌižQ4˜]Š Veã‚Uû¹ønÙ(€—ÄÖ€ß.°þzb—eM» ]ÉßdÆÎ1jŸ{[ x¦LœªZ"§·Y‹,ûöE×`KôRs˜Ùàûþ( E»²ï-Xm@í=etœ$SÀËnDŒ]¨ÏPÏê}/$X½øzlÿ µÂ–-{%¶gS+"mC!²„¦´sú)Ž¥ãäâ•è†òße7¤D¾ƒ¾ƒzæªNç¸ýG—Ý.fú=¨b6KQGûáȯÿPd=YÔÃúúy2ÀÉÇ]еb.½SÀ¯Jm‘ãô¡ÿÓ±uŽç¹?\\|„ZÑuø ðô,9Y ^\ Ü'œ÷àÔ°þIà×aýáÈJ ÔR¬î•ÀQìÉ+€‡…ý!Ú¿!¯€ƒíyShÿ€ãÂþ÷ AðåÀ÷cõÿ4Ôs)Q|›û„÷³G÷¨´:ž|†ÞçUè:ú¯²â´•/¢ßù¤²â8ePe +w Œ²~L–Ú "‹ *¸®t+f1XvÖ˪°¹ðÞ)QfÅ0˜@×brÙLc.¾Ø¾xl‹yáY…œF˜uÕy(ùÉÈÞÆ.ŽãÔ§ˆ8VŸBYw¿¼Z·°¹À•À›Ãö0pNX?8 ¸JlôàÓ(ÙÑúàAÀïÂù>«û3(LÂãPLÊÁØþ/ÅÚóià‰áØdÑ{ß°œüxGxÿxfÂOCâÙ]C÷þŒ¼ Þ‡¬,¿>ã•)uü Y½ŽY“z¸ŠÞÆc•9}M•+úÙu(™Ýqº•¹D×™Cäz9Ž\uÌ y7ѵ?‚Ëfšl>ñ{ˆî£a{"· ñÌjí4ižâÄ—}Û\soä ’Æ(p5ú æB¹ ݇¥Þkó0޾“Ù¨ó³-ûtg†éÚÓIÏ ë_E‚Õ¶°ÿ%Àëñw#ëýðA“ãä%-Sà'‰¬•!‹!pt*Óy ð¬°~.µESÔº$‰\æV¢çïaaÿ%H¨BÏœKÃöƒbïy µ1øI”½Û,0­=ÏŒµçÉöœÖÿ„îõxDh¯Oˆð$\mCžõ¸ÎÅõqÇqÚH•«¢Ù“ÍNìa,^’[¤”ÏÖPvãÀ³ ÄÖÍ%pï2˜DÞD®v¯±˜7&œ¥‰kCh ±7µ‚Ô¬&Ú0Š\*®N”Y™û–‰XÀÞR0§ì/vNü^Œ VU`%²"8 ª¯þ A'#aqJÂñçðšqdiðdtž‚,>„â3Î{W8÷c ê{u86 ü ¸]/¡ ñ ëQ¨çmh€ûSàL Üqœ|¤YX[§Öb¨YvS{oÿð|ä²·(Ô­ôDô_7¿Ã‘˶µ’åwzÞæa6Š›•Ö÷]ŽžaóÐçØšrŽÑÙˆ½oÚØDëìR[á8%QeÁj3ÏnÔí¸K`u° ½íNéÚ«, åÍÈÁ²/Î"Êjd”¡Žè<¢N˜‰\–¢ÞÒíZ6GB=³¨íÌ. u%ÎYDôÿZYÌGdQN[Ö%¶×‡åvZË7–,Q«ƒh ²}Wý~Ÿ­õ\l~@ºÔw±9¹ ™`õ4à19ê{²ºØŒ®‹Ÿ‡ýgÅÚ³¸ &Ÿ‚‚¿þ±•ì8}Nš…U³\<8Y6eM˜ %ÙxqâÜ߯^€âF}Y^™à³ köú_/CËv¿Úê±ö< ‰cyØžÇÖ×½‰éŸ ÛG‰ggoGâþ‡‘PŸVÇ*$Ü÷:Þ7ílbu0ó,ÇéQª,X9‘UJÖ ’ÓÜ|fصǢÿÐEe7$ÇYfv g#a½×y<ºŽ~TvCœ¶òAô;¿©ì†8N¸/lµ1—¥Ýhî`Ü2® ÌÜ¿ûÖð ÕÂ~‡Å™g9Â\l@–|æÂšåÆ“ÅyÈEè©À·cû³ê[©weªü|(ãñ'‘%ÈÊÊqœæYÊ¢\ÒÛÉÅÈbsEÙ ÉÉrtë1p¦ì e3ñ3îÃâ¨z¬2§/©òàû§ÀÈ´÷ú’ÛR³ˆÜÏF¨}íAæä··¢H¼¼%,ýœa±hÌz` ó,§f=â/«ýÞù©õ\l²Üx²¸ ýçֹѠ¾o ëa9zÞü¹×@tØ}ðó¡ÏÞŒÇYtœfÙÊnUvš`#ÝÕÞ"p„ÞÆ„IUæô%U¬îDûRÐW¡Øún‡dY]­Bií¥¶ßŠ\;Ö£™q‹]3Ž„ýHÜëãaÛfÓ«s(‰ÅŠÔ¶`Ûñ`ÝÈÛbAxÛ‚rÇcÕ«÷˜°î³X­‘ð:åâ<;GQp=ðØ”ýçPëŠóžŒz‡Ûwm²>s¼Y'|>vÌ®›åý;Šó`dÁuwtï¼ø*±Ç©õG\ìuf‚MLû³Üqœž¥Ê‚•uÆÇ2Ïê]ÆPÉ£Q`Ü5±c³‘‹àjd¦½¢\–¥(Èî‘´Ç„­-¡mÔŠ^“a}+‘8Í Kಅ¥²èóvà.ÕÂ~·tâÄùÍÔº¦%ù¬öBÏÇqòÓï}\Çqò³-” Jm…ã”D7Výj‘±x X&­ÌökÃ’År«\‰„£A$ - džÃþÅHÜZÖ—„íÅáÜe±óªb¾¾ ‰dSDÁ†'‹Ën"—§q`gXìZÚÎÙA4 ueÕ{;µÂ¡“ÏxY-<†•“Æнò+ÔZ~X'9îBø]äÖ4?,;:Ñ@Çé’V‹ä"ò$ JÜç(ùÌ†é§·Ä ”ÔæÑÕ×o˜«X•ÇsÎÌñLåN_SÕœ¥›ßE¤*÷#3íÈl® ËL1Áju´S+z- ëû¡x,ã(õp³ÂÒ.¢Ï%,9Ý… Vý*@W û¹…•ç»(}}ÒÒ¬cã1èv Àí¿ÄÅ*Çi¬ê=Çi_ŒÁ¼Ùó’‚ÕýQœÔ¢Ä*B]·¢L„Xo¿àÏrÇqzžª Výn]UEÌ%°QGe¬vᤜˆ´oÌFBéœÿÏÞy‡KR•yø|çÎÌ0 Œd1®¨ˆ `@VE]L¨«‹ YŒë*fQAÅEL(AA%¨ dP@0„É7MwÿøÎ±N×­®Î]UÝ¿÷yê9•ºúëPU§~ç X¢Õ­é» ‡¿¶¤î%º‘[ÖùÜ}ñÁ‚_´Ø!:•fxXM_ÅŒoŪ‚þÓmÛ4Àòܼ.ìgŸl œ\„å·› ÜäŽù*̳ʟóOÎöÇî¿^CT¨!ÎiÀ*à ·ü ,wß ܱ%X ‘Œ´Qr}Ñ•ä]°RE±â±Éµ³2µBä´Vƒ˜èŸ†«X\F0Qu&`%µ~ÞOÝXMS!¢Z%8 Ñ­øsª‘ûÏ à.¬ú籘ôJ·íËÀÙ˜¨t,ð¥`[üK0ái¬(ÃEÀG“±‚ žO»ùë_bbW/F\N¬ÂÙò3gãtàH÷~×QZBTïsÏÎÔ ÑjT(Gt5y¬ÆçÕ>QQ•{²ñÂÈÀ)1ò¡¦›ùÁ1ka3S­1ìAÂO«cËáÔnOÐ>¬s:DTýrÀ­ *t0äÖõ%¼æîX¬D5”ó°BÔG’‡Õ€ç»yïÝðÌs=‰ºöÀg‚õb¢PÒ¶Ià<7–Ö¡;Qš;õ4L¸Z¼#åu÷a÷Ø'cEƒnt˺c‹ÚñƒOs3µB!ZH^¡ÍÀ½Y!êfvóìEUá„ñ'à-nþ©±mc˜èN°je;¿Æ:Ô#˜pîÂ’¸nƒ XI­Ÿ÷Ó V˜`aŸm+•E-?.$U#>5“?4ùx¢3‘‡•Í%I°zO0?A©wS«ØJä¥S Ó‚ùùDUŸ{¨\TåÛÀÑÀ"Ìã*Db¸BˆDò*X‰b³¬(\ãûÖËvŽyao-&$lt­/j0âÖM”yÍ ¢|!Ò‡•Í¥RÒõj9øæMuu°þª`Ûë€kê8¶ïËùŠ¡`bÓ·ü-à#À®˜×Iný_™:0ðsÌ+k–³Ê³x°û„ªÇu ^ØnU!!r+Ñ öÁ:8Y”kùå² Ûû°ÁcÝò…n~Y+Â<:—5ð>Ó©,jùiˆÊBR%ñIˆv#+!šK3’®oöÂÂë&¶½ <;oªãøgbEnÃ’®_U]ƒÝ›ÎÅò`] ¼K¾^N<Ù\Ý϶ë÷Ç6Q;¾’úœL­­ÆìÎÈÔ !„¢ËÙ:×o§4ô È<8ëð QD.ˆÉÚ!:„obçÔq £ÝÏÄ’²§q&’%1 ×öŒ­ÿp@C–u/ƒD¹mNÙ§«uDy ÷Ç*ñyBÞ'®òuǺùcݾI6ýx›ß¸>ضŠ(g£ïÇß<&ØgLLs?öÚ)kCDËø+ö?µÒŽB‘'z°ÄÕo~L4R•&X%u"Ò:×Ï~ïÚjxvQýY•û‹Î#¯ž›Bˆú¸»®¿"kC„膰óiulý0aç&`K0ÿ¥2ÇYKi˜nØß{p9p›;Æx•¯[ ôºù·¯'ì_.ì» ¸'Øö+à—ÀQD•€WQšci:òÄo^°Ú9kCDËøöïžµ!BQ —/®>¬O¬’:iŽÅ˜GÖk€Ÿ/­Â®c°‹êÙÕ|Ñq<­Ý3kC„MãìºþƬ ¢Ø ;ŸîOÙ§«´þÞ?°B'.<Å_W­`îgp0VTå·nÝ*,Ÿ¥g:S;Q·`ÿ§ý²6D´ŒØo¼ kC„ÈyB—뀃€«°œ@s‚m¾ùLJ;‡_Ƽ¥.t릻×?ÉM¡z¿/Q²ÝaJŬr|Çíû¾*?‡è fŸÂFt÷NÎÖ!DQ+!š‡?Z™§µXâæ#êVâjà7dÊë~œ,ïÌ/.Þ<Ñ­{Xì³x°J›„èv”t]QXŽÃòRG$Üå$ó¤ óøøöa̳ ,œð=Á>a§ãJ,ÇÕå®"‰=1u8?N©P*L,þ,ѹ)D‘øv~Ÿ’µ!BtÏÄΧ?¦ìÓ¨‡Õј×õÀGcÛÒ^·–ÏÔ']/÷º…ØÀçínúD°í,WêmÀ›Ýº¯`÷AÏaÀW§~$Q7aÿ§'gmˆh }ØïÛi•C…(<>4­¿ÒŽâ_.Ù¾=¸—äNJR'"­Ó?¶qÞ‚ÝD'±jCÏJß½kù&ö—µ!BÔÁ°ÿï§+í(„¨ÈÁØùtiÖ†´‘gß –¿ƒy÷‹Æ¹ û?˜µ!¢%<û}—TÚQˆN%¯^»`î Y¬ÌÚX{‘›<§óIbÂ2àeU[ˆ8ÏæÿœH:$JñÂñPê^Bä_J{0u/!D5øÁØnò˜ø#æ}½{ø^ \›©EB…Š®'¯‚•™ŸÌÔ !D%NÂ’ùŸŸµ!9G‚•(2¬„h^°je«<ò¢2ó¢16»vv¦VˆVáû£©{ ÑÁäÕƒÉ'¤”·Fñ¸«ó”¬ M§Ò²Ô`ç¨ÄªÊ¬rí6™Z!D}(éºÍ£[+ÑÖ¸vn¦VˆV!+ÑõäU°ÅeØMë*í( ÃlàíX^¹£³5¥°ÈÃJyX Ñ<ú\+ÁJr S«lûÎÕ±õOƪ» Ññä5$P—a×®JÝKiÀ«O{¸u/ÎÊÌ¢â"ÁJ™j«ÅÀ^À|·<@ä•9Ëwç óTnqï1â¦1·NÝ”ïGt6þ­NX·~ÊQ…h?ݘt]´]›“©¢QÄ*¼?88×­O¬öþ ûíŸ<8» ÑqäU°Ú=kDEþx/ð_D¥Îû°Qô5ÀÆ`ßÀ-À À bÛDþ˜üX„‰‘ÃJPoN{‘Hå>àSY!DxO±ë{<—g9&Ê>ê–Ç0¡ 캿&ØwØêæg`^'CnpËáÔ‡…F4’n¥Þ[ã˜h°&X^㦑„å‰`9>(Sz±ûñ`!æ ç—ç»u €½ÝünÀ?3±´³ie«^ì·œWs™êy÷vóvLò "*„²;üù4At>ùsG}!jãlL°:†H°ZäÚGƒýNÀÎÙs±ˆ–?û ÑqäU°º7kDE.ë7ŸÁ*:ú0•±}ß‚ušV"±ªlÀ„ÈÅÀ”>d !º1àÏXGz+–ÏîoÀnòó­ ‹ZqA+> '¬ë¶uS3ˆ X~Þ?´OÄ–ýöñ`yCp¼0„ÒÓËTωYDy<Ó˜*@- ú¼óݱjá©H°jÕæ°ê£ôwœŸ0mKô{Ï'_‰·ã¢–´BQk"Øg$XŽï£¼¨åñßM­ç·È?> îï‚ vúg+ïa5{ö Ä-_Ü …È€¼ V"ÿ\, < ¸šhä;¬fosó_Cä!à Àbë¿Û~S„9e+ð|`'l@iS6lÁ±FD±9”zoõc¢À¼`yž›†–û‚åa·ŽòÌ:ì¡g°Ìµ+ƒuË±ßø1±àÙ˜Ùñx«c±ÄɡЊOõ„x­Å~Ó•Ø9³Ž©¡°qoG°s<)§Ö6 §ûïûó©è|òçN›$¼¾V¶PYÔ'ƒÃÐáp¾Zák&æiþxÌã0oÌÀríü‡[7+;sD“~‚ý¦Ç`Û»mK]{$ö¼õ',-Ë“÷Ïi«¥B´ V¢^¶b¢Æ‡1eÿjàFÌÕ<åyÖ¾¸´½&¶„iXçåo®½ë(…-#Dɵß%B\\| µø.ÖÙØ‡ÝtönÚV1×ÙuŠ[Þ•ÌB”gv/20Qfy“ްü¼hï‹-ûíýÁr(J„!”žµ”zaÝ7âù&‰„ /@­$¦V¸cUâP×^ƒBºZ…ÿíqSÚ~áïéÓpòB£_Γ'R\Ôò‚V(jõû Ëñ}æU¡n„ $ Y«cËŊμ;Ú]üa.Ö7óÓnÁüÎX;äá6Ú&ZÇÙ˜`u4ðq"+ß:ѵ_Çž#ÎŽ@¿¿è`’âÒ…¨–Ý»±‘ºíI¾‘_ŽÌ¿ørûLk =X.§‹€W`£Ïç’.X­Â¾§ÕnŸ Ìå÷›Øw³vszšÛÿ:àÀ©nº¢ÉŸa:ðFà4LHø¶y_“ßK”òŸØwþQTES‘OŽÄ¬ u¡§Ò¯€_G%>]Žý¿ýtO°ÿbà.à5ÀO—6Év°Ê7c¿×$–ÿ%M<¾Hçì{ÏcxB¼ »N}/kC„hs€í°J§û/Âú[ÇcƒJŸ¾õ]}_i &ëÚ1­Ç¼ù/¾¼ ‹ZØ›©‰ôEgñAì?ð¢ÿØuy¼ß‡=„Ïå ¢Qhò؇¹½>’µ!¢*Ξ‹ÅZŸÛv"6Rð=’s#뀃€«€°ŽE›†ÝDfRz^†%O<û޼œ 9fa|‚ýI¬ÐÕô,B¬Š‰O(í…ÿ*`뙉Uù{˜è7¸ˇ%òÃ×`%„È7»bש{³6Dˆ6³–`}-¥¹&wuë× ”¢½œAi"þ÷fk`©`öÀœž„EsHD-%I×}(TRBj‘_¾ |ÆÍ¿Ðµ_Ånò¢uÌþ@”'ìÏXu‘+³2H”å'Àß±0Q!„È#«\»M¦VÑ~ŽÃ¼Ï£4µÃ?˰P¬£°¾­(;bK›°ÊzñdøyåXuHO¼¸U3˜lL;óÛó‹°”&I)K¶`…  ¦‡Ýº‡bí¦„× ‘Ê´¬ H`6ð`#°$c[Dõl‡]œ¼Ê¾ó–[QfÿA ?˜°ß|›6b¡m›éŒ ƒ­äwØwý!,QîdúîB!DY6cÞÑ3Ñ “èfc”·ž\Û~8ðS,·ÕÚkšh0áçå Û|¥Æµ˜ˆ5 *ãDÏ[ÕXßz»&މ_þÙ%~,ÿ,?V­ø>ýÕÀ³ƒõs>ìjÈÍûiÈ­ïÃ<¢±ç¬¾„×, ¶h˜5˜ µÌ½~!vÞT«),uû?;Ÿ„¨H+‘_fc¹A’§c£`qÖ7cÄØ¾ƒu¼·¿!ø¿YŒ–Ù¶»±¤mó7›uØfŒüwΧ1UZ„Šol¿9B!:ŒåØC̶”t¢“x56àWNš‰¥¹ØdAKä— €W¸ù1‘e7j„L¸ ů͘°Š_›1[wwÓ£˜Ý^|jVÊ_XÀOóób"U’gÚLL„Ú{&ñíö±uÛEwÅ8!Ê’ÇÀn§ø,vó\%Ü[AtÁX[¿ÌÍOÔðÓ±P²¤i[¬£_?ˆ](«å±n*Ç(vqöÓ&ˆÍsÓl÷ž3];ÛMÃ5ØP~%³z±œµø½ÓØ€7ƶ=Ú~s„Bt(«±~À0¬Dwp‚kÏLØæCÉÎÆúa' ÁªHÌuí‹°Ðη½{ÆéÃ~ï~¢ç騵p6?ë‹Ïrûûg—ø±ü³LüX>Z­a×ñÁþuØsÊ8öÜ2L#ný&€bÏY ¯YAãƒõ›±¿‡*ì7 KsYÂç¢,ò°ÊÿÝke=‘ˆå®Qìš$LÕƒwm%Yp^ƒÅûŸCtA‹í[OˆŸ¿!ø¿Y –Ù6»)¤mó7›¹Ø÷”6bñÒ¸VѼ8ÙÍoÂFZÌÀ!„ÏŸý§×el‹­fw,*`°˜(Õ×°")'ºù±bÜ~«Ûn©¨‡«1¯¸ÉGÑ!"áÊ‹_31a+¿fb‚ÒjJÅ(/>å="¤û· ðZQò°Ê^qþp.&.-Àb„ý¼÷€ò‰ðæc¸ÅnªÄVLÜJš–yp…Ó(v“®Äû«Ø§6º©Õ?‚âŬ€«°ï¿ÌŽ>йÐüËS%±ª˜<8 ø=ö[ !Dñ쪈&ºã°¾_<Ùº\píýÀ%À¡À›€/¶Ë@Ñþ÷ËÔŠÿ[•ºWçâ…6i¢jòøgù7ì¦ñàˆŒmÉ/XÝ ÜVÃëæ Z>¬o1J¦D2“D¢Ø2l4m#vÛCu¢]3ø6²ðGà}Ƚè<«ö² VBˆüâï­Á"àÚoÆÖû¼ a:Œ31Áj—Û$š‡ªÏç ŸÂf^¦VˆB‘GÁªØ KúÖÔ;°«ª¨ÊŠÍg%Qi×v}¿_Â’~‰‚O–¯Pl!Džñ#ÿµæX¢ˆ<×MŠ­_çÚž`Ý%ÀÁÀoÛ`—h¬„(8y¬ºïaUOž'ÑÅ«íhŸ`õw,Î[t^„HÝK!²E‚•è&&+Ö{O𞽉UEC‚U¾ØìÚY™Z! Åô¬ S`•?|•¤z“ÕWâXà™-:¶BQ "J1;S+D£è¹*_¬qmoê^BäÑÃÊ—]—ºWçâ/¬yI(,—À¢&w>–3áp,WÖãéÞÿ½Bˆ|à=ô@!ºÿ`Çg%QÝþL)DGÇ‹°ÉؘºWçâ]5–»vAyð=¬ á(ðtCítä-„( _Â*uŸ›µ¢!˜?6¹vNê^Bä9$°[ê亚?|罿 ÇšüðL¬ºx"êu×'?ÌÚ!„HÁžÌMÝK!ò¯D·&u¯æðg`+Ö·÷¼ Ë‘vzǤúªñiûÖrœVâÅÞԽ„È9½X•ÀnLö9» ÈÓ&_œý.ïj±Þ뎵 ø00£ ÇB!šÅK±ûÔÅY"„ °/íjNvïõŽ`ÝnÝjl)ðvàDàÁöc€•DÂÒ$–7¿œ©ù¥“ž«ÒŽñ ›tË*sœñØ1&±çÂvä–`%j"ÏI×»‘-®mÇïò ðGÌõ;Ì{¨øq°Ï£À'° ðe˜¢ÿÊ`ûÀ#À;SÞ§Ò1öîsï{0pJ™ã|x6bð1`?੟°yø|j«bë^¬þƒè÷› |»A~Ø« 6Šüó8à“²6D!Rð Šç¥î%Dgã«•ÏNÝKäß/oW¾Øó1Ñç˜'Ôí” ÿŸÇ¡#€÷¹u3ƒíKÝëÓ¨t Ï´ ǙľŸC±ç¯ƒÓžÜRë]«Â¢Ð܃ !ÝÆ®´w4G#Õ±Ô½ßÂ`ÝžMG럄ݠ&± òû0,Œvô !D= cתø ݄dmˆ¨›,òñýè9å#±m«€ àx,Âe8Õm+— %¾¾Ò1&1GJ!ßwë~Šåè=Åmï«îc6Äýî½wjÃ{ Ñ2ÖbänÝÂ>÷ê6½ß˜º~-vñ‹_(WcÀk‰ªÛê¶U{a­t /XùÏ^N°àrÓÁ$*4›MÎÿ^³°Ž“À9nÝtLœò‰oÇÄ+!<aÿßfmˆB¤Ðƒ* 1;6dmˆ¨›ý°ßð–6¾ç‰DÂQ<Ââ͘ú&*Õó\UéwçbÏ#—Q>ÕJ?–/kö|õ &bµÃ@‚•¨›i˜woÁDŒp”áÝXrð›‰ÔÛ…Xä[±dO öß7Ö¦±û#o[¥íÄì³·Ë+G#é ¸÷ ÖîÖ݃]ðÀ<¾¶bá€rsqTyKQ¶b׫Ja%Bt2¾Ÿ*ЉÏCvOÖ†ˆþŽý.ËÚQ,z°joÀr ½Þ­O¬Vanã‰?^àæ÷£4yøuÀó1Ñâùì¹û#ïRñ†ŸÛ¦ÒŽMB#éÄÃ4ŸíÞ3S“°Ÿ†…} ‘„+!DQPR\!l°RçAqYˆý~K³6D”p ö»ì—µ!¢X\ ¼¸øP°>M°úV!î(¢’¯Ë›‚)T´cY¯Á¼d^šbO7ÿ‘ÿ}öݳ6Dðì÷ø+¶xŸ[þxÊk„HâyH°Bƒ‡±ëÕöY"D†,ÇÒ”ÌÏÚQó°ëXüyVdK7?ç‹:ð9y®Ãò«\……6Í!ŠÙžF”¿'ÌtæMu–Xû ,Œíìâg_,˜gVš{¦¿°ô§ìÓ©xQp(S+„'¬øU`g,äu[¢s£Õî':ýܘº—Bd¯âÔ“º—M7¦&é$ÖåDV_{óWb®™—SZm-‰Ë°‹ÊÁU[ß9øÏ~PÖ†À<}2x_ùo³›?*¶ït¬zà‘Àg0ÏÅe˜g;o^µ!BQ€ !:…7ç«°ßäÀ¬ Ť7ÖŠåî¹ø(¥!×` ×oÃò QbÒnÇr•;v‹°ÂvT*Èçc'±jóÁ±Øï±‘(××Và À3€·`žW×ãÁ>áôïm·Z!„¨Ÿk±ûW.kCï£T|Zƒ…³n$Yœºø&ð¾Ý …Bßa÷´TÚQ!rÌØµlϬ ÿâì7IËg-Ä¿˜YyÑf”Ã*_¼6¶Ü‹•ÉÝŒyú77Pê…(„BŸbn¦V‘=s0ÏúM•v¹ÄçEV¾¤ü°ÅµÓ3µB Vùcµk‡3µBxuV¾… S7aá°ëS^'„B•Q×*ï‹èf.ÄÒ£¼ ¸(c[D}¬qí¼L­!3\»%u/!¬ò‡:‰ùb̵Çggiˆ(<Çc ŒÏÂDO!„È+ò°Âr“ dj…hß×o˜¼SÆêÔ½„päÕïxàAà´¬ É…æ ƒKÝKˆÊœìšµ!BQïAÜ“©BdËF×ÎÎÔ Ñ^tìÏÔ "ÁJÔD^=¬fdmHt“‡Õ<`;¬c¼‹3Ï[Žÿ;Œ¦î%Deô_B=ä …“USá\ä…æ V¢&ò*XùŽR7ˆ6q:I°ÚØ Ø9hýüNÀü2¯[%5Ç„¬õn~æ¶ ·ü¶1·ÿ6¶X lÀ¾ÏÍ®ÝàÖ¯!5«„<¬D³ð^“JÌ/„B䟵®•ØQ\”t=¨?,j"¯‚•W\gjE6ø“÷`9&”xñe5‘ 3îÖ 7#nÝìí¿Ý‹6þâÝ(ýÀîXÕ¼pÚ¦*…¬–b#Ws0qh‘ò¾m“ì,Ç&f1ÕËË c{¹}å#EVBˆ¢àsW­KÝKˆÎF!ÅGÞ¢ù¢˜E>£jDNÉ«`õ€kfjE6ø‡ÙéXHd«Â"'°ñ‘X3† ZãD^H«‰<“ú)¦* JK±ßòþ õó+^3{°ŸhÍsóƒØn·z‰.zC®íÇD²¹Á¶aìÞïÖ÷¸ù™D 5ß±<¬D£xÁJ#JBˆ¼ãЫõF¢Ãªóú¼$*ÓI‘+€ÂEÍäõ|¿kwÊÔŠlðÖqౘ`Ó‡‰4ƒD¢Ë [×çö™ƒ 0^°ñÂN?‘è3L©Ø°M¶Ž÷÷Ʀû°ßp}ÙW–g+í¹ˆM#ºú(õòšE$šåÖË+F4ŠHõ_Bäy„ Ÿv“(.¬ò…î\•©¢PäU°ü†°?v7ý©Ç€ILhZææ[Á&È ‰5ÃDžMýDŽÉÆ)¦–·È¶v0I$Œ-+³Ï àÿ€-DI…¨‡À[0qtsƶ!D%äa%„è|„Ä@ê^¢]ÈÃJÔL^+€§PŸ—NÑÅĺAZ>ä/à+ZtüN@#Ì¢Yl¾“µBQ%Êa%„èäa•/”p]ÔÌô¬ HáŸt§XÑI<”º—h5ª(„¢ñ ŠÇS÷Bˆ|#«|!+Q3y¬ºäƒ^×®MÝK!„è,(D”CV=Så V¢f$Xå]\óÁ„kûR÷B!:‹y®UþFÑÍœƒE{¼*kCDÝè™*_,víC™Z! E«Ù•wé8|(dO¦VdOüž\«Ñ5Ñ(ßÞµ!BQ¬„7H' À|±‡kÿ‘©¢Pä9éú+€»§glK»ñÅNEóÕ‡±ŠÕ´~Þÿ?·`7œq,ì6b²ÖM#X§z­ÛgÌí·Æmóûº×®%òžJC¡hOÀªnÆ*O !DžQH ¬:_}}˜F몯‹êØÝµ÷dj…(y¬V`7в6$öqí±›ävÁÅ™ 7¸e¿nÜ­›Ö5«ÂÝL`>&$ÍÍol,‡âÓ¼¤ƒUÉ8ÖiE¯f2F$f­æCq ¢ä³BÔ‹*N !Š„’® a}Z€•™Z!¥Õ×Ee¦»a¢á½Û" Dž«G€MÀv˜`±!}÷ŽÂ{ôlï¦F'¾O IDAT£TÄò"˜_çŰÙL£æ ¨ß•vƒ{¿ÕÀª*[?¿Ùc†{ÿ~,ú<ìÆÓ‹•ÞrëzÝ>n¾¹ü~ƒX>ª^×Tù¹¦£QѬ„Eb®k×ëü}p»‡ê¾(:?P*‘£ØŒaÏ è·Ì’ÅØ½e ¥÷!Rɳ`µûCï ìHwźŽa'ô¿aai}˜3Ì÷aÁ±uîõë¨^˜Ic36´ʵáü*`y°ŠOÍȱ…HÈj&ƒDÂÖP0Šcÿƒ ˆHlõ3äZu”„E )$ðTàV78ðàÃm´Iˆv2 ÝDui$D~Qâõ|°Àµ ±5‘gÁ à~L°Úî¬|Ý]4Çß{…ÂW°nÀ­ÛÀT1j%ž96å¨Åg”Ê"ÔG1Áª“ÿ‹æ!+!DQ(w½úpÖGy>ðŽv%D›éÃú¿›²6D4Ì´X+²áQ×n—©B4™ocîæoÍÚ63‰ÜìóÂýØoѹÔDó¸û’µ!BQ…ØõjilýÇ€×`Þ&ÿ|¼Ív !D=Œb×´¡J;Š–2€·³2¶Eˆ¦ñÌýCYÒFú±‹j'z4‘;°ßcï¬ …æ…ÀñXü¾Bä™°ûÞý±õÞ;a"¶,„ye=W則°ßã1Y"ŠCÞC¿ |žîrÇõá€k3µBx|â÷¼Ÿ+"ßüÎMB‘wÊUôžß}±e!„È+;ºö û-ÄD”Ç`ž<³ˆ®yž~¦>¬ÅòöúŠík‚åQ7¿¾FÛ;‘‡Ü´$c[DAÈûCx7Š6½®mF²rÑ8JÔ(„¢›HJ¸.„Eħôè^ê–ãÚÝüŽDÚ[ÉÌÓËWo_,{k”¨ª{¸ì·Uz/âsò ×*â@TMÞ«nÄ{XI°ÊÞ»O±ÖB!ºõC„°P²i¨¢YÑñ•év.NÙoæñó ¥žPIU"lj"0<½ØµÓ·š,ºù¬šûp­¢ [1A+.€‰ZØØoŸÀD¯ñ`»ÍÖ&Ùµ-V0m7¢âi¾Ý%ØGˆª`•?¼'b­ó½èMÝK!„è $X aU0ON>’±-¢~.„˜éXœ%nº?˜X×[f`•Ù°0ÃyÁ²¸‰*¹‡Ë~ûQ•÷^š+€­Ã,Ö8ÁTOÛx8d¥ƒú ™B²¸øY£ÆŠî¡(‚Õ"ì„Y•µ!m@!ùÂßÀæfj…(2ƒÀ×G€÷fl‹BTB!BDåäaUlV`BQØ‚ýŸšõŸšŽõ1ãØ ‘¨Õ‡ Z~{ößî¶û×ÌuS£ÞO+÷­Ÿ€îÊM-š@«3€wリ±-í@I×ó…ï°ÏNÝKˆò,^‡Ý¨%X !òN¹¤ëBtÞkE‚•È+[i®6°|XcSŸâá” P+‰òÿ ÑŠ XýÓµ{fjEûˆ{Xí<xŠkw öõñõ0µŠÅ<삲së\ç&?¿»À…óëÝr8ízºKLó¡™™Z!ŠŒóÕ \Q¼Gq;Bd„È+¬D·áŸ÷–gmˆ!E¬îvíc3µ¢}xaä(à…X8d½Ìưm5*P¼Å.p~~x¦›ÿ9Qu‹ ” f#nO¸ìGtçbâèc]ûV·¾çŠÈ' +Èž^ìšÓ¬„¢Bt2 "ªb¶2S+„¢Ë)ÂCø?\»G¦V´û‚ùE˜Ê}p£kï ¶`âL­b±û}{°f—ìç}¥Šp¾Ç-‡óñ×ö`CTf1°OûÅ©EÜJÚ×÷:+·ÜO$RíDäµr[ŸCˆcuz+3‡(‰hxÝéu“ÏÓ0;oû‰\Ø}þ} çC|%œx믧µ¶^ˆ¬Ô Qä*„%x8S+„¢Ë)‚`µÄµx‰K€—c‰ôn úüõ°ózjE²úP¼Š?(žIôþkàf,Dq¥‚Ù[ç“Ρ´F+<Ã*±¸ óì» ¸¸ …¨_R¹E#¦ŸÀþÿËê8ÆL,aç¬Ã¿›ßÖ-/$J:€]|%œé™_–µØ5Ç'(…æUÕ©–PÀªE»øq›mÝÍ×nÈÔŠúÆúD}ŠÍLõàŽçb©¶|½èÖ`}‡²6D!º™"Vk1¯?ÚÞ 9.ÌÚ€*)³þ” M»‡y‚UC­âV|_O¼óYnyœH¤z F[…¨„¬êÚI/¥ÕI·×¹iÛ¶óºð¢Ô¶Ø`‚`y~6lÀ®ù>¿žÏ·ÖMcØ9»;oljÊ0‡¡É#±ù/ŒÅ[Ÿ°ÖÖ _µ¶µ°øE¯¢ü}t}ê^ÍÅŸ‹¾z•›|òßÁØú·~ a}+IµÆ°ê_ñ}6`ײUZ?¯ÊYùb߬ BQ Á ,,pÖéÁªÈÌoÆÂì^ü®†c¬qS4¯Õø0§v‡ê…‚Uµ‚اÝkº%‡£ÈµT ô}³$Q©û¿'‰JÃÁ>D’›ÁjgûQꀙDŸËÓOi?8ž6 Ü>qѹYÞšTµ|Î+tS!DÇRÁj¿¬ U±-p8öÀ:3hO 6ÁJˆNâ7å/X]ü;æeñàqDa{³±‡R/J-%¤–£\]å¨'§Õ ,D3‹ðèNã+X¸ýƒXˆÏØGí#Xèý·®ÛÏÄ—pÚÝm;Ø5X?H©”Ï!× |>¹JŦq·n4¶~Ì­KXßJ’D­ì\ïÓƒ»ÃA;œ°Î·}n «AWƒ/m_É“ë÷D•·…BˆBPÁJƒÿÀh/ÅBõ6¯ÄòØ,ÍÎ4!DüCÒƒ®]\ê&‘ ^ü“`Õ'ºùJbÀ£˜põ‘ˆåE®‡Ü|<$,kf0UtŠ‹$妸×QÈaU¼÷&Ê‹Jã˜P’$*­ö£49ÏlbªèÜ,oMïyVNÔ*'z `¡Ø•±—;SÜÜdB!º V¢YL# üæ™ÑO”Dþ¢ð!DþðVdj…ñÂíNßiêÚoßÇþëÛ»v1°&d- ¦§¤oœRO­D‰¹×¥.ØB©ÇOöêsÂáÚP°)'@•›ÉÛ´™(ä¬Ò4J©”Ï!'šÃ˜›î¯ñu3¨ìÉõnlàp!Ñ „(Ïb¬_û(Jº/„™"ÁJ4‹ç{b¡K°Nm?p>&X|–öä¨BÔŽ÷<‘`•$X5Âu+pUÊ~Ó1±*±vpË;ºù±{Û>nÊ[H˜V•YNÕä©ùf QHv9]K$XUæ4àÍÀ[±Ê×B!2¢(‚U6º•—Í+oqíYXçix ö€p/°p0ð›L¬";ÎêսöVݪ•í\«ÐÝü Áª9øèæ–‰uBdÇlì¿_„DÎÿ‡ÙzTÖ†ˆ±;ö›Ü“µ!çØ÷øú¬ "#~‰¯ÈÚ‚p3ö}¥… !„hÓ+ï’ |âÙm3µB”ãhìÁü"Ws/\=|{`9Ѩ•Ý€O–½*S+ªc…kdj…ñ^JºÞÞË)oÉÒ…hòÖ¬ yX !DN(Š`åãòõ •?Âdë>Î6^´ /Z ü ™xs» "C|Õ¦•©{å/ªIÉ ãi=®ÍsH®­dĵº–T¦{ÞØ,ËØ!„èzŠ"XéA*¿<K¶¾ó°Ëí1™òIÖ¿éÚã(ÎÿNˆFñ^¡iÉpó‚<¬ò‡Ï+öh¦V/X­ÍÔ !²C‚UõÌqí,,,P!D†E8uíüÔ½DïZŸl’«]Üå¶½´=¦ ‘9Eò°’`•?ïÚÛ2µ¢ø ºVñD·"ÁªzFû[²6D!Dq«®Ý©"Î,ÌÃj &Xy’¼J&oaWû¶Å:!²§H9¬¼¨ÖªC€?ë0áà×´.¡í$SEžS··èýZ…¿VÞÞâ÷9¥ÅÇÏ…ŠnG‚Umì<1k#„B‡=±*Igmˆ˜BðâØºã±Æ3cë·!ò¢Øxð´¬ ©‚'cçí -8öK1±úNà­À{1A{Ø«ï—$X%­ ™Õ;åÛ˜ÝÇVدÑdâi¯Ÿ‘²­(<€};UÚQTäÏØ¹Pyöýžî–+k¢ý†ý.¿ÈÚ!„Bˆ¬9ë}"kC„U³;o—´àØ×`¹»ë^A©°È/ƒåÅÛŒy¬}•HLÙ+C>|¼Ì±Â)\7Vò}s°~ø>æ•°¸ÒôJÛ›ÅÎÆçUدÁjWà¯ÀÀg‰<.Nüdo~ïSÀ_€ ±£[1Aó©ÁqÓ¶å…ì{”wI㜌}—ïÖ]àÖ=Á- ¼¤Ív‰tž‡ýFWfk†B!Dö|ë½7kC„UÓƒ·ëZp쵘—lH¯{¿ëÜr%Áê¥XH߉ÀynûánÛ%DßKxí‘n݃n>|/š…á‚?t뿼ËgrE Û›Åî}ö®°_5‚ÕDžZÇRú;Ç_¿8*Xþ&\ì\_å¶¼°û{*í(*²#&>ÿÞ-÷aÿ¥¿ûÄÏ¿4÷nÿÇÏpótÛV`b·hœ'aßíMY"„B‘5gc£c²6DQk°s··ÉÇM¬¼ÀQ‡ÕJJ=¥>ì¶­ÁÄ(°ÛÕ†Nb¡‰ñ|Žã±÷™ÄÒgU¹½Y<ꎽ]¶/`Ÿ7yHÝ|©Ì±Vý®=”VÌ‹ Vë(w–Ç¿‰Òß2m[˜Nôûˆæp öŸÛŽH þx°=~®¥ ¼/qÛŽÞC¶¶—›ÿT‹>C·± ö}Þ—­…a°ªN.„™33kDG2ìÚÕ™Z!„¨•qLÔè§TÐh”›€§c9„|õPï•s«k'‰Bü’¿Þm?óòøÉ÷°i5Ú¶”©bÆ$ö@þ `cpÜÉ*·7‹´*“ï æ'0Šf±…ÒåÓHþO¤mËý®ËÔŠÎâÇØo~8Ñy|^Êþ/sí;ƒu;bïµØÿí™Xßá7nþ™n¿«šcr×ãC€‡S÷žÏoƼQÏÎØ!„èjŠR%°Ø X”µ!¢*ŠTMˆVröp· kCªdܵý©{ÕÎ'0Açb¬(Ã{°‡€ ˜§˜µ;ðzàŒ„cLb÷¬!L( ùð,$ð‹elX l%Á¯Ä˜8v,º´¿³³§ÊíÍ`>&È­¤4¿V5ì„å¬ ¹ûÀ¼bBao#07åx¿N–÷®r[+yVv~+&^”ÙÏ{•µ"Ôµ[9ûÞ߈…êÞŽ…¯–à ¼‡»éÅný–΋T_ÆD•7¸÷PH`sÁ¾Ï:£B«QUE!„5q(Ö±¹0kCDU\…ý^fmˆãCØ’<†òÈ ˜½OnÁ±_ü&1æ…ÁöWË€Qà˜Vôfì!⢼7§ºm{`âÅ,¡xRøßÀ&,ÌÎS®šY?–×j™³óA,Ϭ*·7ƒ=}wV±ï&@Œ%Nߌ%“ß>Øg7ì÷'] »º›Ò¤ë! ±ûïínúD•ÛZÁ6À7±‡ï0$óaL@‰{Øíê¶ÿ³Åvu úî?Û?¯¾ïÖý«(xŠÛÞç¶Ÿýž›ÝºëÜþ7¶Èön¥h÷£,ù0ö]ýwÖ†!D·£@Ñ ¼—Æ`¦V‘-3°‡ë-ÇÛ°UV`¢†t8øNô;·î'nò¼?öú³) ÍøP0ÿ¢ eÿ™ðþïvSH¹ðÁqgã unoÕzªbßÅß1‘ Ìsí˜wÀ#nݽÀS‚×Ìÿ—›<}”²Œ(¬+NÚ¶f2 ¤>l›°}{Ì£ñ¬‚;ügOx¨Ÿó€g»ùWØ÷혇㫀WbÿÉ+±ÿ)Ø ×»0kûï> …6›ìº2Dr˜±ˆ‡•Ícë«„í·-¦<“©}йLõ`ï§TǘÆÔóuÖX‹Ý[&°t5Á4ŠyúúåqìüŸ Jù 2¦(‚•µ®5,Bdƒ?Ággj…ÙâG±WÑüÜF­¢•‚UÈ™Xg`w,DEù…¦â­/-³ý ˜p‘(söÝž…%CïöÆÎçT±ï³°Dþgbž?óÜú¼æØ**_uSq!¸’ÀûÓØkÞå&Ñ\$ÂT¾+!ŒY”ŠMÃLŸÒÚ"çÍÛˆõUG°û˜µÆ0¡+¾ªÂDE°ò9(Ödj…¨ÿ;¥åD¢Óñ2EÉum;¼#І÷(2I ×ga^*'‰7“À¥À×0¶-U¿bs±Ðœ÷QÛÈ LÌ{-ð=·n}ùÝ…è $ÂT¾+‘÷þæÚ;°ïÐw„è;z7vÝ]\| ÿ.VdapðW·ÿ¾X¹oE{Øðò¢R%j^òajbû¯„­¿'Ç tmfª¸³Ž©÷ðqJY&)MuöŸ›Àt„>7 ó}Øg–û±ÏÞ‡õ;¶¡yÕB½ç–Ÿ¼¶ ëë­píÊ`ŸßÔ$šÍö˜wÿžXzßÞ‡U÷^ÑŒ7)Š`%Š…OnÛŒ‹œEŇ-I°j—‡•¨L(Xmy§O”—j ¡ü vÍ]‹uBGÜTô*­/¾‚åÞª—À{ݼú;¢Ûñׄ"{<´‹G±üå<\;à;ÀEX®È_^Xࣘ×ôj"QëKX÷åÀ~XXÿÓܶïpÇ?¸¢I¶‹òìDíâàf"¡)œ’D¨¤¶È}‘ÙØyë{QkºBá«Z!¬^|ŒRA+IÔŠ‹^ÍòêÚ¢ËTq*žBÂóxà·XnÕ†=þ‹ÒóMª9ÙâC›™€Xˆ¢á«–mHÝ+_ødÛån@¢}ø½c1ÑÅWõ]‡]cûݶcSŽï`†Ó¬#¹»ÆÜü8æ%»1a{¹{ðt¢Î›Étó½Xmž›ïÇÎ ¿mصsƒc ÐÜŠ‹`¡‚‹‡š|\!Š‚¼†ªç/Àã²6"C~ |óp½8·Š×\ƒy´ž‡…ù‚V ¿Çp0ìU˜Hõ!à$ìšICV‹J<6˜?“òT8/ÂÒMlÄ„ få¡õž[~òBØ6Ø å×ÎÖùù7Õ2ˆçíE.ï•6ƒè9¥—(§Øæ]9ë·%å Y命¸ËÍŸç¶=¸Œ&ˆVE¬¶` cïËØQ>M¹2ãBt7`NÎÚ‡U~ðBgÜ}®›¶bÉ«ƼzˆFùf>œzW}/h af‹K­¢Hç¢ÍF‚•¨–뀃°Â`³þž4 ¿šIésäaX!•#°P ƒ°ÁŒHÎ!¸/Q˜Ó0pOS?HÂÿ†WoÍÒ.Å‹ƒõ0@© •$jÅE¯~`‘›a ¢¼ ŠSIbž¬þ†‰V¿Å®u‹VE¬Î¥:u_äïa5'u/!:›Gˆ*´ Vù!)çÒlûlàGTöÞó O‡¦9nÛLtpóýD£mñí3ݺxHÑV¢‘X_gÔͯÅ:hk0ï°1ì¶Ö­ ö ñLà ,ßF#lpŸáLŠSü@ˆV ÁJTˇ±|Só°pÀ1«°¾ë0ajFðšÅ˜7Å_°‡Y°*À'_pË>'À)Àó±{Ù‘XÅYÑZüóYQ™DĘ›î­á5>W(ryíg ‘ƒÉZ¢þäÖWÚˆõÑ’rƒUËó±pà'`ׂÒYDÁù öÿTÖ†!jâ8ìÜýfÖ†ˆ]G? ‚…Xltë&±Ð¶Óh,ÇS­xÁj±{ßmhmÇw&p2š8YÇt>ðI7ÿÙÚ)D8 ;¾’µ!¢0ôÆÚC±æë±œUáƒì5ÀÍÀmÀ›Ýº…X1ÛÝô‰”c‹Ö³v ¸%kCDGãû``¯o'úßm[îEB´›wbÌ/gmˆ¢&ŽÄÎÝfmˆà½Øoñ¿Áº…À'êlÁFµ_Kçzµ.ÎÂ>k5BÕÝÀ‹Ýk?æÖÖ^“…ÈGaç‚"ªcWÌ›HÞ(¢Sxv ¸3kCDG V`}8/ZÝJ¢UQBE±ð1éJº.D±PH`møäâåÊA‡!xƒØ=×»^ûrË`£Ôë±rßÄ\¦}ιÁû-Ã<…þx6ðàÕXn€ƒ°„šçߦrE§"± K.ÿ ¬êÔ3Êì·óHû,QH¥ØLÊ¡"ZÃ<ì{Äþ¿=˜g^[ró¡gE¹°ƒð< I*sSK×zìQ,ĵñÕºX¿Æª`íEâ&D‘ñ÷A‰°¢,# |Ò¼g}n}'[ñ•1G°kÜ:ìšççGbókÝü(¥¹Ú&Üü¸›ßD¶(‡Umèû†ï“É¡@´›eXQ†Ë±‚ W¸åªr×E°úÖñ~+–8Uä%õÂì^†¹¿ŸL{(¤ª|RÇFD¯õ˜Ðä’ûÝ|¿[žƒ=PxOŽÐ{#þ@ÝìNšO./ ¶aÉèÍDbÙ,gÎæàénÚ3xÙeÞûãÀÑÀîX_žŠÝ+Ä’Û.iøæIà»XN¯`eºÏ~^fJòÆéVæ;cbTÒ´}Ò‹j <ß½'T¹óÝ–ØŽÛš”ãfÉçÆ v½ñBu5Çö"¶´›‰¯²9Ž}Øõnöbß‹Ÿ_GuÂYµÿm/Úö¥î%<¬ÚÇ –˜Ù'†^@Tíìn¦æÄÝKúîÅå17?ŽUÍ-R¿«øAIå Yà=­®ÀD«Ë©R´*Š`5ßµ+2µBTË:×vj>!ªÁ‡šéáÀ?@Þü‘(äÃ{z)…âá=nÞ{(•«*—¾š](šù福Ÿ ËF™*@­æ·´ÀƘËô»1Aªc8a¨Ê_Ýt²;ÆhÂë:…q,ŸW%¼`uðßX?böP³Ü-/uëüò£DÕsŠLVeñ ØÿáñX“°ÒôåX܇U»ón÷ÕCQ%|Hœpë;á{›Ktmëuóƒ”^ ç¹õØ÷<»æõ»ù>J¯‡iU6Ň<¦yzÝü¾ÉïÛéøʼܿŠÎlP¥øMlÛS±*bI\ÅTÁjW,ü;‰+±‡âç¸ãcçÆ"áüzÌ+9dglàÇ‹ìaÿàQ,±|ñáÎEyþÇr’ÃSE«¢üa¸ve¦Vˆjñ ~?ÖIÛK¸¶ÐÍÏsÛ}éL_Ê?ŽDú²šþ¡Rˆ¢àÅŒV…µOx lÆñj½ÂÐ<ïMµ•è!Ù{]Å;žÞÓ î­>Pg’S [°JK—‘.Xý9eÛZ¬ì¸ˆ¼v†±ûOµÞC°ŽÕr"qk9Ö©Z–°mMòaZÎt,é",×Î~X'p?¬’c’0µ¸£’¦GZhoðL«*íX#³±k¡ÏíÕKä:76?LõÂÙl* a=Ø5ÅÛ!*ã=¬~\‹ ·!§cÏ'#D}VßW=©{»í¾_ëÅNeG`à™®ý7ì?|S«‡€«±ç­À{YÎKÝKˆÖ²ó¬úÖWñáK˽ (‚ÕB×–ý H/ð\ì!k váó]Yßàz°‹í¶ØHí¶DbÔB¢2ëÒü~ÿPê=$üC¨Ï—3†=ð…b˜÷ªðb˜O´ÃÊ[ˆFHódÈþAfcê^Õ³;WWÚQLÁWX‰‡7‰Úð9Àþ‹¨¤ò"×.ÀîYÛËÛ¹u}ÀcÜT k±NØ£˜—Ör"ˆî?Ý“ 4!x<9øjLpó÷ÙíÛÝ{ˉâXòý[°‡ÄÛ° “SZÁG´‡njöõЇS¦yzÝMÔwLÊ‹'¦òà7Ÿt#–ç-‰_3µÿûë2û¯Á<ã!Ü?À„ï-çCå7abJ\h9ȵ¾ëûÃÞ+¸ÙÅ¢"7Ŷíˆyh†lÅ®AWb÷µÐž;±B"Õr7SE 4."´úÝüv.$uøµ bs)/ˆ%ï{ bs€3ªÿ( 3‰ýŸgÐ/q!6a©*fSþyb9ðB"Ñʇ&j=E¬|H`·xXÍÆÄž‹Ël÷]ïêí]ó½¸g –'°‹æšà5«)/Êô{”™CuáÓ]J?âÞ{vð¹%|‡Ëçuñ#‘Þcnl¾.Ú^ »x%U&ˆ]ïû Ïb IDAT¬) ±ÉZ‘¸¡âáCÇ1¡æa’8âÌÅ,/yAk{×úu‹Ür/F²sm¯†­XïQìu+öPx+p/¦º/JTÂʉ+"™ßböìüŠs Qqßwõ}Õ¤ÐØ»±¾¤ÏæóÎ#Y8x&V'ñ¦ VgcBQ;3U@ºÊÙ¾‘Rqk3pS= ÿˆ}¾øHH?¥ÂσXÁ%XÈÝŸÜë›íµX µƵ b¿$Yë-ó~b÷¢°pÈv2†ýî}tvÚ‘k‰<‰ÓÀ½hõ[,uœ"ZA°šŽ V“t`µ‘»ì5DÕxüÆ'm–h³R/®Øèm9ÖcáåØׇIxa*\ßìD·ÞýÝ'PÀÄ.Ÿhu(ÖúRòýD ßYˆ‹aåŽíŸ‚%ü¢‘`%: ý®5·Ò:ì¡"TŽ>"o-/hy(M²ïïIPšl<œ÷÷¯1¢ûé#؃ór,„Æß{5J.Dkðƒ­Iü ÆcTf}É!ÅG‰`^Üò¡mIåà¯Åž—|Ö‘,LìlSƦw&¬Ûè=aQ‘^¦z*=¾Ì±;•Z±[€÷cÿ¡¬¼¨‹äý/:—娵ñ2à‰Dá%ƒE¬æÁlí–N™wï¿ó´Šã;»CØižk‡ƒù¾`»Ÿ‰ªeù< ¾ŠÖJo\k1—ؤi ÙÚ®!òkƒÀ—€7‘\iHˆ$ÖbU³ÊmSÍ "k|5¤V‹°Þ“ùÞ¿¢³H I «…#kÜÿ©D‘ ~p×{‹% ü/$ªN©0ÿÆñyyÛOj”èù/)—ÍbÕ{ñy'œ½1Ì]ÂE¬Æ±2ßÝ„…/³½ÙyaæRêŵ ½Æ(ðO7¯òÆ¢Z6ayŠ„ï8IdëTüC9‘üSX•^láûXž&QŠ l–·ïÙVþ!º] Ђ÷Èú3 !ZÏ}5îG+Œèb|:–"<‹ Q b¢Ó©/lgìyä_Ï":IDqVdmHŽñ ±<¬D'ãï ›3µ"{ú°â»»»É//À®—ãD¹Ç‚åqìz1[7æöõ9+U=­äÅú_H ª†J@!EjZ!V‰ÎÅ? ÏÊÔ !Ø@ÓùXx`;ñ÷B=ψVñFà5¾f S ¡’­«|âsXTÓÁíA‚•è|‹×c%¨ã•4}ëËq¯ æÃ ›k‚×n$*îuåÍ^¢DÚ>ϯ ·=‘@µ]…ã4# ¯çs†“¯çmõã½+ „µðàEXÂóWyàî œ‹ýv£Ø£/ÉÃBO© àtàpì?y‘wæ®Xµ²éXއð˜iÇØ{0šæŽq<•½…kýŒOÎÂÊ×ÏÀR9¼K/ò?tM"{n^›Áû6£ZäaÀϱð­¿7áxµp ðÉ6¿g; ûEe’ú¢Àö¼âÿA,ÒC‚U>ñ‰ðš]‚VÔó.úEDˆ4|ÒõhmE)_yÓW&ò­/½=‚Ýð|»:¶‹•#¥Ûöe¬R×YnÛ—‚m•޹{öÇ {x±é àkÁ1çÔyŒ3€s°\ŒåŽÑÈg¼«”u:vžƒÄ*!„È>gZ#¬_‡Ý_^|¬a‹j£U‚Õ º'Wv^Ù×µ… ?ÞxÝ%¼{(;?kCÄ¿x!ö›ü6kCD¡ø–8µÝe‹ëåØÿüsXÕŽ—a×£7a^ïÂb?|{x=ø?àÇÀŘÇŸ0‹;1±g9–|q³;~VÓÌäÏXgë»î³¾xbUÉ»ªYÌ ]ì†ÝãžU!=û®ÿ×ÙÜîÑËNbHà,Ç€›Ü´%˜ÿR™ý× Ÿ=”†Ò¯JÙO¬Ú´–Ò0­pÛÊØ1CO±ð˜iÇíšCeo³z?ãlÌkàÏDÅcD~¨æ|Bt6Waפ¢^ÕЇõ£vfjÿäÝØ`ÇÍXì>õàF௘װg!Öo¼¸Ò¾òB,ÒíXØäóÓˆîÓ?OyÏ£ëÜ1nÀ¼=X>п`•éÒì!öºðššdOÚqwž—ÏRþúœö9*}Eá£Øóôø†"xX„ý'cËnÀw:U©+?ÈÃJÔÀçÝòŽ/¯|'­gÝ{ a¥¾Ž-¹ýƒv62è«¥úv=öð¼X̯pËËinXX£lÂþW•Ù¾vßÓ} ~|¸\Z÷ó˜xØ*¦a1ïùåÙJã`•ŽÑŽÊ¾ó±ï|&f©xƒBä ]®7_ã+ß`¢ÕrL|ñýÛbŽ&«ƒã§yì~ 4¼ëóœM$Ä|ø=6p: ëç]õ‹ÂûtÒ{†üœë“‹¨˜ÛLÌØçýQŠ=Õ’dOÚç¬Ö‹:ísTúŠÂ>®-¤‡Õ÷±ŽÖ›²6¤}æïfk†ØûMîÎÚQ(¾‚ýoÞ•µ!UòÌÞ×gmˆ`wì·¸'kC Œ¿nWû–+/²8ÆÍu"=ÛŽqËž;°P=°¼OëRÞ7a ytÊëÒŽq!æA˜ôÞIÔû/Ä®®}"_ÈÃJˆüЋy°Úæ÷½»Ôû¾bø`‰µ?lû~Q…Þ4ÝåD^Í7Qz¯^‰‰Tqâ÷º¤÷ y&ÝæÞ#̽.öš4{Òl¯©IöTúœå¼¨«ý•¾ƒ¢ðì¿ùì¬ ©‡ß`Æ¿8kCÚÈÛ°Ïüµ¬ ÿbö›,ÏÚQ(>Œýo>•µ!Ur>f﫳6D°ö[Ü_iGQ–ý°ï°Ú*LÕV˜Ëý˜[~Þ° p æö[öŠå@» ;¸ißÝÜû$… T+X=6°÷sT®\Ïg<ø©›Ÿ…¾ Âûˆö2;ªùŸ !ZËØùøp›ß×÷óêIø¾ &¨ÜÜ<<@”{yp0ðm"/ý4Á*k⬠:Á*é=Cþ<+xÿraõ•ì‰Ûà?óÌØq’ìI;nµ‚UÚç¨ô…FÃU3å/˜ñÿ–µ!mäDì35kCÄ¿ÐȤ¨‡7cÿ›ïdmH•4:ò&š‡«Æyö^›µ!3—¨s}86J+ºŸÏO‘-Y=W4âI<–·4ä÷Àsܼ/Ö3L4ÀŸæ±ûcJÃò÷æ„å»ù$ñ«ˆ •{Ï¥X¾-0°4ïæ4{BÒ<¦“ìI;nšuHÚç¨ô…²‚Õô©ûæŽm][iD°“ðŠòX¦Vˆ°t¸ÕòˆkgjEõxWâõ™Z! ú æ¦î%ÒÐÿÙx<Bp®÷ÎlÍBˆ®ÇW‚ow ?¬¶jrÈ눒{~êÖüKü}–³ ,Iú^˜Çî;)nNŠZÝî¦7ÛÞ‰%Z¿»=Å­?óšöv$½gȰP³ë1AgCÊçK³'äý˜ v=–¾!˜-·gmH›˜†}fßBs«bìk;•'S[HRÖÚ·ÃWgãŠ}‡fmH™OiN ?ÍÏÒ(Ñräa%D~Èâ|üš{Ï·µéý4Ð/ª¥ì3HÞ«ÎÆJ8ö“¯êN­Æ{X5ó$_‚)ÂûbyuÎÁJ¸Ç™EõÕŠ^Gs¦>† Baî®f¿G½øê€y°¥Õô`!la"â/¨\±¡ÖªßÁFNuÓM²=o܉=4ß—±Õ2ϵÿ‹ýæóˆ*ñ­%ªj·:˜×­¤;ÎQ |…ÅÙ™Z‘OVÒÚŠˆB!ҙƧy\µ_,rrnÑeä]°ÚaòÄWa"Õ‡0WÙ÷^ÆZÌˬ(lïÚý‰âóke+•…­µØ¹´ÁÍc×ûQÌ tÝÌk`;™ŽyHÍÂÜ9˜—p¯›÷"à 0ä–û±kç<,¹)”/q,*³ÔµÛej…B1•1¬ÐõÚÏ}Ô®¾E_å]ëúg­kIvªˆs ðÉzŒª‚ ª·=¤•6u:þÙŠ×aÞ«nÅç,ifÞX¥»ý°|:KbÛ÷À¦0·Å7°ja¾déû\[®²Ä×#1Ïš%˜ñ,Ùo? 5.¡ôøu˜8å=ÁÊQM{¿ÃD—aÿ·0þÓÀéX˜ì{0ÏÔ7¯!ËEíLÇ:ç[Èn€®ÑÐþSxm%à°zb#ï#ŠÃvÒuEˆî¤Ñ<ÉØýâÏXÿæåÀG°~Ü?—û¦å²Ý‹~¹$ ïWá}óhl ýw ŸZä4ìÞ~Qö±ˆ[±H›ƒãL`Ÿù/Ø{RîÝøçüŒ³ï¯ÎÞJŸ+nÓ9‚|¨Bï 0‡Ž´cUÚ6ý–×aß{'T÷¾ûoî—µ!ÝÆÿ³wæarTåþÿd™d’aYDPÙ•EPñªȪ.²øcTT¯pTDP¼²¹^D…”E„E‘UÈžÌ$“uæ÷Ç÷œ[§kª«·êªîé÷ó<õTuÕ©:§º««N}Ï»œEi°ó¥þLãj·Ï‰µc]Ì @fÝ Û“›×*X-C/™ïvu¼ƒèE!/ÁjnÊçzù”;V-ªùÖè&üÙ ê/‚q±ùÁÀ?Q†Š (}¸ÌF7ý¹ÀGܺi(رϊñ¥”cF#‘:Ø e‰ÙÝ[Þ‡“5ç¹ÀPã*à:$ ýuD×á3ÀßÝçû€t‡+{Û÷k¨ó)Ôá8 ]ׇ»zÞ‚:Û¹¶M%{ë*£q^F÷î*¤9Áa=f#û×âÂnq;¬ £³ñï¦_¯sÿ>à8·¼32‚8*øƼ½‰3 â`Û­À nùJÁÃçѤ`y'$”%•…4 yk°m%<õ,"ê³%¹þõ¥+í¼Â6í üÌ-߃ĥQhðöcU+m[Q¶ÁÝ©k¸ø º6ë IR[¡—‚v5ç”ç;(µx)»ë—ÙïÇ®Üѵ£’hSnûOݶ¯¸iÀ­+·ßܺŸF ü\¢ÁõnûOÐÍ.n­´‚=$¥ŽZÛ”•`åë:§B¹Qè¥öNd]æÅÊj^–Œ¡ÉÉhäé¸J ÃÈœÑ}x—J©N´©5#jxÌJ#©^à¹Ò' ×;b„o[­õÔ2Â^iÛȬ £³9Ý®¨T° +(MdÒ‡Þw=ásg>¥YhÃð ‰­ÇR>$|nîçºcô”)¡Âc.ˆµ3 4;¸Ž!9}_ʱÒÎ+lÓhôLìFï…ßB"ÖDØÓŽ•¶m‘ Ëw¢ksÿ¢R+_B ÿLÑ É€ÍÐèÿKDÂÕ ¨ú-±²7ЂÕ$õºégD)¯},£ÐÝn²N˜‡ºˆ5*Øþ=t“ZÃ`W½oºõ¯ëâmKkSRù¬««Ü±N*³}+djú ÑïÛƒ\Ÿv-³Ñ|]_(º!†Ñü ýÿÞU© Õ VI£²Õ޶¦•»‰(~ã0"Kèx›ê±N¢·Ìr¥zja¯4Šmä‹ V†ÑÙ| ݾ[çþñgE\( ?‡¢Tœj«½'§•ƒtÁ*^vòúJ⤠Viç¯çÈ-ð"rã|”9ܯö;ªTÏP¬þ]›‡T*Øjdí× ŒD?įш¦7þu"îpëZÙõ(ÔÆvÊ€Ö7¢ó="X7 øð[JË¿¢Ì…0 Ý¿€ïÝÃè@®Aÿ¿ãËl¿Œhôr]°|y™òI£²Õ޶VmNzfÄ;¥õŽX'‘&XUÙ­v„½Ò(¶‘/ËÐÿÁú'†Q<û"¯Œ r¬óhtøq¥‚e¨E°J‹e{+Ñsy&å]_%ŠEõ‰X¹ED‰ÊâÇ<žR7¾x»“bï†ôÅÚwK°-í¼âmúð< %±¡[þE°=íXiÛ†¢`õtm~¸è†ÔŠWÚ>Ni°3Ï4säv<ôiÔ±º tœ‘(†Óë›áè„2y²%Љôo"±c9º˜[]°@íܳè†äÄoÐ9„nœ_$Š2€nߣ ýnÛ„‰h”7tÓ/Ê yÉ×à›PL£sÑî¤`J*¿ QܵݑûË.À¶( \#ü%îF+á-µ« *^…UÒ¨lµ£ iåP`Uïˆu¥¶ÕRO-/,•F±|ñ}Ã0ŠçôÜ;Ç:½ÁÁuî_Ëý?-–íVh€¿š ëÏ‘?÷“Ô @[]Ÿí>—kwRìÝ^äÆþ°kg<èz¹óŠ·é@ä=äß%ž&JfUéXiÛ†¢`õmtmžZtCjån¢‡ëË Û§ÛãÓ¿ÊÏH(· Fw'”Ÿ„¥s‘hv4zù|ÉìÇ#·ƒÃ‘¹üI(¸Ý§P‡8¯A¦‚!÷¹±¶uJFºvÀ‚»—RkªGPÁN7¯ƒ>¯CBÏþHp=œäô´oC±Ì~A”%î4ŠŸçÄûÜ'MI~øY–¿&¡ü.È}çCî\w |Lº]‰,ï ÃÈ—ÃÐÿïg• R`UkFÔð˜iån$Ù%0>Z[ïˆuaÛj©§––J£ØF~LDÿ…¡ðbcCß †çÅÁ®Î;r¬Ó0ªábtm~*¾¡¨4ÏÕò5`Ô‘Jêè,D³…htr)rÓšB©yºg4Js¹¾+³>A,N(¿! _šã”.dKÊ[Q<†b4…¬‡^Ü˱(e›‘È,§¼uΟ‘9íÍ”ÆÜjuÆ!•ê¼v¡ÿÎS±r{ 1f¼+3Ñí»ðKg>œ üw™:¯¦4}:è¿sd™òÓÖÍC£ +Qg»Ï-/F¿Eœ‡Q |\»ÇÛ’Ê¿H4ú?‘(“åt01ÎÛЈPœUè¾ÆÝóbûkʆÑ\f¹yV#Ø?A÷Åh0 4x ðQ÷ùàÓ û¦•; YŒÏD1 OGZW¡xsQ8´cœ‰žI§¡€U5œW-õÔBÒ÷eƒ!f! £5XãæE¼«\Ä0re™›wÇ7ØÅªx7ºY<Û¶²’š€F»Ýr7 ž~T¬ü†¨ÃÙƒ^¦ûÜòwì+cåwEÀˆ: Pv¹iH$y£hF¢ß²ÜåÏ v3ëFiî=KÑï º&ÖVYw·Û/ÞÁÜ™™N@‚R²Ä¡¸uÞÑ(KcÒþ‡ Î`w, ˜ŸÄ ”¦†Y1|]÷+Ðù®DŒw28¸ãÖèú^‰nP½Áòb’äVâmÀûÐÿt7Ÿ~¯Ï¡àŠž‘è°”Òô¼†aäÃ?`¼5¥±˜ £“Øx Pm_¡¬aÍçfàPäpsNuî\g!£Z9YÂ4J/ÍMı„ä>÷½DžYK5œMôî´#2dYW7’A·ÙßcžœŒ¼`®DÞKF¬‡‚‰Í&r9êþ€,NÆ QÎ’ÝbðßËÿvk‘«W<öÈ{(ïbv{ÂñDn¡óEÎÒ |R`Ä#SŽÿÓ„òÞ-f²V|Y!ÍB>áq¶CIf¢é(KÇ.$[@b=’Þœs[ Ã?D÷¿ÝÃ(ÝÐÿàþ¢b þ}–Yá«aoWç=u…f³-=«q}.ÊÚ4Ëz‡’Åì±èÚd0Ñê.C‰mQ¬źY„,²®¦Ô%Ëÿ.ëòjœ‘Êh$D¬v¶A–wÇ!+ª7!‘é'È’èA$jý38Æ$"ë¦$ë¡)ÈR'NO™6=íêZî¦^tsîEÁ ãÜŠ\2ú¶%ñƒÝÊ”‹óÃ\[aFÈl$V½…ò–£C•)ÀïÖïÂ)ƒ¹FkáݶóÌ êß+ꉽ{zšƒÂe‚QÝ€< –"+"ÿ’¶­3‘uÍXdð1"o£^”ôêÈ êT¦–çfÒãSŽï-’’ÎÅ'sÛyFœˆâLûý.FÞSQlê7¢˜`¢OÄËâ{<}w}À¯óhO”u 4šËhà”µ0´Ê™…:ÏånPOºrÛåÐF£2 ßc^lýäúJ߇Р¶–´Ñ£Ñ r+äZf®c†aÙðtožStC £@Þ‹þ·V*hF.|yE˜cû û@R²±jˆ Þ·'¸å(½¿¤m+'œ‡ï?;¡¤Wž>d… +òD°íÖX]åÓ²ãÞìç–w¤4,OQø”‘±ÀQÁç°-•H:÷,¾ÇE(„´§›à¾èÚ¼«è†tŸ 1–ÿ¼¾ŠýîwûìÖ¼¦µã‘Pó:ä[ü.äöv8r¯Û¹£mâ7MF"Ol~¿§”ÙÛ^@ô›÷ @¶»4»†aFYF! ÙÕnÙ0:‘cPßäú¢bFa|Ý~^çþq±eJn2ÄXPå¶r‚ÕH°˜‹™BO“”>ÃC·¿…±ºVTÑþxæ»:ýƼ\Aé{g¥ ÞjɾštîY|·#qëòµÚË ŸU}Püns l.ßEJíHµ]^å~Þ$n?ôgX†L—Òº¦Ü“Ý4 ™™úy¹åIÁ4‘ú¯ÅuDA»}û·Üë¶õ¡ï> ¾Ô­_A”u®¹ëù to6»4¥ÎBAôE.ƒouó™(Û\«7 ÃЬA–Ë;¡AG‹mŽa‚w³X–ZÊ0Œ¡ŒC÷i®G–L³‘è²(ØÖO”Y± GFIbW?ôò¬¢õB÷¼iGÇ£øÃíDY—@¬šË*ä[+þKʰ 'KÜŠYáä³­%m+§:ƒÄ™)nZßMár¹Ïfœì Ú·$X|Œ×CâÖ8·<eÆM$–e‰·Š ¿«jn’«Ÿñ È¥óDt&V†aÇ#H°z V£A‰ß§eЮ¬)—ÉST†$£xL°2 £QÁj5zó.w÷ äZä"7;(›¶­€Ýò‰Tÿ~9+¨ëÈ*÷‹ŸËïQH—ËÜçíÑ`Wdñ=Î@ñ»D±ŽÛ oM7(¤Ž V­‰ïL<†¬&S‘PT/k‰¡Åî˜^„‘²_‹ÝäŦPtJZ^LK]›êaê„G7 nòÙÚºÝrúþÆËë!l‘(6ÙÍÇ2X«Utz øuœ“a†‘->…tZöÝjÒCYÌ®®P®UÙ+X6Áª³° ë†aLuó©¥Êsô™‹Œ2NGÙ?‰Þ_ ʦm+ǹ(¾Ö<äâ¶*½øÿq& º~mªÙ/~.§× Øb· Ï™Z †ª%‹ïñ'è>?}í†]o3.G>ŸHØ6 K[¡ oû"ÀãПô3À¥Èñ§ÀïPÀº§P&»åD1–’¦%Èg÷~à7ÈRèÛÀ…èó!àÝÈÇxtãkÔºª†ÄªÀ9軹®È†auóNtÿuJ™J/ò7#+ç9(ã/({Ïl$ˆÍrŸ=ÓPH€ÇQ§xß2õ„±/Î@#£YYO~åêø+¥q·D‰>¾Jå8¾î‹‚sùEõô"ë/¨ðÀg?£ Kï©P¯Q<ßDÿÓ‹nˆa€<1GÙæòâ§è>pxŽuFµ¬C×çð¢bTæ‹èǪGÕ­†QHhz ên‡2áÕk]5Ô9ýß,º!†aF]Ì@÷ñSÊTcyRK†¤›€³Ýò0¢QÃ4Á*)ËOZæ¢j3#%µ¿¨ IF1|ý>RtC ÃàdôŸ¼"Ç:ïêܯRAÃ(€ÅèúL m`´Þ¢ç+E7Äàô{\XtC Ã0ŒºY„îå¡«÷eDÖË——9F-’’‹!á¡`•”å'-sQµ™‘’ê.*C’Q 7¡ëÿƒE7Ä0  Áj®«s‡:÷?¿†²CÅýøb”\+©_ÿ>²8gŒF3VC¹öõL]Ÿ›T¿QG?Ö•E7Äàëè÷¿  IDATødÑ 1 Ã0êæt/ß©Ìöz,¬Ò«”¬¼+ýÈØ1Gï¾ÜéÖ…¢Tœ,«´zÒD¶¤ÏFëñ+tý¿«è††h`bÅ'Ê‹W]Ó*,C-ÂHÑ‚UVžC‹(° ‰Ÿc» VåÚ_Ô3ý1t}¾>\iþ­‰k-&ºùÒB[a†a4‚ï€eijî³÷Ààì=w¢((u |ØÍ-Ai§Úgù9e4„(s‘gû`ÙgF‚ê3#y|†$OZ=FûãÅÓžÔR†aäÅ@Îõ G!aú©/èúEè™Æ>œ‰â?Šbî–°ß$Hý÷9-^bH¹8iuö— Lyo«¡®rñ(oFï÷Å®ô$}ç¹òÏÇΧš¶¼›ŸC”•ø2à.·¼¥bg¹zg’ü}%µ?^wÞ”Íh´£›ØE7Äàgè÷8¬è††ausº—ÿG™íõXXmAÔÉí>{|çôqdÝõv·þ`àŸ(FÔ”ŽdÎveçÅ ;í_ Êo…: µ]uêÿNiÐõrõ˜…Uûó(ºþÓ2e†‘ïEÿÉÛrªoš«¯1"þ,€vB‰9²SPÖ¼P@I‹—R.dZ+)Í¢Wm]iñ(Óúñm}À±nywJã;VÛ–8{¢wQÐ Ùý(õ爙Òê­ôµ¿A×èAáʑŴŨ@'XXMF7±õƒùú±Ï£‰2®B®½hTx‰›/wëÖ @m~Ýr·¼8ƒ¶úèJÁl Ã0ŒÖ¥’…Õø2ëÓÊü xK™²óPVÝ8¿t“ç¢`9éXó(Ÿ…ïŸ(è¹çœ2å[Îe2­ÎuÈʪֺ²¢½O&Qo[V£ke&²Vóî‘[OVQoÚ÷Õj$í˜`ÕšøXI[!Ÿß8ÞêÈ‹0½±ÏË‘eQüóò`Ýâ`[ØyM$@m,o€)/Jùíõvü£¯‹‚ù¢ØçÕHŸŒAÁ`Ç»6Nró.·n”üº.·<9ØÖ¨àd/9†aí‹¶NL-ÕþLAñ¨â잯FgbV†ÑZÌ%߬YXXù؇Þëdð¢[>‘ÁqO~„,‰/pë|¼ÄËÜç퉄—òRwÜ HШTgHµuùx”×28eñï#jÛ’Ä=ÈRí#(Á7åU5qÐÒ¾¯jÚÿåãmeYXµ/#¥¤¤v¡ö·<Žl˜Å(ðk­nˆkP*ìÈ`ƒE¨¤åþ¤ƒ5‰QHÔò"–»Æ£ïrŒ[7 }Ç~ÝDWnàµX’Ã0Œv¦S,¬o,ºFK1õkV# r£ý™€Ü~6@ÈÓ‰–7rëÇ ûÞQøŒ>dâCh¼ <<\œ×a U²°°º YøÌEAÔÏEÆó€Û|éGÖ=?Gñ¨.N®!r÷»øtB]§×!Ë¢µÀéÀ«¨3¤ÚºNG®€ŸD¢Ø1 e’ˆiTÛ–$îAAÕïCÿÛ•n]5¤}_•Ú߬²ò"Ѫ–l2F¾¼llJ¤Šz†S*`u¡xü³iÆÇ>w!±+üìYM$@½,ÏGº¥üöN´ú%à|tS¹¸à¶†aõñQàjà{h”Ñ0:…õ‰ §Ô¸ï{P|—ÅhôÛ‡§X†¬ÃuÐ'lcP?Þ{4L'òt؈R1jÍy‰\ üø°žvË#Û¸GF1|êþÚ ¢,Šö 6 Ãh_²r <øz>Ÿ üÒ­ßY¡t#qì4øôe4‚þWîóè¹ò5‹æ:䊳ÅÖzÈ•ë¾ ¼År˜›RvKàfã2L^/Î]„\„æÏ¢ «•Út1ð>”qê àý`$*|¸£Šú|©×%p}7¯% ºi²À[3M Thö¡C&Pú^“”HȻÅx—9ÏJÒŦåÁºNÁÿžÖΖ¼3wnææÏ7pŒ™À)è´e½{Àm;Ãmë~ž•½èÙw iszÖ@ú³f P¾%qNGŸñçURž¤ç[Zû=“€ÛP6¿_ThgHR›ÿP¡ÎZžó!åú7£{åý(îÔöme, O›ñd¢Ün¦|C‘ƒÐoñ›¢b†aÔÍ4t//¿£šÐ>àX·¼;ðD°í6à·|p«[~¥î£Î(ÀD#ë;RÚq^Ii¦¢´²·Æê®”b»·Ìr¥zúPÆ'€ÑËûQÁçðû0Z‡Ãе_k<ªÝ~y¥47Z“©è:XX© QBßër¨k¬««Ò³¡á€ÏNÀ_‚Ï‹ˆ2Ø{q³äç"¤?knB– AÙ‹ÏñçUR!ñòiíïEV¥÷¢A˜jÚR®ÍiuÖòœ)×ßðçѮ쌮Ó$‘ÎhAîD?ØAÕï͸7¶B#¨» uû4º|8ʺtp2êÄ¿ØýÉ6chÄ}ÚýÕ¦5 Ã0ZÑè^¦t¾ŒÈf]°|y™c¬ 48r˜mŠz9³j=‰â‡¼Ò¸”óƒ:çÏÛú(µI+»0V÷Š2í÷¤ Viõ¬@ßcØÆÁgËך®ýkjÜïŸn¿­2o‘ÑNt¡ë 7ÌhŒSÐ÷zEu½ÖÕõtƒÇÙ¸ YÍ¡Ô:ìv$¤Côìê£üs±Ò3-ÉÊ&þ¼Jª3­|Zû— k§´v†”ksZµ<çCÒúí,X%^§æغlëæÏ"áh,‘)óz±åp>¥ÆÏ»ˆL¨Ã¹Ïâçq“ê,@ØÅÈlq Q–˜%)s_~ÅfŠ©÷Ä0 Ãh}ü³-Œqf°Ü‹gÒè§¾X3?CV.ÓѪg8Š'•$0­£4°rZÙ,I«§ŸÒ¸«È6¶–Ñêu=ò1¬¤–2†:^¨—Zʨ?¨¿4µT6d¿êzde;½w†îÂïGÖAG ‘ü€ ÇJ{ÖTûÞWkiíþ ¼‹R×újŸ½åÚœVgQÏùVŲ¶cˆn,+¨ >ÛŠŸ/#ʸΗ¢ì:¢ ™cón$ºM¦>ÖeLñÙQ^ æ¯Û*ýÁ×C1¦ó©±Ï~î—§¹}‡Åf†a´ Þ2ªYHïAæk‘›Üì`ÛMÀÕèy³O°þ÷(žÅeîóöÈ+‰´²³‚º¤¶çÕjôlô®"µ´Éh¼«L-#ï£Pÿm ÙŤ2Ú÷®»²Â„çÃ*‹øU w=/zHé³fz¤ÔB&|6…ÆiÏš;QððK]Ðu^•«Ó/ŸÖ~ÇÐPü« ªhgH¹6Wª3¤ÚºÒúíŒ VmÄ*à‹ÀgÜò ¤¾öó¾„u+]ÙU óåDi€Ã¹ÏLâçñ´ÀY0 W“)³âÂV|î˯\)6Ž8^"1k¾;N(>Õ;:´ Î5 Ã0Ú“1nÞ¬l\§?>‰:©a\ŠÇQìyJchŠÜ´>ê>ß|ºÌñÓÊž‰‚­ž†:ï«í]ž«€G‘»Â!5¶Éhê±°ò×gÜ£=éC‚Õz˜`• ÀÙñ¬ŒÍ + «s»Ñ{Öí”>k~‚®‘®È a[àadthP>íYs >>½—žü‘ÁÏ«¤:CâåÓÚ2Â8ø9ŠGui…v†”ks¥:Cª­+­¿ÑÎ,G¿A²6ë³1Úƒ‘ÈÊi:°‘[žÌ7 ¶U¤V"ãEÁ|[ޝ×e-â†aù²Ê¢ó<ʰcÂÈrààÊ*÷Ùì?…FùÎæ9tÝÂ-íÅ•Èúçdà¿s¬×[æFµ,!2^Y fae´k—ÝT‰ñDbÖ442ŠOêl†ÑéøààEÆD4Œ"¨ÇÂÊǯ²ÌpDq–†B2¥NÄg¦}¡ÐVFe–yb™`e IzQܯ¢b†a­‰åíw–)(Fœý1¢Ó¨G°šêæv­†¶ÀëíIV‚ÕùÀÅÁçJTÕXWùc,AV5Õ7 ’ê5ŠÇ÷ÑþO^PC Ã0 Ã0Œæ±e=ŒO&@tõ]÷ïí˜rðÝàó…(Hñ.9Õÿ‘ØçY e¾‹â´´ >hõz…¶Â¨—¬bXßhC £ƒ¯›`e†aF'àGëjyi7Œ¡@=VKܼ-ŽFÁ=û¡ìœëšù¬’¸ µ³]ð×N;‰l­ÎQ(s^1ž|…E ã"$XÎ~¬?¸x88X? øððWÊ Æ¯Ææ•Ž»9ÊŠ÷ƒ}LÊsPàsP–½»Üò~(@yµõÅ’˜)Ð0 Ã0Ú…nàµÀP¢sm7ªeo¿êž¢b9óºök žþ&·Ï_›Ò¢æò ¢—SQ”?¢˜(®¾ì…D¬Ý2nû÷ÑKù ÀP6·™ õ¼e»(­ü{ƒúÞéÖy «s€³ÜòxJ_ú[›ÐõpDÑ B¼ˆ¾ÓM+Ì€®®FÇø€Op¬[ÞÝk<7"±`GàêI;îm迉›ßê–÷~æ–ïAb×(àsÀÇj¨Û(–›‰e”´Ž¾a†QãPœ©h$njðy °›Âõ£Ž3˜ïæÿv˯¯¸u~y>éé„¡½Ò—Zª6†bì‹{‰^ÞãñJŒö¤—Àv¶°šB4Jÿ_ÈŠåíîóTàƒ([èo‘ÀÔ\‹¦)ÀgPÒžЋöbàw(e}È9À;ÜòÝn~²üx{¬ìÙnþ57ï% lߘK`öxQ5¸ŠËÑo7Î-gÅ3AÑÆÁ¶ýQ¶QO-3iÇÝý§A–S_qË»¢ÍUHlßYWž†Ñ. ²°2ÁÊ0 £ùl|þ¬E/ëÐ(ìÔq]‰:„+Ѓ¶§Š²ËÕ¨³³.V¶™ŒE/@Ýèef¼›& ìîàóx`rìóú襡žà­ËÕ‡D® ˆ²‚îPÅþ‹Ýþc•V-.2F{ãÅÎÕe¶gäuíû"± L°*xwØZ^ŒÛY°Jc«@Ϥ—ÜrW°ýßnù 4ØÉÙE'eç§ÔÙÜ¿ö¨£½­‚ϲm‚Uvøÿe³ûlý~]¤_«µÒú¦I GÏ“z2´§·«ç5ä½À£À¾ÀÖÀ“u´Á(¬ Ã0 à=ÀÁçÉ9ÔYIÜZìæËÜz?â6½´{áÉ‹L^xš@vÏŽ(ôô’° ø¼uªæÇÖÇ;0È„« ényzlyC·<™èû? øzFçb´>£Ý¼ÖNpÈ–È\}8ð¿±m½H˜~²¼x¹ u£’cPgÚ—ýº/ô‡¡8 ˜~4ySàr4zó Çû"ðtŸ üÒm›†,C6Eç~"¥±|âxá.ŒWò,pH…cõ"që}H>y?ýÿ>Ü‘R¯Ñ<:M°Z„žUIýÁr/0íó/Õ¡0•$R…ô çËúßy†ÅÊ-CÿÏk¹úÆÓX<¡¼ñßåÄB[1tðÿɼͼ(Öh\ Õè¹P¥òï‘+Þeîóöd#݃\S¯EBðìØ¶³P¹Ç€o gT¥ÿ²Ñ:˜`e†QÓÝü èÁ= Y`LDVã‘Õ’7×C$ ¥•íB/áÝ®L¼l³„±•¨£¿ ½Ôôº©uŠ–Ÿ{‘8~^„ħzFÝâ ‹©WQ,‘JLAÀѴ狘Q?Þ2 ‘ëî›À(ÆÍ è¿ê‰®ÁO¹Ï·×e/Gbèú4p®ÛöÍ`Û»Ý|Ûà 1,íx/¢X»#QÉ V—#7¤»{Ó5ÀnUœëÀ'‘èäI;Öd­òf`gôâðQdU²3Šd‚UþL@Jµ½´  ûùD7åa’¿B–·U(w Î>œ^G=_EÂõãè™æyÈÕý Ë à7Ⱥør$(¿øuu…(ÆÚŠ¡CÞ‚•”lT°º Y.ÍEƒiœŠžuŸo>Ý`ý ÿêÑói¼ñ܃‚µß‡Îy%¥q+C—w£5± ë†ap-êÏ̱N/Xml…^.wAø€ÃQ €“P§â\7:‡¡ûN@¿ÇåE7ÄÈ•cÑï~}°î2dA4YúårׯB"WÖ±”Š_}”¾Ì-Š•]+[nH xÅê¨æx+(ï¶$XžOt^såW½e–+k‘èG”i“‘[ ëþÙ å’ø—Ûwó åZ.àêêñ×÷FÀ5îû]òÉ—ç¢ká+• U±=ú>ŸÊ©¾;]}ï¨TÐ0 ædt­^áW …Ã0ŒVgš›Ï˱Î5ȲÉŒ·(Òµ¡ÚQ>‹!”InÏ –{)µ&ª•uh4·Q†#׿ïe«D³âˆÔr¬~Jロ¢}ãx %¼%i=‚á$VM"r?m–#wÕf³r×B齤Nʾ9MÅß7Íê!–#Q5/!ß[XÕ;Ô0òd…Õð‚b†ÑI!Xå©V°ª%£V­Tk’~~ÇQ¹ˆQ³ˆÒºÉàX5!÷eãq6ˆ'¦.FA¡¯ªñxåðqD<ÛW¹DñJ²8–Q Þ5¼žŒvŽc•Þ¼žÁ1톛¸ù‹…¶bèð<-ÏÉ©>ߟéJ-eÅc‚•aFø`¬Yff1ê'Ë—°^ä"ñ0Š[²e°m&JÇü(J¯¼[l¿pùË z>ÛÝòÔR†1´hg +Ÿ€â¡`Ý«”Š< +¨QHò1zF¢—ôGˆDªo£ïãXwŒ´L› ÐsÐ Üñw¦x<›ººF£{ôÜúÆÊ%Çz¦ ¼ÃM våγÙX†ÀìñI@ò áÝÐ-µÑê ²°2ÁÊ0 £¹øxE™Ñ¿…â—ì ü°ÊýŽ¢öàœŸ£1Á*Ïà³Õ¼ˆù‘ÈÑ‹ÎK(&ÉÆÊ‡LûwF.žëШün(~Dh¥0¾Ì2±rŸB£Û‡¸ÏóÐ(ünútÊqŒR|Ÿg µ”a -uþk ˆlMÑóç"(~)ðµ û  ÿ÷$ཱm÷¯6þ„bÿí–RDÂÔEÀŶŽ,Ã.A÷íCP,–íPò4nE]' ØU»#kZßH;Ïf2ÃÍ_ʱΡŽÿOõ¤–ÊFÀ\VÇ,¬ Ã0r¦èô¬KQf¹Ï"1%Œ’Àõ'”¾$T BëE€(*‘ ƒÏö‘•«ñè¥áÏèÅæT42?e±2Úÿ´F'ш…•ÿ¯t§–Ê–MÐ3ãcÀïµRR‚Œ³ÐsìBJݲA‚Õ0d1Ü Ü¬¯—O Ë­ÿüÛ­óϯ#Ðsí2à7•³²Šs ð]`àä2ø0‘unÚy6¬²Çÿ§ò¬,†•Ñ.X +Ã0Œœ)Z°LEVB3(5é¯5€kZÚ£Èõ"pv°O¥ ²þ¸ñà³Í¦\l–]Q¼©£ˆ~³EÀÐhÿ±òfÕdF£lè¦MPœ¢MÜç^ä6¶¹Š-C±Œz‘ˆ± &ORÞE­«JqþÊm¿ÆMžÐòôç±ýNgpÖÃøq+}ž¡‡¹uw¹ù›Ú÷S7•;ž§ u+³=í<›‰ VÙããå%Xy—@¬ŒVg…•a†Ñ\vE£­P÷@lZ¼?Ø~< Ä–ùL°o<Í÷+¨£=Ò}~4¡¾xÜ«ž„vô¹ÿ  €ôy[üþÒÕýnôp<øk¬¿GÂÕ˜œÛæ…^67DiÔ§“¯ÛäPã6ôÛæéJct6“PPì€#ßË‘ˆ2x Oñ{d­Ód½úi<|)õ±˜¦P;Þb¶Öx†C·Ï#q²N›ºG{óºv.º!CßßÈë;݉ä~›a´¿A×êA~…YX†a4—¢Í°ÿ…,†V¢ŽJèõ duŠÇñ5ÒŸ •‚Ð&áƒÈ¾eM‰^¡Û`RðÙfãGnÎE¢â¸ -×!kªåÇ¢ Ýë¹åqHÈêBÁuÇ#1iú»Ñw;‘(æÈ°`>9ø<Ü•áöIºEÞ2$4.pó…È laÂäËô&©³ð×ëºB[a ÆYCmêæ3ˆ¬£6rŸ×+w€¯ºéE40ð"BÆÝF¹Ïc\ýþ^´•›& R¾¹¬ÝKdÉQ…UÑ­Êl`³¢‘#t={nAAùó 1Pt_Ô0ªÅ\ Ã0r¦èNÂràÎ2۲ຽ¨B”ÕîV”‘é·¼ J¾ʼn(»!Ë)+ä||œ­ÃQ›½(5.áyà]V£ß± ½|v»iË޵šR!Ë \ó,vÍGnFy¹*äÁ D£uW#q }¯Ë Û‹Î¹/X^éæár¯[^æ–×äuFS†¬¦Ææ¸iJlýFT/à,^¦—Ðë%"aê"1¿^f{¹iOdµ± ¥q¿|¤Æã%Ð(žÑè°=Œl¸(çú,èºÑ. r 4ÁÊ0 £¹´¶ZÎBViÔ IDATV¢¸omðxW£ zW V§ ŽÒ!ÀûÐËÚ‰‚ÈÁ>±Ïô~BYO)VÅŽY…ÕDÂE²nX†,y–Å @VÞ…§?ؾ”(FÍZÒÅ¢n¢—çpZ?ö9,3½ho”rÜ8+Ñ Ê+n>Y}„Ÿÿ¬¸æÓøKwœn$v!QµË}ž€:4ãÜ4 ß87Ÿ”èÖý“¨ÏSë÷P ÿ»-G׉_^‰~×n½_^‰~ÿ>7ùå•èú—ÊxQy4‘Å£Ÿ‡ÿ…©èÅ;.LM¡v—äDbÓ nŠP^”Ê+‘ÄKÀÏܲ¾Úe=]£ªã¸­üü2òazFÎÃ2¬¶3EžFµ ²°ªÜÐ0 ÃhŒQèE~ z2Šg4rÜ6X×Ügn~‹^H½¸göÂf1†Á‚Öú”Z¬OôR?ÚÝ€#Ë%/lùåù¨¿žº]þód"*k¤ãP åQäg‰\<'¹yNtëüòXמp¹Û-'rýlþú‹ \^<õBhˆA“ðÖzåhd_/Ò–#´XCäÒŸ'­ çcÈÖúq€R÷Y?÷×nܽ¶¬wD1ˆö rç®…eèÿØMûœ·‘›¡Xkϛܣ1ü³ÂÞÿVfQ?¬ÌÂÊ0 £Ù¬qÓ(7™ûPñ¬&²^ù°7rÜÛM/ Ôâ×0t¬\V¡íW*0–H¼šæ–§¡ ðÞ]j£`y²›¶Ë¨Í=H Y~‡îsz‰öŸ—åzˆ,™¼ ÓëÖåŽû4ðXFm„(¶ÐÔÉòËcÝ|¼[ï—Ç¢—ÿ.·Þ/%²óË^À™ŒQoñèµpŠMÞ0.L-$ÿzyŽT×#V VÎJ7¯6›Ñºô ´,«PÖhm†YûÉ÷Ó¦ý_½Õ? t û·þ™ ƒ­ü—"áÈ?Gë}–TƒÌZE0Xl‚•aFóYNdY²¤BY#Æ»ù½Hœ:eM< Øøpp;pð¿tž;ÄJ$ÞUÖg4ôÂÖt"1ku–}œ°¨ÔC²ðÔŒÎt³2Q®A¿fˆ›ã`5)¶Z‡ÅI·¼•R9Ù×'(GÜÖ»>ÆçIëÂùªØ±Œòøß³‘kÓ¯w6Þ}ß,ijc;à@”)pVŽõ.G}Ÿ.L°j5Æ O’,FÅ×å]Û³YчƒA ‘U}|ÝÂàLÕÞòÛÇ}œDò`Y9ÖR*bywûpo¿%[ò®d‚•aFóñÙðj} ÍÃÿñL…Ÿf£tõN¶¾Š,¯>|9‡6C—%T×á;¸¸z¼ HQ„çy/Êb—5Í8Çß¡l¦ (mw£¿GµL~L”Ù²^L°2Å,¬²gš›ç=ø5 Ø õo6®q_ïZï¬7!ág(¾Y9¡Ë/·›°5†dÑ©šåzþ++‰œW)¢¼˜_W”+{˜¸Ä/Ç-ÀÂL»ÝÈâ~z u˜…•aFÎx·•,ÙàG™âA¤…\ŸF¿.FâÔAÀÉÀ»É6þ‘‘~Ä0.R¬o5Q9/$š!V5ƒ½€—Ñ ÿìÉë÷XàÚ°'p_Ç™âæ 8† VÆJ"—à¡„¤h6póy9×{p²˜™Hd)Snyrðy¼›fÄŽyL•uûL¹q!ki°}eP>žð&îÇ´†Òg»ÏÞëN²ÀTNtj$›b/‘Åù¢`Ù[@…ns~]¹~I+âÛþ·*˦TäÚtÁ«ø~x¬ Ã0š¥n-F¢Ñ³µ õûKBù~à×nÚ¥7Úï!-“]%â¢VhAÔ |Å&é'ÊF°%p³kÃÿÆŽ98½®EÄ9î8‡ NÝuÈúo ²þ{¨B›¿R¦M›7 ÎáRôâñ\•çùEä*»p*ðË*Î3æØZ”³®ª†³lˆ‘+>ËÊÔRõ3Yæ œ|‰¸å+HrL=· Ñ`'àjàÍ(èÿ'‘Èá¹øp°#в[mú¶Ûß·éò`[¥ó|‰=»»¶{±)íw-º!upjûŠnˆÑ9˜…•aFó1—ÀÖ¢ +£=ñâH<ÓÑ™Ár/¥Ö3å†:ì#ɦu=Š-2‰i‡£˜J•Dœ,(wžýÈý­*#.,6ƒ)hD~úÞÃûÁ°*‘ö{ôS겊òî¨ïöŽŽpëý¼+¥e§5Á2fË È‚ò…¢R3ÝüÚ"atÃ+1 Ã0Ä‚®Wf}dpêù”Ë/!Ë“YÈ á‡ÈúåBWö8äâóVä’³ •]/M°ê<ü¯Ñßüy"—¯#(µ˜ñë@Ö”aºòY±m¡ 2ÈÕôÄØ¶Õ”¾$þŸòl_E›Ëµéž`ÛQH0óT:Ïr¤gµÜ ê–«òõœãÕÀgQ6¾¯럧|œ•,~$f x]gopë¦Óø m.ÝÜ\Á:˘-ß@1ëÚÉ%ÐóCtßûyÑ 1:³°2 Ãh>þ%ùëHhñÙP|ÖŸíÄg9Y‹Ü‚úÑKÂѹñXê¶/C£ö=n¿^wœîøñÌ+yÓ…,|)±i+ô‚Ï<ã™Híé—AßE°$X^L× I¼¸ø8^~|ªŽúÖ"«º³‘ëÛí”í_l <Œþ³‡ÛÎDÁÈOCÂD¸ß¹ÀÝ([Pü˜W"Ñöœü ÿêÓ)íMkÓéH¬ù$º„ÙžÒÎ3´ó¬–3ŸºvÝEe‘±žs<Þµí$ÆÝ‹,œîB‚×®$Ç«kô÷(ÇOÐ>] Ø^÷Ôq¬,+/öæaÕg´&faex®v“aäF=#_†aFmœ‡„"ñ)‚½HÇ| à$ql‰[ïE2/Ž!JIOQë§0ˆf‹Ð çc(®Ì£(˜gQÖ?¥}öËÕX³ýxO•í3Ú›ÍP–çQæ¸fϬgÔÇzè~5€²žŠÄ¤¼ØYMÍ̱Î$®E/†÷Ö¹ÿpt_ï§z ¹$n>ˆ,Õ~ÚÀqŒö唌`J- Ã0šŽYX†a4?2}%}ïB¦õ>+ŠÏ†â³ øl'#‘»Ïv2œÈ=ÜOBƒÝönWÞÇfñuùzòf9²ÒXˆ,HƦ"ê¥2û/¦¾Ì|c(/n½ ¥°_VvoÃ0ŠâõÀ÷Ðý«¹ýæÉ}Èv*¥AÚód ²:­W¬‚l¬«À\¨ ³°2 £@L°2 Ãh>^°Eä––7ãˆÇ|j`/’ÅűaD{ãâØ*ôBçŧ±Ï )ÎqðŠ›’¶޽€uܼ™¿yQÖUSPL‘8û£ÿ`»ñQ'OÞçx`ŽY 3hƒO4Ðh€|ïBmçb1¬²c d­ø rŸnÆPŸ‹·a4Œ V†aͧ‚®¯ÀbxÌb óð1¬âY‡ ©.»a;Ó ç˜5}nÞ¨UŒÅ°2ÌÂ*;¶¾ ü†ö¬&#íä*Ý_hkŒŽÃ²†a4?2ÝZÊÈ ¬:ÿ¢e#ĆQþ¿Ó—ZÊʘ…Uvô¸y;Å;<‰VÓ0±Ê(¬ Ã0šO+XXÞ=¬'µ”1”ð/ZEfÌ4Œ<ñ×z£± ý‹uoƒÇ1Ú-Y¦3™éæ×Ø£ƒ1ÁÊ0 £ùdåšadƒÅdé<|<Ÿ¢\« ž}~ºߡàÍ®;‰C9À“À4ÞwL:ÇFŽ1ømÇjx«˜1 Ç~Ã_K£RKÕàÿGRKµ;»£Ø«·Ü£C1ÁÊ0 £ùø‘év2ʘÅ@çá-ÒÜ@›y=ìUe¹¸˜³ð2e«+W÷ˆ û œ¼Åz=ÊèÙhüÓ¬«è;Ú3ƒãƒ1—@Ã,¬²Ã»ÔµË;øL7¿³P6 ‚®†a4ŸvQ+š.”‘ÐOcŸýºÉ ë@ÂÃ:" ªÅ¨£u7 tj‚Uç1,£ãôßAÜúÀgݶ™À)Èk-ð1”ñÎï^w_>lœ ü¸‰sÜ1ަtT{p °%ºÆOþPcÝßÞĨg€ÕáRàà9Wölà `žû¼¸*hËæ)ûÖrŽÓ«É¦(£Ý‰ÀCÀnÀ÷Ñèþdéu8ðÁ„cÜ꾫û0BúÐwµõ NÃÑ5Õ½¬v2fa•>DÄŽ…¶¢:FrËר£Ãɪg†a”gCà`Yô KÕHLY…:1=nyz¹X ,qåzÝ´ ½®te–ºíE¹kt!—“IèÅf=ô; tãÜòd7çÖ!YˆšDóSüK܇€ë›\—Ñ ü ,Ç”) ;åèCâË÷€÷ïsÛ&¡ÿ+ÀNÀÕÀ›ŽÝ‡D™ë‘sðº2m˜¼xÑ}¾ 7—¢>Üt¿¨¶î•®Ý?vŸoCbOÒù,B÷®5e¾‹´}k9Çïw¡—¸kX½Æ¢ûÆ À%eޱ1£w*ÓÖNe Ñ}viûOtÇXêŽat&Ÿ>|¸ Ø¦´=ÃÐýpp-­¹v:º¿o¼¡à¶Œ V†aÍÇ¿T6›zE°~$2yqi’›w¹¶F/.롗ljn{³\—»6úiiì³_·8a®]#ˆbUMvëÞÜ›¶pë÷þؤs0Z‹r‚Õeè:‰%¹å»ÓŽÓLAÿ­±HHòñ¥ö.FCk×YU†Ë ôòBÐ"1 .Ä,BJëÜç…èú ÔÕÖ݇þÞZfzI:ŸJ‚UÚ¾µœã|à¥àó×~нç×Þ½ˆ¾‡ø1†»ö˜¨RJ£‚Õ ô»¾„~k£39‰Å_>Up[† “QŸË÷³&ºeŸ$!´ïA÷u߯õ ê÷5“Ñ9Ôae1—@Ã0Œæ¾\NC“.Ô H²<ê&‡&¹rãÝä-“¼5ÓD·Ý³Ë3/–£NÔ"«¯en]ê\­"YDK¢–Ðøˆã¼2ë½kÕ߀mÐË¿Ñxñ&ÃêÌ`¹Åjª—ëã€Ùèÿ¹¨L¹~Ê‹@I„ƒ‹ Ö½Žê]»vþ\eùZÎq8£V$l›‚î{£Ðy¥Å +÷Ýt2ˉ^„ë¬,~•Ãj4QßÊ÷³Æ£þ™ºÝä·O¤´_6Éíã?gÍZ¢¾æR"7Þ>·ìÿÿ½èÞ¼šè~ºÝ?}-Ë–¹Ï¾ÿ6@dÍëŲ5D!|݆‘&X†aäÃrÔIY, šA½"ØpÔ)ñ•%n¾u€V£Nˆ·ÐZê¶·k (Ÿ5˲v>ÀmîG WŽ#‘[‡g‘ëÞ‰ÔgžšÒxCÏ#· Ü;3Ø%°Þºï!:Ÿ£àåù²@{/ºg G®?píLÛ·–sü=йu™û¼=ÊHrmü,ŠÙõ䎙tŒéÈeÐ(Å_ïõö÷½@‘$&6Âø`šˆžOãÑu<žÈ*v<ƒE’Ðz6Žî%á-ˆ“èEϺôZâæË‚u=hàeYP6M@J´[ «[Ñ=ðäBýɉ+†£{Ç&È­x3dUè—7vÛÆ&ìÛ(‹ÑÿÊ÷³–ºe/Ž$h™HOn=·ìcvzA$Ñ`ežƒ–åðýÅ~7ñÂb›c´3&X†aäà $(£yÜånZ\©`‡cA×;o)²*µTeÖÛ£‘æCƒmç"WÂyÀíuÖuð(0ŸìJ$F†âŸÌDbÄéÈ­µÞºOG®’ŸD/â¡»äíè{û5ê/ŽBÞ×V±o-çx*Š[õQ·ýàÓÀñîU[f¨à-Q¼ÀŠ[KÝ_ïÝ×{bë[™v²°ZCô~ûÁú!áj‘ 5êÞ…½5Òb¢~V/ú-½èäN¿ÝÿÆþó·ÿœ5Þ ¢ÿ˜iCñ«ÝO½ø5ŒÈ•Ú‹_£ˆúI“\?à ÆܱC¡Ø×í“=€âŸ}‰ÖŽ×e´0ÃÊ0 #þ…²jmA”IË(†•¨ƒ5–Æ £=È*K5Ù³dOd}43Ç:Û‘k‘5Ö½E7¤Å˜ƒ‚%ïŠ2/ÖÊÀo3m‘è &/êx+§^$ øíqë®MÕèEø‰`Ûª„òoAœ„·îš€^Æ'¹xM擃eïî•k‘ã|” e²šç>ÏwŸý¶¬-ÞB6¶CnóÛ Þ ?iI+Z±DV—‡¡ä »!׿‰ åûÑwú"ð2²h})X~Ùm³ ™µã-þ½{z½qô Ã,¬ Ã0r»Ÿ•seh6÷¢81•8où Z ²¨8(ÃvåÍ($V­ÁĪN»²¬K-Õz܇\)¦’ìÒb(ÎÕ L¬Jâq$XíH}‚Õ‘åÚ>HDò‚R/z¦-¥T|ª¶L= C1çÎ ÖM~Vçñ²À[›x+·&¹;†ë'Å7 ×oä¦j2].GÂÕ«” ]~ùU¢ï9´– Ý"½›Ù4Jª$a$ª}¿Š¶‰·zø¹›@×Î6ÀÎèÙï©W0«Ÿfá-É çW9w\èˆ V†aùàGDÓLê›i½QXƒ«½Pçο,¿Œ¬>îË®i¹bÿß5ú»çi]å9°€:Û‰…ØwTŽÇÝ|‡:÷_ ¼5£¶4Â(vÚ»+îÃÀ›€½‹lÀÓ¸þHtŽ 7µiè%šû¼ûì·u¡¸n[6Xo §§Üüi”¨ä´þ wG‹ÿèþ–os ¢¾o3  V†aùP`U ½ÀwÐ Z?2{Öm› œ‚:k‘+ÑÁ~¡XóEà¨#|*ðKà"s`Ž;æ!ȲÊg×3=šö¬|‡Vbd‹…@0:‘¹nþúB[Ñ?BbÍBà#ÈMq ²Fê¦ýh¬þí¦G«(ß…„« )ºüò†DâM˜Á-t‹ôÙÜQ*Pµsö\oaµ$µ”‘'þz[/µ”a†a…så;8¥L5Ö}(Sn~k°mR°¼ð—2ÇîŽuË»S$Þ†9(fˆgcªëP·*¯G¿ÃcE7ÄÈ•+Ðï~rÑ 1ŒÙ ]÷í˜AqÊ ¹Ãë§çn·¾]Ôlñx¿*º!Æÿñ(úMv,º!Fû2¼rÃ0 #ÊŰº ‰Bsˆ¬›æ—§ë'nþcà-ÁúíP­¹ÀוÙ¸É-ßD¨rl†F}=¯¸u특†Ñ)ü őلò±‰Z‘­P†ÌsÐg¥ÕKA™Ynþ C˜…Uëá½ Šp§7†æh†‘å\Ï –{76PÇõÀqÀl丨L¹~ä&P-qwªÚ›Ö2˜`Õ™øß½éćaÚó$|€æ$Ö¢ÄvwËŠô£8V»# ‡YéÅ[‚£€ÿFƒ;Ï!ô¤€úþ\ŠŽce´ë»y¹¾‘?¾¯9¢ÐVm V†aùàS-gáÇJã~$¥/ P f€©/nÏjÔFßÞçQl Üé´§{‰Ç«ÎÄw–[!#Tʘ4 ýׯºå1nÛ·Ü+;Ö•ŸèÖweþÜdÊfkëpµ=öóÅ ëÂù³Ô& µñ¬v µ«ñÀ·Q,F€›Ñs¬œµÌ}H{3r´kÈ0 «ÖÃÇ -*C¶10ÁÊ0 #|àÉF_×Û¢,IÀ¡Á¶sQ\yÀíÔ—Ñç*s`. º> Ø•H°Ú¥9oWL°êL|†¢jþ> üxôH$öLvóq¨>‰FaÙP„›P¶éGA˜ËÑCy±o$:×n`Š›j¡xÝWîEB„YHd‡××Ê×wF®îÛ kä3€«+ì³YíèöÿKzq£0Áªõèws³°2êÆ+Ã0Œ|(—n9¤ZÿóÜç:7y.*sìx=a°öO¹Éóc”mÐg |•_$Z¬:/_ƒ\Œº)¡Bë¥fÇ÷ìV¢{Á*ô‚¾Ì-÷ ·Å•H@òe—¸íËÊ®Ží3¥ÕÛ,F {É$ôúùäØºí€½¶}çû¸ $Ä?…„«?¿æ7±ÝCo}ç­õ 5«aÀéÀ—Q;E–ÃOV¹ÿ,$Xí V† V­ˆiÕÁ£ 0ÁÊ0 #ª¬Z‘û€ ©èErÉñDÚo¹±~j)c¨ñÊÐÙ | BÙuHZA$­D‚ÏR$õ"ÁhUPv¥"TX¶×-§Y1µ;뀅nJã9`´›Â˜~ Ð÷<ØÞMA܃(Óꯀ¿ÒÞqôÊáãˆM&›üä-ØÂuÝ®ëve“ÜÁ[1YÆo€Ýòw€³¨MT…2î |=Û¦mˆ V­Ç:77ÍÁ¨»x Ã0ò!+ÁªˆL+–YnGL°êLÎGîCH8 E¨Ðz)ÍõÍȆϡ|Ko!k«7#Q<¤ý_'!WäÝ‘Õè+ÈšæßÀ«nz¹BûyùZ3Õ+8u“M|CÏ t-÷ ïy1𥠟;ËÓ€ýßQ}<ª0Sà0†¦iTO» 6ƒó‹‹n‘5{W¡­0Ú¬ Ã0òÁ:R­ VÉÊ¢iÏ7‰W wÂ×#áaO$bm…,:=HdœŽ\“ÓXF$^ÍC‚PÜÕs<ŠWâã…ø˜e!•²*Öƒ#¶”HlêAç²$¶®=Kâë|¶Æu´Ç#÷ö=ÃÝ4ųúðP…ýŸwÓf(þÕßšÖR£ðî¯õÄïj´Š`åÅçf&ü0†8&X†aäƒD\É]Æh.þû¯50´aÙð$z‰Ù A+Àòˆ›®på¦#!ËX»0Ø i. Ö¾1²Ðñón7m×Äóðx˼zÅ%ÿyymm5îpÓ&À1À‡P6ÃÓÝô$ð$`=_æ³P\º½1ÁªÓÉ2s- C.ÌOºù(Ûå)H_‹b>àÊŸá¶õ¡}ç!K¤ï +ö~à0”AtO»ØÝ;O$s§¡ØŒ[¢ûèéÀ¾è;˜ãŽqú_=Ìób…›Ç Ã0 Ãh1z‘…€™EKú,èºaÇ£è¸{ ûŒA–WWº}€_–)ëŪ·¢¿wĦ½N; Ë®pÚ”(x¼Ÿ&ÖÐn£6v.C®žþwîGøg2Ø-þdWæÚüšh´(?E×Âá9Ö9 ªÇºúvëÃd6;Qš`‘Õ½¿žû€Üò À­AùýÜòŽDÂÀMÀÙnyº÷Áà>ÎýHÈú“›çÅçÐoraŽu†a†Q#£‰bçųýf¢nÅðCô<±Æý¶^rûÞCôrf =FïEÖ)Á´¹÷„\Iwrëÿ^L3âjt-|4Ç:‡ñYÀ§ƒõ{w!+Ð9ÈŠÒs;p²*ô–G}D®ÇcQ Ï|w ?=l[H²{r\°š<ļŸïªxfÙpúM.É©>cÒìÔ͆a†Å¯j5,Ž•aË£n¾S û섬6þ€^—eÜ.£uX‹âœžB"ÄÝÈÕé”aðy äb:=ÿf-„Ïš§äýÈbódµé®>‹bóí®cÏûo#K§;ª¨c¸;öÝôš`[µ‰v Š'5™RÑ«™x—À"C¬ Ã0š V­…ű2ŒbyÄÍßXeù‘µÂ4$TLgÆ|ê4ötóÙÀ÷·¡X= xU£x@ž½smÑj!X}]‹¯·ÿéÖO^tË'"w=Ï à‘+ß‚õG¸ù‘D0~b`y¶–ï>î–C—ÀÕ”Æò: d'»ã?]ñ̲Á‹d#RK†a†Q({¢Q°{‹nˆÈ:cx{¸í«b IDATÁí0ŒNeú.£ôE.‰=‘Ø?€âºŒnnÓŒâ&ô»ŸPfûžÈRÅ» ’S»ŒÖä4t|«€ºÇÅæ3çP¼© P’Ïl$ÚÏ>âÖõ_þŠ„YÏ4d‰õ¸›¾Ûö+·þ¢~Í%ÈMöeÚ—ÇbYz Ã0 £å9=°“L¿'PÌwK¢`¿¯E©ºg`A~³äçè÷ø@Ñ 1Œæßè¸UJ™}¨5€Ä‹Q9´Ëh^@¿ýÊÂ,f 8]/×ÜŽzª‰`ŽF¿ÉOŠnˆÑ¾Œ,º†a€•4ÅÛ8Å<¨G„Z†\azÝò2·Ü‹‚z.uË>Õú’ |/²TX”éD,†•aχê À?¶ï‚¯ÜŒ^|ÖåÖ:£h66A÷ë'*”]Cäêmt.E¸†ì‚Œï†,™žA®¬_˱ ‡¢ûå'Q¶Í¢ñ¿IR`xè ¬ Ã0šÏkÝün YëæÃˆ|üŸC/fëÐÈT X9Å'È23Öb"1k9‘ÀÕG$Ž%-ï…\tþ›Rñl ÙòôâŠ;v5#‘«P°Oÿ½ŽDñN^vs°yÃ(’Gˆ«_ͽø¢,o@âÅs¹µÎ(šãÜÜ»pF%Ьf×ÿ.B}©‰ú•È"(ù(äxJPÐ øAÓŒº±‹Ç0 #>†„éÀ†ÀÁrWÊ~I÷áý)ŸÁ¥›HÀòâÕx7m‰~zAæàÙ üxävè?w¹"ÖË)n YG$^­¬cy¥q. –G5ØÞƒåç8Žaá¯Ç3¾¹ÿF/€oDA×ïF/€Ë«FaŒ&z®ÈhOЬ6®@Y-w¸õ×õe¶¾-ûk3‘ûk$Ò_†„¯4P÷XûüðaÔ=Þ­ëFá$¾„Äÿ'e{êñŒiý`ÃHÅ+Ã0ŒÆy3²2*GrWxe’™â·Ì –7nþAzºaoÉç`”qfR¬Þ#ʆL&³ºÜþ]¨sÓ²ü·ÜO”qÖ[‹¤q!,+V£ï| ê8> \‰¾çjÒI†Ñ’2~ç|ÝÓ& Ë·Þ…C—#Ñ3ñÏÀ=·ÅhЬŽBý¡K‘X5–Èbj9ê3ýÑ-x p:ÊøK`GWv%ðS$<Š,J+í7Ý#ÏBÁÜ¿AäbýG4Hw êoÐ[Ü) ncˆ`‚•aFã„£[P*FõTqŒÏ»yÜ5¦#€Ï¡ÔÊÃÜþ§£ËHdJ‹UµØMµ0‰aý¨£4¸˜ŠD«+Q§iê¸y+°j—Ç w¾·"kµ{QfWïô}¿¼„ÂWP,‡¯»Ï{Ôq¾†adÏÓè>²úÿ¿K^°Îwå–!‘êf7¿ ô–uuh2 ݳ¡5bðíƒÏÄ7)µTö¼ÎÍÿáæŸÎuËÇ¡x™›»Ï_ ö›‚öf aö|4Èø1ÔOùHûÝO”iЯû…«wkdõÍέQ|_nlm0 Ã0 ‰F(@ìÖuìÿ¨ÛÏö™üÖí·8‡(Eül·þÝu´¥[¸c¿¬›|‰FH¬;…ìF>àŽû$Ñ9VÃ6¨Ã4€¬Â Ãh$ÊêµÖ-_P¦ìhä*8€ÜLÌ¡}Fþ„~ãç°u£6F¢k'ë8š•¸ÜÕ{‘ûüzdÁ=à–¯qËÇ£„; û×úÀÛܶ/¸}¿ä>¿½Êý|ë.®rËd{ª5óZ׎¿ÜÃ0 Ãèx†¡ ÁȨ–Àèþþ‘{]%vEúd͵olû…nÛ7jhGµ¼Ý;ÉUãuÀoÜöt¿ êI”â¼Úã@V(îƒa­Å÷ˆîHtOcD°Ï*শÎ(ÿü8«è†mI/Q²š¼ØYœ÷ {؇‘@³ݳ>ëÚt³Ûv pªÛv¸û|«û<¹Êý Úá×ЬµPì¿ã€¯%öɓ͈hÃ0 Ã0 f ’9€L²«µ:Çíó_U–?Å:@‚ÌŒ„2û¸íUyÌZ˜éŽý£”2‡ ›ýê³< ¹ÀëgU–?›È,oÃ0*óI¢{Ä'ªÜgrñõ–¥3›Ò2£^‡^ü“m6\£sx Ý6®T0cGý­5H¸ú p²Û6¸‘(±Ì#À±n›·„ÚÖ}þ'‘õz5ûm´!\×…Ä­^äz]k¸‰¬˜êÚ´° ú Ã0 ȱ5r  ŠKU‰¿¸òûW(7–ÈD|ø6åƒhŽB±_úQ¶Â,ñÖ[_¨Pn4ãzˆ,"¾Hýé›7BÁ5TîŒnOgë]uÖgFs\À«T0/`÷S½Øe´6ßE¿é¥• Fž@×ÐöE7Äãu€(¾˜a†a-Àh俟Ê.+›¸r H×±%ðWôà_SE;nwå­T°F~àŽûÿÙ;ï0¹Ê²ÿÒ³i» ¡#U¤#"vPTÄ‚(¢TQ,ö‚Š|6»¢(*ˆŠ~Ø `¯ ¨€t”"=tB6Éf’ùþ¸ß×sv23;³SÞ33÷ïºÎuú™çÔ9ï}žrLËo |x$¬w7òŠh$UäÇaª±Ì2ðëø cLwðVôü¬•ûÊt›"/’5(ŒÈ˜‰ðwôD&î|މy, ëïÚÀvd©1Œ1Æ“ˆÉ(d/–Xÿß ËüYˆàžÀ<”kic½ª1¼ O«+^ý x#°I…õ6 6-£¾¾ñ8†Ê‰Ü'‡×ål» níh0î~c :–ÿ ã_g½!$T­ Ë߇„,‡ Ó;ì܉îñ¯$¶ÅÔæÿÐyúhjCLO Þ|6µ!€=Ðù¸"µ!ÆcL¿2ø1Yrï—×X6zÝAVðçøÍ!à•aÝ(¼DñêÏÀ›+^]æï3ß*'Vr:1ŒOÉýF x(ô/£½UabùêÛBÿêO¬¾;:VÑæ¿!ÌÓìA–ÓêØÄ¶˜Êlˆþ7×[$¶Åô¯C÷ü×Rbx:µ½ò1ÆÓF¢$±Q¤Ùwœå§ç–]³‰A/%\Væ¶ûzAx3ð}ÆŠLÍp™Ý/®Êýæ­ÀkQ%ÃXIðµ-øÍj•ûíuÀ3\ _¼›ì˜ÅdíÆ˜îç`ôlXƒÂ™M±8={œÚÓ3FíÜ ¦³¼Ö¦¥0ÆcL<¸‘L¨©· ß&d¡*%`ƒ&lB^U³QhÜËs+^Å®9.¨°Ý;€×36ÿÈ!dyIæµàw+1³áŒ&¶3ø4Yž°‘GF;½ÃŒ1áè¾^ŒžÛ¦L%û°±oZSLñ|tMý2µ!md2*€óyz7{íû*ôõVT•ù2àè˜üåX=ø$Jañ.ôw$ù÷C¹åö¿³1ú9Ž 6}w‚ëãœÆ3ž‚Âñ¢{Xÿ‰Èëé&àyMØñ"à\ô¢Rî]47ØupP˜öU¬D)7¼øzùY]¶Ü$äáµÊïu\“¿[ÍQRÏË€Ñ&·µÊoõœ0~Ê ö÷&·kŒIÇ$à{Àá¨Ä“¥I-2 ÿ¥³»$¶ÅôOþŠ>æ-FaÁ£èƒÔ²0}y]Ž {«€åaÞ’Ð_¦¯ ÛXžkCu˜Þ.f[Û[†á­QeäP:ŠŒ¢ã2\6ã²ã6ŒR-‚¼×šm&„+cŒiŒ—¢°¸TåïpÚûÒR‹S€·¦v¸ß\$ÂÄ/pÍð4”$÷àtôX=´5Hnjò·;ÅÁ¨úà–d æßGc¢¤1¦8ÌBÅ)žü8…›tüUt}#ðÕĶ˜Þá)ÀÅü½fŰôñs«ÐmúµR¬C¹;¯GT×7 ñw1z? Ìýä‘?= óÃa8zéÏD^çùá¹a¸žAÕø4ðÞ&Ö7}Œ+cŒ©ŸwŸAîØ_AbQÊFϵHÚ}Q,"g +,7¸‰l·#Qêvš÷†/ 1àmè¾7ÆcL‹™’˜—ûö!iÍù/1‘e7$}²õßèÅ®9…4Æ$ïßCžƘîâRó0W5Í3åZ‡òÓJ&“Uý5é9£ÛaŒ1Æô$ÿ «÷´´æŒáÈ®v%3o%ÓXUBa•ÝÊ TQgíËr”“az­•Œ1…ã£è¾Wì4oDÇþw© 1=ËrtÍImˆá§è\œÚcŒ1¦×ظýÑþØ>­9ëq+²mÔ†ÔÉó½KP˜`7³%ð2o«ëç&µÈÓ“€Ÿ¡û÷j&^²Ý4Æ$à:tÜ_œØÓ» £kÌ÷uz~‹ÎÅsÆ[ÐcŒ1õótTi¥„’ôM`Ù–Ìëkrb[á]ÔL VƘ~gMžÂ¹^œ“Ô¢Úì\܉’w#¯E^ w —˜Ñ´æ´œ©H¤úú¸xp:½ãQfL¯ñàÈó穨؆i?^¼ øJb[Lïò"àÜq–YŒ¬aäE¿47o°27¾ }ÈÌ3‰HCH@`¬57 Ï›ÐÔÏZô~±Ù=Ñá•(„2?œg6Y5äéa¤# ¡ýß1t»„þVd:ÃoqaÓ¬Œ1ýÌf(ÙîãP¨ÂÀ%I-Ÿ·Ÿ¾ Ö” 3ø°ðaàĤִ€SCÂøŸcP¸ 1¦XL~ˆî×OalCÖLœ9è?vÊvOZsL³7J9p%¡·Ý6¨`ÍÖd‚K'EÞFKB=WFÂÐH^ŠÄ¢UeË. Õ„¦µÜF™ |¥F8¥‚0fBÙ•ÐcÚÉ®ÀùÈKéFäaÕ B³Cÿ÷I­hŽuHx»yIo†ÌÝ Œ< ö®A ‡¿D1ó£Ó¯”ÐG€­' ÏÛ°¾w…iœç#/Œ¿b±Ê´—Áп 8»Ê2¢û|}@ÌÍ‹S‘y¬_At‘†É<²ò"Ôò0¼l‚ûÐ ¬È ßÌ cŒ1¦KyzÑ(!ÑdAZsêf*z)*›'¶¥|íËSÒ ü-¥Ð]„rcŠÅ(亄„eÓ€öé äšß ¢¤óñZ¼Ø-©EƘrA÷è:œ¥^Döœ3¦Ý¼ ]o_Hmˆat.º9……1ÆÓ¦ßDœkQ·ncÊTB €^a&ªÖóÇôÏ!Û÷‡‘ :-©EƘ<ÿƒrX•PÓ8ßFÇïøÔ†˜¾àýèzûhjC ;£sq}jCŒ1Ƙ"3•Ô-¡D˜¤5g„öáFz/„ìp´oW¦6$sÇ\ô¶ºØ3©EƘ<¯D:JÀgé>ÏÜ”LBÇnûĶ˜þà“èz{ojC CdïÞÆcŒ©ÀÀÕèóàñiÍiŠKÑ~¼&µ!m`*Y’ã§$¶%OnBÇ` ð1TÞ“ž—å><õ«†™Ê<³«Rbú†¯ k.¥'ý0+áï‰åè| ¥6Ät/½ö•Þc"ÎGÕô®CžU·'µhâ<Ø ÙÿĶ´ƒµ(§Ó‰è%óoiÍIÂÀÎÛWÝ1ÀßÚeŒ³CÿõÀ\àUH\6Õyièÿ,©¦Ÿ˜úõzõÌDâÒòxžúƒaxv.Ÿ7;Œ—Ï"óÂ\ƒ*;/E•©—äÆË»áÜp~¹µ í}ñXìˆÞŇÛbº»5cz‘ç?F/.B/ÍK’ZÔ{oB_{‘Í ·x°8­9Iy2ʹ¶ÊŸs Ê˱2¥QÆöFBæç‡!Ï+³>Sgó†À®èÑ1íælô±ç”ì; OÕD©v{Y…Âü[áeµ‚±‚V¾ ýòáüx+ßæ¡ãU­›_eú®aýç¢ôÆ4Œ+cL¯q pò = x-°:©Eͱ/Ýî¶£·E‹ŸÇŸKlKjfÞ®å›Ðµ|AJ£Œ1ìüØ5ˆBJ3–}Ñ×ÀiM1}ćÑg½¬F‘´" /GâÐhÙ¼ÀH…y#a8Î[¶=L¸¢º¨çÇáG5¶ÛU÷mÐÄ6LcÁÊÓ+LBUaþ7ŒŸ„ª®•’YÔ~ì‡ÊªŸœØ–v³Úß›P§ÛÏ]+Ø8… ®¾Šªm¤4ʘ>gô¬Ú¸…œ;Üe,_Þ |œìÙ˜v3xKè/–‘ Qå¢Ô2äÅ\4.ö Ã_Eï³QЊÂׂÐ/δЦ(âU g.›¶,7|/öD5M`ÁÊÓ LG!T/G!eÿÆ»'£|NÛÐû"Å$àTMjÔ 4úJ{B覷¢<:¿Oi”1}ÎvÀ€­€ËQÈ˃I-*“‘WÅæÈ³âŸiÍ1¦kØøwn|¥L˜ˆwýLÆ·¦º•ϧµcŒ1ÆLˆù(ì „¾èìŸÖœ–ò ´_'¤6¤ƒ¼íóOSR@v#«¹%ªLj‘1ýÍ–ÀõèžüjXU{-!qÝS?'£{gUè—€£SdŒ1Ƙ‰³5JäZîDaS½Â®H”XB•Þ¹ë¯Á¿JLÞ‹¾¶–Pž$µÈ˜þfcàJt?Þ‚¼aûÏ¢ãÑï¹i„ÈK³„ò–FÁª+'cŒ1]Ï^dèW[¤5§å|•þ}áÿ6Ú÷F§ö;‘½Ðþrõ7Ætžù¨QYîB÷g?s :OImˆ1]đ边øOýÇ&´ËcŒ1 rÊçT~ÌMkNË™‡\®CùœúNr'ò(2•™ ¼%‘-¡Ä¦/Nj‘1ýË\²ðôû€Ç¥5''óþœœØcº‰¿ {çuÀÕd£Jè#¦1}‰“®cº‰yÀW…ño¢ëk’YÔÞœ ü%òíG®öÎNlKÑÙø:*#ºvŽÃUyŒé4Àɪ€ªöÓÙ(Þ0 …­Ï ÃG£0å‹ Ã2a™8<™±ÕÙ¢'IžÕHŒÏ³"LÏ=PÖ!¡pp7Jú>:¡=4¦ó쀒­ t±ÈÎŽ(?Þ T•´×‹ï³¬Œ1ÝÂÞÀY(oÕràÀ7RÔF®vÎMlK*ÞœF‹v0 x#Ê3åÔ9¸)¥QÆô!Ó‘WÄa¨‘ùbTM°Óä…¢Yax‰MèP~xNžº0-?<˜ÒÉh’a¢y±1Vкx(™uÆd|9}ˆÍóGà™aúé¶Ë˜äX°2ÆiÀ‰À{ЋòÅÀQÀÍ)j#û¢°’;çÌÚ¤Ö¤cjLÌF_ÿ“Öœ®aàûè«ìràXà»I-2fb,^r³]‰Bdnd¬gNQ™‚¼.ž4ÎòèY7LTŠãórãƒH@šMæÙ…©ùŒ©ÚEô|ZŠŠ?Œ"a(V5{.úßúƒV†n87¼Ž±Âפ 6GO®<³Ãôô@ûýýÔ†ˆ Ñ19*µ!]ÊŽÀ5è®^›Öcêbºfw-›>ìƒ S| ø;Y•ÌNvËÑ…›‘WÄŸó¡çÉHh{/ÊÇ÷ ”“p?ò³IæãJzEâ©èü^”ÚÓ7ì€Þu—#‘<ò]t-¾<Œoƒ„«QÚòkLáp¹pcLQ˜ ¼…ý-DÕ~ŽA€~ î?¸|qž¯¢„ûoF‰ŒMc\B NA××(yë±ÈsØ¢1ˆB·–²¾GàR$b_˜›6åûdlCn&cså†OE^S‘٨ј÷ba}¯¦Ø{©—‰^wsk.eLëx-Š*øc+. ýy¡+ðàù¨Rö)2ÐcŒéw&£°¿øõú'(V¿Ÿx Ú÷kRR0¦£äë%”»ÁLœCÑKp Ýo{¦5ǘŠÌGר«·™N³5ºöng9cZÅçÑ5·WÙôSÃô7禦ÖÓŒ1ÆótàodBÕßQ?ò;t ŽMmHù0:6g%¶£Øå‚+¡?o®½¸1gKt}ÞžÚÓwlˆ®½ûSbú†ITNðÿ t-Ÿ›6xZ'Œ2ÆcúsÉ„ª›€CèßB¡r#6ݦ‰méf ¯ºëÐýw6ΉaŠÃvèºüOjCLß1‹,ߟ1)9 ]‹ïOmˆ1ÆC»\À IDATÓOl|XƒþˆïCÕ§§4ªD—ð/§6¤À|£¤6¤‡x1ªÀXBÕ)riŠÀnèš¼:µ!¦/‰B~¿~@3Åàxt~"µ!ÆcL?0„þtGÉ*,ˆ½‰@_t—P¹"–Éx2:Fwc³•l‰*b•€ÕÀ{pCͤe/t=^šÚÓ— £ëo0µ!¦¯yº?ÚcŒ1¦—™Žþt@¼k‘ú2ŽAÇæ/© éþ‰ŽÕ¡© é1¦ŸD%³KÀ/Q.cR`Áʤdºþ6Omˆ1ÆcŒiû¢J;%äbÿ`û””(–Ú.à(t¬.NmH²?J6\B¶}“Zcú•~Êaµ/ð'TÎþaàßÀ;Ê–ù0 ÏS®­°½ ÿÛÍrÓâsó¤qÖ5âtŒvHmˆ1ÆcŒi=ÓO‘yküçÆ©ÆÐ1º‡¹ÕÙ·ÞãÛÒ«lüãµÀ)I-2ýÆtý-NmH›yºÇ!OäW_Î)[®’ÀTMtzg˜÷–Ü´Xàd÷0~8ð¼f ïq.GÇkÏÔ†cŒ1Ƙֲ3pY>œ÷áÆn-bÙâ“SÒE| ³3SÒÃLAÉí×¢cýGÆk:Ç$ä%´ŽÞΧvÚÇòÜ…¹áRY—Ÿ«3Ãv^ <* Çó9ÀJä¹Ui]PŽÉ³Pî¦Qà<2­ø¼Ý‘,àñaÞƒ(ÿ]¯q!ÚÏ}RbŒ1ÆcZÃ$ôEw%zÑûðø¤uW£ãõÌÔ†t[¢\h£ÀÂĶô:Ï Ëçr? 4¦,F×݂Ԇ´‘Q6Y˜ë"‡£ãpgŽDÑéSaø ¹y!çsëŸXaÝH¬Àz ðnàv¦òÄ*G£PÅòÛÞ­`vÚ·Rb 0-µÆcL·³)ð²\U_F•ïLm¶DÇl¿4Êû¥ß´ óÉîïO¢$íÆ´“ÿ kn»Ô†´‘À-ax*•=© zHàšÐÙ¼·…éÇ’=+w®±½åe¿ïõiÀ<äiy:ð#à×À}HÀ*/¨oW»Šâ¼’&=ÏÀyŒ1Ƙ¦9…ÄüÆ`ûÚM|£×¤6Äô5{“å5ÆcLƒÌ¾Iö5öä‰aê'z¥•Ú.%†SœÚ>âÉd êÅÀAiÍ1=ÌoÑuöœÔ†´‘g#Ñç6äup/ë V+Ðýö’Ü´(:½ ‰^¿f¬xÙÿóʶW.X¦ý4ØpB˜E°/„ßX¦]–¿¢þ]í*¾Èú‰ëé4»¡ëðêÔ†cŒ1ÝÆ6(k …¼.­9]É`jôrŽ–vò:t þi¼MK~F6t 0#©E¦‰aY‡¦6¤Í쇒|¢W/[æ (üïÞÜ´¼èô¾0þ­Üü7‘ V;”m¯\°š‹BþîGÿIw"+†ª¿4¬se?%Œ±®=ì>b1‡œ›”ì‚®ÃëRbŒ1ÆtO$û|ðè´æt-¡cxajCº˜` :Ž»$¶¥y3]K(|kû´æ˜ã4ÖO&nL'8žÞM(oº‡˜çôöÔ†“šÉ© 0Æt /Fåí7FáOG‰qMã¼(ôÏOjEw³øz~[JCú”SQˆàM(ÏÐeŒ­bfL3,ý ’Zaú‘¡?;©¦ßYú.ÊcŒ1ÆÔÁ[Пg øNtÝ “Q•¥ÊQ`&ζ(Ì U3g.ð²ð£Óq•PÓ<Ç¡ëé3© 1}Çkе÷Ô†˜¾f]‡÷¥6ÄcŒ)2“Ï‘å«9!­9=Á“Ðñ´wZk8ÏãRÒç¼y&”€k€Óšcºœcеôõñ4¦Å†®½¤6ÄcŒ1ÆTgø zq[™Öœžá#蘞’Úa²<‰méwvAbU ‰WG'µÆt3“U®3¦“€®½óRbŒ1Æc*³¸½´=<#­9=Ååè¸îŸÚaÊŸT€E`pYˆàY¨*¦1°/®jÒ°ºö.Hmˆ1ÆcŒYŸíQ"åp °SZszŠXõe90#±-½Ä3Ðq] l”Ø#ŽDç£\ì‘ÖÓe<];W¦6Äô{¢k 1ÆcŒ1cy*ðzY»Ø$­9=DZèØþ$µ!=ȹèØ~%µ!æ¿lOæQ¸xcZsL±ºnîLmˆé;v ÚM0 Ø9×sÉ3œ~;p#pðÉ0m#à—(,þràñ¹åw)ë7ÂL”OÖcŒé{^†•%ฤs;8ßW¥6¤ÙX:{‡À—ÈBŒ+:šñ™®—òF£1íÆbi1ø>ð àGdùZk VóÃp yÿð¬0¼pinùK€g ýz¹]‹[6°Ž1=ǤÔc Á»€O£¯8_Þ <’Ô¢ÞcXŒð›÷§5§'9x\_”Ø3–ƒ€3‘Xu3ðRôuÚ˜j< LGÏÌÕ‰méWæ Bƒ²ñY¨ÀÂ*<†‡Ñ°UÀ’Ð_Y6\T‘ýK±¸Þü8x?ð+àcaúcó/“]¿@BÒQaˆU(:á®ÜòsíÂðæ(ßÿ‡£ê§¿ªÃ¶k€]Ýð1}ÉÔÔc’2ø<ðôçûnà³I-ê]ž‹D«`±ª]œ¼8}éücZsLŽs€+ÐWè'£¢o¾“Ò(Sh–#O†ûÛÒËÌA9æöBaLE…W6@‚a»F ýQ$­Bâ×2$hÅáUHŠ…þyXÕûÑrYèÏmd‡Œé5hL²1ðsà‰è¥óÅÀEI-êm&‹€Í°kw»™ Ü€r>¼ ø¿´æ˜*¼ 8 ¯¿‡÷$µÈ‹§4±~°V qb$7m˜LÜZÆ—çÆ—’yìŒ5»HÚåÒÙ5ôwGÿý匠jŒ—¡ÊxW¢wƒv…bNB‚ÕzÌý²á!2áh0 ÏF‚Ø@nx&ËæÒšñËö6BBLïÅÉØ?å_\ŒBßI&²^„®·)HÜú&ºV¾ l–9…VÚv½ü}äyf6¦/±`eLÿ±#ªd² rK~>pSR‹zŸ=QCà`«Ä¶ô/G¡fw¢dìEΗÒÏ<åÿ؉U‡"ñÊ7êÈcòV$NÌ ýù¹á9¨!9'× ¶Áž~CÕ–’åoyt —­³:,“§RØZ¹GØZ²Ð4ÐþÍ Ý`Y~Ù´5öáAàêÐ]þ—®§·rVNe}o¯jž_³ÂpÏ¢H¶+ª’¼ p[G­7&#>_œŸØc’á@cú‹§g£„©G‰©ýõ°ýÄà~áè ßCe§÷B_E?V{q“ˆ+PÈÅw€ç¡œcÇ_Li”) …þ¿Pøh£DÑb6™ ®(þDknX&.? ³sËÄuŠÎÃHL»yó^úWÓ¹ÀÖ"º%Mlãj$X9ˤduèì`bú VÆôG *]3€Ÿ¡¾ö<é †¾«ÎPBÂÇŸ÷ Š<ýÐPëF£/È'¢ð‰S€'¡œ"ÎÓßIJòsj.UåŒõRj–¼WÞËkZ˜?…õ­Ú–§:oÆ^¡ýÈ»Ð5ðÙÔ†˜¾ã^tíUªªhŒ1¦ƒ8‰›1½Éf¨ºÈã€ûQ¨Í%I-êOPŽž™ÀÀÝiÍéKÿA •÷þÖÓo>ƒ/b¨ñbä=u)ª˜X5è¹3 y§“‚A”—ÎcŒé)vnG/g×Û¦5§¯9‡¤6¤Ïy:—cOnãéÀ=èüÝ ì‘ÖÓAŽDçý»© 1me*¼y#88¸½Ó¬B×C«º[ïž…ÄÐGŽ® lÒs?öô3ÆUé1ž ü½x]ˆÂª¹†i'1ÿÎyI­0_D^ |;­9¦.ö~ <¸xð”F™Ž«D>ø&ªj+ÿ¢P÷¥ax%ò¦YÆ—†ù«;krß2€By碊‡sÉ**¢ê‰q|`#äu¼jŒ×ññ°ïEË÷åú÷…íÄj“Ãoç«0n ìŽÄ±­CwH˜Wn È˘”Ä ©#5—2¦ÇqH 1½Ã«€¯¡rØ? ã.oŽIÀmÀ–èåøò¤Ö˜#‘§Æ"”Œ}´öâ¦`LGNchØ—PŽ£5É,2ífà7Mnc-•E®(lU¹VËÂ:+Qc±|^ /"E¡)Wšj-3¥ ;Ö"O’{+FÝž×÷w!AªUï4“ÇO@‚ø÷æ \üاE¿gL£LA÷F {†›>Ç‚•1ÝÏ$ä>ÿ¡0ü)à}èOΤcOàŸèÅ{ |>R3 …f>U;)­9f‚œ†–‡âÜp½ÊÞÈSøz”x}˜D“ÐÍf…áA$¢ Ae.í‹$XºaÆŠ\åŒP]T]†r.U¢š(¶ŽÚ9m¢WQ5¡)ŽVÛÀ‰ÂÞrtL–‡ñ(ö-Ë/f¬uÅøœŠR*ˆ*Sž œÔ"ÓÏ ¢{i)™‡ 1}‰Cén¦!¯ª£Ñ‹ï›€ÓSdþK>°/ãýN yä\€rZJ¦»ø?àjàgÀÓ(|(6Lo…™5À7&¸éHä$¶ÊE®Ùa<Š\³rë „ñù¹á8}%ïVò"Ršâp%¡©Ö2Õ„·nb-p%ò¸‡š´DQyyR+Œ)¬Œé^Ÿ Êg#Àa¨’–)Q°úyR+Lž ÑWó— /èoHkŽ™ ±Qùàù¨òã»SReZN ÛÕÄ6V‡®Õa|å"WÌáTN­p¹!*G:D/©JL ¿WèUMhŠã®A~Qø6ºîŽJmˆ1ÆcL·ñ<äê_þˆòj˜âq6:Gÿ“ÚS‘…dÇ~‰m1­a{2¯Ó%À‹ÒšcZD‰ñs62ÓLE»VÒÉPŸz«Flz*ð­Üø™ÀSX¿¨ü]sÏMmˆ1ÆcL7ñZÆTB‰‡§§5ÇTaʲØ,±-¦:ïF÷Ò¸dt¯0…F‘ãûÀ&I-2ͲËZ¡žõˆ,#À'€Ë€g¡°í_×—Ï-» JæðiÆŠ=å¿•ŸWk›onD€ŸD¡¬ |lgcwµßAU¯önSç> \‚ÞËQåTªØTkŸNÊ|©Æ¾t —¢kn¯ñ4ÆcŒ1JÊzY#ì£TNÔjŠÁþè<ý3µ!¦&3[ѹzub[Lky3jÌGo«7`Q²[¹Ç k,S`µ xynüH¸رybÎEaÝ„þÊ¿•‚jmó!2èF’)×ú½•evž››Wk†rûÿ¨ñ{µöéJ`‹ÜøfHëvâÿÂÖ‰í0ÆcŒ)<ÓQ%¬Jýš´æ˜:ø:_Imˆ—#йº'ëî5¶~A&ôÿ5¸Mwq:[•Mÿ<L®$ó ºå§«ÄJ$RGÈ­s%psnÞb²çÁL²j…P[@ªµÍ_ Š±/ÏÙÑ Á*o烹yµöáÉ(¥ÀµÁÎå5~¯Ö>=ÄØðÊÉôF¾±z¼úŒi73©\yÔcŒ) ó?¡§eÈsÇŸÛÑ9{RjC̸LBÞ%à‰m1íáedE*V£°0Wîìb^²ZbãD„Ÿ¼ SÎx‚UôpžZ¶ÝZÛœ<ø:ðû*6U¢ÖïMT°úð´*ó9N›"“‘Gc73€®·Ññ4¦Íœˆ®Å¥6ÄcŒ©ÄÖÀudÞ»'µÆÔËnèœÝ…Cº…½Ñ9[Žóõ*ƒÈóñt®o^ÈØÆ¶)&CçìÉ5–™ˆ`õ#à¹ñrÃç’… ÍØpºO Ã//›Wk›±šï|äµ|ÆOkýÞÊ2;Ï©sî#óX{KÙ¼r›jíS/†n‰®·ÛSbúž“ѵø®Ô†cŒ1åìÜCV¢}‹Ú‹3 %FÝ%DÝ>ŒÏ¬µ’i Ç£óvFjCLCü·Ó[¼Ýù¨÷tTð‰HÔÜ؇œtš'¢äÑ1LpJÂý=ô5ûHàäͺª‚öxôÁ`ÛÐÍÝ´ÛÞ¯ü«gÕXf"‚ÕFÀyèÃÐuÀÇró¶E Æ+%,?¸årú ë']¯¶Í‹Ðÿùµd¡ýŸn¢vÒõZ¿7‚¸_ìÍ']¯µG#A¦Ò6ËmªµO•’®¹Æ¾tG×Ûe© 1}Ï×еø†Ô†“'n6¦LF —_@_8¯F%£¼d¥|7·Æv–¢Â¥È]YÙ´ñæÛ5¾~þŠB-^Böµ{6:‡óP>‚tÎæ†á9È$zd jËGÈ0Yòèu蜬EA«Q£Û4Çö¨16‰ÿª±ì|䉵!°)°1jÔm¦m¦oˆ*FÖÃ0òrX‰ÎsF÷ßJtÞW„áeè:Y‰®ƒåax$Ì[Io]ƒè~™º!²û)NŸ_6>/,—Ÿ6ØÛFç(ޛݻ+Q²ïx¿Bv?†u׉)1œ*>Ýv/ò ä w NR0ÌØDåEa„´9nž‚ÓG‡ñ3ÑÇš‹SÔö~üxnb[Ls¬CžŽÆô-v‡7ÝÀ·1¶AW­W­á¶4·Nl®j¡¨1”ï†B—¯´Ì|*7¤vG.Áã±íÓ´ßqÛóPƒy£ÐM”µdÂÖtÜ^ÜÛÄ6‹È,²óXKdšæ†é³ÈÐOBѯ"±± äJĆm¼®W 1k9Ùù[‡^±ÿºGbƒ9ncUØNlPWÐÆãÿÐ}íˆÛÓFrv.!ßFÂðpΦ(Î-ÍÙíi–9è˜ü9Ø;ؽ°L„ÚÆD(Ð~Ý‹B€V£kd€Ê×O;ÄKÉÎå•(ÉüêÿƉè…újwsþOè×zÎÎ…¼H7+Þ2ô7h±½ñ"^“Ð9Xºß“%’žƒ<©f„e&“ÝÓóÂr3È®‰VÛÚ(ñÞ\ØÃéþ$ØËB^R+L%þ†îÿ…èºÛœî«@ÏzPؤ1)‰•Q%µÂ˜`ÁÊG…áV7èJd £ÔöZ˜ÎX¡i~Ùøô&mYG&:MF.ùKʺ‡*L[Bí¯ë3Ñ‹~%Ñl^Y¿ÚüÔË7Æ>¼ª©=Ë~À‹‘Èqé8ËÖb2jtn@Ö­·k“› ©¯˜((AÖHŽ â¡°Íùd æ©H<›ŽÄØhŽeÓ;Aôøº}•Î{ô|xen¼VIúfˆ¢[ê–R] {8رLÄÝõCg_:ÎoæE¨{PÃæ~àî0í¾0ýð›õ0„î³èM‡‡¨_HÞzå¢XX¶Feê[Å{QH($¹œ:.QÈšŒD¨-8U¯wÈR2‘iÓx?ÅéKÊÆ—…åòÓ––o¸Œè^käcƺ†â= Ù=;Ý£ñ>Dö?VÖȶã<Ð5´ðzÖÍÄçÊìšKµ—v{Wm€BËy6Jz^"TÛ¿Êp·ßsjwc:AÌ3w[J#Œ)¬LщžA' O«j¸YÔn¸UjÎ$ «k–èí•ï†C—¯´ÌÆoHM”U¡»¿‰mL#°ŽAç¢Vb£òf¼¸¸¬:â#d^> óYIpÚ€æõ†^’yíEï•|HWl,Ò¾óZNlôÆë:6fç¢ç| 9Êõ§ s: Ý/q3É<fP[@åù9…ƒD¢7Ä‘çÒÜÜö†‚msrvÎ'kxÏ ÃC9›¢87˜³/ÚÓì= ¼åÚ ŠÑ‹&zù€¼›* N‹éþ|-ÝÄ2WõrÁ*†ï´[xßÃõóµ:„(Þ»Õ< AnþØâß6ÕYú½ðŽçV¦“, ý“ZaŒhEžNcºž^x™1½Mlõr¤n"~íne>‘Éeãÿ)>Q¨pc²ØDÐVzFšâR„¼O­¢hûRoÅÀP¨l½ü82 W¨K…=¬Œ1¦`”7)¬ŠEÌÕÒʆïx^¦¸Dªr»ø1d¦Äû¶‘ ‡õôz9¬²‰e³Å@Š@=‚UCKOh`Ù§¢PÙz7ð”veÆÅ9¬Œ1¦`X°2E'†°X°*íøÚ;N¿ûˆ Åj!¾o‹A¼o[Ê Y˜n;Ñ›‰ʤV´†Vå°*µ†Ëæ} ¸ø'ÊÙ&L»‚õ+. \\ \>·#srãç’y[™Îâ@S¶CÏçî3 V¦øØÃªXTó¨i†(v,«¹”)"Õ+S,b…ÍV']·Ø\LzI°jGÞÄr¦ ÂC…$¾›÷…0íqÀ ŒõR<x"ʹx4pj˜þAtö^¦}ø,°ð*à´Üvö.Ë_<£¹]2Ä!¦|¸ ׯÎaeŠ«bÑŽ@‡u/Õ+ŸÓbÑ®¤ë›‹I/…V{Æ|xfŽÞLoÀï|?ô¿‹¼­"{Gäæ})7oG”§j#”è~»Û6c«ßæ¸-[ºþÞ0Ít–äѲ’̳ϘÜúJj…1Á‚•)2“é\µ1Síørïð±î¥šÇÏi±pÒõþ¢—<«…¾#7<‚¼™ÆcòœJkÞ¿ƒ¼¥.B÷V­¼G“Q®ªjBȤ²q{-vç¯2EáÆÐß>©Æ‡š"ÓŠdÜ%àÚ²iÞØÄ6Çû½Ø­B¥ÜÝÀºå¶ŽÇ‡iß¾T#zS ¶h{±áàò½ÝG¼7[®SªÑµò7®-ïåû¶]I×£0¹²æRcéöc} ÙZ,~’›7ûZ±n9ñY:¥æRÝA«rXÝA–cê0Ö?6‡…þáÀ_sÓÿZ6//,Í…áוÍ[ÍØ;Þߩ̶Mrã›wVÙÓ>6 ý’Za ü'ôëýo4Æ“ˆ-ÑK|3/n•­lTú½;Ñ‹íGÃøÅu®{8ð¼ ü^3û2‘œ6÷†ßݸ‰ßÍs^ØÞ ¬“â¼– üV·7Ò«± ²ï¶²é×…é»L`›‡‡îΰÃs]«¨t>{ù¾Ý7üæ_šøÍJܶÛHøR7ëç¡0×›c7oÑoMdߪ±[°åêm/%ñ=àöËÔã}Ê s)Ê1Užtý“(±úåŒMPý*Õ IDATº¾m˜“®ç×;:ØUi›ŸnblÒõóгñ:àc¹eOÊ|¹Ž}2­åYèZûCjCLß³]‹KRbŒ1¦6» ö¿šØF-OŠ¼ÇÆpzáE/–›•­wð+æôàgȳàÓeËåï²p€Ww¡|¡—Ò)UÖ¿yf°éfÆVjv_~ìh”ÛÂ6¶žÀº•øMØ^#µn¬ºµ‘^‹ Ão•WTšˆQεT÷ªÚøº—‡á Ìï|öÚ}û´°þE ®7w…ínÑÀ:Ý|¬/B‚U>Ѥ²õnNG•Ëíiå¾Õbç°þuu._d }igìÔ)ž|+7~& 4åeèZûqjCLᘠÌGÿuAÏâg/A–oŽGâ÷¯£wï£ÿ„òî3aÙ|w"ðÞТëñ#ÀësÝkƒQa†Ý€ÍéÆÓ•<=¬ÿÑÄ6ʇ3V<ˆ|?L?x7jpÿ©l;%à²?‘S€[QøÅ‚ÜrÿF_Gž‰Ê½ß’ÛÎó‘çË›€†å®bküÍÓ€“Âð/Z¸/§31/œ«Ãú»M`ÝJ\¶·wëäsìÊßxÃZÁGW¡†Ì‰¶]>^O¸[éµ óËÓnÁêÇaÞgBWB/†õίtL{ù¾m—ÇÍpØn#áÁÝ|¬GËl­´oµìiå¾Õ¢¯¤na&Ú—FÂN%µ`ð[tOl†Mçùt­}5µ!¦)æ ÆmQn»§û!Aò(ôÁù½Hú zÎyCþ…|_ƒÞ­BÏžò÷ª"v+ÐÝUèÿì§À×·ç»c÷æÓ]ÑûŸóZcL“< =„ÿ>Þ‚5(oT›¶œõþëÈÞ°°;°ðBàèÝèTÁótôžôkào( ãn”S³]bÐ#H¼ºý^†R;œÚ_CïVŸ@bØ[@tz{}…î82oª÷†ß¸õ½®>ÉXϬo qí/è?ãnôÑp¢û6‚òè]ޱ1…ÃUM‘‰¹«:QÖ5þ!½ˆÌó%VŠÄï#¹ñua8ß°¹ ¹¯B Œ|É÷“Ñ Àah¿>KíûðÁ`Côx©§±ZϾܗ³½Q¢7M«\ãvV4¸Þmè8G~W6ôâ™Û…r‚ŽÁ±è|ü/zIŠ<‰ïGñ£Æ±å…¡Ÿ/§þ(Ô^Æw$Kæ: ¼³[Ë©eû8µ4 7bãƒa»\#èÍ!Ë·«“uS•ÀÛèÝû6z§´úkj<Ï–€¿î<Ö—£¯õ!«âT¾^-{:±oкDåÆôý^%ðS¡ÿ”Ü´$d\ŠžyëÐÿI|\Aö !{—˜†Þ ‘5”ÏO/o#è8‚þ[FC½ËW†áaxzWYÖY†GÑ;M~ÛŦ– °Â‰0]Ä~ΗÏ_Ö›þ“ |žöz´Ó0¬L‘¹y¢lŠª[=ܢ펆m¾„,!ê¹è+Î1ax àHäõÓh¸À à÷Uæ•P£c5–š¥ÝûR‰¸Ù5—ªŸé¡¿¦æRëSë8ÃøÃzƒååÆ+QOø6º³‘>KÑËæ\:— ô—èËe|ÁÞœ±yGÆ›_‰^¾o£€1§æR¿¾·òÞ-ò±> ][çŸC÷ÏQŒmàÕ¢ÕûVظr¸‡1õÓï‚Õ¹À‹‘ÇÒµÀ^(¬noKÙÐ Ãè"ß•O®2mcß«º‰•è]¸™gö ²°ÀF˜ƒD¬ÛÂø¬LÁ°`eŠL =@w@ ¾[Îrr/þ*Yæ¨aóô‡}ðgZ'’EŽCâĉÀ7‘'O3¤Ø—Ø kUã7zX5ê¥1Í4/@‰ÎO¢>¿zÀÝÚH¯ç·`ìõϿՎ ý7…þOrÓê™ß(Ý~ßæ¿z·Š¸­Gj.Õ8E>Ö¿F÷çQøÇ*«(Öê}«FÜmÚ¾1½È†¡ÿ@Í¥z—è‘ù.ô¬‰x{¡<™[‡idÂÊl²ŽsÈþÖP[`ª&Dõ3khüãO+ Ý ¨½5ˆþ1ÆÔɯQ£÷¹© 1ÿåtN^Ü¢íݶ·iëTÊKS>í5èèTB¼|¸Ê²åãF^P+PÈñ–Ÿ‹r ܼžîD ΧUY¾œfl­´í/ Ÿ{[hc5bÞˆye69çL±hõ9 Ûë÷FFQAç§Už°©˜ƒö£›Â‹MwÒêüœÝÆ•hÿ÷LmˆIB¿_ÿÆ3aNCÐ7¤6Äü—ߣsòìmo"‰›M1Ø»EeÓ-XÕ蜴ÊëÆ‚U±é•窯3Ó)nD×Úö© IDüx¸IjCL~ƒÎÿþ© 1¦œ‰V 3¦SĆp3ÕÆLk‰¡_­*]ËW×\Ê‘B?®ÿW,X‹ºÙª¼FsC¿Ñb ¦3Ä\.ój.eŒ‰ôs«)($²„rdšþcièwûGÓƒX°2Eç¾Ðß8©&O 1iUC5z|8Éc÷±cè_Ÿ›…ŒnM~Ú«Ä<#­†ãûÃÚšK™TÄÜb.KoL72 V%úS°Ú=ÓÀÏô~% V­ªÖhLËpÒuStâ—ž…I­0yâûVÑÛ£ÕÉíMgxtèÿ'©f<…åÞ‰…á~!æ|š[s)c H¨š”î7bà}5—2½L »¶‡•)ö°2E'VkÙ°æR¦“ÄP+’àFïªU5—2Eeóп+7Í!Å#æd¹©…ÛŒ^ j.eŒ1ÝA ›žUs©Þd£Ð·`Õ¿8$Ð V¦èÜúÕ\Êt’Væ°}{Xu'ñ«ly5BpH`‘xLèߨÂmŽ ¡y­Ë‹eZG|¶v»G]«CЩFüpVäçÙ -ÚNùûÛf¡O‹¶oº V¦°X°2EgqèoÔ ™LCÓšl/æÕ±‡UwRI°2Å£‚8d»ÈôŠÐSW8¯Ži7QÄ™Ss©öÿ~-Z%X•£¨¹TwQªÑµò7®máöRâV¦°X°2Eg1zQÝç\+­®]ï-Xu'›†~þ«¬ÿWŠG»+{À—V?«éuR Vãñä9y%pv˜¶pp ð×0Nóʉÿåw·ÐÞÔºEeãG$³¨Ø8‡•1Æ4Á=è+Æ&ã-Ø%ÌB/;ö¶¶æSì2ä[¡sq[‹¶·[ØÞÕ-Úžé³Ñ¹+oûœ¶—©è91Ÿús­\ÎÉ-¶å·a»Ïiñv‹Æl`Kô¼Þxj~ zv/ x/ù­ö$HÅ–h?nOmˆéyþ‰®µ=ý~=ârù2?Ž ÃÇçÖ9¯|;ßEûþʺ,í.®¥ú³pàG('ëò0¼ ù½äaõ ´?Nl‡1ëaÓ <€Äª Iz4„¥y¨’ïÏ/ÏåÖ­÷¾[ƒ^*V£°Ž‡QRÐU(/ÉÊ0<æ­Ë.G^iËPió¥À:ôõ¤, ýá0}iX®žœC­¬YXá´m¯èLAÞ(£œ¡c°ë5ŒþŠBÌyQþE6Ú8 S‹YèÅwzv,¨0¾AÙ¼Z‚ö’Ðn..E_ã·E×Ô--Þ‡^+Š1x6ðr`wtü’僪‡øÜŽÏéx_Ççì0:ËÉžéñÙïù%dÏäøüÏûø|¨õ¯‡Ð}>—Î>·ãý_~¬ã‡‡S?ñ¹:·æR­å¹á`þvžÁÐoUŸª¿;Œ¦Õ;¿—p+SX,X™n &Ý6tõ0‚÷!kqè†h°™§K#ž 赌LtŠý%eãùááܺõ&†FÓÑ1ˆÓ™¨a<†g…y³Ã²sѽ=5¦‘H2öu>Í‹d­jÅ?ÈÈ„–z=jyŠÕ+àt‚“£Âð#(Äõ>ä™t?:wõž/ÈÎOÞ-½ÝÄcœ?ÖQ¼Ü¾lÙxNã~AûmÂØRÖ¿¢xR.~­E÷׬П†g¡c= íãÙ½ÕJF‡B·$7œ_\6/>c*¯‹YÀÎÀ^¡Û‡ö%ÔE1f„®BM-¬Üs) šËÑu=¦†á C½ÖAaŽßE!­ñ£H#ÏŠøœŽÏåx_Ï'{ÎÆgz|†ÇgöÙ3 >ÿãó>^ƒy‘l2Ù1¯”G¬Û®C&ÈŽÖ\ʘæ‰÷y#•f5²/Úz!pp&S.Ê-[k^9ñùÑoV&Ã9¬La±`eº;€]ƒQ£d#²FG¾‹Ó7@/ús¨dÔÀ¹‰÷†în$(ÜæÝE&:u’èiÒIÆÉž¼Ö V£èL ¿7›Îz••{5#–ÁØp¿MP®²=PÂÓCÐõTÍc©ÊƼWG¥óVMÔŒ"õˆ›‰e•„§òµeèØäàC9Ûò ïiaýho´k~nßòúieÛŠû_°ÚYM4Š¡QøAç}%º~–䆗’y€ÅÜ++<µºÐÀ’\ÿ.àwaüàëÀ¯i}Hiüÿmä)eÊ…›‰Š2ñ:È?‹fÒÚçÄUÀYÀ÷i>éðáëij»üØÄc½:ç½ XÅpñVT¥5¦ñ™¨u#džþ›Bÿ'¹iõÌï%Ö¡ë=¦1ðÿ‡) ¬L7CyxŽ„îÖÖ›Ó×´:é:d‚Õ:×èLÄ㨚€™=öîþC÷6ˆ!ó(ƒL €L¨ªô‚«A:㙋k.ÕŸÄ_ís¹'ô7­¹Tk‰¢b§½O»‰v<§éu¢‡Õ €÷WY&¦XFšœ÷´&˹$7<ŒÞa¯G/Ûõ¡ôsÀ À‡QµÃS÷GÞV•>¼ÄüU÷T˜× ìZcÞbj'Po~¯—YŠÞƒ‡°`e „+Ó Ä€v~Ù2õ=–Ö\ª1b£ªZõ³v“€»ñÛ81ܨR‚Z»–‡Øku.§*(' )ñ#O/„Ó)â³òAàÏdž&±ÈFÌ?7æ¼²W"áêàß¡»1Lk¶ˆH øpp®v¾Š„«¡î|ˆm¬ØÊ‚¦;Fÿ烨ȕ1…À‚•ébÒUWJÒm+ãw7SH,X™n ¾<ôÂËw'Yˆ^Œ†i]ÒëvV)CMs¬A‚ò Ɔ2ø¥§8Äû«ž„¾a¬¶ ø‚U,Ê0HVe2zJDA$VCF ÙX%²è%«ø¿à|*¦ÝtªJà2àÒÐ噆ĪɄ¬]À´eèž—[¾„’®_‡òf–W5.¯nüª*ø$àµÀ£Qx`|Ÿ»ÓïÄw·¡šKÓa,X™¢ó®ô»wÕ<Ö¯xûU˜¶%Ä—•%dUÛÊ»jóòǾ9¬,n´†I¨Šã<ô⺔ö7XGÑy›‰«‰0X€îÙ|·`œþÔ@Y„<œî ãw…þݨñ±Ž,ÉîŒ6íÃ"ÔÚøG ·‹ÌEÂÒlô…¦y¡›ºù¹ysü8×kU¯ØŒÕ#£¨‰ùeFÃü˜¬9¿Ll`ÆeÖ {¶DÖˆŒË¬D ê¸L-z)éz;þsŒ©D¼¯Ú%îÇ”×ꆲé“mxµ íÛ [Û„®¢8ñNàË4ŸGËt/ñÇïn¦PX°2E'†LA‰0—¢—ö•è‹ëô€Ø€XBöbKüX¦xz¡_C{_†cÃk>Y#k^˜‡çW™¨‰4´Dû·½ÄÆp£,'¯¶ Ó˜†yè ì6úÛ°¾(½B– ã¼”LÌʇ®Ò¼Zž Q°šÃØ{(ŸŒBò÷hþ~\ú±Áïßá0ocï÷µŒ½·ã}ÜIò^:Q,É )Q,É )Qp)ž&*"-]­êG1\/†ylRcÙfXúïDÏ«+kÈÂkÊ@Y› ý衵)ò"XH{¼âu³”ìZŒ×ß ²  U ¶ÄóÙ³ª™ÄË­ †%Å{#Þ Ó[åY› V¦SD/þÙ5—ê<뀛C÷óÜô)èl7ô«—W7ÎW9ýÄwÂ|õØÍÑ•«þÅïã¦X°2E'6è&1qÁ¥^b£¹! 2¡i½” R­øZ· x‰P¢R»±…i¢—œÈ$²Fny7œy±RÎV¹íµR°ŠÛú0pY(Nl„•»¹G÷ö%džy÷÷¥dBÍZ²ó9Bv>ã¹.ó—\¹—Mì6Aùm©}ý—‡ÍÙõ“fÖñ˜³¾˜¯íieëÄ—ž¡[@{‰çz"ÂØ™¸4õ=zâ¼(Ì•ïk3Œ QaqY÷Ð8ý…èºÈ >›• oŒ®›XÁ¯V^–‰2•,qðSCºÿnDâÕJ$DE[ë½âu·œìœ “Ûe¡ ݒܼåa^ëU*é>âýT.jM%Ëé—‰×LLÄœ_&6$ã23ÐyšBæ%—‰bq™üoT»¿Odç †+Ó)bXð@Í¥ŠC|ÎÞ8Îr“'/D^Z‘Õèýhv˜ïŠÉýCM!±`eŠNüʾ؃,üh½ôOCÖX¹%¾ÔÏ'û"> ½ä†y±LñLÔ(˜æÅÆu«¿šÇ†×²FVlðÇá%U¦EªÙ†V‰¬1Ü(óȬ?¢cÕJÁêîПFóâJ£Dïè™EŽØ¸­W<+ÑF+:-D!•©FeÀ-À­ú·²þ—ÑidâÕ|2u°ÂðPn8?o6㟗òÄëñ¥ç›À»{æïÇ©d÷^‰¦‡å§ò÷ûTÆÞÛùûÚ/ŒÁX/(–ä…”(–ä…”(¸” Oý’ý@让±Ìt$jíü¦‰ßªÅZ”¼÷¨”úè9CVvª°ÎJ²ÐÅ|(ã"T¥êôÜë´ç\#,)ë§ zR@æQ{bÁʘF¨'éz·0 Ø T/DÅ"¿DÏìß Ê„³‹;l£) 4…Ä‚•):Q°ZÅÄ—z‰æF„0FêòâS·'¾ûsÙ—ÇV6îý‹Ñ‹Õ<äaPîÖ>ŸÌ½=6Ö¢'BôZˆýxŽâù‹ç6ŠQèˆ]ÊОeÈK®ÜË&v÷¢Æü-4~ý¯¡ùû& GCd¢V³NBž3å!«ñú 'm'ñ˜ˆ06L&.-c}ž8/æ[Óæ}i«Ñ½ï¯v…¹Ä÷ˆß_ ÃÓP~•=Ð=z™8Õ ajE`Õ…³è™Ü U¿,X™NÃß+U¾í6EÕÀ~Œ «¾…þ½k=¦OAï? !«_ˆû=T¥CùGɼ¶c?~´ŠýèÉ_Þï¦B‡šBbÁÊøâ݉’Ö‡Î.Ñ•™…ùñ¼UÜú›¡cßÉãE(`ÄМ(~Õ#žUÑf2VtzyŸT¤Š.€¬¥ºèô:$X•‡PÜúíÊ›TN|)´R™µèzžNkï]È„°|ˆíàªÐ™Î³0ô{¡jÁÊtŠNU l¯>†.@ÕÏ©6ØÍEcþÈ(0UŸ*õ;Y•: W±_žÂ"寧ïÃú%*-W*Û^¾ß(¬L!±`eŠN|qpÈôĘöV†‚ÂA¹w¦}ùëù°-Ó8Q¤(÷Þ¹=ô·Âèt€ö V+j.e:IÌu× !±aiÁÊ´›nËaUΧÑò³½PNÁg4°þYÀs©]MºZWjɈéü{w'G]çü•É9I&™!Âì"®(ˆDƒ¸®"xàŠ".XQAü­Šì®®x?qA÷§«»Š·âñDW9C8\D’Ž3I&39g~|¾ß_U×TWWwWwUW¿ŸG=êèêªïLuWW}êûý|ízðìâ€Ð°K¯ÐOs=ßîÆþΡ*ãA,€éká÷†Æ¾¦¶ûZÝѱ¯éïksAð0¡]²ÝÄ×Û…,ÿ ÜìÞ§VR( XIÑù¤"ç3éþ‰KR¯q؉5ò½„­ÉxûÒ:¾vX4×jìâèÚ„”‰¶$áÏúÆß?¡ï´¦e5€]Û b7"N5¬¤]ÊP»ä À]Øop­dì`¿Ï`¿Ñ{`Á =Øo½®>ìšo!Ö3¬HˆÕÌîI±OŸ?Ò˜ªŸâÆíü½ò+?ަ°ð©-¢ãps™X«?Ü«m´?ž™Or;ð7ÝÉß)!¬¤èü“.Õ°ÊŸonÕŠÄÖ«±‹—PÀª“ø‹¾hõz!‹ÅŸ?³ìáÐóǾ›””Q™š‚VÒ>Þ$ÐûM놛¾ŽÚ=GG‡yXÐÄÏgaVó~5V[{uhx K¯0DçÜì$¨ÅßÑW8ÏÁR9”!h+%¤€•Oæ¬Vù[‡=±ß“ìsáLrãɉkIÑøVѤë`ÕË÷B«¼ùZ‘­¸U“Àb)Ss@PÀJÚg;Ö´m:vM’e3·N°xÊ õè!}€Ë›°ë‚ǰŽA|@j8i$ÿ’LÍ…U‹ïp@+)¬¤è|O| Xåo {:´/Vkæ±äÕ벯Íp›Òz>H—óã1àX¬Êÿò¶•Hâø¦a­øÍWÀªX|ð?M“šN €•´Óf‚žp»á3çši><†Õè,K­În¶;ž¾©¢Ò9H!”å‚FÊË7a锪¿e÷¸Vý°êL>Im4é:OôæÅ¼&íåkXŧf)`U,¿sã¿ÌµÙQÀJÚÉw$23q­òÐù[¢|­,Õ²’ÂP +):ÿc:óÚþØÍ°ÿO£ò†Ì'ÅžšmÆ.z‡h¼Ë×nçJûf¸MßÄp#ªI×i|³Ð¸ß’CÝ8MâWi-ðoEïW¾ A; >)­Îãù¤Æ>òº|‹Ó”éX>¡íè·AÚ£,y¬Dµ ûý˜K»V$W XIÑùÏè¾À°®zrC³ ‡ Y~ŒYtE—gÝc^Ñ=éÆYÖ°òÁ¯'2Üf–|מ——1ìó6'@™ù¤ëqOâŽwãïºõfb926bùuüàç×UyMµ+›çoÂâr5ˬj=¡Ÿ|“?߇%…Z6På=³¨ ¼bçò-Ø÷rØ•¥‘ù!Ê‘·æwÀ¬–U'¬üyE=PJ»øÎ#f'®UlŸþûÍ]|ø@•u}+†U^—î£ÄëR8 XIÑùS¯rCØÓX÷½ßAåM“(ôb7:s°p¿û¡fj c79[°ªä3°^šî!èqcؽæd#n Mom#ëªÙ¾‡YØLŸfºeýØÿhfŠõqÛÜ;Ãò5Óp.vÁ5ûß÷bežæ^‹›ºžé^ÛÆ·¬Öö&‚€¦ûéÁ*Ë£¯wr ‘í}n¿Ð´ÿy{Õ¹a‚Öz,ˆµ TlsÓ;Üžhýg IDATxAÊØÿ×/ÛâÖ ×:¸¸¸»ÎreÁ÷Ð3@ÐcïêÚ×õŸIÿyõŸeßýušmøÚ¦Yß„õ¤¸’Êà“?¿†÷Ÿ•ݹfzÝÐHWìq¶aŸÀÚê†ÍµsãjDhŒD7Ü"“°ïÛ!À3Üx‰{mpk›ÊÑ j(íæ¯¿ZÑ|º]>@õU”oúØ®óU™MÁ®g`¿Ï[¨üMñX6SÜüPþ|ÛŸk)DB°’¢ûðOØÅÃMX èn7~<á}iÌ&dùa fY8Ð]ÞçÞ½!{I“e"fU z21À4»y󦙴¦jûO2Ü–|»ï']°©¶üïwbÿã~ìü9× 4±ý-ĽÖiüx=Vcb­Ï9ÑKe÷ÖqÓïpëFó}íÆ[±Z‘>;• +l?øù=«¼æ 3üÛü1õßÛ_¸òúãÚöy7Û>ð2ÇMÏÄntý´~…k3eõtÃ×ÛŠýØù'=¾6—ÿñI¹ž¿ÑÌʪÐô;ë|ï&ìG?\»&\“&l$¸Hq¯Çm#\C'l&A èFàïš~짪,¾îk …k%¥1J| +.Àµ…êA§jÓõ\°ÿ&2ÿ#àÀ™˜áé:¶ ö½ñ¬ù®l>0;ÃMO£z ¹j5ï¦Gö±„öó5ì ššî"ø o%øLúÏ«ÿ,û'´õl#kÛÅÀ+™|òó[imóYáÞhÓ·À}TÖý,ðÌ"Tûïotˆ>ИIðjµ§±ïÚŸ€Gܰ¸¿ ûn%¬¤Ýº-éºo&¾k:øZ‚Ë>ø±É-ó׃۱süΘe;Îûþ!ÏÎȲ¢Ù«7ì©ö¡ö=ó.`µžÄ~Ÿû°ßç°øk”¸ÞÍò×üþ·Ñ?ߚŎ«¿>Ø„]«}5 ”RÀJ:A‘sØŒc?ȃÀI¡å³±òo6¸]_;j&A-€hЫ—‰¦ð“šðT‘ýx#V[` vq3Dº`S»š^LðluÛô5ã¢Á­ùA?žÕü˜‡ÝDïï†V؆]À $rŽ›^,¼w ¸3£røÀÇcm,XõKà˜Ð²·QY£Ï7©ó5£Â–q"ßTÍNýgØ_øû ö-4×x‘<Šw:ÕGgçcç£ k€{›Øîj¹Â5zÃùÅü ‡>ßcªÇ‚SY>@(¬¤Ý|^ҾĵÊÃ?˜Å®-æ¸ùù-Üg¸öò°›Ž^óE—…DÆ=œ ?lôMíöÆRìï¦÷ `µû®Ân¬UÇ*7^ƒƒwÃcAª´|MìpËÿÍO‡ÓuÌMû¦ðåä5{ X•¡Y¬”ŒV"ÙxÖ jÁßÛÜÞ•7,e÷ý¼ Ò¹¡é~àok›Ü¦¯ÉVo×^âYq®>ìó´Ú(?]朇`Áª‚——æV"ÉÃàÝXðð-Xõ߀÷WaAéF“ï"ø>Ic°’vëæ^_Ieeü˜KДÝ7OîÇ.ÑeÓš]û‡,pµKÿ ,wŒt†™À›Üô­XÀêa7~V»FÊïÃMÃA©Q¬¶Õ¯±?®ÅÖKû%¬öþ ;†ÏsC½¹½&ÞÈ®ÅjV„—mhpûÒº-O¸I`»´2§!X°ª‡ÆkÇŠHÁ(`%Ò¼3±'B¿&H þ?XÀêlà_)f¢II¯ J‚å>Z‚%9> r\œS¹¤~§aOno#È=¶ X…)Úyñ.í·ø_XÓ‘·01Þrà ,—ÕUÀ )OαNâƒþxùàÔ3©žÌÞç‹ó¹}®n”}.¹pŽ›ÙÀ_¸!Év,p®­­Å1ˆ5ónõ¹4/œt.p”[ÞMËb7õå’‘‚SÀJ¤y¾æÍ—zöîÂòZ|/‡rIvNÅŽíÀܲG±€Õ[€¢ d§x» »I«]q'V;ò¬·$)§pSÀOQ½s€K€Wc7°ïÚ J{ù¦|Ç»!l«Éü0ö€è!7~ «í\¯™LLƼ/Ö;XxÙ<¬ û…)·ë;CñãÍØõAtù&7øe[Bë†Iw³¹Ñø<}s±zz`whSÍXKþ‚âñ½åµs£a‚ßéz\Bå9;:ßén–ºék€Wd¸mß<º‘ÿ»HK(`%Òœ%XMªuÀ±›]° ¤/c«·£€U§óÉÖÿƒàiý(pvCûà[9”KêsöÜ€%ú÷|3°ïëb,­€Uy…›~(a½‚¦®wï‘öùð|,)r88õ[‚ÁYÁFü¡ÆzÓ±ÀU¸Ç±hOdØ5€gÑãÜV*YƒT½¶`¯¤`˜ïU-ªKÔå“eÇñ9¢z¨ '›’JáP­à{ÆþG#XMù϶hEãk’¥ÍãÔh©ÝаšL6çc°&Ê>ÏØZì^äö ¶ A'g´=‘¦)`%ÒŸlýJìBÐ?¥ê¾]ô¼8ø]ÛK'Yx6v° ø–œìâúKÀW° ¤VÅçkWý'pôMÿ¦cß×ÿ ¼;æ¶½tÒj‹In nx%vPM—öYCÐD«(¶cµ¸K¹¾¯ýãÇs° Ltù\7øe}¡uç`¿7³°`Y·ò¥,0·ÕMûÀœm04íw#ný¡Ðkê}²5ßÄ>·›°t þû2 |8ËCw>ð“ÛûpVëí¬v»ßV8x6„}_.Å®ÁW¸uˆÌ¿>ã2_L(ãX Ñ÷a×W`M–ÏÅ|‚õXûNìºägXª‰¸eKëBû¿Þ-Ë*`%R8 X‰4nk*6†Õ¼à)Õ4ìâèØÝÛ°%é<>(y Á“X° %”|1Ö<ð·m/¤5Ë_5ŽÕ¦‚ ¶\vlý÷õín,å1»Q˜ |šêM£þ x A°ëÓ­(œ”–¯ýÔ¬YT²¨ zõa縤`˜ïU-jˆøf”;©Þ{¢Ï5Fõ@H 6%”Â5 ${õÖ°Jã ,Àÿ5ààr,5Øùw v>]Œ““‚A“ßïwÛú|h[Õ|ë<åˆÐ²è|»Ê8 R}ÀÍûkÆ_b=O_‰Õúöå> ¸ÎNXv,ðÉÐþïD½ÙŠˆHby?-{±[¶ÌÍÿ•›_O÷t“\&½Ø…Â8ð·ì9n~¥›ÿ¢›ÿ|ÛK'õx'vœn-{µ[ö37ÿl‚.¡g!eò)ìØ>Dý •_‚݈bÍ EDÊàbì¼ø‰„u.Ãj'­Àj˜úéË«¬¿kÆ v®]zm„ʦ¤C5Ê7š°­hs(áµè|»Ê8JåïÍ:‚ÿß à‘Ðk7?Âj{ÍHX¶‘Êæz=)ÊXÀ>_Èp›""’ƒIX>q¬Ú°÷·,üôþ·ìïÛV:ÉÊ›°cwGhÙ!n™¿Ð8œ È¡ dq­ÄŽÓCËŽ¥2ÀŒ›ÇšŒI9,ÆzùÛå¦ñìsq;Êí!"åpv^ûRÊõÓÔtK %™âÔ XùÜkS"Ûn&`•uÃ6„ÖšŒ5ü*ð‹„e©l!ÕC¶=’¾ûLü{†Û‘ü5vBœÊ¼XZ¶Ô-»µm¥“¬,gbðbo·ìɘõÎn_Ѥ/ÂŽÏT6‰YÌÄó™1ˤsMÇšdŒcµ¬5ø“ÛÎû2(—ˆHÞNÅÎißN¹~š€Õõ×Bg»ùjïO òÛz3•¹›&xqANJ° NoÂ|»ÊÝÖw‹Bó‡…¦tã¬&Vµe+°N¼}©¼çh–V""%ñ¬jô‡#ËÆNô†–MÃ~h þ¦(’ß(áýµ‚AÃX¾¦û€{±klïDì!Â]X®§ð¶>õøym•ùv•1º­½€cMÓ¢òšäVà~ìË[–}x]è}¯ÃRSdE+‘YÄÄ{öÃNôk#Ë_MõjÀRLçcAޏ Óêù ǃ¨Y`Q=‹‰ßWß¼óO‘å/$>9±tž*MY À’X‹ˆt²£Qâfµ»C€%X"xï*¬Û¬œ}&¾‘á6ED¤@æÔÊ‘Î÷\à1Ë7aÇ9|Óú*”ì4bÇquÞ‘–š®’†ïbŸ…·§Xwz{‹/"Ò‹‚ðÒ˜Qî$²M`+"RTþ\—gó÷yTöÞë‡%)/‚¤2ÎnsYÀšrÆM‹”’V"ÙÚ‰=™˜‚õî±;ßâH‹luc&;ÛN7VÙåÆj *"Ý·èϱ €#rÜPF‘ÒêÉ»"%´Ùóxê"íáV:ÆÍ׈Tgâ¿ÓÊA'"Ýb7VÓx2ÊË'Æ×<×çA C+‘ìmscå3*/ßÄH5¬:Ÿï [Dz»ùó¶V"ÒM6ºñ¹–BD¤ ¬D²·ÝÕ´¤¼|-:=ê|;Üxj®¥¼é¼-"ݨy¬ªi4¡ù%5æó’G‚v’´Ï~žSD¦€•Hö|0cN®¥VR «òÐ÷U h¨ü"ÒMê Xåp©W^«ÉmÚO=ŽÖb‹¬wÓKÖwãI-.—Hj X‰dÏ?©W^œòR«òð‰×U³¦»ù¤ëE¼ái•¬¯/n–»yo¸¸x81Åö>ÜÜÙV˜ÿ;.Åšv¯®™ÏºŒÃÀ'€»—{?uÛ¾8ªÊû’Ö{³ÛÿJ÷ÚÑ¡×Þ üëÅú“)¶µ¸.4½[V≈teØŠcó.ˆ´ÌØ1~WÞ‘¦­ÄŽåáyDrµû\“wADDÚè+عï­)ÖMSÃêGÀ9nú,@âgºéÅÀÃ5¶5š°­j«¸×¢óY–qpFhþÛÀËÜôáÀ]UÊ‘´^8xøà7¡ùµáüCÓ¤m­öÍï‹]÷Ts,öyX–°Žˆˆt¸Ÿa'ûWç]i™O`Çøâ¼ "Mû v,_wA$W§bŸƒïæ]‘6ú4vî{o•×/Â+°^ýôåUÖßHÐéÐ ¬š7Be¾Èp)Îh¶š Xe]Æp‹Šuÿ£À#UÊ‘´Þ _º×¶„^» ¸ÚoÒ¶6RYs¸§Æß¤€•Δ¼ RB¾Iàô\K!­¤nËCIׂ õî*"ݤV/…¦‡#šØ×A3ü,L‚+SÈîž¶Þ2î&èe, t øJ’´Þ7€³°f‹3ŽÀë°ÚT§g/O±Ïh>ªñصÌX¨|"… £Hö|~#u^^þ+ézçóO.|ìn>p©Üƒ"ÒM²î%pL8 º4Ãoë4,ß”·Š ·Ó©TÖ"ÚAå5xt>ë2†Ý œš?¬õú€5nú\*Nû7a5âž›b[«€¡ùÀêêÅÿÿ×Dê€D C+‘ìù'JÈ]^Jº^þIªjXu·Q7VÍXé&Y¬.Äòa=àÆ6±­ÝÀ¡XÒõ ¨¬íõ^,wÓ]À!­¾Œåiº¶Ê|–eŒ:8xÈ g6°Þû[°¿m€Ê¿í[XÂõen½ZÛZP1'q-)»Ýn<9q-‘rrãþœö¿æzl‡N(cV°’ÂQ“@‘ìù“¼zØ(/õX»ÜX5¬º›oþ  ´ˆt“¼kXI±(`%…£€•Hö|#u^^ÊaUÊ9'ä°š–k)DDÚk Zõ  •(‡•V"ÙóIœÕ$°¼Ô$°<|€¹7×RHÞFÜXŸé6RËj¨ö*5 7øZûKJvž¦ 7bÉÓç?o¦P TÃJ G+‘ì)yoùùrå=ê|>À¬š5¢#E¤¥ Xµ" ÓMŽiò½kõnX ,É¢P XIá(`%’=_cC7À"Åç/À|õ)"Ý(ËÄë [€ånÞ.îN¬±­ƒ{€û€O×¹Ÿ°hͬO¹mÞãög/à§nû÷GÕ(ë^À‡€•ÀKcÖ®cý~ààõn~)p]èõëݲ¬)`%"ÒŽÆeyDZf.vŒ³¨ž.ù:;–ßλ ’»Ç±ÏÂþyDD¤¾‡ûN©±^šV?ÎqÓç`o8ÓM/®±­ë#ÛM¹Ÿ¤€ÕhÊ÷}x™›>¸«FY¿¼×MO"xÞæpÊõçaÍýTþ6틺²ö+ì³ð7-ضˆˆÄ‘ØÉ~EÞ‘–9;ÆÊ» Ò´±cùã¼ "¹{ û,”s9DDÚé?°sß¹1¯]†]Ï®v‡¦/¯²­ͪg`Í×¼*k°Özè·!²­‘ÐkIû©°Jó¾uë à‘eë¼ZÀªÚú[ûã#Ë7“Có=´æ¡é½ØgáÈl[¤!Sò.€H ©I`ùùjóªaÕù&¹ñx®¥"ðM ân"DDÊ*)‡ÕE¡éaàˆ&ö3FÐ#k«MÂ~×§ÐØýn–7j¤ÖŠN½×ÕÖî^ÜymR̺YS“@)å°Éž‚2+×RH+ù‹ºÁĵ¤Ìsã ¹–BŠ`·+‡•ˆt“n¼GÛZœê¦OÇòL5jyh[§Q°IÚÏ*àh7}*•5“ü2¿ÍåUö}3p^hþ°eýðv7nâ×Èúïö. -[,Í/VרG#°’ÂQÀJ${¾½V奀Uyø ô‰kI7ðÔK ˆt“´½¦q!ðV,Yù[Ý|£.Þ…%HA †Zûy/–ƒê.àÈûv‡ºm^@e ²°óã°¤èäÞªæ,qúCXÂç5±þpð\‚ÊŒ\K!­â«Í%®%@9ÄÛǟȵ""í5Ž]ÏL"ŸfÕÔê½7‹¤ãÃl#i›iʘT†±DëóŸ7S¨Ô$P I+‘Öð?>jXNªaUX3°A`WÎe‘üíåÆªa%"ÝÆ÷ 77aVw’$¬&Ç´« Mh¦ŒÇkõnX ,É¢PU¨†•’V"­ás ¨ju9)`UóÝx}®¥"˜Ž5ÝŽš‡ŠH÷I°Jc!p+ð°ÜÍ{ÃÀÅÀÀ£À‰ Û¹ëi{pmèýŸî^BemøppppÊ×Âö~êÊ~/pTBùüú?V/Yg¸Žõû[€×»ù¥Àu¡×¯wËZa pöðîÏ-Ú‡ˆˆÈ=Xëçå]i‰ØñUO«íØq¼#ï‚HîÀ> «ò.ˆˆH~ÿ:a45¬~œã¦ÏÁ‚,Þ(p¦›^ <\c[ÑýmΨòúhýV{-¼o/sÓ‡wÕ(ßw€÷ºéI­*¢´4ëÏÚ†ƒx+€ýCóûb®V8;þj("Ò%ÃNüæ]É\vl7×ZQ ï5ر¼!ï‚HîŽÆ> wæ]‘\_Y~8YµðÓ—WÙÎF`¦›žAe æ`jh¾VÐhÀj”ÊܰрUµý&½ÞÆ:‚¿oµ›ÆmÀ® “Êž®¶þVà~àøÈòXÓG¯‡ÖåN}9vüojÑöE¦Ú"­¡¦Få¥^åÊCßSñ¸ñ“¹–BD$Õš^šŽhbcX õFíÆjYµJ–7j¤ÖŠÎxÛ¯¶¾¯éý*&Œ¢©EêÝgZÊ_%…¥V"ÙëŞ䌒þGO:‡òW•‡zO ×E¤›e•Ãjpª›>ËgÕ¨Ø5uZ~¿§aù³Ò¾æÝ œš?¬Æþ~¼ÝM‡›ø5²þ;€½±Ü]Þ*‚‡)¸éÕ5öÑ(¬¤°°Éžn‚ËM«òØÓ×åZ )‚}Üø‰\K!"’ßÔ¬¿Éí\¼K\þV7ߨ/c9›®­µ"VûêP,±úTÖ Kz-ì|à8,)úCù¶ª¹KœþÖ„°VÞÚ¤õÇ€³€çä¹ZëòV[¿Ñò…§îuÓ=X€Éw´‚àAÀ¾ÀÊ嘃óMµV‘Î÷uì¤f­¥c¥©2/a9v,_”wA$woÆ> Wç[ ‘\ý;. -» †¬ÀR^øéj9•63Ýô *k£SCóC5Ê3Bem¯Q¬G׸÷'í7) ”uy«­ßhù¢¯Ý ¼Þô¡ IDAT«Þ~øÓ“¢¼G`Çü¾ëŠäbJÞ)ÿDj]®¥VRÂõò™”w$wþ† /×RˆˆäË8úCË. McŽF;ë\Gh~;4k—FÊ[Ïúø*öepeäµèõÌxŠí)ẞrX‰dÇWËÝk)¤•”¿ª<¶¸ñœ\K!Eà? ³s-…ˆH¾²è)ppª›>ËÛÔIûõ¹¹ê}_–-_ÔµXíª£©ì pÄò®dõ˜,…§V"ÙQí›òÓ1.Ÿh[+Q +‘lV×ïÁ~gÏh¶Pì÷ËX>§××ñ¾"”/jð+¬6\¸¶ÙràùÉ,Æ‚dµøk Í‰k‰ˆH)lÁªßê)}y½;Æß¯µ¢ÞW°cù¶¼ "¹;û,¤IP+"RVWbç³ó.ˆT5 Ë7õ—‘åK¨ÌÃxpLŠí};æÕZQ$/j(’iX jézQ‘ΤVåᛩVø'ËÍÔ*étq9¬¤8žüø5ðûÈk·c=ÎÇR”ìÜ–b›þwO½Ja©I H6”¿ª;è8—‡šЧ&""Ù4 ¬×<àæ˜åÇQÌk­<Ëû0kV…Pe:‰šJá)`%’ Õ¼é X•‡Vâù€•šs‹H7Ë#`µæzl·N+o-ªa%…§&"Ù˜K9)0Yj(Þv¬9÷T`FÎeÉK+É—VRx X‰dÃ×ÖÐúrSÀªàààÈkŸî^’°OÜ>Ü|2aÙRàºÐû®wËò2{» ɱ""Ò&Ûq”´·Ì¾Šã·æ]iÚBìX>–wA¤þ/öyxUÞÉÉ|ì<¸®ÊëijXý8ÇMŸƒe¼QàL7½x¸Æ¶†±Þ™o£2¸õmàenúpà.7}L«”ñAïÎqFʺ 8#Å>Áòšú”³–­ö½o_`eBùZmv¼×çXi£!ìį'ôåõ9ì¿'ï‚HÓ°c9TkEé ßÇ>oÌ» ""9™Šw„–]†ZV»CÓ—WÙÆF‚´ 3¨ †Œ¸}xµ~·b5”Ž,_*Ç ,0åÝ ¼;¯'M(ë(• “öy¨;#ôž¸e©l^ØC¾× cÇûO9–A¤¦)y@¤D¶`Áª¹¨-xY)1sy¨I „eýÝîöÞ ÌÂn€¶aŸ½n¼Í-ßìDTÉÏN,¨4Ó #ÀE¡×‡#šØþ˜ÛGZãÀXÍ×›BË{°|PqÍØ¾ ¼X\ÙP)Ín‚•jíóuXí«S³—WY0)òÞñ&ÊØ,坒ޠ€•Hvü)%^//9Êc7Á…ùlº7™¬ü›Iº>»Yz1ÉÍPjÙŽ}6‡±›»A7vË·c­X +m ,º]‘¨MØïâ\Ëk´ Ò\œŽå³jÆ;€¯cù©>è–Ý œ‡Õþ8 ø­›¾Ö­;•tù¡|YOÃrnU“´Ïý°€ÚÝXÞªjËVa´5n~°:E[E+é X‰dg«÷æZ i%õ,W.›± ó9(`•…o'aDñ“Ø9Ò^vaÁ”ÝØq#¸htcßÌz“{}³[‹{¿¼ø N3Va¹O66ðÞ>,¯Ý?b5ªÀþæÇ±^ Vcû,ìF¿ÏÆ>Ó±‡©Øgqºh­p l+vŒ†\ÙNžhñþE¤x†€}°óT#ç€ k°Ô ›©ÌÕˆ1à,àÀ{Ïçcµ§|>Ñë€rÓ;€_aÇîÛÞ Š%]Þ°nÒ>¿…Ï'ïOX¶x>AÀj1àË‹øº9q-‘œ)`%’°R0£¼|³!Õ°*‡ÍØÎ9X`Awö4,Óê`KœhÌ×(òÁ²jA2ŸËì»XÙßFel2°VkjÈô<솧Ø€õbµÌM7jöàcÄš‹°|mÀpÐkéa½nÛsÜßäÑ^1eø"prƒˆt¦¤žÓÔ@ý3ð¢*¯Eßß_c[~ý]ØÃïià5UÞ3 xtOãb7TÛwš}Æý½qË®ÁjiùžO¾’¢Œ­¢VÒ°ÉŽ¯&å¥&墜dÉÅ:ø+ì¢ú³T¯Étk¶áƒ(>pâƒ0>ðâƒ-S±ƒ)ØqèÁ.¢'ÜÌøÀJ¿[>×­ç/³Ývzɧ†ë¬™ÈgÉ&ðéƒnƒµVl‚ÿ?ûãчý_|س…û‘âJ Xݳ°`ÐO šáÉíÀ‡ zcÜë1/=n<–cDjRÀJ$;¾IѬ\K!­¤&å¢d¼^à_€ÿE€ÿ(–Èöàg1ïñ77k±@K+ƒ-ÕDƒd¾F‘–U ’MÆ>>ˆ•A²XSÁ nžÞüŽ øÙ)ƈ?FS±€Õä˜×D¤üÚ°š‡å‡Š:Žúkª> üeÛo&ga£N¨2‡]n¬x€š> "Ùuc¬ÊË8:ñÉ£L¤€ÕD'ÿ,ŒymöäúzàÝXÓÏ8ò|R»Õ yËÊ"‹äó"Ò¹|O¥µšëeeÍõ<˜÷öE¤Åzj¯"")ù¦2Ós-…´’å¢&g7`Í)â‚Ua'aO²ÿ…  žp(ykgó½N͵"’—Nn("%¤€•HvüšžL——墤Ø?À±‹Â'Üà§Ç±ÜN ݰ7ðL7ˆY_{)05 énþÞpWâZÕ-¾‰]#mÎÀ쀼> œŒÕf>øIŠmö?.®ö®Æ~·wç÷`ÍÔ7Ÿwïûð4Ö‘Hœaà àxà}XÓø¸íFUÛ?XoºïÄ®wçw¹×Þí^ÅzÛ½¸Æ¶–by%½ëÝ2¬¤«(`%’_ÃJO¦Ëm3vÑGç7 ëvÿ ŒÕò©mIoˆèÅ.Î`ç}n<ˆY6»ðöÓ¾6×0v¡ÚªñìÂ9:>»Xv^¯1¬–Ù¾(éz§SÒu‘îækÉnmðýWW_Î.Ç:ê«Å´X솫©°š‡uò±Ðº—Ÿ~ îöw´ÿ Xõ§¹ýT3 R}ÀÍ»Êv£ªí,Àtµ›~ðànþƒÀ!؃±Ù)¶u,ðÉÐ~ïÄ‚rYQ+‘.ó¬Æ¿ç]i©{±ãü¼¼ "’iÀ%ØÍÉxÃØÅõ27lûöÛØ‰<«Îò_ŠàÆi.W¸LF–ý+öT<ºž¶a7i÷·ZâÊÐ*þÿ©›‘îó ìû±›¿ Xá†Ý¡éË«¼#A ÍTÖº¡2>T£,[û±PaëBåX<zí&àHà•À÷kl”ʇÌIÛN¹Þ ±ßƒÝk[B¯Ý€Õ;#´ß¤mm¤²é~µÿgõ8;ÖË2ܦˆˆØßc'þoä]i©ÿÆŽóKr.‡H–ÄFiU뱦¾·áûÜò#ëØç8°kâðì†`xFÛÅ‚]KÉ&ÀLª¶l5öÿ#nþ¶”û8 »™j¶\õ¨§ÆÔÛŸz é>Ñ€UXšN5’VÑ÷× ¾ _>Y¾êÍ–OÅš~xuŠí§ÝîpÊõþ¼ÈMÏ hf |:ø*ð‹ÛÚHek¨¬vVV°é2‡ø¿—wA¤¥nÀŽóßæ]‘xðªªvÿ̼o¥{ýð:ö œFe-Õ~àëØMÍÖᾑ÷‡ïl¬×Â]Øÿ žRG÷™f>nÑõþDD¿‘2ü¸ÊýG¥(Cšÿϰ'úõä£YåÞ{@­E¤t®À¾ÿçǼ–&`u=vþïOxš€UÖ;^¸)Üw‹Bó‡…¦§¿ÃÎɵ:‰–'i»Ã)×{ Kð.*sèÆXͪZÛZõTìí‹ýÖfE+é=µW‘”|{ÿÞ\K!­¦äúRf?žåÚˆv*p/ÖkÑÛ˜˜\ÝׯÙBãnpãç»ñÿÎþø »&´þén¼&4 –OëcÀ…XówäP©Wµ}€Ý Í^ŠåA[ÓDay?‡Õ0û×e¨õÿñÛ}¸ aßQcn¬kD‘îãƒ<ö˜w!ðVà7¾°ÉòŒgÏÞë–<ä†3Cëï~…‚êý’¶›v½÷·`‰ÖòÛ| kâ¸Ì­Wk[Ë ~Áòqe\R+‘.ó7Ø“Š_ç\i­ÇŽó;ò.ˆH‹„õÈôð$0Vcߋ։ŠÖ4ší–ýÆÍû\JáaŒÊæmqMåÎÆšY„ß÷ÏUÖ¯5Ÿ´,1+q-étj(2‘¿ž¨· Æ\¬™ÛaXMXp,÷É™X×è×c¹<–b½0%åRÇ>ýX0'lÖ­øR,_W#À>XðîÚÐò­‰së)C#âÊÐèÿ§5é^>øHâZٙܳü8¬–j=fbͪ,·Ÿ‡ªL‹ˆˆÔmv“òH­¥£]€çj]:‹”Å",ò–;$©öh4!xþ=»°š@? 2álÖCÔÓnÕX’ñZMß‚%ó}Ë#5Nê·½MÀgbÞ·½Ïcµ ž¬±^Ú24Ò,1® µþ?ö,x³{ïËx¯ˆt¶k°ïÿÒ¼ "-w0v¬ÿ”wADD¤= èê\ÊëÍØq¾:ßbˆ´L/\¡2?ÒÃÀËcÖŸî^ßóštžcÇóÄZ+ŠHé(`Õ=ÄŽõcyD$‰z€ÉŽo*Ö—¸–t:ßôSÇYÊèD¬FÕ‡˜ØãéaXwߣ2¹ú 7nWi­mn<#q-)#Ÿî`sâZ""m¢V"Ùñ]×Foò¤\Z™ÃªËC³7v~îÁrüàæ}lA®´Ÿ¹™Xm¿-Žß c¿Üâ¤;-þ 8)ź§`ùŸ>Žå›òŸ¿mUß!Äÿ–MO\KDÊh’×ÛÄ»“ äè OÜHP»ìàm,—ˆÄPÀJ$;þfméz²’ÎÔHMºYX“ѽÜxo,0µëõe›ŸYmm°“‰A­Á*ÓÑùz{!“üÍÂjDMÞ\B}ÁöYXn¦³¸eÛ«¯.Ä׬RRD¢Á¢šLý€¬ÅzmÅM/Ázë+#u°!A+‘lmÆjÞÌ¡¹îÝ¥¸|M¤9X€É×ˆŠ @í¨Òö9¬Á+ïÀAþs´+´ïXe`7•£¡÷û Á÷°ZZÀ1ƒ_Þ‡ÔöJYÖ°!,€µøÀÖöÝtãðÐLofÝ®‡Êã:'-›œ‹«þ¢‰2,þÓMwb7è2Ñ€æZ Ƀ¯YÙ舅À7 ®ƒÏ È‘4 |8Ø8øIÂ¶Þ ¼»Æùp1vr%–0|7p!ð+·í+€ã±ßµGÊQÍRàºÐüõnYYV>¨79×RˆÔ €•H¶Ô”¢ü| «Ã‚FµŒc=†­uãDZÀÔ¬‡´5äÛ,o*ƒZq¸ù~7ÜÀ~}Pε¢Ã{=îµp¬“~Ϥ¾`SøÿÞ¨íÀWšxœC°€íãoWÚK+‘îÕl Ë+°€Ò×€s°•}SóÉØµÎb7\MrÀêƒØïÊ A- +€_¯ÁZ3øÚîS°^Q?àæ”PŽjŽ>š¿¸´Æ{D¤Å&Õ^EDêð–Œx!°*ç²HkL¾]øl#@­­2ý8å¯m×ÝäÎ#>°Õ=å ¨è‡¬ª¢o#^Õóù 2WXí¸i¡ùÐt8—Øß2ÕMO¢2 4 ÇX4Õhrëq‚mqÍ7«5éN¥5=]Î$¨ñ'i5VCôìæRDºÇ2,pób`9pðR÷ÚáX§·ļ#vþÁ~ÛÖóÝk#Xo_wˆä/7`¿sß~€ýÎobâ¾Qì7ÕÚ’ÊQ-‡ÕF¬æ—¯yÔã–5óp¨Èæ\•õo”P +‘lùæM­HÈ-Ű x=Á½Øÿax´÷úÄò>¨úÝëq¯…`3ÜÐH“Ƽm£¾`SxܨÿĪ—‡6±°ŠIÀ(XUªa%"ÞE¡éaàˆ&¶5F}MÇ_¼ {Àr6ðrªç‡ÝM6y÷¢•9ÊœV½^KGPÀJ$[jØÆQ°*+cØq³7Ç3‚WõÀv$Žç könn.g8—ØÅ¼¿~F¶cOzw21ø”WçFà9Ø É?ÓXm·;€þ‹ –št._k0úÙ‘îà{&n´fø2,Àtp:pkeÙ¸ ¸ø½[ö àíÀgšnŽyo#åX…å õ5K`5NËÊ_÷è·[D¤‹,ÃnTÍ» ""u8kv1žrX¼»Ð]ä–ý¡í¥.§ÙØñ8 k^ÞΦ °cùD÷)"ű;óZšNR‚C¸ñA ï¯õàïVà~,7Õ[ܲ½€Ÿ¹×þ¦Ê¶Ó–#<ý¬V—÷:à‹5ÊØév`Ç{j­Eò¢V"ÙòO¤ÕE¬ˆt’ÕØÓè/cImŸUe½1,Yû%M7¸ñ¼V°ÃL¢¾Ž ÂÓÓb¶VãÁç_‹Ž^ÛDÐAA­®ÞÕPDªIsmûgàE)ß_+·§W§ØvÚr„§¯Î#è)ð$²ïœ¤h6a¹½ú±Q"…£€•H¶|Ó!5 ‘NôKàH,™î‡¨¼˜¿ ëbüîÈ{±¦”nzkæ8„ñG±`É÷ÚVì"Ù¯~Ï&ì‰÷¨[?M¥Uúš—ÎLûñ@h:|jÔ0ö?ÆšçÍ Ífb0+ÐòÛo&?šˆt®f{ ìd·Æ8ãX“ÄÛr-Që aï\°’‚RÀJ$[>÷ͬĵDDŠkðYìióg±fi_Çž4UyÏF¬©F?Ù7aóù”†H†b™Mnz ÄŒbªÂ©pjre'9y~Òk;b¶•ù×¢cÿ{-ü7Îvþ5Ê¿¡Æë"RNþëöĵ²3¸9fùqäs:¡ÊtYù\eÍ> i¬D²å“'+`%"î àŒ”ënÀV/ÀÖÎÄ.€g¹é>7Ì$¨54èÅ‚D~ÚUfºñ¬‰Ü4‚æjí²»˜÷Ý~‡§ýx04 >eÍ÷ÆÙŒ>âƒZnúX൨I ˆ´ÇšëyP𣀕žV"ÙòOÆ«å )£õn<X›ñ¶§c¬~‚ÀV?øêÅ.}nz6ñÁ°mT:…RáT^M[i‹¯òú.,`¥&"Ýi’çZ i°jgç"uQÀJ$[þÄß—k)DDÚ«•‰×·»Aµ~Zo7V/"ÝÉ×´Ùœk)¤]TÃJ ¯'Œoóß›k)DDš÷làWX"Ö&¹æ¨z ,‡=ÝXÉwEºÏ¬Æêå X ×xýF,ñø|àç­/N¡øfæ XIa)`%’-ß«JÑšÎÄšÆì <Xå†EØMf3 †E¤óÚ"àÿ?.V…^û³Ô²I멆•H÷ò[4“Ãn!ðMìZuÖqÇcîµaà£ÀÉØ¹æ|à' Û:øv­{ð6‚\KÃÀÀñÀû€w3°\|çw…¶õ)¬×¿1ìwîQ·|)p]h½ëݲÛSÿÅM+)<¬D²å“®OO\«Ò ¬éÍQÀ‘nül·<+#XsEß=ü•?R¾+wß“× ìcÕ]µ‚`CTïÊ]DZovñÿÊ람մúð¿±ïî#îµCZRºÎà“Áûäð›Ø¹v;v.:Õ°é^áVº¸øpp9p’{m2Ö‹ìb7\MrÀêónø/àMT^[O>àæûÝöž|ëµÖï÷÷Àû]™>*Ó±À'CÛ½¸´öŸYJº.…7©ö*"R‡¥À5ØÓ¥¸îàgc?¤>8õ<¬&ÕÔÈzcÀ€{‡±ÚK£X¯U¾×Ve{+vÃèoˆÂÁ¨Mn>­>‚àU•éj¯7›h~+É­¤éM1ÛéVÓ±ï¦öˆÌ`ûçcA–÷aòäÞûð.ì|õ(VÓêà¦J_L½Àó±æ#ÏÅjÌÅÎ}~œ¦)ø,ˆ5DpÎvË *øóºæ›Êü£XN“Û€û±OþûïÇiš5ÏÅ‚Xs°Z[3ÜôT÷Ú ·¼«å4— ?ál·Ì׿òAþiØgfª[g†=vÓÍšˆ²h¸ 8¸ 8 5ê6¬ÖÏ7°ß˜¤–A}XÍ)€scÖõe: XZ¾ Xzï`ueî4 XIá)`%’­ÿ!xJ= ¬ ¨9u/ÖËV½O—º…oóxïDrm®ð²è0»ékæ&»ZÀ+iÂð*¶9Ôl /kÔÕƒMµæÓÔÎ<«‰²Õò2,Én?8™IŒ™…ý?g`Á•>7ÝGl™ãÖ›áÞçk'µ³‰Æì¦æWn|?ÙÔ:ò7öÓÿ¿¢5¼’foDç#‘n•EÀêB,5Æ{°‡qé1Òºø®ÛÖ/I>7½k¦ø4pÁd°‡‡b×ããT6}[Ž5÷ö«ÅXЭ[(‡•žrX‰do:ð ,ÁãîëJ1L%>˜åko%»|sÆfmÆ>/¾)T8_™ÏEæsØì"è•1šËl'AÀbÐ}“ŸÇ7¡Ämgù'…ŽöhYk¾ŸÊß°ZóÑÚ&qó³‰>5óp'ÜTµžàS;ò²€5 <´ÉíŒcÉn_‰%í>€àâ?+>(íƒ,µ‘ê ŒMÅ>÷°ÿwx¼ËÇõ õåÿ+ƒý±škˆï%RDÊíËXO|ï¾”sYÀÎï۰ߘ“±¼‹/ËxK°ßìæ¯Â¶ß–ñ~ŠjO,È·Ëÿ%R8ªa%’½íÀoó.ööéIDAT„Ôe'öcÝhWîÕ^IC8æo®¡ñf ãkL&¸qôÁ߬g Aòf_“Èç²ñyl ~ÍvË}Þ_[·)yròíѲּ¯Y–v>舛ßJ|ð©šñ|k–ÆFàŸ±'òcØçkV[/Úë©tŽ1ìüЃrY‰t››±L/wÓí0¯Ê¾Ž£8³²Á®=gÒþNDjR +‘bðµò®AÒCð¤-)ˆ6‰ÊZ_+í|4%ùZüv£rðÌ*ë_.¡2“€¶£)£´Îfìû?K‘n“E«zm ¹ž¥y›š×+`%""""…6 x–çi<4Ü ]å=áÖùc; (-óvÌ» "Òvk°ï¿rØu—ßbǽڃ*‘ÂÙø&¨:—äœcGc¼w·¡\Ò:+±ãxxÞ‘¶Á¾ÿy¦ö»;î/È» "qÔ$PDDD⬖¦\×7 lEÂui%^éN½½òåÙcp‘ “ÜÉ¿‘ׯhy‰ZC‰×¥Ð:¥‡&).¡«€Ugó+ݸˆt—4ù«†^ëD“k¯RÕ1ÀZ,—ãz7½$‹BåÀ¬ú×ɉjX‰ˆˆH³š©aå“ûCàßó=cz¾7L°^7cÛ›°œ[Òã¢V"ÝeO7^Ÿ¸Vm ±¦ä¾ã†3°Üx`¯'»ýü$a[ÑNC¿5IÛ:øÖ9ÌMÀÛ"ï»8ËÕx ðNìwgppWhŸŸNÀ:9xÔ-_ \Zïz·ìö„¿§¨TÃJ M+iV\/½À~À>X>¬X2ßn~÷zoFesûtãp0+: UY¾5£²t*5 éN¾†Õ†&·sp%Ö›ì9ÀåÀIîµÉXSóÅn¸šä€U’¤m}Þ ÿ¼‰Ê‡S€¸ù~÷^€ç_!Èå4ø=ð~÷·|>ô· |2´Ý;Kü[òæ4)`%…¤€•ˆˆˆ4ˬރåñØHùÞ-Ø“m°.µ·…^¶‡æ·;Üô,,°2× }nŸi÷g7A@˵6»a‹ûå[b–ûuwD7\ýÀÞÀ|¬VÂÞÀ^nz>pº[O7."Ý¥Z“ÀË€—ºé^`…›¾¸ f;ǧ¹ék°JÞ8ð7}'ö;Ѩ¤m½8ÕMøRä}?Í?ø8vÜ…ÕÎ û–Gÿ–'BóOÒ¹½«ª†•šV"""Ò¬ûBÓåÆÛ± úDZükÝôÀ*ì Ù%øíÁ.¸Ü8ÌŠýU–ÏÂnÜö 9Û ‚YƒL z…—G_#L쥫kÞâÍ`bÍ4¨ÛƒÊ Ô>ªi¤Ó‰ÍZD¤qÕV…¦‡#šØÇ°³Î÷L‚LS¨¼o­µ­ñ*ËwSùPäÀYÀ­Øy5)‡W\ÙÒì³è°’BSÀJDDDšõ¬fÕ`5ŒZ׿2ŒaA Á&¶1™  åƒZsÜÐçÆ~y_Ìr¿ît,@´'Ų;6ë±ãó4ð”›^ï^[ü1¯ŠH.²Êaµ «ÝtVcóÖ&¶µ 8«Au*铤ß¼ FÂÄÀRXöààܘuýßr°`Õê¤ÛÓ°€H¿æO¹a]‹÷-É| +¬¤p°éNiV3‚Ms#ÓU^ ÏÏLØöv,pµxxxXëÆ»åêúË$-¬¤°°éN{ºñ À—‰6õÓ›ÜÏ,02ä†ÍX¯„{»2è†$ÛY>˜õÖœq#0è?½¥É2wºiÀ,ìž¿ËU6K 4˜„ÛÜúÊa%…£€•ˆˆˆˆˆHwz*4ý¶„õ¶›6E¦«¼žIØöt,pµ?°ØØØ×÷sËç»!T°ÂÓÑqtÙŽ”û¨e:V»Ì¦³©<«±†[ÞãÖ›Œ¦¸÷OuÛ›ôb5àf¸i¿O‘Ž7)ˆˆˆˆHn–bÁ¢!âƒMCX³½¼Í dù`Ö>À|¬iã€üt_ûÚJ|`kDª<š‰Œ|ð(O;°¿cVÛl7V³m ;¶ã ×ï>ã–‰†V""""""R6S© `…§£ãè²i•a;V»ÌvÃ$Ýx“{}³[ |vÛÙê¶;ŠÕ€Ûæ¦ý>E:žV"""""""YͶbA¤jÁ£,`äƒG""""""""""""""""""""-ðÿ1uéíNŽIEND®B`‚gaphas-0.7.2/doc/gaphor-canvas.gaphor000066400000000000000000004700471175456763600175230ustar00rootroot00000000000000 1 1 0 (1.0, 0.0, 0.0, 1.0, 643.0, 88.0) 149.0 128.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 380.0, 80.0) 113.0 91.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 480.0, 286.0) 192.0 147.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 809.0, 564.0) 128.0 190.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 256.0, 379.0) 100.0 66.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 869.0, 105.0) 100.0 66.0 0 0 (1.0, 0.0, 0.0, 1.0, 747.04856186000006, 582.70251697266451) 0 0 [(0.0, 0.0), (61.951438140000221, 5.4708042845209093)] 0 0 (1.0, 0.0, 0.0, 1.0, 480.0, 397.05033661514511) 0 0 [(0.0, 0.0), (-124.0, -0.78142370140449202)] 0 0 (1.0, 0.0, 0.0, 1.0, 869.0, 134.0) 0 0 [(0.0, 0.0), (-77.0, 1.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 643.0, 139.0) 0 0 [(0.0, 0.0), (-150.000000000005, -2.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 556.04999999999995, 286.0) 1 0 [(0.0, 0.0), (0.0, -72.0), (-124.04999999999995, -72.0), (-124.04999999999995, -115.0)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 194.0, 72.0) 100.0 53.0 0 0 (1.0, 0.0, 0.0, 1.0, 380.0, 102.0) 0 0 [(0.0, 0.0), (-86.0, 0.0)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 192.0, 137.0) 100.0 53.0 0 0 (1.0, 0.0, 0.0, 1.0, 380.0, 154.0) 0 0 [(0.0, 0.0), (-88.0, 0.0)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 78.0, 549.0) 100.0 53.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 199.0, 550.0) 111.0 53.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 316.0, 549.0) 100.0 53.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 438.0, 549.0) 102.0 53.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 441.0, 687.0) 114.0 53.0 0 0 (1.0, 0.0, 0.0, 1.0, 489.0, 602.0) 0 0 [(0.0, 0.0), (5.6843418860808015e-14, 85.0)] 0 (1.0, 0.0, 0.0, 1.0, 266.0, 445.0) 0 0 [(0.0, 0.0), (-129.0, 103.99999999999994)] 0 (1.0, 0.0, 0.0, 1.0, 284.0, 445.0) 0 0 [(0.0, 0.0), (-35.0, 105.0)] 0 (1.0, 0.0, 0.0, 1.0, 309.0, 445.0) 0 0 [(0.0, 0.0), (51.999999999999943, 104.0)] 0 (1.0, 0.0, 0.0, 1.0, 336.0, 445.0) 0 0 [(0.0, 0.0), (154.00000000000006, 104.00000000000006)] 0 (1.0, 0.0, 0.0, 1.0, 358.00000000000006, 602.0) 1 0 [(0.0, 0.0), (0.0, 93.0), (82.999999999999943, 93.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 243.0, 603.00000000000011) 1 0 [(0.0, 0.0), (0.0, 107.99999999999989), (198.00000000000006, 107.99999999999989)] 0 0 (1.0, 0.0, 0.0, 1.0, 122.0, 602.0) 1 0 [(0.0, 0.0), (0.0, 125.99999999999989), (319.0, 125.99999999999989)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 12.0, 131.0) 100.0 53.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 10.0, 228.0) 100.0 53.0 0 0 (1.0, 0.0, 0.0, 1.0, 110.0, 252.0) 0 0 [(0.0, 0.0), (96.679069767441831, -62.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 57.502396350068466, 228.0) 0 0 [(0.0, 0.0), (1.0, -44.0)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 297.60348898618474, 283.17760904768005) 100.0 51.0 0 0 (1.0, 0.0, 0.0, 1.0, 480.0, 307.83455013117498) 0 0 [(0.0, 0.0), (-82.396511013815257, 0.69254377562924674)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 11.502396350068466, 318.67902962736684) 100.0 51.0 0 0 (1.0, 0.0, 0.0, 1.0, 59.502396350068466, 318.67902962736684) 0 0 [(0.0, 0.0), (0.0, -37.679029627366845)] 0 0 (1.0, 0.0, 0.0, 1.0, 111.50239635006847, 343.0) 1 1 [(0.0, 0.0), (144.0, 0.0), (144.0, -153.0)] 1 0 (1.0, 0.0, 0.0, 1.0, 11.502396350068466, 342.0) 1 1 [(0.0, 0.0), (-62.823326582626635, 0.0), (-62.823326582626635, -186.0), (0.49760364993153416, -186.0)] 1 0 (1.0, 0.0, 0.0, 1.0, 192.0, 157.0) 0 0 [(0.0, 0.0), (-80.0, 0.0)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 590.0, 494.27796453556323) 157.04856186 115.430787133 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 687.09712371909973, 362.41750333637344) 129.95143814 61.2912483318 0 0 (1.0, 0.0, 0.0, 1.0, 580.04856185954986, 433.0) 0 0 [(0.0, 0.0), (59.0, 61.277964535563228)] 0 (1.0, 0.0, 0.0, 1.0, 725.04856185954986, 423.70875166817342) 0 0 [(0.0, 0.0), (-26.000000000000057, 70.569212867389808)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 864.0, 225.81818181818176) 100.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 913.0, 171.0) 0 0 [(0.0, 0.0), (-1.0, 54.818181818181756)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 859.64671952078925, 422.0) 100.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 909.64671952078925, 484.0) 1 0 [(0.0, 0.0), (0.0, 80.0), (-5.0, 80.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 749.64671952078925, 216.0) 1 0 [(0.0, 0.0), (0.0, 101.0), (152.0, 101.0), (152.0, 206.0)] 0 (1.0, 0.0, 0.0, 1.0, 930.64671952078925, 306.0) 169.0 95.0 0 0 (1.0, 0.0, 0.0, 1.0, 950.06078202078925, 401.0) 0 0 [(0.0, 0.0), (-8.0, 21.0)] (1.0, 0.0, 0.0, 1.0, 150.0, 139.0) 272.0 174.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 237.0, 206.0) 100.0 50.0 0 1 0 (1.0, 0.0, 0.0, 1.0, 623.0, 774.56617647058829) 1 1 [(0.0, 0.0), (50.0, 0.0), (50.0, -58.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 164.0, 521.75) 0 0 [(0.0, 0.0), (190.44444444444434, -150.83333333333337)] 0 0 (1.0, 0.0, 0.0, 1.0, 366.89814814814815, 237.0) 1 0 [(0.0, 0.0), (0.0, 52.287009803921592), (-137.12037037037035, 52.287009803921592), (-137.12037037037035, 80.916666666666629)] 0 (1.0, 0.0, 0.0, 1.0, 829.0, 661.56617647058829) 0 0 [(0.0, 0.0), (-49.0, -73.0)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 578.0, 525.56617647058829) 123.0 61.0 0 0 (1.0, 0.0, 0.0, 1.0, 690.77777777777783, 341.48284313725492) 1 1 [(0.0, 0.0), (111.22222222222217, 0.0), (111.22222222222217, 40.083333333333371)] 1 1 1 (1.0, 0.0, 0.0, 1.0, 579.0, 654.56617647058829) 179.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 103.77777777777777, 370.91666666666663) 0 0 [(0.0, 0.0), (-1.7777777777777715, 60.083333333333371)] 0 (1.0, 0.0, 0.0, 1.0, 530.0, 525.56617647058829) 0 0 [(0.0, 0.0), (82.777777777777828, -154.64950980392166)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 459.0, 525.56617647058829) 103.0 61.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 46.0, 431.0) 131.0 52.5661764706 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 118.0, 521.75) 100.0 61.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 55.777777777777771, 317.91666666666663) 100.0 53.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 328.0, 47.0) 128.0 190.0 0 0 (1.0, 0.0, 0.0, 1.0, 622.0, 525.56617647058829) 0 0 [(0.0, 0.0), (18.777777777777828, -154.64950980392166)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 590.77777777777783, 317.91666666666663) 100.0 53.0 0 0 (1.0, 0.0, 0.0, 1.0, 445.02314814814815, 237.0) 1 0 [(0.0, 0.0), (0.0, 37.819417211328982), (193.75462962962968, 37.819417211328982), (193.75462962962968, 80.916666666666629)] 0 (1.0, 0.0, 0.0, 1.0, 804.69158878504686, 527.56617647058829) 0 0 [(0.0, 0.0), (-141.91381100726903, -156.64950980392166)] 0 0 (1.0, 0.0, 0.0, 1.0, 913.69158878504686, 525.56617647058829) 0 0 [(0.0, 0.0), (-88.691588785046861, -92.0)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 453.77777777777783, 317.91666666666663) 100.0 53.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 864.0, 525.56617647058829) 100.0 61.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 334.44444444444434, 317.91666666666663) 100.0 53.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 231.0, 523.56617647058829) 100.0 61.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 754.0, 381.56617647058829) 144.0 52.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 523.0, 748.56617647058829) 100.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 391.2037037037037, 237.0) 1 0 [(0.0, 0.0), (0.0, 60.56617647058826), (-5.7592592592593519, 60.56617647058826), (-5.7592592592593519, 80.916666666666629)] 0 (1.0, 0.0, 0.0, 1.0, 340.85648148148147, 237.00000000002251) 1 0 [(0.0, 0.0), (0.0, 38.566176470588005), (-231.07870370370347, 38.566176470588005), (-231.07870370370347, 80.916666666645057)] 0 (1.0, 0.0, 0.0, 1.0, 418.40277777777777, 237.0) 1 0 [(0.0, 0.0), (0.0, 52.287009803921592), (96.643518518518533, 52.287009803921592), (96.643518518518533, 80.916666666666629), (88.375000000000057, 80.916666666666629)] 1 1 0 (1.0, 0.0, 0.0, 1.0, 183.7777777777778, 317.91666666666663) 122.0 53.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 720.0, 527.56617647058829) 118.0 61.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 799.0, 661.56617647058829) 120.0 61.0 0 0 (1.0, 0.0, 0.0, 1.0, 269.0, 523.56617647058829) 0 0 [(0.0, 0.0), (112.0, -152.64950980392166)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 403.0, 655.56617647058829) 159.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 510.0, 586.56617647058829) 0 0 [(0.0, 0.0), (-1.0, 69.0)] 0 (1.0, 0.0, 0.0, 1.0, 523.0, 775.56617647058829) 1 1 [(0.0, 0.0), (-55.0, 0.0), (-55.0, -58.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 895.0, 661.56617647058829) 0 0 [(0.0, 0.0), (24.0, -75.0)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 342.0, 524.0) 100.0 61.0 0 0 (1.0, 0.0, 0.0, 1.0, 392.0, 524.0) 0 0 [(0.0, 0.0), (77.0, -153.08333333333337)] 0 0 (1.0, 0.0, 0.0, 1.0, 495.0, 525.56617647058829) 0 0 [(0.0, 0.0), (15.0, -154.64950980392166)] 0 0 (1.0, 0.0, 0.0, 1.0, 638.0, 586.56617647058829) 0 0 [(0.0, 0.0), (-1.0, 68.0)] 1 1 1 (1.0, 0.0, 0.0, 1.0, 238.0, 289.0) 117.0 62.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 242.0, 157.0) 100.0 60.0 0 0 (1.0, 0.0, 0.0, 1.0, 292.0, 217.0) 0 0 [(0.0, 0.0), (1.0, 72.0)] gaphas-0.7.2/doc/index.txt000066400000000000000000000007321175456763600154260ustar00rootroot00000000000000###### Gaphas ###### Gaphas is a user interface component (widget) for working with diagrams. Diagrams can be crafted and exported to a variety of formats, including SVG and PDF. The modular design makes it easy to extend and reuse this component for various tasks, including modelling (e.g. Gaphor), EDA and system modelling. This document describes some of the basic ideas of Gaphas. Also, take a look at the Gaphas tech site on http://gaphor.devjavu.com/wiki/Tech. gaphas-0.7.2/doc/ports.txt000066400000000000000000000055651175456763600154770ustar00rootroot00000000000000Ports ===== Port is a part of an item, which defines connectable part of an item. This concept has been introduced Gaphas version 0.4.0 to make Gaphas connection system more flexible. Port Types ---------- There are two types of ports implemented in Gaphor (see `gaphas.connector` module). First one is point port, as is often use in `EDA `_ applications, a handle connecting to such port is always kept at specific position, which equals to port's position. Other port type is line port. A handle connecting to such port is kept on a line defined by start and end positions of line port. Line port is used by items provided by `gaphas.item` module. `Element` item has one port defined at every edge of its rectangular shape (this is 4 ports). `Line` item has one port per line segment. Different types of ports can be invented like circle port or area port, they should implement interface defined by `gaphas.connector.Port` class to fit into Gaphas' connection system. Ports and Constraints (Covering Handles) ---------------------------------------- Diagram items can have internal constraints, which can be used to position item's ports within an item itself. For example, `Element` item could create constraints to position ports over its edges of rectanglular area. The result is duplication of constraints as `Element` already constraints position of handles to keep them in a rectangle. *Therefore, when port covers handles, then it should reference handle positions*. For example, an item, which is a horizontal line could be implemented like:: class HorizontalLine(gaphas.item.Item): def __init__(self): super(HorizontalLine, self).__init__() self.start = Handle() self.end = Handle() self.port = LinePort(self.start.pos, self.end.pos) self.constraint(horizontal=(self.start.pos, self.end.pos)) In case of `Element` item, each line port references positions of two handles, which keeps ports to lie over edges of rectangle. The same applies to `Line` item - every port is defined between positions of two neighbour handles. When `Line` item is orthogonal, then handle and ports share the same constraints, which guard line orthogonality. This way, item's constraints are reused and their amount is limited to minimum. Connections ----------- Connection between two items is established by creating a constraint between handle's postion and port's positions (positions are constraint solver variables). `ConnectHandleTool` class provides functionality to allow an user to perform connections between items. Examples -------- Examples of ports can be found in Gaphas and Gaphor source code - `gaphas.item.Line` and `gaphas.item.Element` classes - `gaphas.examples.PortoBox` class has an example of movable port - Gaphor interface and lifeline items have own specific ports gaphas-0.7.2/doc/quadtree.png000066400000000000000000000342071175456763600161020ustar00rootroot00000000000000‰PNG  IHDRÀºþƒ pHYs  šœtIME× $À_ IDATxÚí{xU¶öWõ- ¨r 3H„èaúq ˆ&rDPÔG§!àB>G“DèhÂH;£(3ŠÇ+‚HœÈ-œƒ„áS $‚C! ¹uw}TR©Ô­«ïÕÝïïáá©Þ½÷ªªÝÝoÖÚ{×^Ì?÷}Gloüȱvm¡ð%Ssì0w´¢è}":)±?Ñè+õ?cµ>7iâÄñÒ.^¬?þRcCcSS: DÄĘcãbo¸áº^½ºïÚypÛöí&"êÛ¿çÅ‹WþõýùK—àx|Oí§Ïþ𣻭úõ¹>¡ÏŸüÑ;î91ãá„>êë[ëë[[š[‰Ø¾ý{‘‰{»±±±©©-:Ú‚Oà[Žž:qö‡ç>pwúMM&MÔv»ãÀ·§ÞúïψhØÀÁ¾2R÷ã™Á½3Ëz'›ÐÇþ³ëé"'ËC$>00t™Lÿú‘©ýá=Û;±Æ_ߟˆššÚÛ}MY¢,—/5ó3ï÷Ü{«[½óÉÇ{ð(qö‡辄„^MWÛˆ´ŽÚÿñ‡î[û·‡Nò•‘ÞoÛî¾çÖ«—›éÀ)GK« Æ£g‰èî{nýìm›qñó\áåŸÚz\Ýé¶Ø[ f†oóÊ+¯|òÉ“D”™I;vPf&Ò„ ´sg{…éÓ¿ýé§Ÿ¦O?ðÔSO €”øþ×65µzЊˆx…ñ‰ÇÕf¶•5GG›ë~jëÍF™íbÆì`E–«­æ+ÍN;ÛÖÜÌØ[„FZì-D5Yá)ŸzêQQ»b ÿçøôS"¢;dÚ€ˆs?üån«ÆÆ¡ÂøÄHK‹“µÛYÇU£Ãq_aý}éÓ¬‘±ÛN†á~µìe"ú0çqGsKk›“q8eµÎ${Ê“'çÌ;77ïE"ºkÊ­™™T[Ûî¯iØ÷íÝ:hР9sðݸ‚!»ÝA‚F­”ŒdO™¦Þô‹-e8Zíd`ÛZ™¶&{é©:"Ê¿~ÎÃöŽ v¢ÙëÞáÞºðûÿKN‡“eÉé=‹‰ˆ†ˆÈ``ˆáÉ ÃP~>íØ1á­·¨ €vŒ†íÒí% 1D p?÷òÑÿþ€SO"jµ·ä0µ99­#–ˆ!§ÃÉɦ‰ˆ c00F“‘ézW&“AzeS²FYöOáÔh42,± Ë¥¥,+ûü|]í5Éiµ§ŽÑ/nÕ=ÚÒhwÈâ¢-Wí½}ºûÛ_ÿïÎâ5ÓXÿPå7DÔ#&ª¡Íî²òÚ×_>TùÍËE¯§ M¶;¶u¯»u.üzCüë‚ÁÀ´iø±ˆ0›MDdì"·Œ´µµIŒ0DdokcX™rêž~2þå?ujè½÷=úñ‡¼zÖ=ý$9ÖѲv;™ŒFb‰eXbYÃtx ŠŽŽb‰˜®Ÿú“G%$PB ´³° ¢„ª­¥e/übÇÊÏÏo¿¾ö+c‰˜±ã2ËÊ>¯®ù.%íæÚ“ÕD4nL¦“X³É°zõZ 1õõÅÆOß'¢X³±•uã{o`-÷RVö9¥ Mnv:XOÏ…ß.<ÐÆéd=øb:,ñ‹–„F¾ØúÞ±c{=Сïñ5;Œ˜ÚKb ,k rÑé ¼ö¯¡¼zž^°@xäpQ‡—If“‘1ð¨Á`2YÎ70g%$´'$Pí@ª%ª}‹2ߦ‚âþF“©ãKÃŽ9šˆ¾9°Æý<øÿˆè?núEÃNŸšÉÕÙ´y§‘¨øµ¢ÒÒMD”•5uþ‚ܧŸüMÍñc¶¿¼×¯oÿ³çÎX›%b]öRÁ‰cß¼9ƒ:FI~‘>rüØL““ýÓŸW‹ÎòÄSKˆèÍu2mmçÎkµâÅüW_}“¿¶ÒÏv’Üåq _}õ¥ö/˜·è®ì»³îžÐ1|ÑeÎ¥qçÝ k6»ùó…Š^gêtÚÛÜî†éy”3¢n—3b4øÚN;Ëtý}Ϭñ¶»îîâÛYÃÄÃ2F~ ”12³‰%†aXaX¹î-J¨íÑu”ð6%l§Úª}‹èíÎj³‰ûÉrÍ33o«©ùn÷îíD4~L&1eé¢2ÿS¾ˆîʺÛ`1ÑîÝÛŸ~"ïµØ¸-[?{ì±y¥_l$"þ8.6nü؉†(³¡Ã×ÛúÅF"zì‘dz±à¿^r4µÁ`ì8WYé,¦hËêÕ/îÚYVßÐ@D5ÇSttçƒØh¥†íåÙíå¢[si\X[å\¢{1˜ÜÜ]þOð *‘t:X‡ÚßF®¤Í ü•‰Œˆ v5â`B#–(÷™ØÛÚÈl6;Ä’“1Ño¿ØÄ«'Ë¿~çT"2°NbÈÎ’½­ˆ,#Q»ör Ú›™Ž$ÁŸI”@ùD…;)ˆ¶Síœ.¾§pŒ–¿þŒÑ·þå/k¶”n"¢›oú…)Êl6wùýsúÂ;q äpþê¡9k׿òÊKõ S²¦n)ÝÄ?ú°•ˆ¢b:Ÿ1={î wMDfs7Ñõp•eÏ’—·`ÿþŠ?,ý¯q·Lh‹%áå™ÍFÙ†|9Eu‹ÖçŸ~zžKã]ûMñ\¢{qßû‚=„‡€ŠBxG[›û!¼Ù T‘‘¶V5ƒïš»Œ‘Óá´3F""Ö9¯ôs^=[­Cl6^C×dý§“ˆXr:œí΢ÉÌ)$Û¾l‰ á d4™-O"ÚIoí¤uåSmÑ¢Ds(sG×{´˜¹?¹ CDLZj7 JDCoŒŠ¶˜Ì]Ü´îqÝëêK?Û),œ1ãµk‹·”nê÷dNnùîíœOɾ;*6Ê"°À5ïð|eàÊeϲ»eB·î1¢úü±lC¾\Zß-ã²6¥çRj4t"x|]åÌîlii#¢Û&¥ :ZZáϯ¿!÷M6v*ŒÀ‡ðXJ绌‘ˆ¢Ì&îCq:§Óɲ,Ã0 ¾ÚÊ«ç±GÍý?ôÍ¿òúÚmÙ,Ë:íέÅbd‰!–%†!§÷@³EÆÓÙNT0v&­#ÚI42·SB-%œ¢ÚZ8cÍ]Ãg&O¾£¬ìËqc2‰¨[\ŒÈ윹½öÚËïø÷~?ë÷Ú««ïñÀO,$¢{î¹ï“O>¼3ûn†¡÷ÿrݺ¿NÉšÚ=®{\X~DØ|þSOæå==`Àϸæ¢eV:˽÷ÞÿñÇœ9w¦fïQ®ò™sgzèÕ£{÷+õõ\[¥Ë»ýŽì?þàó­ŸÍyìQѹ8”Œ‹*k9—¬}B»ÝaïX¿(]ÂùÅÖ÷ìr«v‡0„á^ªŸQhÄeádªÙî41Ä’1ÿëm¼z™óïw™óXòº¿pzßÐA…·Lt8ÚGA-íRαœ‡Û¾4*ÊDlÇ’z""š6mÚŽ 2‰2¹X>“ˆˆv´‡êÒM7ÝtùòåŽÛú¯Ì˜1ãÊʾ¼9}dL·¨¨(±4ÿö·ómöwÖ¿ÕÐØß?>sÌíM Í×^ßcÖ¬_}òɇwfM½&næÌ_­[÷×;³¦vëÓ-ZÚü“O>|óÛ€~ýÈã\óΰ7Ê¢t–çŸ_fom›k5wÎcÓ§ÍøtãGçÎùæ¦üÂ?.ZøDÖÝ®Vº¼çŸ_ö}mí«kV}ôéѹ8”Œ‹*k9—¬} À…V º†ðÎÖµ%œ²ïšÌN"Šêp#DFÌÿ­ƒíF,¾ˆ%ÖÉŽn:;<{ލíáÙs†¯_GD…£'جƒ8‹í^"ãhk%nCå¼Åö¸öÚö‘–íÓûº—_~ùºë®sÙ)ß~ûí+¯¼rþÂO]‡pY"æò¥ú‹çþÝ«oÏk®ëÎ×?~ä IHD—.^þéÇËN'k4ãöá¼ô榖ºÚs?Òßl1·µ¶}üL|Bßè™§_/]¼|ùR½Ãî6Úç«IÏrþÌÅúË×\ßÃiwÔ_nì÷³ºÅÅ4\iü¡î¢úåQÝ©sÍW[Œ&£Ãî‹Cָ贜Kz/øõ†T ¿a®X÷Ÿ“'556ѽ÷Èoööñ'{D3I,Q·ØØÏ˶åçÍÑhDd°«‘¹DTv÷}7&ôw¶µ Æh2¸ú¥8Ybv§Ói0›¿«=3ù³ø‰¢+?]^±òÍöžs’Y"bèò•†kzÄiìšËWˆ%–a–åFΖXbz÷éÙ»OOQåäôDþ¸Oÿ^}ú‹sZ,æu,³°¾ÙæÒú²Õ$ô“ìÙëÚž½®UoHDƒ’~¦Þ!²ÆE¦å\*÷Bct ¨ÙÖV;mx¿ÜVQ¬ pöÖ7}ÞØâ 2ÄÓ`ìt-Õ4Ôélr¨ÅÁ_ CÄ2‚I$ƒ‘̳`žmjjéøÊpªHÜ «p©Ë² WAøeaø?øí"‚GtåPemŸÞ=ÜmuâT­0úöƈÅÒþѹó­}o°´Cv§öóÜùÖÎáJb‰ƒ‘:Ç@Íf#Ø·d2¼rÛ„´?¦Ä¶·æüM†e‰–é< ¯²ø U@ñó…„ꉛÓS¾9PE4È݆g.ž¼9=…[Æã¥³ÙD,]ÍYÜ­då¹ó×¹kÄî¸t5g±Él⤑e¨óYø×ö4!<À?dOH!¢oTuþya˜o‹Xeëæô”ì )&£ÁFŒD4ýŽ›?¥ÅÝJV Œ´¯J"UÇïjÎâéwÜ,ÔÉ×ö$néOzæÇ‹çñ1€F®ïuÓOþ±}ÕÑcPÐʘ^7¿#}Mõ±“'/ SÀ%ƒõóRÛÔd6]{]¯KÿïH^î/Ñ5>'qhSÿþáÿ@Ñ™3mr›DtË-±øb‡_ÝXôÒíõŸ\¸$'ç…›ÿ#™›Ú?j½kGúÔWÌýÍÇ««ú÷7×Õ¥…ýÍ2ÌþH¸Ío¬lhpîÙ3 _ïð ;»šˆå=«íKÎ<¹p‰°Ä$û³G·zÏ[o¼ŠN T8^]¥^AÖ¹4yf €]PðÍÍÍ[¶þ£¹¹ n`·;¾(ýê_ÿªûj{¹Ë‡Ò! ÐŽÓé,Û¶ãüù DtútÝ®Ý{Ô5Ô„.:¡°ðlAÁŸ¥  ~~?ô6å÷Tœ:õ/þåÑ£ßÅÅuysªo4qèðšc‡s'<ÐPOî,P ÄØ1·ÜsÏ==®é²]Ó•Ë—.œ?ëK42Õ šXv¤ÿŒ3Ì~ô0PçÂù³Jr)c à!žhâÐáüÿ| ÷OZ‡/U5¨RAú®ÆB-W¢±•ð®•.9xÂ×;, f]sZÃËFÁÂÊ¢ J65º¼-­jŽÝ5by ϶ñã,¼Pb\ÊJoÚzp%Ô„4 ãa ߨ¿ãYíö=»ÄãD2¬¶4nRõ€úÕY…Õþ¸8›D8žíÆäãYx]9- Õç àŠŒç(ïr³+^ºr¼©á[ÂIOêÙ•¸ÕJe² ÕꊴCVõ\«ÇѲ­\šòìJ4¶òl2Ê3.\°O˜ðÝ‘#ÍAùZàyG´€…ôzäÂûĉǂ¥ž¨§*ˆÜøU=nêÙÓHDÿþ·cøð˜íÛ‡öî  Ï;J(¥ê9|xÌûïyÿý!ÇÇ>Ü4qâ± ìè  Àµznß>ôºëŒ×]gܾ}(4€ á‡$¥ Ü ~½i‘zöîmúþûV"êÝÛ´}ûPî­‰2–Rb„‹€2tjƳgi•ÔSøn„hh~~¿Â³8 ¾« p Àܹµ‡7 ¥$Žœ†Ž}ôðᦹsk7oN ¿N((è_PÐ_B` T46:‰¨GCl¬â'kèÑÃÀW@@Ñë¯ÿ¼_?óMÓ¦¿zUF¯^uN›vüÀ¦~ý̯¿þsôPÐÎ7Foß>´_?sYÙ©†rêYVv¥_?óöíCo¼1šˆ Ï2Ì~•O t­¡²êI ݧ‰\þmÀŸ .4´¹ÙÙÜ,¯ž<,;Rö_H÷ƒ7Ò‡PA æ,¼ú†F²ïj/ i 8ñXYÙ•óçíDtðàU%õ o<ø3€5¤ Rf`¤î' „Cx‘~r82ÌBx,Ü ‚BxYåÂz&p¹-öm <=PõøÚyÕ˜âXiò*ÁæoDœ€*i–RÂdõVmÊ–`b!<@@  P\ÏÂ{“­"W@†A€'ŠÅ’ Xà)U 0 t‡7¡âV #tHR úȧ‰Ïî=xJ„‰€Ññê*t“0ÛBxP€€  ñ˜Ð:ëÉ€*ƒz¢ÙwÕ Ã,±Rׄc9:ºö@ý‘3̱c=9PJaáÙ‚‚3Þÿõ‚tÂ+y¦ÒH\¶Ðe,/mÅ• …rxã½zúÊ€êÇø$é…ùc¡ÆÉj?…ô€ÒÓƒp‚eGzÜw T=P’{SBÃåãk©·˜S@‡ð"¹ ˜®Á-„p/ëlr#¡÷‚ ô’¶ÖW:€°ò@—455UUUQJJJLL :(§µµõèÑ£UUU‡ªªªª¬¬øàŸÿüç5kÖ¤¦¦¢óÐ’Êé‘ŠŠŠ±cÇîÞ½{Ö¬YG}æ™g|®žQQQÏ<óÌÑ£GgÍšµ{÷î±cÇVTT ÿ€€†*‡š2eJß¾}wîÜùî»ïÆÇÇûûŒñññï¾ûîÎ;ûöí;eÊ”C‡áSzœ>}zÊ”)111eeeãÆ ä©ÇWVV3eʔӧOã³J\¸p!++«¥¥¥´´tÀ€¿€”––¶´´dee]¸pŸên¨ì6K¾µÒS^ÍÍÍS§N=qâÄŽ;¸§‰‚BJJʦM›233§NºcÇŽèèhüH¾€ ÕÍbê V®\¹wïÞgŸ}vôèÑÁ½’Ñ£G/^¼øùçŸ_¹rå³Ï>¢ý‰¼F 333¸ÛÑ3 ò¬z‰ô-éË„Lï1ÚS+µÒ :O:¤Ÿ”MÂ';Ý}Ê ÂM@‘âê[a©*÷Rø? ½W2e³Ùðáï† \Fx.;¼Þ®ËÏe‡úŰ,Ë«¡’«#­É²,oJË[­Vh(л€Âý²jÕ*†a-Z¤«H“eÙE‹1 ³jÕªù àw Â$RÐS댌ŒÙ³g¿óÎ;ëׯøá‡urUëׯ¯¨¨xøá‡322"äÇÀ²,fáÞ4)Že+{6ï–-[öñÇ/]ºôÞ{ï‹‹ úõ444,]º4..nÙ²eº8iü.‹sQ‰RhÏ0LZÚZ"ÊɱB€~Cx ">>>//¯®®nÅŠz¸ž+VÔÕÕåååÅÇÇëD:•¨cXS¶Žt$TXSú–ÕjµZ¡žj,^¼xôèÑ+W®Ü»wop¯dïÞ½+W®=zôâÅ‹ñ¹ ¢££7mÚ4xðà©S§VU-UUUÕÔ©S¼iÓ¦èèhtŽÒZ%=Y  áFïÞ½KKK£¢¢²²²‚²ÃÈéÓ§³²²¢¢¢JKK{÷î­Ã.‚(PdÀ€[¶lijjšyò䦦¦-[¶kï%·\Qé’xQ ’Óãz^)[/ð#FŒØ²eËwÜ1a„™3g®\¹Òß39uuu‹/Þ°aC=¾üòË#F„DG)=ðÎà!w|Å_ï ‘‘±{÷îyóæ½÷Þ{›6mZ²dInnnTT”ÏOÔÒÒRTT´|ùò†††®¿Ì*IDATqãÆ­Y³&555tûMøuå—7AFAÐÏ ‹ÔÔÔ;wnذañâÅøÃÖ­[WTTäÛgÒ7nܘ››[SSo³ÙfΜê/¥KD û-à†ð ˆþÔ¬Y³¦Nº|ùò¢¢¢éÓ§ggg¯^½:99ÙKËGŽY¸páÖ­[£¢¢žyæ™%K–èaõ¾ iik¹g„TŠ‹Ùôôö1…´ãù"$’”‚>rKò|î³ÇÅŽð sæÌÉÍÍݸqã¶mÛ¬VëøñãSSS“’’L&­ív{uuueeå®]»l6[kkë´iÓŠŠŠõÜ¥£FѾ}Ä/nW9P9v÷ŒøÌ=^]…nÒ‚_gÛ?ýôÓ­[·.\¸°¸¸¸¸¸˜ˆ,˰aÃRRRFŒ‘’’’šš:xð`ƒÁ@DN§óĉ•••UUU‡ªªª:zôhkk+g-99yõêÕÙÙÙúïÕŒ Š˜gñBxàO²³³'Mšd³ÙvíÚUYYY]]}ðàÁƒnذ«“’’BDUUUMMM]>i“)99955uüøñV«Õb± ?€€F‹eþüùóçÏ'¢ÖÖÖ£Gò>feeå‰'öïßODƒ!11155•÷O‡ Ñ :Å4-----mæÌ™\ISS÷hJJJLL º(ÐJLLÌÈ‘#Ñ<Ê P×^”Ã𘪡\ê'©ÒÛB)„‡f  >ÓSîŸz!ŸË“/”¶ÒbAdMvlôÂ˺ŸÂ—ü±l¡¨?& ëÏ*YŽ*À„°ª=ç%@@ê~  p?@3šD’ï®9v˜›Æá_òÒB·piÊUQ(Ù·¤…J3H*3ìðh„páÂ…×_ý¸­ž'NüÝï~Ç0 z=àzÞ¯Ùz[êyøp¸­`Y.[…³£+õ>|xøi(a+ X&©+õܾ}û 7Ü6·ÆÿyæýP†a´û¤Z*óuܲ €ÏBx õìÝ»w8©§T7ý§qPO…z†'R%•:§J…·TTR‹eˆ,€€B=CF4U‘/”ÆàÒqywãtËÐP…z†ªãéÍ€@›ƒˆEGë@•r “ A=ÃßjÛAPš¨N5TX!KÞÿý÷d:Ðws•ÕMyy%P€>´™?¾Ñh¼ï¾ûÂX=E!¼´Dz > ¤$Á*µ4 ”T”&“;†ð·¤n©´0D7KþÝï~Æß¹}û¨¢ÂíVÅÅlzº;()ñË…edШQÐî¨ödÈx@‡x žV«Õß¡- ¼ªÑyÔ¸ó<Ð99úº?yµª;¼Oš* ú£œ€0@ûºŒß].£x ¾ áe áŸ"B@]&.v™ÍY‹‘—0þ¡°°PötÈÅ@ø„ð‘L~~¾’ÌyOAAA~~¾T=¹góGŽïaÙ€ MAAë”|O~g“xÀ'· ÛÅêXEàŸá§}¡22(#½ "@@‡$¥ ´Ã0LØL[EÎ~øøÑ=^]…nÒB8å†z Œ¨'Põ ‚Ô(ð¨'Pà!PO  ÀmòóóÇŒõ@ך8t¸ðŸËÊ¢ïkY vïÞ õ@;~Iº¿œ»©;\ÖÇL„ðÕìZò¾R„.ü_c,/5¨qôô«烓ÍQ,m®=H—”Í~ !  R—Óå>ó>9„ƒ€ŠÔ-ðn ‚w@H ¨¢f¸¥Ÿ ¯Yøû†pE¡ä '‘¤þ ÿ®º“è«>‹úP—²%zWšÓX©¹zM¿NU€€@D ("k"ä…÷;gδ V w ·Y]ݲ4eJ5¾ØáÁW_]¹}’?4œ²õ‹úzgØßcïÞ¦H¸M–%"ºtÉoux`·ûÓe]ì%·Ü»gÏ0ôCx]}ù²h8} ~PL쀘Dÿx C’RÐGÚa>;ÐNŽWW¡›´€Ù6€€@pCx{<‚ƒ€"¼C@‘Îî]_yÖc à!ž{ *™9”êûp¤–Ì~Ùä>á…:⥬¸ÕÐ> á¥Ùâ¨W¾¬«(z—{)ô+Uš‹*ËÖ”ŠŒ«ŸH¥ž/À¿ª¤VJïrÞ«K±ºº¼zJkª7wyê…ð²¾áõ‰v‡QcMx €À…ð¼0GO8n+Â@TÝSséÇÌÑ“ ö#ÍÍÜ·**Âöî22hÔ(üÒ¾Cx¥ÑÆàº¢!á¡zÚl6á[üKQ¹ÇHíh·¬TSéš¹ã0þÛBÛê‹P=•Bc—3÷R½¹KS. }Õ<ÌÈÉ!†aX–åþç çÏ<'ÇJDÜÿÞÃT)ÑÞ–:žb–óÕ†IK[‹9У€º¤‹V½¾vã²5V9U•ßy+½Ö‘–ÈÊo\ØJöŒÂBþ¥Ð²Ð˲éé6üÈA„ð l$R$I²Õ„*Æ5¶âëHKDÖø3J[ñ¶Òó*]§Ê[è"„á§¡Á5%j%O ô‹4döÞ”7­¤c²ÚÇ@½3atì½qÑD6'[²–eß’õ…ÕT ’`èSØJvHÁ‡>2ÐRO¥¡IŸø\V«UÖ,o\蜊F3Uê¯YvS4×Ä·ÕäKdoVz*÷@@Cxdë m9hnÍsîgZÚÚ’’.>iZÚÚôt§A\«ÕZ\Ü>µ]\Ì–”t¶åkŠLñÇ6›k"qoµZm¶NõpoI˽w{ýC~ümZ­Ï•¬Éÿè£mÛ¾*ÏËýå®esóÄñê*t—ð=ùÐÆ={†y) ÒˆU]U\ÚÑ¢ÈRÏWºiÃ-·ŽÑXô­ãrr^˜tÛ¸3&åÌ+Ä,|h6óã˜%ÂýÄï¼)É–úxi·–Í‹æÓ¥C «’ž(¦ï)*Qšû&¹¹#ÙIs—>©R+Ùùqu›xd@@Þ¥V:ÂèὯ6¸á& î&Iþðæ"dç·Š (#UÉ›ä2ƒ1Rûœœœp¾;¬9>O"@x ¨(ݱÐßäþ • „G€ðá•¶ w™îXK>dø€pPêšÜ°'<êMðîAŠ:쀈P3é¢]Ý“€ˆó@5 ®¯DHÁBBI@¥IƒÕÓ‹ê«çC†’jgÔ(Ú·/"n€°ò@•4N)ݱô@ÉÔS;”‘n@+XHP€€Â“HC’RÐGÚasV@@;A‚9 ÿ3á@@ „ˆ’hò/½Ïx¬dG»eÙ¶Òj²oB ©\è©§Rfc¦·^§èšUÞ(¦g*rî”Ü@—%BoQZGzF-î*äÀÁt蔜PQ5¡–ñ%.úö*Ü©¸˜µÙlV«µ¤„l6[q1KDéé6«ÕZ\Ìr|9_“#-m­ô˜;}+0 ­1•îJçïE”a°3“G •Lõé3U¥•Íf¾äÔÓjµ†P"­1T)›1PB8nè+S²“ÏŸÿxNŽ•;µÉò5E ùcÙš¢V~»‚1ÐЋßyùSÒPî-u…åëðÕÔ[qå¢V¢!©² ë‹”Wå-àú,Àm®¬¥ódyV%Q](úž¢¾\:©-G©%ƒRM©•ôØåÚõ êõ”n)¯½P¶Bää›ãýA(á, *™<ÔqYSvkúˆÒP|¹sUÉ\„ŨV%i(¦ãP„ùR1Å‚'ƒå>„’€ PÙÅÚ ¥fñIk'ì“c=UOA¬¤†îÚÁh€» ¹1ÁBz€€   Áõ,<²õ€'Š„_à¡€b%(1Pð:$)}¤†aà³íäxuºI ˜m!<(7„ž}TQ¶w—‘½ì<ÐF´2W6™°7<ö˜MTb³Ù4¶•Ö´u TM»qŸÆ¨w+Àç+K\†Ð‚úè¤È9å‡,Eo‰$XÖ tªZ©•ôØåÚ/ <Täß¹LDÌrÒ¦"pM©T³Yá$ÇF°â ¨…òa­ÝThMÄ{ì®jgÔ(Ú·/l¿îx DD0SÒ?ÂÉÈ Œ tšÐÝBzi°ï±º…è|¨Û1>§kü»*É‹¬¬)ѱìÙá„t' *Â$|K¨€^Z–5¥>Ýõ„|ð@}‰ãs@@}ûBx€€BxÈÖ™ˆvñùFÈR;Þïê¤tÍžoÔ‡P‚SÏäñí5û¤>PLãN´á“tÿ'l…§®b¢-ò„­dÏ(ëlвŒ(í^Šø ŒE‰ ¡Ü!ZübDî Ð(ˆX ®)_åñ~g?  À=Â&w¤ „Aˆß…®Ÿ’Æé$w‘èÊi+øž¢ŸçQ?µ»¹CT.À'1z á —اN;ûöQE…ÖÊÅÅlzº;­ Øš„…€]¡]=9¬VkˆÞ&„³€ªdÖ”nÑ$Ý)YX“rs%rrÂùî/„€J‡A9SÊÇ©1‰±R¨' |Ô]Eó~³zðú]Æ„„güJ„,/Ç*z&!<Д‘n =Pà(D@?$)}¤†aà&íäxuºI Èÿ BxP€€  E@ Tºy’g%î‚]ìáæjÜóX˜§=ˆPu)—ØpõNg•þçëpÿTŒÈˆÊyËÂ<Æ¢†êç’ZP*–À¡ Âñãvv"oT´·¼Ò±´¹HÔ\&1&ɶö.Ïå²²ì¾=Àu>ŠeÝULŸ\¢;ÞŸW¶•Æ=óð@gN\?Ô—®"P‚a–e¥/Eå¾²ï–e¥š²×Ì0 ÷Ò'— @pBx’ŒfÊ–„œ+ꮵp„EÒ#D·2${µÜpÈV "Éò=ùÐÆ={†òv8gmþü.!³(Žöì@:àÁ[.'²Ô/€scsr‚óUÉή¾|Ùàøõ½}Ò†[n£±þè[Çåä¼0é¶q3fLÊ™WÔí¨<$–Øl6¡6 eK„–Fé$¸z+~ÎGØŠ¿-ªr\ø€Àvvá‰4LæÅKøR(v¢šÔuʈŸW)–KëHO!Û\ûtàF:¼smÀ÷Šl½‘ ¡è|/ X (ž›ÏCx¢¨‚  C’RÐGÚaF>û¨Q´o_8÷3Vуñ@#a!½OÐÏl[FedàÀï`(@@ P€€€€@ÐTšÇ³ïÏ ¡ír[»T7a&"t: BÔ¥\†DÒ ¾ê2èVúŸ¯ÃýS1"; *ç-ó…Ò†Ï%´ @à+À•øw3õôñJÇÒæ"-S· ÌN,,Ôx.'Zözð@=‰â¥IÞ¥Šé“«ÔbLJçU¿)Â.€x ÃüÀ´ÒøWú õLªü$p.Cxð6„'Éh¦lIh¹¢²¦”n c €àx Ò‘S%§RvÉ”g±¶l+¡8j1Ë×Ѳôu#ŽV™iê”[Á¸’°ª”h9Ö~1˜>ø8„Làu‚‡.˜©@@½ÒPt!<@@@Ÿ!¼~²õ@( (Ã0è#ðD@1gJ` üãIJAi‡aøì@@;9^]…nÒfÛQÆŒ›¤ÑCjkkuO@ ìqé)ÊzH€ Æ@ ü‚çÿþîaɯœYXð,©lCŸÜBÑK[ð5 nËD„Ÿ}¶äÔIÏÿúX@e×´“«‹ø†JB&²,”TíÆ…öe ½dQÞ³ã–eñD »w}…~àѾ¿§Ç¾§ïT]n¤ï*Í I—vºu^o^ú#„çÐ¾Û v°n¼½üÅyž…hHÁ²ìÿî©@?^øãr§Óq® 6ívû?ʶª¬êÕ»/¾Üø›uo¯_ðÄÓÍÍÍð@CžæææMŸ—ž¬ýžˆ®¹¶':€PúeÙC?zéÒ%hh³mGùùóИo¿=°hñßÚt= 9>ß2)sÜWÛËOŸ®CWHnº)}ÕÊåPŸpJDGGOÉž\þ?GŽAo²î˜¼ªè¥èèè€ (öÝð ÃŒ;ºGîè ÀœGf/ý}žÁàûK$• ^@‘–rù§£ð7xf‰Ÿ,c)˜\¼p@è"ã¾õƫ蟣}.³vòçKû›'Ðõ>Gû\fíä/ÎÇŠ,|Γ —x¹ã À-ÚÚZƒ  +ŠÞG×€Û:é¶qèp[@g̘„¾·`¬ÖçÐ àÿ=R¡=ªò¤IEND®B`‚gaphas-0.7.2/doc/quadtree.txt000066400000000000000000000051101175456763600161240ustar00rootroot00000000000000Quadtree implementation ======================= In order to find items and handles fast on a 2D surface, a geometric structure is required. There are two popular variants: Quadtrees_ and R-trees_. R-trees are tough and well suited for non-moving data. Quadtrees are easier to understand and easier to maintain. Idea: * Divide the view in 4 quadrants and place each item in a quadrant. * When a quadrant has more than ''x'' elements, divide it again. * When an item overlaps more than one quadrant, it's added to the owner. Gaphas uses item bounding boxed to determine where items should be put. It is also possible to relocate or remove items to the tree. The Quadtree itself is added as part of Gaphas' View. The view is aware of item's bounding boxes as it is responsible for user interaction. The Quadtree is set to the size of the window. As a result items which are part of the diagram, may be placed outside the window and thus will not be added to the quadtree. Item's that are partly in- and partly outside the window will be clipped. Interface --------- The Quadtree interface is simple and tailored towards the use cases of gaphas. Important properties: * bounds: boundaries of the canvas/view Methods for working with items in the quadtree: * `add(item, bounds)`: add an item to the quadtree * `remove(item)`: remove item from the tree * `update(item, new_bounds)`: replace an item in the quadtree, using it's new boundaries. * Multiple ways of finding items have been implemented: 1. Find item closest to point 2. Find all items within distance `d` of a point 3. Find all items inside a rectangle 4. Find all items inside or intersecting with a rectangle Methods working on the quadtree itself: * `resize(new_bounds)`: stretch the boundaries of the quadtree if necessary. Implementation -------------- The implementation of gaphas' Quadtree can be found at http://github.com/amolenaar/gaphas/trees/blob/gaphas/quadtree.py. Here's an example of the Quadtree in action (Gaphas' demo app with `gaphas.view.DEBUG_DRAW_QUADTREE` enabled): .. image:: quadtree.png The screen is divided into four equal quadrants. The first quadrant has many items, therefore it has been divided again. References ~~~~~~~~~~ (Roids) http://roids.slowchop.com/roids/browser/trunk/src/quadtree.py (???) http://mu.arete.cc/pcr/syntax/quadtree/1/quadtree.py (!PyGame) http://www.pygame.org/wiki/QuadTree?parent=CookBook (PythonCAD) http://www.koders.com/python/fidB93B4D02B1C4E9F90F351736873DF6BE4D8D5783.aspx .. _Quadtrees: http://en.wikipedia.org/wiki/Quadtree .. _R-trees: http://en.wikipedia.org/wiki/R-tree gaphas-0.7.2/doc/solver.txt000066400000000000000000000063741175456763600156410ustar00rootroot00000000000000Constraint Solver ================= Gaphas' constraint solver can be consider the hart of the Canvas (The Canvas instance holds and orders the items that are drawn on it and in the end will be displayed to the user). The constraint solver ('solver' for short) is called during the update of the canvas. A solver contains a set of constraints. Each constraint in itself is pretty straightforward (e.g. variable ''a'' equals variable ''b''). Did I say variable? Yes I did. Let's start at the bottom and work our way to the solver. A Variable is a simple class, contains a value. It behaves like a ''float'' in many ways. There is one typical thing about Variables: they can be added to Constraints. A Constraint is an equation. The trick is to make all constraints true when solving a set of constraints. That can be pretty tricky, since a Variable can play a role in more than one Constraint. Constraint solving is governed by the Solver (ah, there it is). Constraints are instances of Constraint class (more specific: subclasses of the Constraint class). A Constraint can perform a specific trick: e.g. centre one Variable between two other Variables or make one Variable equal to another Variable. It's the Solver's job to make sure all constraint are true in the end. In some cases this means a constraint needs to be resolved twice, but the Solver sees to it that no deadlocks occur. Variables --------- When a variable is assigned a value it marks itself ''dirty''. As a result it will be resolved the next time the solver is asked to. Each variable has a specific ''strength''. String variables can not be changed by weak variables, but weak variables can change when a new value is assigned to a stronger variable. The Solver always tries to solve a constraint for the weakest variable. If two variables have equal strength, however, the variable that is most recently changed is considered slightly stronger than the not (or earlier) changed variable. Projections ----------- There's one special thing about Variables: since each item has it's own coordinate system ((0, 0) point, handy when the item is rendered), it's pretty hard to make sure (for example) a line can connect to a box and ''stays'' connected, even when the box is dragged around. How can such a constraint be maintained? This is where Projections come into play. A Projection can be used to project a variable on another space (read: coordinate system). The default projection (to canvas coordinates) is located in `gaphas.canvas` and is known as `CanvasProjection`. When a constraint contains projections, it is most likely that this constraint connects two items together. At least the constraint is not entirely bound to the item's coordinate space. This knowledge is used when an item is moved. A move operation typically only requires a change in coordinates, relative to the item's parent item (this is why having a (0,0) point per item is so handy). This means that constraints local to the item not not need to be resolved. Constraints with links outside the item's space should be solved though. Projections play an important role in determining which constraints should be resolved. ------ The Solver can be found at: http://github.com/amolenaar/gaphas/trees/blobs/gaphas/solver.py, along with Variables and Projections. gaphas-0.7.2/doc/state.txt000066400000000000000000000173041175456763600154420ustar00rootroot00000000000000State management ================ A special word should be mentioned about state management. Managing state is the first step in creating an undo system. The state system consists of two parts: 1. A basic observer (the ``@observed`` decorator) 2. A reverser Observer -------- The observer simply dispatches the function called (as ````, not as ````!) to each handler registered in an observers list. >>> from gaphas import state >>> state.observers.clear() >>> state.subscribers.clear() Let's start with creating a Canvas instance and some items: >>> from gaphas.canvas import Canvas >>> from gaphas.item import Item >>> canvas = Canvas() >>> item1, item2 = Item(), Item() For this demonstration let's use the Canvas class (which contains an add/remove method pair). It works (see how the add method automatically schedules the item for update): >>> def handler(event): ... print 'event handled', event >>> state.observers.add(handler) >>> canvas.add(item1) # doctest: +ELLIPSIS event handled (, (, , None, None), {}) >>> canvas.add(item2, parent=item1) # doctest: +ELLIPSIS event handled (, (, , >> canvas.get_all_items() # doctest: +ELLIPSIS [, ] Note that the handler is invoked before the actual call is made. This is important if you want to store the (old) state for an undo mechanism. Remember that this observer is just a simple method call notifier and knows nothing about the internals of the ``Canvas`` class (in this case the ``remove()`` method recursively calls ``remove()`` for each of it's children). Therefore some careful crafting of methods may be necessary in order to get the right effect (items should be removed in the right order, child first): >>> canvas.remove(item1) # doctest: +ELLIPSIS event handled (, (, ), {}) event handled (, (, ), {}) >>> canvas.get_all_items() [] The ``@observed`` decorator can also be applied to properties, as is done in gaphas/connector.py's Handle class: >>> from gaphas.solver import Variable >>> var = Variable() >>> var.value = 10 # doctest: +ELLIPSIS event handled (, (Variable(0, 20), 10), {}) (this is simply done by observing the setter method). Off course handlers can be removed as well (only the default revert handler is present now): >>> state.observers.remove(handler) >>> state.observers # doctest: +ELLIPSIS set([]) What should you know: 1. The observer always generates events based on 'function' calls. Even for class method invocations. This is because, when calling a method (say Tree.add) it's the ``im_func`` field is executed, which is a function type object. 2. It's important to know if an event came from invoking a method or a simple function. With methods, the first argument always is an instance. This can be handy when writing an undo management systems in case multiple calls from the same instance do not have to be registered (e.g. if a method ``set_point()`` is called with exact coordinates (in stead of deltas), only the first call to ``set_point()`` needs to be remembered). Reverser -------- The reverser requires some registration. 1. Property setters should be declared with ``reversible_property()`` 2. Method (or function) pairs that implement each others reverse operation (e.g. add and remove) should be registered as ``reversible_pair()``'s in the reverser engine. The reverser will construct a tuple (callable, arguments) which are send to every handler registered in the subscribers list. Arguments is a ``dict()``. First thing to do is to actually enable the ``revert_handler``: >>> state.observers.add(state.revert_handler) This handler is not enabled by default because: 1. it generates quite a bit of overhead if it isn't used anyway 2. you might want to add some additional filtering. Point 2 may require some explanation. First of all observers have been added to almost every method that involves a state change. As a result loads of events are generated. In most cases you're only interested in the first event, since that one contains the state before it started changing. Handlers for the reverse events should be registered on the subscribers list: >>> events = [] >>> def handler(event): ... events.append(event) ... print 'event handler', event >>> state.subscribers.add(handler) After that, signals can be received of undoable (reverse-)events: >>> canvas.add(Item()) # doctest: +ELLIPSIS event handler (, {'item': , 'self': }) >>> canvas.get_all_items() # doctest: +ELLIPSIS [] As you can see this event is constructed of only two parameters: the function that does the inverse operation of ``add()`` and the arguments that should be applied to that function. The inverse operation is easiest performed by the function ``saveapply()``. Of course an inverse operation is emitting a change event too: >>> state.saveapply(*events.pop()) # doctest: +ELLIPSIS event handler (, {'item': , 'self': , 'parent': None, 'index': 0}) >>> canvas.get_all_items() [] Just handling method pairs is one thing. Handling properties (descriptors) in a simple fashion is another matter. First of all the original value should be retrieved before the new value is applied (this is different from applying the same arguments to another method in order to reverse an operation). For this a ``reversible_property`` has been introduced. It works just like a property (in fact it creates a plain old property descriptor), but also registers the property as being reversible. >>> var = Variable() >>> var.value = 10 # doctest: +ELLIPSIS event handler (, {'self': Variable(0, 20), 'value': 0.0}) Handlers can be simply removed: >>> state.subscribers.remove(handler) >>> state.observers.remove(state.revert_handler) What is Observed ---------------- As far as Gaphas is concerned, only properties and methods related to the model (e.g. ``Canvas``, ``Item``) emit state changes. Some extra effort has been taken to monitor the ``Matrix`` class (which is from Cairo). canvas.py: ``Canvas``: ``add()`` and ``remove()`` connector.py: ``Position``: ``x`` and ``y`` properties ``Handle``: ``connectable``, ``movable``, ``visible``, ``connected_to`` and ``disconnect`` properties item.py: ``Item``: ``matrix`` property ``Element``: ``min_height`` and ``min_width`` properties ``Line``: ``line_width``, ``fuzziness``, ``orthogonal`` and ``horizontal`` properties solver.py: ``Variable``: ``strength`` and ``value`` properties ``Solver``: ``add_constraint()`` and ``remove_constraint()`` matrix.py: ``Matrix``: ``invert()``, ``translate()``, ``rotate()`` and ``scale()`` Test cases are described in undo.txt. gaphas-0.7.2/doc/tools.png000066400000000000000000002270601175456763600154310ustar00rootroot00000000000000‰PNG  IHDR¢#)êÚbKGDÿÿÿ ½§“ IDATxœìÝy¸eW]àýoÍó”ªTŠª"ˆ‚ÐH"ý‚DÒØénƒ`‡ HIƒ¶ hGiºÛ¥pˆð‚­6-¼­¼" ±!•LBRdòeJB%©„Ô\uëÞRuß?ÖZÏÙwß3ìsÎ>{Ÿ{Î÷ó<ûÙÓ:k¯{Ϲçîß^H’$I’$I’$I’$I’$I*Á¼º IªÕ`}\NÉ­G€©˜v*îç·'CÀà@%¥–$Isš¨$ŽU4‚ÊìrJ“íS€S5%—aðHny8³}¨äëI’¤9È@TRöǵh€ý™å@“u³íQ±–Hf—u™í•MŽ­af`¹¸‡ëžöÄeoný4°XÓ.‹ûùíå±,g+:\o ôãq‘$IcÆ@TR]Î #ýÚGë 5¿œdvÒ§™][w 8œ;–mª !øKAájf’kâ’=¶šFй2s¬ ‡h•Ùeo“í½À.ÊoJ{*! m¶œE#¨…ð»\OhÚ+I’ƈ¨¤º¼Øì^J#h[K¨õËîç·³ÇFÅ~B ™]öe¶'š;ÀÌÀòXå¥îÞi„ ôËqÿÀ“µ•F’$ÕbaÝ4¶6Æõvà}䳎æÁj~;´Îgv»P;™µ˜ÙMM³MU!)(<ÈÌ@2ÕÈf¤tNdŽ‹ïÅ廄Zñeí“K’¤Qd *©.[âºßÚ°}qÑÜ’‚–B’$Õb~Ý4¶RèSµ–BuIýB D%IC¢’ê²)®¯µªKðiiÛT’$i$ˆJªË3âÚÑñ”F#^^k)$IR- D%Õ%5ÍuÄÔñ”¦Ê)kêI’4‡ˆJªËæ¸~¢ÖR¨.©i®£æJ’4† D%ÕÅÁŠÆ›¨$IcÌ@TR–§Ç]5—¥LÓm–2¯q‰ùÕ%Mß’Ÿ»U’$ç•T‡4bî÷j-Eù.ëß¶fö5[š¾eE­¥$I’46Î'Ôì}µî‚ Èý´®]|œ0XÏ¡¸}JçG¥Fô?~–߯» ’$©zÖˆJªCšºeGÌýàµ4°ÿ×ÿ®àùQáô-’$I’*õµaª» Ò®Ftx,³ÿ©LŠœ•Ñ«?Ë_Ö]I’T=+’T‡T#º³ÖR¨NŽš+IÒ³i®¤:œ×ã8uËgŸÞ÷·Ÿèâü¨8×¢’$I’*ñ·„f™¯©» Rd°¢‰¸|"+z~Tšæ¾”ð³ÜRwA$I’$‡;AÈÖ]Õæ\Âgàîº "I’ªgQIuØ×£6¨Š³¨$IcÌ@TR6Çõµ–Bušˆk§o‘$I’4pkM2uJ¨‘v ás°¯î‚H’¤êY#*©jiÄ\kCÇ›Ms%Ic¢’ª–šåÚ?t¼¥@t þ/’$iìøÏ_RÕÒ@EOÖZ ƒq½ªÖRH’¤ÊˆJªÚ3âÚ¦¹Jµ¢+j-…$Iªœ¨¤ª¥>‡k-…†ýD%IS ë.€$©2«£7[ÖæÒfG³=B#hÌŸ›{»,S¸H’4¦ D%U-“µ–bîYÂÌ q °ŽÖe6mJ¿`Àeœ#¦;€ñØ£ñØîLúˆ®p¹$IÒ1•TµEq}ø.0œ<ì‰Û»ãö®¸d¯¸¼ýZ—•q½.·¿Š(f­ŽÇVÓ(—–P–C„‚š-ûsi×e¶—2³ùlöܲXÖgÆò~\Z™"ª[2yH’¤1b *©.+€Óãö3»xÝABК‚ÕÝ™eW<Ÿux:³”Ùµ±ûrû‡ r ×03 LÁáÊ̱8fÊ|s×~efÐx –»U`™M›ÒŸ(±<ͬ$¼§[ Aæéq½•ðo%üN¾/.©vÔ@T’¤1c *©j©yèAB0º!.§Æõú&DzÇWÇåÙ•–ºw‡â2×ûrû‡böØÁxì €òHÕïÁð`\ZYœAP¯.Æé[$I;¢’ª¶2®'5“;âRÔjà4i>pÍ÷7\ÅÌïº%Ìg]n0M#(<ÀÌ€2‡™c)pÌ”ùæ® ïù?Çåuñ˜Ó·H’4f D%Í5ãò­º ¢¾9}‹$IcÊyD%IuqúI’Æ”¨¤ª¥f˜‡k-…†Ó·H’4¦ D%U-u xºm*›æJ’4¦ì#*IêÕ" QÇ SÃì' òT”¨$IcÊ@TRÕ¬^ iŒ>|*atâS3DzûSšä1MHŸ&Œ¼ø pG\ɤ=×Nß"IÒ˜1•TµlѰä§C9÷›M‡ræsp؆Ê•„>—Ë¿ë5„Á€–¦©YF O61{ÎÖn'š‹óîæÑ˜çTàlà|à—â±' éWâëÁÁŠ$I;¢’겘$A˜ ´_S4‚Ö}Ìœ÷óPæX>ˆ=FcМd‚F¡ÙéÁ\šIàhe]H¨ÌŒËAÙÚ¸ —Åã+âöjBй,敟/µOË]qù^\ïn²ÿ°·Iób9ÓÏx:ð#À…ÀKÁï«ãòÑø›æJ’4f D%Õå°„¬¬‰ËJAÕB°µ*s|-lUæØ*ÁÚÆ*ˆŒÔ$5k 8·Ð(û¢•á8!ˆ>¯}˜P³<÷÷Åõ^B@ù$!¨LæîÊ0¯CÌó!à3çÏ&¥?|¸’F-¹$I¢’ª¶2®'Á螸ô+Õ ®$Ô"®bf».·Ÿ‚ØÅ™2e˘ S™µœH'Ù&©I~?Iý'óã!hÜ·³ÁãT<~8n§&Ê©&x®4O~(. ¼(³¨$IcÆ@TRÕÄõ‰’óMÁÚS%ç[Tj’šµ X·SóÞ|³ßq樹’$)QI*G¶Ij’ß×L¢’$©ùu@ÒØIM^‡µV0?pQQïì°?×}žÆ»Ÿ+)Ï4}K¾Ù³$I’$•ê^Bíá9Ó÷öª×ë5y·n :')äBà#™ý”ïbÂgáX yI’¤9ÄQI£à à6à>àÖ¸ŸLïî.)ß{€{€¯gåòÊJ£ä¾‹Ð¼t;ð©&ûe—q¢C¯î^FEø³ñºwçfÒ¾ ø&ðuàÚ6Ç®>yÝõñX¿ŽúÎ.®"’$I’èvB-Xѵ"5‹7Ûâö6B ”LWÅíó;ä5Õ&¯Vh³sùýªÊx„0%Jò1àqûà«™s{iŒì»²Í±íÀÖÌë6j¶Ë°ŸðyXÓ)¡$I’$õê3„À£]­ßu„àg;¡Æ,m¿¿Eú½„éT ŒR›s’™S±äçúÌ›j“W?hUeœ¢1R/„¹<·g–ïdÎÝH¯Ì¼¦Ù±½Ìlæ;¿@‹zŒðyØÚ)¡$I6…’Tµ½q}J›4oÏlOИo²')w`¤y„Ài!å}‡–YÆ„ZÑd>¡çd“´—jK/~øñÇ üÜYÓ%•7•Õ‘s%I#ö•TµT“–Ÿs³·'€Ë }1û‘òz¡?g²8/“&[KxŒ™ÁT~¿ª2æÝ ¼9³ÿÜÌöà&àjà…mŽí6e^· x´§RÏ–dQI’ƈ5¢’ª–æÖ\×6UwÞ |øà 3ûHvëðý„€¦×dÎ]Mès¹‡Ð„õhæÜŸúMÞ¼ºÉ~UeÌûEàÏŸ‹ûŸþKÜþ+B?ÐÀ¯·9v+ðbB3ZýXoé£üYãÚ)\$I’$ ÌÛÁÓuud«zj˜ ˜=}Ë…%å}áópqIùI’¤9À¦¹’ª6ˆQ Ö„‘r7ë Íwo/)喝¶i®$IcĦ¹’ª6 èzBßɼ‹Ín‡A»2®lr|Ð.n±Ý¯Cq½¢Ä<%IÒ3•TµaD÷ÐßH¼U˜ e,Ãá¸^Uk)$IR¥lš+©jE¦oÑø°i®$IcÈ@TRÕ†¡FTÃÃé[$IC¢’ª6ìh¯#Ò¾³Ã~]ªa7ù Üâ¼$I’$•â6Bðqa‹óׂ™íÀ‰Ìöû[¤ßK£içRBsÐd’™µmÙà±™©6yõˆ–]Æl³æ]4~GÛï´(G»t/¾ÜÏÊœ»‘H_™¹n»¼ö2³Éðü6?“¨$I’¤JÜ@>þM´EjDÛyí‚Çf:¢óâöÂ\Þý¢Ý–1Ÿ~­ûXNL÷màG3å›Ìœ[¼øð…yíeæ<Õói4ÇÎÛFø,üy‹ó’$iÙGTRÊî#z pYܾœPãÚ”×ëý9“4úN^ÆÌZ¿cÌœ‚$¿_v³nÞœÙnéVÅí7Ѹ¶7W/,×`Sfðh‹2¥ÁŠ–´8/I’$I¥ø#B-Ø[ ¤-R#z&€n‹û­^_¤¶ñZÂ`Ew3s°¢K€‡ý!#—×5À·h N”ß/»ŒYÏúÞ>¼»EÚvéÞ|—æ?Ûm„ŠîÞX ¯—fö/>Øâg¹‚ðYøh‹ó’$I’TŠß$¿Us94Éì˜Öý D%IC6Í•T‡TÛ¶¶m*ÍUwFÊݬ'4í½½ÖI’¤¡²°sI*]Ù}D»µžÐÇ1ï" <Ã`.”±‹[lçkûˆJ’4F D%Õ¡î@t𢚮]Ô\(cÒè¼KÛ¦’$I#Ŧ¹’êjôÖ×Z I’$ÕÂ@TRꮕ$IR D%Õa.¢¦R)¢Ý4E¦§éçzE jW†ÏÚ|®ŸBI’$%¢’êÐͨ¹ƒÔÆI«iSоv'°;.; S³”éé¸^Tr¾’$iˆˆJªÃ`Š0@Ͳò;¸ ¸¸5î'À;€;‡K:äuð5àà½]^'+_“úž˜ç×â5šÙ|6æ7pn‡²n><Ü üX“4]¤_ | xuÜ¿øtæüõñX™Çõò’ó•$I’¤Y¦ óM¶S¤Fô`[ÜÞF˜’)સ}>ð`‡¼®Ïå5Uð:íÑ©‚¯ûðЏ}ðÕeýkàê¸=XÝ$ω‚éךñfõíÀÖÌþfB[¦—>·”œ¯$I’$Ír?!ùÁ&ç®#AÛ™í÷·Èk/µ¥„f¤É$3›}vêó¹'—×dæ\»ët D‹¼nŸu;ðe]Õäx«@´UúÃÀ×WæŽïdöçSNŸÙ,QI’ÆóˆJªK»)\ÞžÙž ¿ù4OÇûx}7悪…ôöý:ŸÐ/s²SÂhºËü[¥Ÿ¾ üpSîܼ>¯)I’4‹}D%Õ¥›‹:¹¸,n_NèÇÙ«[3y½Ž™X»ëì΋ۗ1³&1KyÞÚâÚ7oÎì?·CY¿¼%ng›Úö’þçÓ€weŽí6eö7v¸F·NƵF%I’$ ÜGµk?Ó!]‘>¢gÒD趸ßêõš–žM(( V”Mßî:—úuþFîuÀµ1Ï»™9XQ«Á„ÞÝ¡¬ip£Mk_Þ$Ï|þíÒ/$ô_MýH?\šyý¥À;”©[ç>e÷=•$I’¤Y®# o«» jéƒäÃô7L3¢’$!›æJªË¾¸^Wk)Ô΄‘r7úòn!Œ¬+I’ÔûäHªKèzB̼‹h ¢4,ê.ëÅ-¶%I’zf *©.u¢{èo$Þ*Í¥²ö"›ÜI’$0›æJªËÞ¸>¥ÖR¨nãze­¥$I•2•TûˆJ’$)QIu)sÑ2½³Ãù2ë)2%M?y)c»2|ž0@ÑàsýJ’$I’†ÉfBÿÀÇ;¤DÐÖëõÊêÇ8è@´Ÿô2{Ê– z)PA§>ßà5$IÒ±FTR]Äõšò:¸ ¸¸5î'À;€;‡KÚäó.`°øTæõ×w/cf7¼¸øpVÁsYÏÆ²ß œÛ¦|)ýg€soþX“4]¤_ | xuÜ¿øtæüõñØ Í«à’$I’Ä1BmØ¢6iŠÔôÝl‹ÛÛÁS2\·ÏìWþzG€+[œŸêpÝVç²y| xEÜ>øj‡òý5puÜž¬n’çDÁôë Íx³Áùv`kf3!€”5„ÏÀþN %I’$© »AȆÜñëÑvàDfûý-òÙ ,ÛKÝ™s“Ì t;<ù@t*æÙìüT›ë¶;—ÍcŸo;ðåÛ¬êPîìv«ô‡¯¯ÌßËÌ&Èólh *IÒrQIu:@B×03P{{f{‚þæÑ< ïãõ'µ¢ƒ2ŸÐ/s²`úéÎI ¥Ÿ¾ üpSî\¾™l·×”$IjË>¢’êTÖȹ·—ÅíË ýE{uŒÐO´¨tÝ×ú§=—Ü ¼9³ÿÜ×ûð–¸mjÛKúŸN#ôMv›2û›€G;\C’$I’挛 µm¯h“¦HÑ3i Vt[ÜoõúNM@¯¾ÅÌÁŠZ•g¸–0 ÑÝ̬¨Ý¹$;˜ÐÀ»;”/ nô¡iíË[”«hú…„þ«©é€K3¯¿ø`‡2õæ¹’$I’*õ7„ ä§ê.HÚÉUO;S– ˜=}Ë…¼Þ|ÂgÀæ¿’$›æJªS™S¸¨wFÊÝ@Uw adÝA99À¼%IÒr°"Iuª+]Ohœwa”Ù¢Vöx®“²Ê׫‹[lK’$•Â@TRʬ¨[{èo$ÞAöòI’$õŦ¹’êdÓ\I’¤1d *©N¢‚ÆÀN«j-…$IªŒ¨¤:ÕÕ4· ïÌírÀŸ^´+ß¾¼¿@>Ÿ' l´ø\e9×þO’$I’4p¯ LÛÑl`žd®Nƒ2ìånW¾½Àây\Èì©^.è¡,“„ÏÁ²^+I’æ Ÿ>KªSYMsÏnînûÉðàNàaà’yMj ¿|ø7À¾_ÿ¯ \÷]„ j;ð©L¾ƒ*o«ô½”ï“„÷ãNàõ1íÛ2y¾xkܾøtæÜõñX·R:ÕÃk%I’$©+Ï"Ô„}»Mš"5‹7Ûâö6B@”LWÅíó;ä5üLÜþaà0pyf?ûúv×Í—;»_vy[¥ïµ|Ùí3»ãö|Bp¾>îo¶fÒnîíPÞ¼å„ÏÀá._'I’$I=Ù@BvåŽ_Gr¶ú¦íV}÷€¥Àî̹I`Qf?íM2³Yê° ÅëÛ]·] Wvy[¥ïµ|ùs7?¼ŠPcšÍ?û»™_ ¼ykŸn_'I’æ0ç•T§VMsßžÙž ¿95OÇ»L,³”Æ`:U襼ݤïŇ€7›€?Ï›—ÛŸî2ïÕq}°ûbI’¤¹Ê>¢’êtœÐ$s°¢|n.‹Û—úEV¡ÝuÑzðªÊÛkùò>E¨ =™#ãî §É&àÑ.˘ˆ>Ýåë$I’$©gjÑ6·8_¤è™4å¹-î·z}§& Òg÷Û]÷à[4¬¨ªòöZ¾f¿ó?®ÍûpifÿRàƒÊ›wáýï¶o©$I’$õìB ò¼º ¢–æ÷ÏÉ¿€ÙÓ·\ØeÞçÞÿ;{-œ$Iš{lš+©neMá¢Áxð àŸ€oæÎÝA¨ÉÞ@Iw p{—ù/‰ë£}”Q’$Í1V$©nu¢ë››¿ØSa9Šª³¼2»&4ëâÛE­Œë"M°%IÒˆ0•T·Ô§qm…×ÜC#ñVm®•·V$IÒ²i®¤ºÙ4w¼9}‹$IcÈ@TRÝ DÇÛ¢¸ô\¨’$iˆˆJª[MsÛÅ¾ŠŸ' (´™ó€ƒåq=Yk)$IR¥ D%Õ­Sè(†E,()Ÿ Àî¸ì$L»2, D%IC¢’êVFÓÜ3€Û€û€[ã~2¼ƒ0OåÃÀ%ó\ | xuÜß|6^ãnàÜxü]ÀÛ2¯{7ðÖ6ùNï!ÌËù5à¬Ü¹k€»€—µ¹&ñšß¾\ÛæØÀ§3¯»>6Í•$I’T¹K€ià3-Ω½Ø··‚­d ¸*nŸO˜Ž¤ Ât)·33hýðЏ}ðÕ¸}&!H„ðpï;ñõ­Lµ)ëàÊר ¬‹Û+ÛÛlͼn3po›òUí¿Þÿÿ\wA$I’$%"·fŽ]G ¶'2Ûïo‘Ç^M<—š &“4jÝ Ñ'µ•ÄÅWæŽïÊ”c;!àLn~xðÉùOµ)ëTðQBÿÏäãÀÛ3ûÏÍl/¾—wç^÷'4ê”ÿµ„ÁŠîfö`EYí®y¡ ñý4‚¸fÇ>\šyÝ¥À ”³*%¼ÿÃ4€’$I’¤1p‚ŒÌÕ‘¼çËçH[õt4Éì˜0¥Ë°¸žðÞÿÛº "I’ª3Woú$–2¦p©ËóÍrÿ‰0uʰ¹ƒÐwa4ß-„‡Eê¿út­¥$I•ZØ9‰$ Ü~B¿ÆµT3Îzàæ&Ç/" Ô™]Ú.ÿ•MŽÚÅ-¶‡Áê¸nÕ4[’$ QIàêÑ=À‹æpþ£$M­s¼ÖRH’¤JÙ4WÒ0˜ËMsÕŸ4=Íd­¥$I•2•4 ÒH¶kk-…ê°,®§j-…$Iª”¨¤a0L5¢UjÛN]eùŸ0ÝJ;„éWngfÐú1àqûà«qûLB0ááÞwâë[9\Y ßT–"é²}k_|%³¿—054¦Ži—×v`kf3!À„‹ïû”¿$I’$µôQB@’jÞ®#DÛ™í÷·xý^£¯.%4)M&iL‘Z9L¨=|eîø®L9¶Îä&à‡€WŸìÿT,c‘|' ¦{ ðEàþxîPæÜ„@ýÊÌuÛåµ—™M†çÓùwÖ««ïû_ (I’$Ij)ˆf©mˆæ_ß)¨šþøÃÜñ=™kä]¼økà_È¿h¾Ó}øÑ¸½”™S¡, Õ¢QóØ.¯½Ìœcz>°¯EÚ~ýá}Ï€ò—$ICÊ>¢’FÁ-„`àrBÑ~üÜ üX&ïk€»€—u(G+WŸÎì_ºgÇõ$ÿ+4 IDAT·k-…$I’¤±u/0 œÓä\‘Ñ€mq{!˜K¦€«âöù„éVÚÙKÑ`e\ÿ5puÜž¬ŽÛG€+ –£UÓÜíÀÖÌþfÂïcÔ}‰ðžÿ‹º "I’$i<åÑëÚvàDfûý-^¿X·—š¸&“À¢Ìþþe¹‘P^ó‚0˪&i§2i:•£U º—™Ízç(ã(x‚ðžoê”P’$I’¡ßÑ¢ tò¯>|!ÛMó@4Ÿw¯hvNçù„¹5GÙjÂûít=’$)ûˆJ©VñH¯¿¸,n_Nè§Ù«-ÀM„¦¸/ŒÇ¾¼%ng›æ–QŽ̬Ü<ÚEy碳ãú¡ZK!I’$i¬}—PCvz“sEjDϤ1HÐmq¿Õë;ÕˆÞF¨è~àñØFೄÁо¼¼EÞEË‘ÝþpifÿRàƒÊ8×]Fx¿?VwA$I’$¯v註øHfÿÄ)]FÙ!¼ß¿[wA$IR=lš+Iõºƒ0Rî`=¡iðíµ–hð¾?®¿Qk)$IRmvN"I—¾‹ž®èzë››¿ˆ0BnÕ.n±=ªœCT’$IRíöšj®©» ªÄÂû}JÝ‘$I’4¾ DÇÇZÂ{=êSÔH’¤6ì#*IªÒsâú›µ–B’$ÕÊ@TÒ0X×Çk-E¹:M;óyÂE€Ï ¾8Cãûâú[µ–B’$ÕÊ@TRÝ–Åå0Ùä|‘yDë° ×^ìvÇe'a—q`¨$I2•T»4`ÍÞ>ò8¸ ¸¸5î'À;€;‡K:äõ,ànàངþ«Ù¼®î^¼!æ{o|Íy¹¼ÞóùpVæøÀ§3û×ÇcãÀQI’$Iµ{aðš{[œ/R#z°-no#vÉpUÜ>x°C^7¯Û¯¯OŽWfö×f¶_|%wÝVeÚlÍìo¦õÏ?j¾Jx¿_\wA$I’$¯#&ÿ9v!XÛœÈl¿¿E{åq{)¡¹k2 ,Êìgk8;嵄™Í…§bþÉK€/÷ÇòÊ¥mU¦½ÌlÚ;¿@¹FÅ>Âû½¶SBI’$I”ד¿iq¾Hh»@4ÿú"貸Dóy}øÑÌuóAk»@taf>ã1ÉJÂ{}¸î‚H’¤zÙGTRÝÊè#z pYܾœÐ_´W·‚c€×óÚ¤]<·ßÔ$m*Óë}W“À¦Ìþ&àÑ^ ;Ǥ@~yÛT’$iä-ìœD’jC\÷ˆ¾ø(ð+ÀAföãìÖÛǼ¾HûÚ»_¾·’$I’T¹ß'jW×]hšÍŸ"£e»øHfÿÄ)]ÆÁ~Âû½¦î‚H’¤úX#*©në⺟Ñ2=øa€£)àgp;€ß&ÔO[M‚ÇÁaBº 8PsY$IRM D%Õ­Œ>¢ÝZÜÜäøE„éE^XA.n±=êŽÇõ¢¶©$IÒH3•T·õq]e º‡0©ªw0®WÖZ I’T+GÍ•T·4XÑî¶©4*ŽÅõÒ¶©$IÒH3•T·aë#ªÁšŠë%µ–B’$ÕÊ@TRÝêè#:Ì&:œÿ<¡yð¹Á§ti:œUµ–B’$ÕÊ@TRVæ”<ÌÌ98³:fsÍ‚>^{!°“ÐŒywܾ ŒBU(5Í]\k)$IR­ D%ÕéÔ¸ÞÓg>g·÷·Æýdxp'ð0pI‡¼òïþ‚y= ¸¸xo“×]ܼ xCÌãÞøšór×|OÌçkÀY™ãWŸÎì_Í%Çõ3k-…$I’¤±u.aͻۤ)R#z°-no#hÉpUÜ>x°C^íÑvyݼ>n¿žF_H€#À•™ýµ™í_É]£ÕϲØšÙßLfç’« ïùûê.ˆ$I’¤ñôã„ ä ¹ãׂ®íÀ‰Ìöû[ä³X·—2sÞIfÎY™ ,›iˆ¶Ë+[†%1m2ÅÌQb_|¸ŸðsÊ¥mõ³ìefÓÞùtþy†Í¥„÷üƺ "I’êã<¢’êÔj ¢·g¶'èoÎÏ“Àñ._3,-dæ÷d§¼¦[?A¨MþøBsâ¥t7PÓ¼‚×VÅõÙµ–B’$ÕÊ>¢’ê”æí·è-ÀeqûrB€×«4úl^FñÁ…n^·_Ëì€1kðXÜ~S“´égy¡Ïk¶l›2û›€G –oX¤@ôYø?H’¤±e¨¤:­ë~Ñ·~8ÈÌþ˜ÝºøX,Ó´Í7ïíÀÇc¾Hcš’f~øðT“kœ¾Ÿ0XÑ4àBPúbAìù„ |.™žžlaîÒ’$I’æ¸ëÁÖ¯Ô],£Q³ùS„`´lÉì˜0¥Ë\s+á}yÍå$I5±Y”¤:­‹ënúH«çzø/À/àwFÊÝ@¨MÞBh<פæ¹Ï®µ’$©66Í•T§Ô4·ê@t=ps“ãÑ{3á¯/ì¹DÅ]Üb{.ùV\;`‘$IcÊ@TRÒ`E»Û¦*ßú‰WýÉX$I’ÆMs%Õ©Õô-m6Í•$I’T›§ƒÖl¬» ªÔÂû¾¯î‚H’$I?O» ŒŸ„÷~}§„’$iôØ4WR]Nû ©Æ‹Ís%Ic¢’ê’ﺊ0ÉÏ#4××äu Žœ+IÒ³9œ¤º¤@ôlBÍfNFÔÝ—'3ûOߋۻã¹,¯Êåȹ’$1QIuY—Û?Œë“„mNN‹Kǘ´¦ ua`œ£ÀD\Ž×)ࡉð`5ððgÀ[h$÷ã BýoÈ?ëo€\ Ü^rÞ‹€•ÀÚ¸Ü |x°XA¨é^·WföÓ¨6Í•$i ÙìMR]Þü¸=ŸæßéŸÄã"Ô–žÏm6œ2í&GiªS„öp˜8%Ñ“q;½“1m6þIà1ÿ›€É`‚ÝUÀóiŸûÒx¹’® <X‘9¶.sne“ýµq{I å|áý”$IcÄ@TRÞ|’ĵsðÑÌþ}À‹ 5 Y‹ é©„À4©§‚©%„@jeÜ^,–RnpÕ„Û¬UÌn±2Ÿæö`ùÊUÄqBp= l¦ñ?eø,ð]B ~€´Oäö÷V[dI’4 D% »MÀ„>¥Û€­Àoï~³Äë¬î×ûuà<ൄ¦¦¿D—×´šž.!4é]·Sл<¦Mðª$îh¼îIBз*ßI¨ÍˆÇS@x8sl_æÜD“ýýqû(!ø¿ 8‡Ð$÷0ðNÂ{vnL#I’$IsÊõ4jØ ôK¼‡Pú¢¯óWñ:ÿHxHw ¡Yî4ðæòuÌë¡Ü› ?Ó4p¡†÷ܸ<‡0ˆSv9“àæ—fµ¡?HhB|’F_ÌwÆk}›@—å·c¾ß¤Q£ü@<ö»%^G’$I’*q! ÙK¨ M^DDï!¦ýz 1;H §÷‘ÿÂàIÓÀÌßHýwø¹>òÏZ@¨ÅþgæøBBÍå4ðG%]+½'€—fŽ_@˜¶ì‡’$I’4P› è4ð†&çSMÜoôy „lóAb’jJ?GïÝRÿÐ$×RN°›üZÌï»4šã&çšÊæÇ^¤šéVíÄse=,$I’¤»‘ÈÜØâübà^B`uN×ùD¼ÎhhfÕm=äŸm’{V‡2ôìBó&¹y¿A£)í²>®õ[´oê»,^£Œ‡’$I’4po Ñ$ws›t皀ÞIh’Ú­Ÿ¦sZËýÌl"ÜI¶Ÿi³ÚÖd#aäÜ^ƒ]hÝ$7/[“ù=^+Û$÷emÒý_„ ø(!H–$I’¤¡´…F“Ü_ ý{bÚ_ëò:Ùà¯È`D©Öò³fü3@jçßÑv{i¢Û®In^ $Ÿ¦17kQšäæ} ¦ý ½=,$I’¤K#É^_0ýRàŸ MR ‹ëüM¼ÎMk› \ßP ý¿‰i'€g,S¯Mt‹4ÉÍ{w¼Öt7wjjÚ[tôÝ•ÀCôö°@’$I’î„€ea>Ï¢Ò(­·S¬Öíuô6@Pz]§&çæëœÞÚEþ½4Ñ-Ú$7o aÞÔiBPZÄ ÍlOÒ¾InÞEñ5Ý>,$I’¤ÚJh–: \ÙÃëÓ(­¿Ò!Ý&B Ûë”)_{C›4©Iî-Àü.óOÁnÑþ¨Ý4ÉÍ;ŸÀ'Ì_ÚN¶IÀŸÅ×ÞN÷¿I’$I*Ý<àÿ•¿í14Jë$ð}mÒ}ŠþF¨Í²W59ßK“ܼOÆ<þO‡2öÒ$7/õ±½—0q+Ý6ÉÍ[D^Ê\þ¸Éu~g×ù…Lþß7€üÿ‰æêª\køþ&×zmùµZú©Å•$Isœ5¢’†Á"Bmd;[ ý#ŒÞšÌÖæÒN7Éc!íô™ Üs”ü-!Ì·ÙÕt¹÷„‘d¸9;ÜõŠÔàþz\Þ—vŽ~yÍ~ÇÍÇ„x‚ÐÎÛ—I{8¦?”Ic_iI¢åʈù 2-k›[Y—aá÷³ot2»¦g‚Ùƒ²ì¦3ûÙ§Õɚ߬¯6æ/<%.i{y\¶ø9î¾NLï‰Ç|:®Aô¢P] º4.G˜]K5Hë€Ì®Ó`¬"œ/~$.§xݾ̲¿Åvú¾]ÈìKi<¸IVZãd­afW Ôr'+ÛêfEÌg%áfú¿±‘r¢¬ŒK \‚Õ¸¦ZÙ ZÃ’¤ˆ¶v.ð4‚ÄTSÙ,ÀLÇʰŸðO/ý3Ì. 7ÍŽg÷‡Ùrf§ù õÙÀ9ÀYqùÉÌë'Ái LîÇ©)FÑ: iº)Ýç›–—­Š ±ª@´êÚШlë)Âü¡X†ƒ™ýtƒ wì Z¬ì¢†³€%Ôx¾øAfv¯x¸ø*°ƒ™Áe 6犔® |¦WÑø^HÿƒÓþ긤ôéÿs:¿*³”%[+›ì¾ø»¯!I#Ç@´¹çwõðºvÁá5•­ÒÍ¥ƒ^MÆå±iÏ^¼˜þPðà`S\^‘ËcÀô>à!B?Ôì{8®( ‹î—-5Ë7—û½\+k”+J7ïíû2Ý—dÚ§Õ4nà³Ëº\šUÌìó¾ŠÖ7û½hUK{4žÏwU8Îì (?€N¾›Â4³,¦|Ìdf×2çÚ½g·j_“ñØá3pˆ0•̓·Õi>áo3»œšY²Aæ&ª­>Døý5 bÓC¢{€ÿ]a™Tï!$©Ñæü'2ÚN¾ÞEúãÀB±7³]ïi’¦Ê@l-°‚Fpº.®Ó±µ„€5[Ãì`vMÜ^_ß僧Gi¥ùe³×ü2ˆ>6‚ͳï‹ÛiɆƒšZ¥ê©¡vÆõàÚ>òÙOHÓú!P=DïÁmú]äÌMŽm Ôt1 < <ˆý³L:>ûˆJR¢ÍˆŽ¶qý Àÿ¨³ Cê8!Ü×)aKh¥ùe³×ü²<.[ú,GÖ~f›ß¾|ø^‰×)RŽS ?ç®_køŸ%ä³–F@:Ÿ .¤œàvwnyªÉ±ÝñgF‡hÿÀêõÀK+*‹êg *Iˆ6Wõ¨–ªVúÜ?]k)FßQBp×k€·œÖê4÷ï0Ú¯#4šDgûàž Ô”íî±,e«2-KªqßÛ6•šI#açûk4ˆJR¢ÍY#:ÚVÄõáZK¡N&ãòxÝ€TÛÜo?\Í ‹âzÐͰ5ì#*I̯»CÊ@TÒ U=`‘¤êx!Iˆ6ç?‘Ñ–FÞÖþf}ÖˆJ£ÉÚPI*Ȧ¹ÍˆŽ6ß_UéµÌivxeܶFT-ö•¤‚ D›ó‰¦¤2¼ øD›óEçÅ”478$d Úœ£æJ*ë2Ûi.Éy„šÐÇ(gZIÃÃQI*È@´9›nŽ6oTµÿ \[w!$ œÿ_$© +jÎ@t´-ˆëµ–B’4j¼¤‚ D›³i®$Iê–5¢’Ths>Ñm‹ãúX­¥$+’¤‚ Dg[ ,#)S5—Eƒ±,®}%Ie²FT’ 2ÍÚPI’Ô kD%© ÑÙ DG_š»qºÖRH‹âúx­¥PcB’ 2Í"£Ï‡ ’ª´<®'k-…ª`Ó\I*È@t6ƒI’Ô Ç ¤‚ Dg3•$I½8×Kk-…$Í¢³Ù4wô9˜„$iöÇõÚZK!Is€èlÖˆŽ¾…qýt­¥$QI*È@t6QI’Ô‹ÔšjMÛT’$Ñ&öÄõúZK¡AZ×Çj-…$iÔX#*IˆÎf :úÕP’4¢’Tèl»ãzC­¥4 ÅõªZK!©*6Í•¤‚vN2vžŠë%æiSÐá1/®§kºþ"„ç+Ÿ‹uq½‚¬,&ÜÀ,¥Qs á){¶ÌS4¦ 8Áì~͇h È4<Ž“¬Wíd\—ùÐ/}†ÖÒø,¥ýÀ’˜î03¿sŽ“™ý§ Ÿ‘CÀwãy ŸEÀ©„ÿIë½À¾¸8ºûðI5¢€ç¾×Óßê*`9áïð(ïñ#™í)«ÇptwI#¬ß@t>á¦9“<—í!|ñOvJ­žœlž lŽËÖxî´˜vŠpã]öÇ%<-siNôóà À<æÖ{ßi0ªU„ùÕ„@p)á3¾8žk@®ŒÛki«ã±l>kiÂu™žžŒë'_ëtî)êUxaÍ×/ÛRàlÂg%Ù@rá3•ß_‘9–öS¾“ÀNà!àá¸<”Y?AyçéÿÆú\·™µÞÓ4’f–ËgÐ27ÅuÚ>•ð¿£]7‘“„€4œf—fÇÓ±a(UõwÀÂ{¸Šð7·†ðý½2sl]nuL—=–šäž |£¤²'¼_éáQzðx’ðw”ý¼í‹ët_v “îd|Ý *'bþéÁU>H–¤éç¦x>ðCÀ]q?}1¦'yéËm?áË.{.}áµ;w ¾¾Ù¹AYAxŠyð¦X¶S7§Äeáa3°…b“V#ü¾Ê¨ž uк¸øL ×)b5p3ððÀ?Vtm˜yÓ¾6³½ŠpsöWÇeE<þã„ÏëŽÌk—n-ÝP¤Ïò>O½Ñøì§'ãI>ˆMÁm²€F¬¢ñ™[MøÌ® ˜iB0ú!@yx#ÕË€ß~™ðÙÚ‘[¾×~ŸeKŸ¥åÌüì¤ÏO>X\ÎìÏÝò¸½†ò»@¤ÏÐ~Ÿ¥´˜FÍf>hÍÆSð´ŽðmQ‡ëžGãû¾/î‰Ûé:Ý`§ïút3œnš÷Ñø“þ^Òrú_“Ե˯, Ÿ“5qý/€ë ¼î1Š:ì"ü-î¯OK?Í?ÓNÿøÕ>ò*Ó<àÂgäIÂïl'ðhf{G\?Fû`)ùév îÓ€Mq#å>à9Ëõás˜þV>‹Ë é{|if{¿Õô°³NéÑg€××\I#¦Ÿ@ôÙÀ·âötŸyuëáFãÍØt.ݰ¤s i”Í–^þí!Ôì üÓ|”ð2ý“|ø^L›½9Í.ëZÏ.k)^£¶œÁijпµŠß#¥ßûé†f63ÊZ醸Uð˜€UÌ <!ݼ¤q£›>K‡h@NÐx°’~®tÜÍgZ¬ Ü×5« Š IDAT›2Û3ç6‚Û¬_þGe|;ð‡Ò$ü}`öç}_n?ˆä?Ki{eåíä(ás’nF'rûé;j_“ýÙciãZpœ œ×gÅ傘æÀ/õySit8Iõã¤Z ý„ 5=ðœ ñ·›ÛiÂwP6àLë~ƒƒI-R™Z"¤í]„ïÓ=-ò€ð7º.·œÒäX³ãùÏúRíƒÄVN§ñÿ£ˆÝ„>O©Ò©t~¸’u€FSõôÀ÷ r‚ÆßhÚ?Óeµ« ïUjŽŸþW¦©uÁ<ÿ×Åuº_X“I7?¾n•+cþéÁU>HÎÚ@ûÏ£$u¥ŸºÓãú£ñŘžä¥/·µñ:Ùsé ¯Ý¹5ñõ­ÎAù#Û&®QDö&fðrÂç`+ðº_;ÉÞ´ïÏl"ܤýƒqIûé†#ûÚ|ÿ¹Qu˜ðé[ÒͧÑLð/¤ºÁ7RÍîuÀÿ"|×d—3ã:µJØRòõÓgi’æŸ|°˜„e?w“4>gs¡‰ñÓ„ÚçGšœ{ð÷ÀsK¸ÎYq}7p.ètƒ¾ëÓÍpºi^GãLª%J7ÊéÍš˜¦]~©ùlºYïU fS÷Š}™íìñfë]”×ïï!ÛÝ)a‹Áé#4jå†ÉãÀÐèîòLÂÿ—´þö7Äå…MòHA~úŸ‚ûôàɸÿÃ=†Cª½†Pî*-%41Nß¿¢’JÓO zF\§'—©ùTÖʾ†æAj:—nXÒ¹§ih¦e˜ÿ%é‰ícMÎý$ð„›½A¢éIóàÏãap† iô‹ÍöÊ63ÊÆ“jM» 58' 7iOŸ ÜäU5òkºÎNÂç¸Õgy!¡F7õÃÍÊ)IòŸ¥´=ìýæêjÉžWB^ùÿÓT{ƒjÖ‚ÖôÀs%ï¤ØÎ£ÑG?PŽÂ2Ç AØ]ÀK©¦kB) ž"£wH¿‘n%܇¤VH»LÓýqs¸—F zOûä’T\™h•ÒH>™›éá¸>«mªr¤Ú±C¹ãߌ‹FG «ªf”©F4ÿÙÊ{šædT®G ïÅ3\?êü¿ÏTUMç‚ôvXjDÓ£¢ÝKRsf¤ÁÙ×§·M%I]êçÆ²î Í–ѳ+¸Vzˆ1šjné4²±ª5M£)÷súÌ+ýßx¤Ï|Tžð-i›ª:u,®Ù D% „èhy(®«DSÛNµVUÝ ¦¦¹¢Ã#½'ýÖ$úcø¤q†¥i®¢†¨¤è'M#´í-£ *ÅÉÜzRŸ¼2§DÐpJMáóÓà JºÎ¶©T¥gÆõ£%å7ló!³ôÞͳU¨{„q5¤Á°N©µ’FN?húbÚPFATŠ4zh§ÁʦºqÂk•­UÿcÕc#¡ÿàSôÿ÷¾3®7÷™Ê“^ªj0²NÒCNÿ· toaŸ|I¥ê'}2®7•Q•¢Ê@4Ý´ŒÂè‘.6Í.eÖ†¦©«žQB^*Gê矟7¸.é!§­m†Ç™qýHe4‚ú DÓPû§–Q•bk\WñÔ25ãrxüÑ—šQö3Êv7ì#:\Ê DÓC²²ç~UïÒßYUMï;I÷%6Íöí–4ý¢ƒz²=Ýf)ó÷çŽ]ÜIh´ød‡ôý\kPRs·mS•ce\w,dßËIà‹À³º|}«ßå ~ÏU½wùëÔõ7ÐJš[sEÛTå):}K?šýü¿ü¯7Lïi7Ò%;Ú¦*¦ìÿÍ~æ*¿s[™KïwS`XjDËxUõç¢Ùwx7×öÏË™qýH)%‘¤¨ŸŽAÕˆ^׿G¨á»¼MÚ²¼ ¸‘0ýÉÛ ú¿/)ïËéoÞ½n”= H;i¨ÿnkD~ xðfÂûüS%–«_‹¨¿–·Ž¿a±œð½t„Æü†UùMàà÷\~OÓ%e L7Ê]:²ßséýNßʶ©4HÃþy93®©± ’4ÃÅ„'lŸPþ÷Óúiàzàã„“Cqû”.ÎçŸ ÞFx*ü™córé¿ü)¡Oäw€s3ç–ÐäìiÂÍÚi<]nö¤ô[À‡ µ‚ù¼úq8æ_Å0üïˆ×º¦‹×dKãþ]-ηÚÿð—„¹ï>O£ö¬Ý¹µÀ_LŸaæ`)é=¹™ó¢¶Ë³Ó{Þî=~6ðuBMão7ù9“*ÿÚ¹"¦ÿhÁôý8-^ë{¾N§Zè¤ègçï ¿ç7Kø¼¼·Éu‡å=íÆïǼµ„¼Òûûd§„MÿL4/-ùßC»ßÛÇ Ãéê{ãë8î÷úݑ̅÷û?żþ „¼Êðs„ò|¨<Š|.ùžßïô9J†õór<¾¾ÛÊ‹ù„ÍÃ25¤òÄ/¦¯ (ÿv_ÈŸˆç~/.Ó„/Ý¢çó_È“4æàl&Ýœþ ð»qûÆÌùŸ 4éûÀ_Çó?•ym³ÞVyõjyÌ«ªÁƒRÙÿ[¯I7§—ÅýæÎw D§ãµÿŸ¸}ms÷ß\MèçòMòýSf6Íl—g§÷¼Ý{ü÷Mòí6-ûo ×ÆôŸì”°›ãµ=àVþç]<öhÜNŠ~vÞIøIi&ô­ÍOw0,ïi7þ$æýæòšO¸©=A9}Ž[5gÌþÚýÞ~2îÿ‡¸ÿð`æµ½~w$sáýþù˜× Zô¢Œòù\ ò;<¿ßés” ãçeE|íD§„9ßGÖ§ j$©TÏ |Á ª?b»/ä fÈó3û“u:ßK ú$¡–taÜ¿7sþg=Ìü‡÷_[\«S^½ôû‘wm¼Þ;ºxMþ¦à›Ìœ2 H šš¯‰û_.pîP“kŸ¤1àÒ4¡©y¾Ït»<;½çíÞãÃMòí6-ûo KbúÏLßù„Ñ2OÐ=sšýüÍŽùì¤<þ9î¯"ÜNÏËå7,ïi7>󾢤üžŒùVB^Ó„ ÿÇ3Kþ÷Ðî÷¶˜ðwüyBWz¨ôúݑ̅÷û1¯—W~‘Pžô‘G‘ÏÅ ¿Ãóû>GÉ0~^º½·X@h=‘ÌUy_"iŽéç‰ô¾¸^WFAjv7p!ðBpáPöÂš_eo<þðå{¡ŸæïÓþwÛ.¯^U=åE¯×{„ðúW‹€·¿ÏMÓhµ¾C>óº87Mlþ-~‡ù÷÷{4í(’g§÷¼è{ÜîçG' ÍàžKÈêŸë-N¡ÏNºÙ;‘ÙOŸ¥2þ¶ëVöwËBº†rš`¾Ðãkj’¶jE§™Ù½ŒïŽa—æë䃟n¤¾ªýVÖésQåwx‘ÏѰJó:(ö¹ÀŸ/!ül_~„Ƽó’4C?7Ii0‘¥Tÿ쳄áÿß—-„¦2EÏçýn\ÿðBÓ Û»(Ï4áw¹–ð¦U¢é=ïv`ŸÃ„÷ârÂÆÕ4š/î WÔâõ[wš8ücs×n8¶š~ž\GøìvÒ*Ï~Þó/Å|7“o·Êþ&߉ënFT.Ã$áéÿ«3ÇúùìtkXßÓtÛ4¯•tC»¦mªòtú½}”€¼ ¸•™ST òý–÷{*®«{XTù^Æç¨®ÏK‘{‹…„ÖQw‚ÐG€Iø›’¤ÙEø2ß0€¼‹tÚŸˆË'˜YƒÖé|³&*ÿš0}ËQÂÒß¶IŸß#a‚'AË4a*ˆ"¯-«yÕE1¯^kºÕKs½üÏúßâ±ßŽû¯ž"üþ¯IúiÂàAÿ/áAÈM̬¨Ù¹U„>\Ožn?J8"Û¼®Ù{Ð.Ï~Þógšy¦18J¯ƒ•ù7ÐJúl}±`ú~½/^ï—xf?ÿ¬dÒéæ³“}¿>·ŸŸ»Æ°¼§ÝøjÌûÅ%åwSÌïÇKÈ«HëN¿·y„çi}E“^¿;’¹ð~WÙô¾ˆß!”§›ñòŠ|.ùžßïô9J†ñó’š5ßÔâüó ÷N©¹ñit¹9‡ÙÍš%©4ß!|Éœ]wAÄOÞ‹¿©èzŸŠ×¦©W4/%¼×·Tt½ÔGìÿ®èzjïAÂûñÜ’òûdÌï5%å§þ¼Œð~|©î‚D×ÊÓÍøœ×Þü`u }jÆóß^žKc *©­~G-L}8VµM¥*TÝ47=É=ZÑõ4>ÒÀͦ7PõæzÓ\µ—ú6/h›ª:é¾äDÛTªJ³{‹·ú!B-èû ƒ|U5j¿¤Ño š¾˜V·M¥*Tˆ¦›È~”òÒÀƒhò¯î•ýݲ?®×–”Ÿú“ ËåÔWµ¬êOöï1!à|'áaô·ý^«j-#iÄˆŽŽnF¶+Cúì4›Ä]£%=å^Ù6UyžŠë]Oí¥¥¬‡NÖˆJsGú;]Nè/þB-èšæNµxÀ’¸å–Së€eqYKø=-#üÞVÄíÕ„ßÙÂ÷èá¸ìÏlOÄý¹0’²Tš~Qo(†GÕ5¢é©µ5¢£/=l¨ªéž5¢Ãc9adÑIÊ›¢Äÿÿ?{ç.IUíí÷Lž3'ÂÌ03dxA@ †+ˆ "(&ÔkãEE1û™³bÀ€9¡1"ˆ HÎ0Ä œs&GÎ÷Ço¯[uz:ž®î]Õ½Þç©§ª«ª»Ww¥ýÛkíµòEÖ¡×Íb晬mapoD‰ž.«ã½–x[àt”àrJ·œê"¶ £ßcSjš®…°<Ý£f£û`zyˆD|fÍZtî¯Bí9ª&bmû[‹XÛ¶2õÞvÿÇŽÓîíÚ-Dí³±ê^ŽÓ8+Ðø°m‘øõ±bñh…Hq!š/¶ ó,jºf••óh›|PZÓûqÀC ~Æ®ÀeÖ¯F0K‘@]ŽÎÃ¥ayYx½,ìoâq‰ÁA¶–ƒaÛ@™m­`‰½u$bpºÏ™\‰ÎëÁ`Çœ0 ¥–ûH<ª½À¼ŒìGYÿEüºÜ޳ž¬¨sh·µcßmµçœÖ3ŽÄè|ÔZZ}w§…¸í|,ò`yÕ½Ú‡Ÿù¢·äõÀ46.ô^àwè\ÛÝÛç¡ûKí«¼0Bâ-\‰Ú16­C÷¹•aÙ¼ë¸L/’ˆÏ¬éEí*Ü&TMÄÚöaÊ‹Ø9$âÛBƒgÅ…¨“CÜ#Ú9´{ŒèŠ0/í-u:»ÎÛÙ0\FÒXq!ž’y¸ÐÈ6;/×™y¿²ò9ÍaͯF‰‰.EåµÞŠ„Y-î@¡¹¥ô‘Ó¹è˜oæóºayœD<®DbpŒ­…åXض²Ì¶"°6LËjíX'ïÞŒ_ONNq!Ú9dP¤>ޝ{ˆ‘ü/ðcTGô਎èÛ¨Ï;ê´–:¹¦Y!jYí:QˆNvööLM‹Ãú)•ß è†¼ÊBu²Û7»¹-ˆjE÷0 J _š‡,¥ëæ¦ÖOoð;6WGã€.î$i ´³à½ãÝúðœ…Ê$¦Gû‘$ +‡ S˨XúzMj]iÆÅrÇIEÕºß5‚‰ÛN¢½H`Z… Ñ· ñÖÀIþ·q¶lëGÙ:Q€5Š:©†ÖT’l}–D ]gÌ2æõ’$&°óf(LæÑœLR¨1’ìƒ+H+ "a1lìGçíŽÀcÂd<ü+,Ϥ}<:Ì?&»¬A÷ »GØ=Ãî!vÿä|Êâ½Y2ŒÙ Ñ=Ôæ R¯waëûófàt,ÓÉ(,‘…%þÈ ¦$ÊÈ¢ìÀ0ŸK’$¤4aH¹Ä ¶\š•rm™÷O†¶ÎÄ™^No+ÍÖiÛiî¹jϹђù‰Xµ¬ 6Ÿ¦íÊ|^#¬¨½K[°Œ¬G2ñXZ$‡µ) ¹NדÜßFÂÜ®óq’ˆ{n¦¯u»/¤ÛÖö°{$mkÛ´šé$çZéù–>ï*­ŸKe'Ájà&ôí†î9{„©{–¼N{G/@gæ}+^6$/ø0*'×4+DK…š‰®vx1ìa` {˜ØÃb=lLè®%iàí–«yx6£q ·7§¦[QÏa­šz}¨á^I¨6»½’ÐÍ2©H5¬—m3:ÌÐ,«™(PG˜(lM´Ö°Hc 4醊‘nÐ6f6[‹ÃáÔòÉqèCÇ)-"{ÉV¸­GKi¿ 5K×-O­ßTö“*Ó×ÍØM¦™-óž ÑÎ&†GÔ¢5\¬ÄÇ2„·+ìßiy¢þlÉæhhWF~'[¼îäšf&dk•2q†€mRÓl”pè¡Ô|2cncƒni,XöÐt™ [guÞúRS9*Õž+WV’:v¥X}EK4F2®¶UšBÔ’|xÈwcX ˆMè^’Eb$ëäÊ*ìß:Ë:­‡~T†f[”Äm.¶`Ù³·”ö°¹•õab½ËvaBô¾6o-ºåÙÒNfŒõ6ìüK—Ÿ)ņ^eY£ØiîurM³BÔB5ºíŸ‰Ã%óJËéuõô*®DH« ùPù 1Ó®1¢ßÆ‚¥ð·ò–‰7]¦úDe¬º•“Á²«˜Xu4õºœ€]Uá}FŒÐ\Ë„\” ³ÍbÙv­´Qºˆý [ׂ,ÝfË¥¬E÷‰r“Ý;ÊmKwŽX6Õédƒu`NvìY^˜œŽê%îIvcª&`­V­uf™h°:•Ö™e5n-c·Õ¾µN-«±Å6^7/s {¶xh®HßkLÔ¿gÔ[—tøðgào$×}»2ò;ÙâBÔÉ5Í>8ç‡ùùÀ¹$e-6ÔN´Ì¶öÐ\–W¦¶’<8W‡eóü¬'é=n–9l]㲟òåBªÕ½lFì2±Á· ÔmSs«·KŸkºùU÷ÊóؽX \ƃ\Ld奎}sQòŠùay^x=/µ}Ù×v3Áfu@W¥ÖY·Z^Étº4åÊÊ@Rï®kPô‘44J…sVâÙ~ë6áu;=¢ö·“”3°š«&¬×„ÉÊê¬ öŽ¥ö ëlß,Å´ÕG¶ëuNƒ¯ÓY·ÓÑh%®v¨ö¦2¬#¹G- ë¶ÍÈÆ=Âü©è¾eÂ(í¬$ÆJEX%ñU*ºÒŸ=Y¦¡ûÌžÀY¨vvºtÔC¨ Í Ô‰¸<ØbcSHJ{ “ˆ Ûn寬Î6èžÖ޾զïªû_O Û#$¢{Œ¤ÌËʰlhÖ¾XCÒ~H·Eì3ìœÈ«¹k×üìÔò,t¬{ÃòII—a’çÁPê3¬ýÑ,Iéz©CÀa‚ä?š— Nûq!êäšf…è6©å™aj•Ézƒ­Gx”‰%ëù-VwÒY°5:FJæ•–Óëêyð ¢HZœVšÛdÞv&wYæsç•l[‡Šcß, 묑˜SC 0–&.íõ|?§¬kµ>Ó^¿´`¬GT¥\$ [óò¦;RúRëKl•÷ ¥>¿Ñt/|–âQj瀉شàNRÇÐ<Ž¥B2Ky³­@}ºˆýXj­°Í–KéeâP€ôd÷ŽrÛf£°ÍíSŸ•UR:»W£sÖÙíŠ>°ÿº–ÈÆÄûR©_| ø*p3Ùy­&­u^™H°ºÇ&VLÀX-e«qkXVûÖÎÕrb¸\æ<–`¿µç‡ëR1»ž¤k}˜zÑ8 ý×$Qýa9«¥RÒ÷‡±Óåïõ¶ ¶Eõ?†Î¯_L|Œ¨“kš¢6®äÀWÐzÝ´úIrÃ$Ñþ°<Ú>Dò í Ëæù±Äé4ß 5É*ôàM½¶i¤Ì:kÚëJc)²Â ·5ðž³O¡†H»¸3Ì¿ŒBxöö óƒÂÔ,›P£o9Jë¿4,/ ¯—¥¶?@w¦‡Äã“•x6qú'äo§ÝŒÎeó™ì%Ö&­Amƒ©}ÍÛhû¥–›°†kZÀ6ò:ÝùѪÞêµaj´<ÇlQúQàXêí«ÅMaþmà4’c\Î;X*Æ*‰°RñUNtÙgÛkhÌã¸Ýgn¾|ŸÖŒaÞLr /mÁç§9xIga^øk˜?ìHÒŽ(õZ›£œ ¬äLF+:ÐW£{ƒ]óë(ï¡]î©æÕ/õÖÚgXû£]¬~&€§£ÿÍ)ÖÑ×­í"'ç4+DÓõ‰6„¾´kX£$ý@$é .ª—Uxo^±ß–•×·ìw#êˆHÓéÞ(\’ût˜ê òL˜°4qi¯—R,¯d'a=ð²ÕÎã`c×ÌË—åw›µqÂ&bÓ!´›H<§æq,’\¶jʤz/‰ˆÍª!ZšˆÊ:8ÊèókQIäZg©‰ÜÍL¼/­h“}í¤Ýõ§ëÅž6Äg-­¹ÿØ1$éÐH‹Y ³Iê_)$ؼ¨†ý¦¡Ðêd6&0[ímê,¤/«†¨EmÄJDµ…öyóŽÝ»³Wß,ökuã¹Õ者œ·3ÚÊÉ#êäšf½gÞÓÒݤ=âNg²yn¥}Àv5Bú0ÏeV¡¹N~°a&y+ÍaÏ¿ÄÇοþ‹‰urM³BÔJ‘= '£Aá%¤º5ªNLÌs™•GÄ:¼a{nä-4×½8ùÁ†ý)‚ÎIp!êäšf…¨ŸàùÁÂgÚY(¿P® IDATëË…hç³W˜ßØÆïtoH¾0!Ú[u¯ú±çN'±- .DZØ1ÈÛ9âÔ‡·Ó\Ó¬5Ñã ŠøXjõvf¶s!Úùìæ7Gøn´Èš[®®íd°ç†ßøäUd¸ÍíL„èdC¢ã¸urJ³7{xµºœ‰S›¬ŠÔƒ Q#Ú¹ìæ–ÿhÝÌvoÉJ¬X½w`ÅÇŽÁ`Õ½ÚßòCŒDˆN6¸³ÀÉ=Í Q3U!ogòØ8Žv†æz²¢Îç®0ßµßé¡›ù¢-|ŒXç‘×dEîuœæñëÈÉ=Í6,lÌÐÚf ÌÁ½«“Ån4yHV”®j…­Ð|기BÝ«Pi'ŸÜæ{·ñ;Íó¶®ê^Í3—£µvìrìþžÕ½Å¢66VÝ«u "O›wtøÑJ ‘Ô&·ú×ö,[Êþ,#Þ9ÜNìäÍkîÔÆÇ‡:¹§Y!jc†LHÌöHM‹Ñ³!’Ôßi‘b…ÅÓŒ£†áJtñX±h-¶n,L«SÓHÉ>µÂ¦xư‡Ò*`7à,àÀÂI|Özt¬ì8Ûòª°œ­éi¤d_ïÄÈžÂümüN»/•6òP¨ðNÀayç0·ûÈpjÿt:?z¨Ü˜Ú„îkјȕá=£ÁŽ5©mcaÛXx½6lßö·ŽÛ¶:|þ(Åi÷÷¬¢-l{= ƒ¨ÃsNXî ¯ûšØ:&&–÷‡åÂòáõ2’q²FÞ…h¹gxZίûÂòêèè Ÿ1ÿY¨ 2Ö „׳Âöaÿ^Þ2BrŽ,M-ÛùcçØƒ·V©·‘Š‹ Q'÷4+DMt\ŒÛ7ùykÐCe˜‰ Êfc¢8 óAà À¿Ÿ†ïE ÇuayMX6±c ÎV0=ûÃ|(ÌÓëúÑÿ’^gË»…ÏiWarûž~à’R>4ê!yø®B ôqtÞÙoìGYÀü&mz˜¤ƒ"-PW…uÃò:$~Ç‚­+SëV†ýV†×vü;É{²m˜æ–Ìç…幩},kn;=¢Öiu8ðs`&ŠÍ¬Ø‚ΫYÞw*±9|§[¾÷Œ¡ûÌúÔ²‡«Sï³Ï±å¬‡HLœ^g=Fì3è\K4fÍXøìíÂT&8–¦¦å©É‡½n‡§¬‰¦5L"ÄÂr?Å[évÈŸµký3À˜è¡lÖµš¤“ÉžeÛ¢gÕ<’{Æ^e>£”蹉ÔûôÁ_ ܇γõ>£Lœ¢{io°qv˜†ÂºÙ$×Ûltžô‡å>’ó*-»ó,.DÜÓ¬½8 Ø7¼Þ€D‰M·¢l›ËIz¿Ó"e [÷xšc]D}èæg¢ÅÖ ’ÜDm.Ùg µ_%S#Œ‘4G˜Ø€4¯Šyd§¤ìdkaië³ÊH÷¡Œ>§épÚeÀ×/3¹2³˜øØrZl¥ÖÙ4\²oÖiLä¦ÄF&zÅÒ¢"í 3O™yÖÌCV®‡|€‰ž'ëµ7ÊE”¾§¤±”•¶<ÏÖg&ñžÉò@jùøÔòjàn` j¸-AcX—„צÿÓq$4. ¯ <©ÂwZ¤†5èÐýÑÀsRÛöA’F½5ô‡H:Zl[_ø|ÛÖ*±k÷VëXYCrÎÚò:t^n&¹ÿ¡ûnúÜš…î‹;¥>ÿ§Ù™¾GœTc_»×®Aÿå|àz2nÛÖ’t0Ú5Wmè÷š˜˜:Uç¡óeAX^^7*8@ÇàA&ŠUóš-¶ô‘œOv›Goå…fz{–üyÉ÷ð#àÔ°¼[ɶ´0LßSí>›¾Û=úùè8ÿ?tþ¥;Ó”õP/Ã$çÈüÔ²?vŽmöÝžú:ì"ñÈߋΛiè<°vÏžáw˜¨´çe«òD|¡E#vˆ»ãÔ¤§ö.U™<›Dx.!aƒL§CÀk§¦ö¾BÒûg½†sHÂy,l§U=ÇkH¼xŠº²d]i8êÊ2ëÛé¹;,̯$ã<§t<¤j?ïÒ!YÓ™¦•ÝJ‡sµ3 T«Aâ¨Q¼"LËRëÓÛ–ÓþÒ#}Ð=åî0Mv,ç(„÷<àYW†™Ô×€F`G©i/Å’s4}*õ^ÌJ-·"™Ð­À·ÃtC}á9è÷¦E£‰åRѺ¶oGÇñ‰Ào3´¥LpÌOMsS“ {=£ 6­" #75B"Ä,<܆-l¬²ýzZ?&»QöD÷Ý LôP6Ê>Àµè÷íŒîg1˜Î‘…H¤. Ó|4Ô`>°gÍ”a3qžîx ËíUÚyžŽ²XGÒ¡•îÌ)â°‚nçTàBà[Ài‘mqœ²4ÛpY†zÉòŒ…j'OA7ëÃý‡†m/«ó3-”l6j ”†¹˜d sû˜]Y²>o¾.«½K[y=p[1ÇDnZ@Øø£ô˜$io˜yÊÒã™,´”ÒÎëµ7ÊE”¾g5‰°L‹I›!Ìøaj–W Æ'À73ø¼J|5$_:å*±™Ö3¯®u¬XxÞ,¶ÕK{¿¬‘™>·Ö#OÌU-²õ» îÿdto~—½95±ûJ½HP¤ÅªyÍæ’ˆpsl×¹yôÖP^h¦·w:YÕ.~êtÿ2ñD(¨Íqo˜j± ‰G~{tÞlFçÁ:¿ÞÖ_‰<¾ÒyÃHœæð‹Ž“3¶Cá.ãÀëú‘7opP$»Çi ½H¸<«…ßñHÔø[Ƴ:ÙóAtß>'¶!N¡Ø ÷MtÞµ¹;ŠþDþÆù:ñ9vü9ŽÓzPR¥qÖ•KþHX9ÙÕt§;øº|8¶!ÎhŒsž9 EÜ8ùàCèÚüVlCZÄîhøÂ8Š q1ê¤9—Ä6Äqxº —³u‰‘>’žÅ³Ûl—ã8ÙÑÌØªÉpÉ}e¨Íßíä‹éè\ÈÃxyG×ãJtLbD;½%zg‹¿ÇÄèàˆ—S,Açÿ± qœng?4gxF…}žNRÃt2µ0ljËP6ÝG·éû¦סûÆkÚôN~&y†8ñéECp¾éû߂·÷µá»vGI¼'ÍèÌj¼µãdN+²,æY(,gJ(òã ûý•'8øpJ[¬s' Î>…BÂì[Í‹QFÎ[O·áûœ|c¥ƪîå´‹µhØM,¬Rir¹Vp[˜'uŠÅˆÖ™’nYù8[.GiRF£\rF£R’Oû¬’láNŽé!ú~`”íðu5ö}%p4*-pð«–Zæ8N³lƒD u}xc›¾û'(ôéx8f«x%p5ðר†Ô Q/ïä©({ûÚØ†8ÑÈJˆ–ŠÉÒåtr[ßÛäwfÅf”| S+•hÙÈ×Ôš¶í#©måÞëdH§ ѧ r çQ;…õà\”täSHÀæ­¶šã8âxà‹(”~ªüå6~ÿýÀ‹Úø}ÝÆv¨c¡Øe<Ï3}aîBÔd¼z=õ…³f**WµÕL÷òÍñBÔŽL×nCfùíˆÜ„Žýœ0Ùy0=7w$©e[MhöÐ8ëÐi«KnËå˜CùÚËVò®åÊßÙgY n«¯=\eÿÉ`µ™Ë‰ØÑÔ²'·¿Éðû;ŠN¢óÑØTÄþŸu¾ïã¨&×ÀÛ·µÄ:Çqšå(ôý3p:pGTkœ¬9%úù¡QoôÇewt¾Ä>3ü\Èa«YŽÊ×\Œ‹Ñfx ðÕû¬#©oµëÓ¢µt}¹Zò+i^ÐÎ@Ç~Tvªk=†2ç. ÓÌJPR1Yºœ®Unëóâ‰71;L"Èû—¸7¼“m ûÌ©ðÞ~’˜zypiS¿¤CéT!jÅ« ±² ›—Þ\\ŸµŽã4ÍÛQ†/S~¬ˆS\zи_€/Å4¤F€ï¡a N<¾‰’´<•îÍz꨻x<.F›á²0ÿ.©ùP˜ÏÓü&¿k=[ T­¥ëç"Q™ÛQŸ÷r)ŠèYæK©.4Ç›ü]1Ù¦‘ ?sD˜¦El/:'Ò"öÍá=ûáB´,“q·³QhíCÀ÷Nâ3>¼Š>Šb_ˆŽStf¡‡IlÁyò¸ü¿'´’cQ(Ó]ÀnÄ?îN18=³—;whM/ÉÍá¹ {Õ¥H¬ü WÊ‹§ª(Ø}¾Z{y‰8M Ô´híGžµ’}Óë§6iëF$,ïAíÞûHÄæ’ÔºXçc7ò.TÂéܰì”ЉÑ}õ`íŠ<£é´ú«QÄ&*çy+ðLÔ›øBj‡f8ŽÓ¾†BícfÁY<%>ûXd[:󆺷ÛiKTö)âçwXK|Ñw üjËü‰Q§>,óñªûÙ˜Àf‡ô2Q¨– ×ôú¥HT¦EçƒxiÞ°q±•2;ÈsÑ…Øè4‚<¨÷ ›÷M©m£d;ÐÙqœÚLC=‰›Ðuxq;ÏNv,'N:üna‰ˆM(äÌqêaÔi±…-: ‹Q›æ¸Ø†ŒA¼6°Ó§ sè»± qÚÇTT7ôO(´ë/À•¨kµ¶–¢Ð„F„êíüŽÓåìü]{[÷qvD{¦w{^ÑŽna'”éÜqêåt}~"²y¥#àZ Q§YŽEçÐocâä ÑÉqjxméw@c’<*LûÆ0Ðqº”— Ø8p;pd\sx/²çf$J§”ƒ“ˆvÚËö¨sy°K\SœbGtß¿'¶!Na9CÿŠmˆ“?¾€NŽ—Æ6Äqœ Bì¾Hã)Ò[Á‘(›öfTÁqÊñYôLyylCºiȃþžØ†¤¸ ÷9,¶!˜Ž†>ôÆ6$Çì„®é»bâ–Eèº/¶!y¥›C56‡ù”¨V8ŽSÊ/€kbèGÉ*>Bý§–Ød,ªÝÉfT¶%OX6Ôf3¡¶Š£N“cðlºŽÓ*|…‚®E9¥¶÷_–£ñËé²"•¼‡ãÀ h¿M¥Bì à^4„å!àÓ$c"kÙ¼ø7Öð®2Ÿ]úºÖo4®-ùij‡zŽS=Bô)aß‹ëØ×é2\ˆ:N{ÙyC_K~‡NRª%õK»ó:Œàcöú¹ 7gÇ6¤ÓQƒÝ).“¢kQýëJ|/|æ‡Â4ŽDŽa"ðsÀyaùg lÿvX÷qà¨ôÌRÛ¿¶8 Fã”°m ½‡•Â\ÓBì)(¤÷là»aû³ê´ù—aÝyÀ×Ê|véëZ¿Ñ¨&Dk‡zŽS=Bôa߯ձ¯Óe¼‰ä$s§=ÌŽm@†€;HˆN{¸ýççF¶Ã)GçÌ2ò}O):3èî1£OEçÙ/xO-!º A0î!IžIø¾uZx}MÛW±µ`|uŒØö©Ü\) ÷àØÔTÎ#º¢ä{ß^§ÍkøuFÖ¢µ~£QMˆÖ:õ§z„èkþ­cß®$oá,íÄN(O’â8Ùs00‹­ën®‹`K½|Ø%™xW\SºŠWRŒìÉN>xc˜’|ßSŠÌ j>€Y7ÖLÖÜ«€Ç{7§>§‘D9ËÃþ•êÝWÛ>ì=‘¤tP£ß_Ž5Ào«lÿ( Å}°#ða&jŒZ¿Éè©°>M«~c+°pÞ‡¢Z‘còç8N1™ŽDÜeÀ·(NŸӀSQ/èiÀ¦¸æt«€’ß2N¾Ø%=[|&²-Ìà 4DÁ³éÖEÓüxJX”Nvt1¦ò0mOc×Zü ¡q“‡Cö}>p![‡æ‚:¢1Ø0Ž4ňð'`ôß}¼ŽýkýÆz¨u²:N–3`é$Þët8/EÎbâ8Â>À?Iz+?Dc†˜ìŽ2žÛ§ô£ŽŒ'Ç6¤K8=ÃóJ§„ [6]K0×mbô¹$ã)áY³~˜ÚfIpV‡é{$%> vXj­×ýÀç‘ðÙŒŽß7HÂV-YÑ Ôáš‘%]Û„Bi+}G¹ug¢äA÷ïeâp‹Z6/v¬A’ÍþF£ždE•ŽC£Ç©6ÖôÙuìëtoD'Çcâ8g ð"7ŽÆÇ<>ªE“cjí]§,û³õ¸'§{ùºÛ èf1z*úÝÆ6Ä),–þ ± É+Ýš»s˜ßÕ Ç)>/DÞÏ™(ÂàŠYú¤‘q@Nsì ¼†ÎiÔö‡ùê¨V8yaŠé„"ö·G£ÒG¢Œ¥ŽãÔ‡e‚‹j…“K~†z)Nˆmˆãœ©ÀPúvÇ©‡ 鬈”ãÐïùMlCœ\°ñ É=€ëQ"žnáeè8~>¶!Na¹C»Ç6ÄÉ× “cÿ؆8ŽÓvú_£$N{ÙëÙ€TtÏDÏ“Å6¤ƒy*°Wl#ê`t.,lG+è¶á /GÇÒ“b9“e:‡¼¦pº94w§0¿;ªŽS,NF5x‹Î§'ŸˆmHòVÔ ý*ë´™¾0_ÕŠÎe&J°rp`d[jacÁŠ8<¡>|ÁqÃCs² £ ¯ëã8õ1|¤ؾqÍiŠSÐïX <"²-ÝÆn(#ã&`×ȶd‰{NZËYèÿ½*¶!uð dë«bÒf¡’2ֻ˚û­Ø†8…¤?ÞAélÅèäøWlC§<¸ƒD¼½$®9M±'°ý–"ÿŽ¢òyôß%¶!ódTâáy± é@¦7¢óæäȶÔÃ7Q-Ænös:.¿§3Åèè÷u¢wÛi= Ñùs_lCœüq<:9~ÛÇÉ1ÓQ=°Íèz¹ ¹¢2Ü€~ËE‘méF¦#A± %>qœz°†Û€i‘m©—™@Ol#ÚÀž(¼¾SÅè®$%ɧQöBçÏ ± qòÇKðLhŽS‹G#ºxÅiVâ<’ZÖ`* Ó£bá†i$ÞÐG¶Å)O'‹ÑÀÃ(±Z7t,Ä “`Š®‹Ëc’gŠÞ°œ,–©±Se8N+¸x3ðw௑mÉ‚w!OÅgQèœÓ~6—Æ6Â) G ïù­( ÔÉ7£:£¿G‰š~ŽÊâuÂ=v#°Ø˜<ל\3Ø&LÛ¦–K§mK–ûÊ|Ö(pÆt,ŒLw9Þ“f%J®µ>,¯D ƒFR¯ÓëW†mY≊êÀ…¨ã8ÕøHl2dðÆØF8ŽS7B ÖÍ‘mq*“£D5ÿÕ¢ìX‚„èóQçìŠ0-§3³÷ Ñ=—Ê"²œÐ,'(ëa [{E‡J^·³o9j"µžmcHü‚ ѺèV!ºc˜/‰j…ã䇧/DY;ñáê8­äÉ(³ôŸû#ÛÒi\Û€:Ù8‰çnLNbbt€Î¡÷*³í!$H—“ˆÓ¨vd¹õ±jËöóÀÜX–†×ó€EaÝ|&.»ý¡ßúP…iEÉòê2Ÿ5ÄÄPè9ÈãjÌëÒ¿¯T¼„ß1;, a8æ%ëÂ6[× ‘(5¯î¢&?¯£éV!º}˜ßÕ Ç‰O/zÀ¾ÝÌL礪Ÿ‹Ê³tBXq‘y:Ï>F¼†X«9‰'àB´[9øÊžüÜȶÄâæØ´‹œûòlÍEÁ´W°Þ$~“ˆÒJ5–ºEò€¼lëÂòf`UÉç.D‚ÒÄ¥ M—õ¶÷ÇQòr*‹ÈrB³œ œ,¥a¶Y‡ÌV#-RÓË¥¶Ò¶A4hnê3=š£ .D§{y$*5°7JÆðVԈꦢßòx”uó‡qÍéZ¶AãŒûѹBÔÂÒ¼^\÷rT˜_ц<2‰©ñZ;æ”ëÃüÏÀÙ©õ=HŒÎ-™Ï Ó¶eÖ“Ãv2ŽÆº.E"ó°|x½ yñmŸnŽŠ£ùPÚ™H”¾Õ¾¢Y£:™n¢æv_Õ Ç‰ÃàM(yÏ àZà4ŠWŽA\Äã5H„þ¸2²-­ÄÂIJô t+½Àב½HÑ G…ù¥mÈÖu5ÅŸK˜—–œ' ¿­—il-^ç2q d:,µe˜…ÂLísúSDå2qiBÓÄ¥{åÚÇt,¬crED[œœ2Nq{ç§Yæ¢Ôè±7+®9™s:º¾7lK73„BªÆÇE¶¥ÕØï\ÛàM¯äÁ>Èf²šÈáè><ŽÂ׋ÈÎÈþ;#Ûá+÷¶Ø†8ùÃ…¨Óí Ûˆp(G3¼(²-ÝÎÛÐqømlCZLIèáôû:Õ@¦qàI‘mi„³‘ÍÆ6$‡<yãÆQ.‚¢1=S¶Ðy¶Nk9÷çÇ6ÄÉkÐÉ1ÙtÓŽãäù(ö8ðÉȶt;}$‚âÈȶ´š¹$azNs¼ý—ŠmHƒ|Ù}VlCrÊKQÎfŠ™Èét|÷ŠmˆS(^‡Î›N*ƒçd„e%¬µ£ãœ£ÐؼnœŠnø—21Õ»Ó~öþ…|t:Û_>Û‚3Lâ\´Î‹g_vmHŽ1ïÐà)‘mi”kíûÇ6Ä)/GçÍgb’gº5Y‘ãt:3€÷o@¡E¯EÙK;™-ÀëÑoßÙ–nçà ”)²ÓY¼$¶ÀëѸâߌlK£üÏÌ]‹÷£ã{&JžS$<+vuŽA%l楦(ZÄ^÷ ¡[P‡Ó–ðz#ú_×¢NŠUÈs>öKí³•±±²6caß±ðz5ËÜÄfm˜÷Fµ"ç¸uœÎcT–å t“~𾨵¡ùÀJ8N=XÒ;bâ´Œ· È{jí˜3¬Ò“PBªµHmd¢2ÁÔ-ôDÞõ`ÿãpÕ½²!}|¶ akõW70ñ–ȶO9¼™‰"ºVÖsT¡§ö.ÉCèB˜‹§Uv:‡àÀ‡Pš÷[çQ¬ì“2‡îzð;N'3•=pœ<ÑhrK/+IÄ‹yìÌ«gÂf„D ™7Ï<~iÑcÂ(½-fÒÍ©À—€¿nF×®M qóözyħ¢aqÓ‘§ÙÊÒX©šÁ°ÏI™Ûgê°û §ö™‰<½a¹”zjÍ›;8ý7h³M…¡[=¢«Ð <¢Ngq"ºa …ãvr]ÃQÐl‹#vîŽm„SX\„vs°¸?¶!˜‰<_3€£vó,$~¦1Q ™`2AÔjÏ_©÷®š—¶Qq¼%ü–yÀBj;Ø>ÌwC¹Þˆêv×Ãh˜·£ÝmÇÇí:†sÐïšE"~›È6U:Þíð–nõˆÞ…L;ã&§³XˆJ˜ü$¶!-¦%Ây$ð+àôàtâq :¡¸5'ñÀè|¼-²-N{Ùø…Ÿ‚‹ÐØLAžéi¨¾›x pª—èB´1æ oLé}¬(ê1¯Ã…èdéCÙh†:uòÅ3%µ\o(¬ý†‘Œm)¥Ô3g¸œ—¶Qq<yR—¡vÅaùÞ0¿¸¾Å¿¯(¬ S¥ã—äINޏ ÝPvŠmˆãL’½PyŒcb7 ëw%frâs::&wÑ}Eß~ûѱ ) _þ¢8ŠÈб}lC ÌÀ-èüù+½5Hò¼qœFØ;×Ä6ÄÉ7¢“cïØ†8Î$8 åG53»‰'¡1,Ol‹#z%è||^d[bpúíûÅ6¤`ì†Æ¯mB¥ŠÈõèØ?:¶!g1pú/ˆÂó¶Ȯ‡bâŽÅèܹ%¶!NþðâÄN™…Æ#‡é›$õͺ…s}glCœÿãíè|¼’îÌ;ð úýÛÅ6¤`| ýo_‰mÈ$™‡îEkðaNY°?I&Ø<%ŸÛJé–˜ IDAT‰$ÚÃqÁÏ:èä›ç0꩟B+úÂë>`›°Ï™(‹˜e8H½¿Z(†e&+Å2•cŒòã`,]÷ÃaŸµaÝ(I&³V3pòÏÀEÀèœøÖm¼ø ðר†8€î™o ˯'n9LAeÀÆQ©§>öB‘÷D¶e²<u¼\†¢4œæøŠxùʬ¤c§QLt›Ã !ò D-[؉p"É Ö;&‡RË¥3½_=¼&«ÑL°Ž†åµH¸®GÂx%I#«Gµ*¼^¶¯ Ë¥‚×É?'!z p2Ú­ü%¶Îÿ±<øcd[b0‰Ñex¬F8…_~¸#®)“æð0÷ûQv\ŽÆýçIôY{ÒëU;bçq´Vn©Fõ à&”šy2à0ª7´#° Hß!µnIö®V0‚ÄÖ:$¼V“> Üo B³kô2õþ©Lô¦±ôÙ¥X²rTJµméº-ÙÕ/@b|Ü­$íy-'xÿ|¶Å68Õ™ ¼øÏSÇqâ1xІySd[ŠÂW#oèb”…³ˆÌFE‹*¦Ú ü¸ xTd[œâaQBÝ8l¥.*ý1éðªQ&ñÔ^æ3‘À܉‰Bsšõz%­Øî(‰p%)Àkßgbq4µ\*0Óûu½H”‘Ô“&IÍ=–ÍS<‹¤P´îµTÞ¥‚·㥠œöóL`_à½t_اãt"CÀkQçÚÛ#ÛâäŸ!” ê’Hß8ªüT®ÇØx<‰C%í¼0$NsT”¶y˵e«msŠÅ&t~L'_žþÜPɹ •+QoÐ&ñÙ#¨§s ʆvO˜lÝèbóSvSjEæ¶´çµTðþ4ìs.DÛÅI(£äb™ýPB“~àZàÇqÍq'Fÿm„SúPDÖ>À3€_F°av˜oDíág[b•³¡UkØZ¤Ú6sÚ¤:Õ¶9Ù2%¯[H¢³æÐ}5¶ë¢’µµ‡/@^ÎA’1›ëÀ¼›‰Bó>$4;Ñ+Ù©X1Þe¶½ ¢Cï´Ó¨.d&ðaà•h¼Ù%tïXÐaàGè~óM\„æ‰)À§Q²¬«#Ûâ8Nç²=¾<ø[›m°è¾£Qd ±ø rªlFÏl6³%„$Ù䆰¾4/J¹|'Õ¶ ’ýp­t4ájäÅ!IÀi¿g,üÖ±ÔïYÖ=TcÿN`0 1\€"@–™ÏgëDÑUr¡¹3Ð ´‘$´Àé^ŽFE¦KÃRœlÙeÅ=]o@ãA»‘©ÀÅÀqÀ?Qø“wlå‡3PÉ;Pxš'éqº©øyß.zPÙ²³ 9eØm{7„åÛQéO ŽqXäÚÊ‹ÔtbÏtÒÏjÛÚAZ¸6*t«íŸ®Fa"ß(ÍÛbÃÚŒ9á=FI–ä^& ÌÈË9¥Žßº•õº?L_C)NÊyD- a]; qrË–’¹“=ϾŠw ¬¸WFµ(.@"túo\„æ‡^àÝaùø}á©(Œþ¼hy5€Ÿ£|é³¢ñ}à‘ÀéÀ¥qMéxÆ—£(™g¿E¥snmÓ÷ßì‰ÎÛ<Ôƒ#{cºâ„‰±á07¡;‰Ùia>ØxsË‘`ï­²ÿL*'î,[PÔç}H`Úüþ’uKñgcÝ”¢Ö›à©ªPOèâr²gwà{èZü1ò6usI½Q"“À£p'?¼%¥ûð­È¶ä3Ñyz2.D«ñNtî< Ò¢³JÔXn8‹“=[PÝÙ!àXàëÀcÛøý~m¯dòÙø?ü …£nŽ¢|R%óN6"tëÙ?í¡´DP†yU «a¬abǘy_mß{QèµÍd¢Öi;£¨;#Ûáäƒ7¢óჱ é`Þ üžÞÛ85ð|±%¶ŽŒlK^øú?žÛ³jìmFcýŠÎLÔXÝŒ_j7½(WÅn± q&°kãÀe¨ÃÀq&Íþèdº&¶!N.ø8:^ÛÇq¢òyt/øQlCrÄ è?‰•A³\‚þ£ÏÄ6$#¬tSlC'G슆£Ü3ªïî8¢Ü [;y:a‡Ó<;„¹‡H6ÏtàlMÍqòN sZ‡<øŽØ6Ì—Gµ"¿<÷^Æwû†ùõQ­pœ|qJ$u#p®!œ&8õhü9¶!N.¸‡Æ6¤àì„BVÆsãš’;žA}™èœøÌ‹m@Ž˜ŠÆ mÁÏßrô¢ä.–l¦SxúMï‹mˆèžt>ÞÁ›f×ÞÅqªsºÉ^ÒàûÆÃ´ èý!‡+½›d Òâß[ɦkKÖ‹¼R¥ûUš²¢œ-ÇW ÁÓ#LLï\nÿf¾k², Ÿ·C­#ЃÎ1›ÃÖƒäÓ‰^Ü üxX7• ùpð¨Ôþû–Ì'ËñÈc2Žfiòó:‰³Ðÿò½Ø†8Nƒl‡ÎÝc’SöE™M¯¦³DÂgQÄi± qèAæÇD¶ÅqœŒ8]Ô?oð}ãH´œ ¼ ‚QT¾=ÔOE™±š¥œ +·î”0™è:%5eEé÷>=ÌnC½Åg¯ag½œ>? Æ‚-}^V̾ <Õß<5¬¯&D"Inç×wP­TÐØŸtÁê+P’?2¹d$Óà}˜äšÚ¶ê;š£(ÂÜx ÷^Ñg…¢+gk¶ÎC2§<³ÑرNcîùÉ G‘Ì'ÃôÚ»dB„¹ñt^ÝJkC•óJQŽÕÿ’ÜÃcxE‹"ØÛAQþ‹iLŒtrœv1ø ºgý¡Æ¾ÕðNŸì™| ›5èùä8y=:Y>ÜàûJÅÏs˜(hW¢0Ò§džé‰è­ô $>+“S¶¶=Œê¸´E¢ ö§ˆ¥¡¹Õ~Kµÿe2½{’„ç•Þ’ùñHàÿx'E=–×g†uóQ˜øuazo•Ï®Æ"âxÒA˜ƒ""ºyÌHÞÕ’ÎÃX=ØEìí (ÿÅ«ÑyóâI¼·,cáT¤ø:Íu†¥Ó§hL¾€ž)ë§Ç5ÇÉ3ç åüßg!¯Ï%É~›NVô°ÏPb’·"AÐWò¥é!tx ê=J ®;Q­¢SSŸ_Kp­Aãg–ù Õ„èEaÛÂTz[k{¥dE·/Cžè˪ì_úºÚÿ’•}TØ÷Ê:öífŽ–¢Æ[ì0Ó¼óZx±ë|«/¢kÿ‡“|VäY°?xÇù·’<ÿ 2#è¼9~ï/æÕÉ2‘¡“/ŠÒéSDz€ÿ‡Ú¾gD¶ÅÉ1gNƒï³ðÐÍ(óí˜øëG‰w–†}– ñX+4÷LÔ »5´Ò‚ëÙáóÆHÆeÖ\ÿ%y Ìo¨'YÑê0}‰a…µ¶—³å¢±!ü†VÙ¿ôuµÿ%+!zdØ÷Ò:öíF¦¢N—-èºï-wŠÏ¾è½‘|4ò*Ø‹®û'ñÞÉ’×ÿ” oœÆ3ßèuµ;›¼wú/açTåÃèFûú؆8Ñ9úu ÐCÄ:_Þµ¯Äl&†à;ùæ;è¼þDlCrÎ-èj´^v'òXäåXìÙ–V1‰qÔ±îƒELnfž;}§£ù ºÑ¾<¶!Nt&[ʧÓ9y¢Çü’ L–àûÈ{Ô‰‰ŠÈŠŒ™Û3EÖlÆÃÌg oÑd†ô‰ÝIîùN1ØEßÒÝ9 ŠÀcàä‡ ÐÍöôÈv8ñy)IÙ'a{à>^³ ®)¹çè¥s=%N÷±×wÄ6$<ý7£²mÊ1èwþ>¶!NÝôß$¹VçÅ5Ç©ÀûÐ1z_lCœ|p!:!N­µ£ÓñL6ƒr7°+ñåg p½Í(9—ãt OE÷Æ_Ç6$'œH{ê%Çätt̿ۧ!z¿£cwÁGž‹rŒ'N¹0'寴Yêéõí4ÄÉ%6fUÕ½º“;P’"§<û_C”s€_Å5Çq2żû·Dµ"?ü”‰Yß;‘tß¿­ÖŽN®X‹:JîAIr¾‚ ¼ñmY±•€ú"ÞÑßÕ\€‡æ:ÂWÁ¨ž˜i¯Ÿa’D.>¾¸¼xžTª^B,ÇÆ6ÄqœºØŸ¤6ò+#Ûâ”牨Äâ8*•4½úîN§òIüBuÄçÑ¹ð’Ø†DâdTŠgøTd[ŠÄ¨<Ò•xVÀ"°¤æiõzŽm€ãL‚QnOÄ–_G9%6£‚Nbƒ†ßÛ':Ý:^x *jý0úý_&)jíÔÇaròÏÑyþƒØ†8…`&p ª{½Md[§Q<,7ÿ ˰s'׆+å­¨6èkðìˆõ²øò?Ù§~ŽCeuFwG¶¥Hœ ü¸(²-íd&Ê„= uØuz–Ü4»—£Z©ˆl‹“‡?þ< 9œ|òq4fô8¼ÑQT¢§!Ï —µH̬D"f 5%Û6!aSºm,¼/½-¦p p A¶"ǡJ¦©¹y5'¡ýsP¯élTg&JÔ2è û ^óªZ‡Á`}Ölâ†<™¨µôÙyèí`:ïú˜Æ>u”<7®9NÌî>†îmN}<yÇè.!úN`_àFàí‘mi76Þ}IT+œ¬Amµg ü(ž%Ÿ ¡ å{ Ë÷DµÈÉŒjBt*‰Pšßb;ª‰Ôô6óꥷmB¸t› Æ™¨ôÆ"`{”!rô@Ýy ¿ŽÆCþ­¥¿²~êµSˆ…Ê¢¶ÒgÍÛ¦…}ÓŸescÉ,ÔÓ}}lC Ìg]‚J€8Åá'À¯ñz¸²{˜ßÕŠö2ˆrlAÞàn É5v soüv· Oè¯P~”ë€oFµÈ)Ç(ò†^‚Jgý8¸=¦QN6T¢W °-.}H …÷  †üì’mÓ‘°)Ý6L"zl›‰%>Ûfùãjð0ʆúäË&ÄKCeÛ…‰ÚsÐx ›"ÙÑJ!OÞîhÜ“÷t7Ϋ€¡ §“¿ëÈ©M· Š,°¤NÝÔC À'Ò]!¹†yD]ˆv¿þuª~ ‰Ñ«£Zä”cp4p1p£Ç7Ä4ÊižjB´ö ¡j"5½Í¼zémæ%Lo3Q< ºûÂt/ E»Ýlº©G»츛°x8–!-âѨôÀ"àÔ©â4Æ€ 15gân§{0!Ú‰tÕ¸uÞv#;…¹ ÑÎä³À#ßEåC<)NþEcDŠÚ ¿FẫcådÏŽ¨qywlCœè¼ Ÿ‰mH†¼yÆQO¨¶nœà*¼ÌS™‹ŸóÍЋ:æ6Œ¡w:Ÿ_¢ûÝScâ´Œ™è¹v íÎsg6£'Ç6Äi ƒL,5ât//CçÂçc’SI8Õ˜<8L{±ur–{QÃî¡0OO¶nmØw%ÈI×`íf„m¥5[ÓåizÃöAÚ7˜~CøÎûç·ô_EÙÔV»’”$rg¸)üI¸¶SNF% nöC÷\Ç)Ç®À5è™s"ð³¸æä‚ÝQÑ›€"Ûâ´—SÑФµÈñŸ¸æ858øj#ÿ7ð£¸æ8õPÉ#j=æ}ajW*ëQäN/'bÍ›gž¾RkïI‹fóöÙ{§¡ú¢Ã©i¨ÌòB`O¶þVWG!žWÔÚl'SÙZÌZíÔ(¬º–˜-'ˆK?cfø¾¢d‘Û ø1*¼>œŽ‹Ðf˜ŠÄçbT'ôE¸-"‘Ü]„Nž>TòéTo°yúßÁE¨ñ<à\Mðθ¦8mæ[ÀQ(ÁáEÈ!áÃÕòËoQ’µ#‡ÄÀ Q-rjRIˆ>æoCáæµ›ƒDˬ°<#l›Fâ©&J&€ÌÓWúÞ°ïòÎ…ïÝ&£ß× ëPhç?‘GèJtRçÁs»õj5G–¶á»šåàûÀ|äùy::^Îäy?ðdÔÙò ϾS<~Û€`1pzŽu*¯–_‰mHŽØ!Ì}HBwòä Ýø ÿtòËGÐ}ú9¨Mø_ä£ÝîT –Gt6j„¶ÃëgB´’ˆ5ožyúÒ"vJ˜Oc¢h6oŸ½w3òœŽ¤¦Ñ2Ë÷#1Ó®¤@yÅjÉV áÎ ‡¿CçÅ%À)xÜfùo4>l#ðlàî¸æ8Ntæ·Dµ¢µl>Ûˆœ±c˜»íNÖ¢á ÿ@Þñ￈j‘S‹3QŽ›óqš{* Qó€Ío—!(äϼ|ËÛø½Ne6†ù̪{ÅçJàbÔ@<¿ñdÁ¥HÜÿyŧ۱ګ7GµÂi7&D—DµÂ‰ÉÀˀ݀_F¶Å©ÍZ¼ìTáùo$ ¿Û'*;¡ó ™“K“H9Íãÿiqy*½±0¶!Ä×ÑýðE± ɘÞÚ»t5cè¸ÕÚÑqœ® Ø'5å­I“ŽÊ{ êÀü7òrô]Œ`]…£û–Ì»’£Ð÷‘ípâ²=:î‹mH 1Jæ8NQ˜\‡®Ý×E¶¥“ø úOÛ ™‹2¿¿ ïx*Ç:æžôÎqŠÍ4`QŸ3 ø6 Ó¾eV†êBô!”?4¼â}tXÞ…~W z¨ ó®äÉèæûëØ†8Q$©YšNB‚cÒ¸Àï ^‰®Û›Ñxy'–¢ÿµ(¥¬êáÛ$Ïú¼çˆÁ|Tâ¢Ø†8¹¤Ãלɳ ʨ{#j×6ï‘Fú JèjT¢?~ œ†„,(÷οRÓm©ý·GÏï“Ðð¨§4is!9=œãÀÇãšÓq¼Ý»¶÷­CBcìÇQæh';v§³O#ñöíÙÇ)=À{QÛh¯È¶8µéEbo‰Âf:ÝÎCåaÎGI«,‡ÊêÔçNc¢0 <øÄ ¤°•†E‡¢šNB%ÃölÂÞÂòtÀ΋mˆqâ׎ìGõAÇQÄÿ‰kNÇq$ªÍ;<7²-Ns| ÇßÖÚÑéj†PHî8ðªÈ¶8Nù&º~®&ÿ %š²NÚs›ü¬³Ð¸Ï³H†¿\–OC% Â|˜¤ ÉE¨\–±wjùRämÿ=]ìuÿ,:X¯ˆmˆØBtw’ñn+€c"ÚÒ‰ìDrèeŠÍ#P¦ëͨvšãTâKèšÿ3Š6q§1úQøä8ðÉȶ8õñDô||˜æ#†zKæÇ·£±žïdbhî_Q¢¢kQÄ)H`þµo¯CöJŸÝÑTrOÿÁ{>°%4˜b›û‘ÛyЇHêp–[¶ù:œ"²óAâ„è¾…A\ <ƒ‰qôNsô¢äÀ¯€ðÒ7EfwàS¨æëK#Ûâä—½PÃg#p7å8Nã<¸ Õ0&j;;ùæ À‡P6ìG7Å5Ç©$Dÿœñw­§¶h-'`GàuZÏL$8m~¶ ¡ ·Ýô PÜ/ã™ ³¤%*yª¿zqޝ“=ÓQ»ãTâñ¨.ªç¨ÎQ¨Ãîr‘ã8¥¼ø:?î‰kŽSƒà»ÀÀ³PG‚‘JB4ŠùÔþ‰ÉUHŽ Ïè6È;:\aÙæ³›°s-Ã6ækÐ@à ¨ñ¼>Ø·2¬[•zÏêù­ôž1ä¦/:3˜("‡Ã| d½­ Sz}µqÛ ÿÒé zÑ…ÃÇà^Ç©ÆïÐýòT¼½›ø*±plXvœRzPœ'¡°ËoÄ5Ç©ƒ9$ãäÈT¢_Ga¹þ–ÑwÍ¢¶h-'`‡‘àm%›©,^W…åRñº1l[–Ë1ÈÖãoúÇ"Í,¶êÓIê S˜(2Ó²¡o˜HC¢~$,_€bÙ[Í ”-¬ßåè|Ú…é9ŽS™Ôp™O’lÂé|þÆ[–§sQÈû_bâ8E£’íAB¨’Àj7½È[7æsH³PRra]ê=ÃHàTzO9±XDL,¯DžãQQiëÓ¯mŸôúõm·:¡ø!JFô<à[mqœ¢0ì‡7€ZÉ<”Ðk”¤(¹ÓÜ,Duýî‹l‹ã8NÇQÉÓ8N~D(ÈS¹–Ö…†N£²xí' {M‹×aÛ*/ö»š­Ço™§5Í&¶.ûpøLóT¦Ed‘“A-.F½Î7Ä5Çq Ã;Qù·ˆlK§²8ÌoŽjEs<8/WE¶¥Hlæ>>Ôq:—íPµ×wÆ5Åqœv³pêü¸Ø%ª5ËTàBÔ u:ƒ=HʵÙ–NæùèþTÔ(”lgœ¤æS›~ôŸ•v;N-¦¡È®JQ‡N¾ø ºÖÿI—”LqG<õ4£±ÈÛÆ5§£9ýÏ÷£–S|~‚ŽécÒᜇþçwÅ6d’<äÚÏ"—@·°úßnmˆS8~ŠÎWÆ6Ä©‹AT=`åDq§KxºðŒ7ZÉ)(¬{#*GàŸcѵ3†ÂŠœÖñ]ô_ŸÛIЃzùÇQ,§~vD^ðOÇ6Ä)ÏD×ÜzT«×É?û£ä£Þà8]ÆÓQبÓÂo®ÆTà?蘾)²-Ý®$ã‹ÄÓÑyrÊqà8N{ø4É£Ò N>9³ ¨´ã8ŽÓóP àqàK‘mq²ã0ô ¼êµî¦¸]ÿgG¶ÅqºÙÀ5èúûjd[œúù:fÿˆmˆã8Ù2xvl#º úW*gvvŠÉb$H§sPŽ;ð ljÁÞ$IE íïF¦D5£Çéú€_¡›ñK#ÛÒMô¯Ä6ÄqœhxH®ãÄãE(4þ¨Èv8Žãt% €+‘}88®9Žã8u³ÃÞ­‰¢xÅ6Ä)<ž©Þq'{¢qlãÀM(¾ã8“cJlºkQhÝ^± qÚÎ÷ѳˇ”8Nw³>ÎÞq Ç£eèAþw`n\sº†½cà´„£ëÂÜi½ÀfTö¨Hc,=?~ž_Oˆmˆã8јM’ô±¨µ¤§+yºpG• IDATŠtNë9Õ =?¶!N¦Lþ…®§s"ÛÒM†þó«bÒSQ¹ˆË"ÛRt®EÇÿ؆8ÅöÀEÀÂØ†8uó|Ô)9|åßp§<_Õ.KñÂõÈYè¸ÞŽ'ži'¯Bÿû—cÒÏC6ßL‹lKѹý—îav²äÛè¼úÞ>*ÏDeÓÆÏãÇÎqçÿè!ÉJ| Þ[×I ¢c{Rd[º¯P¬œÓÐXüqà‘mé6¢ÿÒK_9Y²JÜ8¼;²-Nc<™¤Ï…xgŸãä†i¨'Þ‰Ãÿ ã2<ܧӸ :¶ŠmHbáЉmH¼€$1œ7šcý—£± q:’ãÐ0šqàôȶ8ñx` ¸u;Ž™>àbtC}cd[º‘ý€uèÿ?1²-N¶ì¬¶ŠlK·ÑüXI1ƸOnF÷ïlžaàK(ÏqZÁëÐõºv)óbá8ÌG½Bã(|ð¸æt%g 2o0u}ÀyÀ§cÒÅ¥dΓÐ}x 0=²-ŽãÔÇ;ÑuûoŠs¯qÇÉ‹[ÐMô–ðډã€9±p';ï^ÛÇqâU¨Sß)6{ãe §mB’@å ü&ê8Žã8Žãt‹Q›øzDß|+¦ì À¢Õ«˜ ¬A®o\„Ò%Èð• à骗aÄq(ðâ„íêQµQrÍà͈ :Áä>Û$úpt‡ëeµðÈ  ¦}iS€UAlâØI·3Û0‹iw¡ù*/ŽÙ¾`üÚĨLEÆˆÆ DÜä'€ã;X/åÏîÜ{$”Þtt~„¹ñçA¯‰ÙþŸX:£,DlmGRqh £—v¢RFfÎ@¼'˵ϻIîFÏ ÚÝHä±NS‘±i^ÔþªHä Ä 0Šȸ¦AVÎÄ ÝÆ1Hû}&bÛž‘;ï£5­’Ñ.D®÷›"¶i0©ÿ£žçlÒñŒóT™M Lm#Úõˈ絩²Â쇌Ý ØÉJF çXÖ>AtN]AMyÒbéA0ft#â–;£® •pA¿Ê­{5AÚˆ¯`m\ú~ÜŒ<‹ó ¼Bž]SŽ%ˆê:œêÖŸDàžÿSÒ;8F2:Tïw¤7;¯¾jÆ$žB ¤Ôôn×2Ùº¦™®ëXÍŒ(žBð½>‰ôi£ PŠ%¥s3 hL÷F¬Uá@y ­§?= ü7ðâÓ¶ÌCÜÀD¬c|j¯AÆP ü+öþ"D˜½ëö°.áüY¦Àˆ–¢‡Ñ[öà þï/_y¿¯Àõ Uœó¼ßÂÆûÖÅÛÚá1È;møyuÒÜ¥Üü™A‰ÔeÔ¢œ—çEß àÓÞoKef4‰[KèûÔR¢Ýog!Ê« l|sÝ‘Ó÷ö6Ä ×÷4|â÷.Wæ²×±¬³SŒãïÅlA´ÔÁkÌ-!‚tÛ(ұ؈¸ý¬ó¶…‘—ùÈx9HRöADPþ"°›+£Ø»•®.3kå ›†Ü~ƒD'w/Âòß×"ÿM§­ÏYãf„_Nt(ò­À8ò"ÞRQ=ëâc¶! ÕÞJ|º”‹½i‰›owÇE®Õr½6!÷Þ6äžÛŽ´ÅfäÞÛ@÷_Ϧò5ËÖõÈh;ðzàsuUÊà‚.§!ù$ c³USÎCbÔ1F€Ûà§ê©Vϱñ<™â¦‡K¯Ãò1ÍaPŽr¿×#÷çDZìa>‡3üg$*«Q/!ý¹S ú³¿þ iŸÇ7 Ãè¦#Æ®{;_ÍÎa‚h1F!m Ò¹ŸLí²Š®G_˜G„AD[ë/· ˜ýI…Êð¶µn[¸üú矊¸ Š<¤‡¸åýÝöSdôY9Í3JøZ‹hkÝï %ê‡Q¡Qõ=n}{ VœWÓ>×å¨k±¹fë„ÜmHûn"¸_·zÛâ®óvâÿ_¯r+òAR6 ®–?̰ïtä9&xöç"ï¡Þïyî÷üP9¿©÷¶Ÿk;´çJÄ5ôÏÞ¼WÛLßï÷ C~‚¸ÇÖÅç ÒŠÜ‹<ók·»+êªTòk$W«râu³­žêF,OEúÏGúòãÀx÷#^kOnD¼«îöBúFý,EÏg!6šƒŒõE¾C=¯@0A´›wÐA¤“æ3L ”jp¶›/ð¶ !7Ül¤Cè ³ácäeòRÚ€tXÆ‘N¥¿<ê–Ç‘Ž—ZÈF¼õ£BïÚõè"BèˆÕ7 KKc¢„¯$ cÁ,¼m;×JàˆµLY„X}ÕÚyp2b5]-YåM+Ý|º+3ÑÀÍFî½È=9ÍÍUÀD¬ÒóܶNñ3dLDšÐ?qÑžF`Y_\ÃõÈõ]Z7â-(QÆh¯Õwr-„–ßEtðUW§9ÈõŸï–£ÊYm¬oV õ¿ .ø_ª·:•q9ð<ï÷Shc5QˆÍó¦Jæ#mºà½ Ï¹*bô¢ïëòfÄûæIÞùï@ž“?Uów siz<½›þB]:—¹i)òMxÀM!ãq0Áã‘ñÌ/A,ù[4DÏGú ÿ޼/öF¬¨?«§šF ³‘8g#ÃA@¾¨èQ`W‚>Üo>ì-/ˆØ®ÊeýÞŒôYuݨ›«÷æf£ƒÁÂrN¥˜ šŸþ á[;U˜¤ÕRê/÷ŸF´J¯':'c×OD"MÞÈdákØÍçÑê*Ü v /µJÍF„Ï(¡ð^d\îiŸÕ6êZÌE×!‚ä åÊL·nš+'ä…l.SmËÍÞIBí&ä¿Íb²ÛzÜr;ÐI”p½–ÖúëòËùûm'°¤Ïð–UaòwEÜV@<öc²~1ا(§!šIXG`}W¥ÁÖÇÏÊk \1€¸Á>›y$Òñ뤂ÆgiÓ­­¶ã:‚w‡io‚ zÇÙAº°\ÖE*ðJÅ‘ÖYïc}Ïi‡§Î!CùZ÷»›˜ìì¦åˆÂr×Ð|¹ÛžU¡µ–Váô~Dé©Ë!e£^DœªœÞŽôñ§ J«eá«ëª` ¼yÏŒDL®Ë,D1°‘ty7Œx`„£û‚i]èwG ]›ÜòZ*:Ôn­·n½+w­Æ ¾i7³ÄÍéà9µ#¼²ƒçìuTÃójÄ+ QÂW’…1‹`µm " …Y|ðÕÚù=dÜh»ÝÆ´#ÛÎÎÆW‘Îê>¤ ¢úB>y¹Í§ÕuˆÀ*¥ë¢ÜX‡ÜòL7…_þU°¹×Ö¦,ë <¼%PV­,ÒévG„ÒŸ¸uK).ˆO6‹ª F:Æ~Á°ý½–@ã»`̽–]ëýŽòôðóˆ®A:Û~ 1Ä*¹Þ›Ö(tZ‡¼¦xóùÈý«Š}§èûCߪ°¤5±ùrßBgsŽ"Ôî*°ï¿"Øó¢‚± Ì£Þ\ï4aV…j¶“Ž­–ï³ Ô¹(sÝ4‡à½äÿžGà!± æ÷<¤’µÃºéÇ<ì¦Gk² KBç~'9ŽMÂéuÀÛ3Ö¡›‰‹¥á/‡=bÔÛE=[æܳzë;.NÉ“•Kz :y⺯5­Ó‰H*—~°r? É Ǩ›¢„Ô‘”msÅë¢Ðä ˜áíe†NM%PЮõæ#ÞòÚˆíÚÕïË0AßS× ¹¹ßÕþ«Áô†À¸(ÇWù+LÍv â’‹GÇæqcÖ‡*ϘUí¬Œ$–*OØz¸>{yœÒn¾_J¹ä¥>¸„V1Þe&A‡.I¨Õ¶ØÄd·õ¸å^EóœÝ ü/â]PÆkàÃn~3"ìÌ%èàÍAÚE­îúA­Šõm¶”VKÑ«â¥HäÜ_¿§³Ïâaˆ€qâK;ú®P«— Á¾«×nÈ[?@¼°<ìÊœ‚DtÎËsÝüwˆF\ë¨u×ÿ¤ï;í Í Õ·Ý¨°ißÏxKÄ ³*¨Ð°… m´³7ì-ë}þ];¡r"®D,™þüA·=«Bk±¢ªpº«›–!ʨ]wò>nz:@«Š+AÆjWå 1±ïææÚù",‡ ”@¼q“9v#s‘vYE´°›äIöœ/ð‡øu]\âæ¿Cê<ìMCÞ´g‡ê³ QÔ®F”@º'D®C„¾S€÷QÀ"¾¥ÁPU‘2ÃÍõ™™ïÊ-ðÖÍC²\€x½™ Z’]Ü<ïøB£}¨0RÄÕ¼‰÷}W’~áv7Ë­¥,B:Ë«¨®í6»©Œ[i?£Úõ2ÁÚô[ô¥D3iµéø{̽ ­úÛïøÏñÊΡuL§¢öÁóËÔëzx³·¬÷j'9¸€býiRžM¾Ž© Â* ys´Ó„Yµ8«pt Ur(»Ð9Ƭø£¿×xH¬ù­^SÛ+®›~‹nI(3ˆ§q¿w¢|Çy_DˆØŠÄø*2Œ&ŽE’ ÉË‘6ÜÅ-/seª’KÃ_Žó|QÏ  1GftÞã”MQÊ¥°2)ìqá£Qõ;E”°{5b™m§ þ8"t¾˜hÃѭ©/¤F­÷·!}Õ¡É0ÃÛ‹ å?I/ÒT±¾ªàþ7#ß‘I©ÆLÍŽMë¤k®‘ŒZ'òäÒÓŽg;#àN7ß?±T0.{Mb)£“¨WÁ¼ÄRɨ5u<¥œ bU¶¿ ­3‘÷z[3t!ÚY,’¯yäºÞAþw­ÞWRÈiÇ_uSKfU(P!a%HÉoyŒ@Ðôw;ãˆ2ñJÄõ®Šø·{Ëg»)<~+"xj ¥4¶"ÖàûÝ\´„ƒ7nò–ã9vsëò‰ˆ¥pÑÂn’'Eš°V Ï‘zU@ ïƒDˆog 0UœÅ½wÔ5÷î6ÖÁÐoû$…¦ ¢ùQ×\³ˆ6}Ñä±ÈLqó^vwí&T“žÅ" f½lúü•£ÏnÁÖ40’v Џ^êæ¨¨.íd –7¹,ª4¨Âz-"ô<±j‚X4ã‚À­@I ý3³ú[V—åN©ikr’E¹]„…Ý/!î™UŒBstvڃĈFß­“äNDó£",J\w£æ4 ŒÑ²ºÙj°0 ÜÕÊ*ŠZSû1êwÓ)Ó¾š+÷ÖŠêb4UVøM¿Ñ×".òoE\ ÿTDð|S"4_Á³kß ATÇú›‡K30‹h…èx¨¦äò3‚6Éc‘Ñ{ß>^Í@-¡iQ9Í5·y”#:“ ÿ]ÕãÛŒò”qÍUkDžaF÷¢ïå*\sõí{-õ[ì„^d…›/K,UŽ4·\£óÄZD§„W©Tù¢5ªA#êåQ¬¨vßÜ6šAVAÔ\s›‡1(š75ëøP£ʸ^Wi!3šöʺæê»¤—#÷+_eIb©rD)1Œz‰5™ šû°6ó‘'á¯j÷Ím£dD5ØAÑÈmFõ”uÍÕ´KÖél&Ú™+âAeŠÛþB­•eÛ[¿åæ!Ñ{hÛåm<‡~Slìsˆ5™ šŸ*ã÷請/›¹Ëë öªr“å1A´{)+ˆªK¯YD›‰¾_óäiVì{Ù_TÕÞú-·1㽇Æwعç0EFóP‹è¤~º ¢ù1 o+MDó¢ã{7&–2:É^nžJÝÆˆ6í,fU…QÖ¼š‰+Ò_0¢þ¢ªþ‘E<í]Ô"ÚÎ`Eün}b)£“¨ág’ç¢ ¢ù©SÃ;€D!Ô9ÀiÀuH²Ø€'yåß‚¤Å¸ ø·n ø0ð{àÿŸýË‘Pû7‡‡¶]†D?¼xð¤y#ð=WîàмèÇ*«EFg˜¶Lh‚2aw7¿/¥œi:[iBÛ•Í#ªcF¬ÓÐLÊXDÕsaqb)£W¨Jñ`O{—MH?t&AŽÒªÑ>žy½55PTamx<“Øé|L³€o§!9µ õA> ø÷{ À¬?œá–Ï 5¡ð·€£Üò¡ÀõÞ¶oïpË’°ëÌuˆú 7ïÇ!mrYÆò»»ò÷¶­FÝEÜŸ@Ú$mÜÈ…®Ü))åú…&´ÝK6¹¸àþÏsûÿ¸²UrÒ>ER° º}Íû¤?X†´÷CiSxŠ;ε¥kd4Yˆ"yœöÃŽDîŸ_µéøF~EÚäæð³ˆæ§.W£K¯¯C,œßpë®n¾J`)¸Æíó Z£‡}ÓÍ/ŽðÖ?ø8"dV÷š£ÿrËÄ?ø,ðÀ? ̦¡Vµ:[›U;š¬ÁÊFh-ƒµ]4eLjZÔÜÉ4ÁÒ­”q½wÓ åòÌö#Mº²R•ǘY´z—}Ùã²çÏ‹YÔ[i»D-¢E=§ ÙˆÓéüD>\ü` ÅÂä,Z5ÏSc€/W¹uãÁAfÑôe5ñ¹W}…-2ÏA:Ë'"ò~q¨BžN>íW¬f¦Í˜U;ž ²)>âʽ#­`ÅXÛÅSVûü ·ÿ…•Õ¨ûi‚¥[YŠ´ÏÃ÷Ïêí`´Ò¤{ +3‘¶.+<ß燥kd4¶ýnÏ‘×K®×i»Ä<+frAg¦¬˜3+Ë™À[ݺÀnù´ZtÜÝ0ð¨[Nw˧ÿã•¿8Ûû} ·ü-¢;ÂkhÕtÿ±Þ\M{óDùäíF=‚èÀ±À¯w{ëŸJ`Õ¾‘Ö±r?@¬á¯ Ðò%)uÇÐéNoÛj²)–#–¼Kh¿U{Ò£ʾϕ}[k4k»xž€´ÉÿÜÿ,·ÿg*«Q>šhéÞNý–n%ÏóÅnÿÇVV£ê±{ ªêl¾ÐçÒÒ5êš`õÊÂ?!mû/iKð2wŽo·ñqØ»$šEH›X ÉŠx¹ íŒúÇìÐü4ä¥=ÒI÷; × 7÷-À«Ýº1äfÿ=r‡op½‰oÎm»Ü­¿ x¦[p;Á ®_'Ðñ$¿K+èPÁõ×m«Q4fÕŽf7¤=Ò(`&€mk&cmÏH›ÜVpÿ·¹ý?^Y²c–îtÊZ¹®vûwºÞY±{ :žL¾oq§`^yi‚Õ+ _@ÚöŒ´‚%¨ëþ±wIMao¤žwe,ßíÙ{ͪý\²§ï°¶kúüÝ™V0†qû¿³²åÃ,ÝéŒ"mT$Zõ§Ý¾gUZ£j±{ ÎEÚú¼’Ç©kFÌêUއ¶]ÖÆsœãÎqAχ½KâY´K‘œÔFˆï ó¥m8öäE³Ìö!DÃ<ŠhU.vñ¶‡Ç´òóMÌ*wn??êDhŠ;Ç"äáX曆hMý2Xm¾äêp'­®&eÉëxÈžW) ×Ùw'ù/&_ÿ,ÂÌy4ß’­¼©Ûw2”ý[Wöçí¬PÂÏFÒóá“¥íšÚNq”uuÿ·ÿ§*«Q~ÌÒÌýHíZ`ߺ"]çÅîò|iëו÷"m´{ZÁÎsûž[e…H¯ßYø.ÒÖ'”<Îùî8MkV¯b "íº1­`IêîQ½ú.Q…ænuW¤ø$r1ߨ†cD¯owçùš·]MÛþ´ƒ îĦ•_¼@§ÄÔ'ªS^7F«ø­n)À#ÈKsùÆsf!o4®×¸ò_¨°Q„¯SšeX•›-æË7íiD³X »Á’­¼Åëß2”Ýß•ýSꑇ $Jìboò·åi÷}íå’–&¯WƒÿR`[¹¿Uˆ7»:|¢àþE” YÞKUÕ¦áw`ÚþE,èEêÖþàÎsH}ÿÑíûáŒå›Ö¶ih\‡ Ú×!/çQŸ7̵î\O)yu×ÿ§Ò5jfõÊÏ2¤]jóyÚ%ˆ‡<ë›#ÁÅ_i‡ z“ß &R¡9­žºt=*dE¹G”e#âVð^$-Âm´F=›@Ô¿C\iA¼8‹YÞòíb•;§v”ãîV¬ IDATßN Bø–ÄRía_à§HªŠw#/‡zÛG€ß"µÄr| ­‚Ç!V¨¹ßowó—¹ùËKø$»Ã¥'K}«@5ºë2”ÕôÜÄRáí6H{Å‘t?…·8Ø3bßÏ VÖO"múˆ5ß@±/¢ù~S¾¿P :Öçá‚ûßïæÝ¬%Ír?Æ=/§¾öÓ­Ê©‚ªH¸v~/›ÀW€}4D#ÀÓhõbê4ç"BŠè§S÷˜ì){®<ß„ºØšÙMŠï§˜8‡hai%ñ±AV"ü“Öqöáú5}.жËJ;ú Ç"ý±»kõà•ß§ý›¨÷B²~+’Øì惉¥ŒL´süÂýˆ–ì•È@ý°ET#^ü=âºr ÁÖ|¦•W×Üo2Ù5DûšVW›ð9."Ðx˜h×Ü2ç44Ï]VÁR!d±À•!ê'Y†µü }¢»…VÅAš:êÜQëê¶d+êÎýö´‚4'ÕòA:Ú›ümyÚ}0¶ bÿ,^RŸrç+®_â›Ü1f¦tDY¤Ã×ítÄ›`"@ý'2VK÷/c¥.bµîf úF·_‘Ä©LþŽ%Ñmmkqqûlÿ쎓'€Y·YÒ‹”Ÿ =†DΣ«ùÓÜy®ióyÚ5÷ä»{€·ÎW>—}¦Ò¶·ã½µÎUx[MA¾óÛÉþ7x=Ò@a¡­ ¾܃hÖ#?ºØ<$4úJ䯏ßíçš›V^oÞÕîœaAãný#Þº¸Ú˜›¾ãÖÅ•oÇ /Ï˸SÒþwÜïiÈõ~Ø-×)ˆ¶»Ý@îÏ $ôŠ|x«&éZämw_ŠØ¾yvŸO ôCàÑÒ)·»8®tu8:­`·»cì›±|øÃª“ž‡¼«ß€&› Èe©åãÆ­ÿÈ­û Ô†YÆîgÊÖ-k]>K6W­²ãº^ìö¿$cùnk[‹ëPU š¼îÜÐ?‚h7ÇxÙï¥2äU~ea#É™Ê>SiÛÛñ^ÈZç¬ßŠ$Êæ7B¼¹ _­»"Æ_Ñ&É=RyÁÝNŠ ¢ ®ÔêºDÓ,ÐЖlåBwìSÒ :ÔBX§{n•‚hZÇ8¯D§¹ÕÕá ´‚ \펑5ßÝ“-Òáëp:ò øÂÌ{¼ýËX©Ã¿³X­Ãõ;VÙºUmA_îöÉ .̱nÿe,ßmmkq„Y®ÜxJ¹,ÉG:AwYÒ£öïõê;·CàMDË>SiÛÛñ^ÈR窼­ŽrÇûixƒ-F¯yéFÖ!ãJækSÊêø„:Çñ¤qiÂ6ÍÇ÷7¿˜É9ú>ﶆøh¤YŽÓDÆ!l.½]îH´É³‰ö²x=ò<xb)ÿ9Áx‹ºYêæ$–JæA7_žcŸ ©¢ø8ÒÉ< ú1Z¿yYÇ­gQnM>?­¾>eê–¥.+NLÔ»euÆòaôÍ£8ꦶµ¸‚¦dHûþfA‡^ %–šLxÜ~˜G±øÀ3wëO‘÷/”Ë&ËØþ0½C¢ª1Äi´£wâZ¼’Ÿò?—iÏTÒöºÞ y¾I¨·ÓË ÐÐ^YwEŒ¿²i“,ʺ²IžÑ9¾Œ´GÖtHw¸òû´«BFff mQV(ÖñðoM+èÈbõYƒA¯~á¶ŸS6¯•ºJ«uÚ±óÖ­j ú³Ü>WçØÇç±nÿÓ :º­m-®ƒPeDóç¸cý$Ç>ÝfIÚ¿×cH¼Ï•{J¹²âÎó‡ y,Ò~·#yrÏB”JÙgªì{¨È{!oË ÇÿÇð³ˆÃ,¢ÍbÒ[˜ü¢,¢ý„Ž=Ë:®hƒ›ÏiC]Œ|ìäæIVˆ,¨5uib©|¼ÑÔ¿ÉýüŒû¦Y©ÃtÒjÝi zÙH—ú|W™F¢ImûsÄòµq•¼ŒÖÎVÞöH+–;Ï ˆÅê¶ÐþuyÃ4Á"ÚM–ô(²œÿÇ^¹ŸomÕ䵚wÊ"ZôþIâÇH[¾Öµ‰V7Óv{˜µã½ÐI¯8³ˆVÌÁT«)0ʱ Ò¦tXû5‹ÿ@Ú#k^Þ_¹òG¶­FFVŠäâUî8_.[!£r4ïòç î_vŒ©Ñ±bƱŸ;ÖŸsìÓm–ô2²n!ñ%Wî´”re™ëΓÅ0at†‘6ylxƒYD‹aÑf¡¡âWd,ßm™Q}ž²Z8‹Œ93ÚCUQÍAº,±”Qe-¢ö¼öM°ˆ¦Ñ$Kzº5†„¾KÚmC¬´s9§lê£<{ºù=5Ö¡§hJ.CC8i+2–ŸíÊoH+ht„ öÈš3îbWþ%m«‘‘•Ó¶øRÉã<Þ熲2*ç#HÛ$6Ib ›¢Ñ»œ‰´óç*8–æßTÁ±Œæ7B~ÔmxQZA£í,BÚbUÔÆº w;fmy­2‘1³‘ñ¥F½¬sóùËÛÑæ`ÑÞ§¬Et"PL¡|~I£¹TiÝŒ¤™‰ä±5Œ¼˜ç[sØÓÍï‰Úh‚h1v Â̪ À`c‰›¯Ì±½¤šCRð†(LmU¤n@{½˜l@ŒÎ¡¹×”8†=³½OÕhìÝ{hЦN(¤ìþišáàΨ6F´8êf45±”fÓ¹“›6!Ðn^Ä]¶Hž»D€&ŸkT¶yVƒ¼T£}Te݆<ÿ;!ÂhÙãÕQ• ºD‹æ#5šM•QAb"H<œRÖèÆÝ|fÎe‚hsØÓÍï‰Úh‚hq´Ó¼ °yùn§ÿÆÎD:;˜KK‰ ›þrš+ì8Ò!]M œêô("4®­/b•±—TsÈú]1ëJsÈ(,‰ïD›Ãb7ßc}WÕ¯´o´QUˆ,H,et‚½Ýü®¨&ˆÃw+ˆJÞ¼ GºqáÝè–×»m£ˆÐºbÇËà8ÒÉÞ‚X}¶#–»íî÷·}Ü•sûa"L#–¥yn>?´Þßµ>¯kòÒá|ÔMƒÂëbw¼ÝÜ”… w<šö¦|䦺:,@®­.…–Ãë{Iïî/½ÿ ¸Gô‚àÛAð²Ö{ÔÂ¥÷ãÃô>­=GÖ{©ßQµPm"ÈÑXÚfUÔC£IÚ8ÂfQdèC˜én^ô{e4}Wõü6åmT‡öU:áÍd÷OsPÃ]d:D‹¡–íHrÖén Ó¾rÑÇWN¶‹ªƒ8š Ñ{è¶NÄÐ>^'ûâF4ÚîQï,D ¢¦‡€"¶«µq&òží–ç¹mCˆÐºi˜¹ˆqé\Ï@„Å©ÈC4Õýžá¶ºòú1RÁX-%YÐÓÒñ\ïæëBëýmQë«îXŒ÷») SsÿäG¢ÖÁtÈòÂG§‡AÚŸ»óíG t>ÆýV7¶¬¨a-rmuy4´^7€üw½' ¸gôž‚àžÓ{2nÞKz¯Ap/jç‚{ªï d½vÚá9Iê­Â–zŒW´®êt r92öiwÄ­|7·nWëS:VoÒ hŸû‘/ØnpËk‘ç~ËÍþH޼2¨°RgÊUÒÍFîo]^€<³ ”yú¾ÖrƒÈs¤÷§*eT!è+b´“¤ ªÌñ UÖ¨²À÷jÐc·›ÅÈ;a%垇~Dõù¢õÞQO"ÿÞ‰»ÇtÛ0Áw¿J|%âFÄýz%¢4ö‡¿¬ ý^ÅäûA­Ý‘Íè{.O¿Æh6‰IÅèP‚¼}²ª#Ý4Lð½?óšóT¿¾‡š~´UFû-á2u£uˆ4€™ Z Dã¬{Û(ê¾ak¬/ÏD>6a³W:ƒHG~ùåêƒ{;òÀ.AƦí⦢Œ"Vò;€ûˆ$uÞn-u» Dý’jU±‚+7@pO‚ܳ*H¼”ìˆZÌfP®­ÒÐŽ’ÀªîõþºÍˆ€©Âå2äÍâ…0ŽÜ7 ʘ{Ýü·þ^&?» ë=yög¹¹*¸æ»m3 >ˆ³¶ÒvrÛç¸òªèÒŽsšµºŠ\mªà(jݹeíäÏ%ºS¯ÿ¹îw7z'$ ¹ê‚¯Þ Qš$!wœ` pÙCÚñlrbyUϧuŠNs‘ûEïíPÎ&ð6ôʵ#+**â†åøJC_éè3“à¹FÞYYñ…ÒÕïˆ'å8F&ˆö‰IŨ šçžy†vAììNôs¯B¥*™ ÞúÍiQÂjøÛ‘T&êWÆW¦jµ€Gw5A´MÓîj£—‰jØ­Œ¼8n%Ÿ…à7¿ x½[žŽt¼–¥KÝ|™7 w!:ow¿û% ÇA‡·¬Òåyˆ šõyÒóýx9°¥ÉÁŠÖ©GBU®=kòAÄÚ~be¿ß­{€bãðÖR]¤Ê0¾`;Ç-/@Þ³€·O¦ü»g'‚¢OtóuÈGo"È«¹ yFUi¤ëªÈ ¬Jºîܺ¼ùÈn$Pæé˜~-7îæê¡ ÷z¨"Æô>ÓûÎT˜Q…¢ïé ÇVA²YÓ‹RFÝÙ[^ü/p½7Ïêý’Ä“ÇùC ¢¶©°©Û«¶6êîQZïõ$òï¸{L·Ð:f¿*|åàÄz´3“‡¿„‡ÉøS˜ƒ*ª› ¢seßBäš/ô~ûóMÃ4ü!qC6ԳŲÑ+h,‹SC€«Ÿ¿Ežù=Ü´§›ô÷nTc± #ÆÝ4Bð½?ó;w¾>—¾‡š~ÔS-ªŒ*ýÃe¾uº*µÒÑbhG£Hª£Zuó?äÜOÝx—yë¶"ÂÀe+eäB­ÂYµˆ*°Î@\¥Û…¾à“Vu¯÷×ÍDL.F:ÛMQ\åA;ÎqQlŸ…¢eyÖô’‚ÇX\÷ܲvòLjîÔkçÌýîFï„$!W]½ÔêÕ©IrUp(åòĪûö¤£t´›_0S|å–ZvµÚëT•[ºz;¬£uŠNcÈý¢÷Žv(7x{å:á:]–Í#dÿö ¢‹Üü8à5Ää ,€*?ž¼¹®£”>×ÝÄ4Z¯› ‹ˆ,u^…’-+áØªPñc$ ¹›i¾á ¹*v"¾ƒzdí醴eØoò܃(‹£ž{*UÉ4NðŽÐoN“ˆVÃߎ¤2QßI¹<Žú®}x*"X<É›/¦¼[ô(“Gµ¨¯Ù¦Â¦nï%kP»™ @¨lEѪ<³Ô»èH7Å¡€ E£nyƒ›xÛU¨ ³ªÊ;,`:­Vã%5ÕbZƒ,a Ì× ×e÷ÛŸÏ"¦b5dC=[ÂC6Ú…*chã9”kÜüvàÕH`Êg"ŠÓÕÈ0—{óï÷ýô^toíË@=ý™s€ ˆy7˜ ZŒÄPÄFGIÌO”@”EÔ¨ÕjgµlX~°æ ‹*Æüèø‘Ã*8–Q UäUËÄ8òÞý›|²âw†ÕÍLQ‹ŒNírK7òQ•RJÑþÕjD¨Ð±z*LéØoª@ÇË©0;æýހܫ*T.!¿Ð¶Öqµ*@®&Z°Ôy'•láØ~P¼,BîLZ‡oøBn'Ɔ*jQ_ˆ¸ãþ8¿ƒç72b‚h1t,‰iPÛÇ4Ä_)¢¥ŠsRëX^ú D«»3â:0‘\Üh#y¯}/§oé6ªDõùµáÍBÇw®(qŒ4¢Ñ˜õFw¡n—ƒ‰¥²£ÊâQÄ57 8¦ÁÚ†Õ¹¿uL°þžçCSÝe0·ÒYØ8¬iíVÑ…¸é´Óbv&ð9òŠ`cŒ›ƒ*•"F&ˆC£ÌÍAüÎâ4Š”‘Ì `/‚ô'û"9%÷AûÆ7 Á€tú“›kG8¯ul+òÑØÑl–éhåPá#ëѦ ëgªJÙ F›UµÑH¨qc„³ nÏgä£j¡Z´Ò"rW¬MÇJ·*ÌÎA„b*WbÃ6ò¢÷H',¼Ûat¡›ú1˜gSÐñò‘ùcM-†~XuSI¹€tà¶ŸhzÔ• ç–‹ ­Ÿš?œK¨jt0²ju&]ó8Dà±”˜pÎÈu¹÷AFpÅžþÇCÑqûAB„ý½‘°ä:à>IÏ_ i*|ÄÝ aTéЋ.y3˜Äb!Ò[Þ úŒëû@ßîUå&­ïÔ¦yèwʦÔ@IÑk㮌VªvÍE¾ š–®Jª­X\€v¢ï‘ª#TDZšà[j‚h}$L-†ZdV"î#êVŽ"5ÀÅ£N÷€¨sIyäF™,dúdU®7[hMr"|Þ‰ ÷;. €ýÜt€›?Iß2ƒb Uð9I¡BÙVäèuë& ÷D(ß—VKó¾” y¡6äÎôÃûÑñ¢"å„–õžÓ{Q•%Yï«nD‡‰µŽŠ¸Ø[_Eþ1m/}¦UÙ¥J+Upi;hÛ¨ÒA…]}G¨Ðë »ªøÒoHY‹¨jJ·'–2:~7ÊÜ—e¾?P…z•ùxW#ÊâEô‡²¸×ÑoýÌÄRÕaî¹Í ÑØ`‚h1Tã÷Eà å¢Bê‡ÃæÏ@>Ôˆ°“sËE…ÖÏ2Yƒ<øqÑ´ƒ«‘ætŠŠNÑN-p½ã¹É9w}hýëÿ¤˜EFƒ¢$µa¸ÛÜ\;óÚi!èðkÇ^…€Q!@;ûªíÕh*'i}Kæîˆà¼‡[ÞÍM»“,Ì"Âþ]HXrž>D0jóiúhÞ3È®%ÕçäIˆÒBïCM5¢®Zú{,bæéË›faÉ9f—¹uK(ö~ÝÂä kk3ƒà¹×w„¾Oô™v¿µ­Š*òR6 ¢>¿ýÎO˜®Á= ’§Ïbr"õÙny­IÕ}¥¢å£Š…È;áoÒ\Â…¬¨ŸY†( wBÞK "­Îa²"4êõQåQÝ Ó¼ïñ,¬AÞ· 1AÔÈ)Ø)±”ÅlùoÙWœ«HßUª ÞÜŠ»”´=&ˆ#«†×ÏÑU&!xY’©‡óÈ©@5N;õMêP¨Ul(±Ôdf!¶‘$Ç ¢½iG_¯›vþõÚ¥Y)Ë­ÖØíˆp“åeº±&‡-ÍwPóÜòÜвæžmbЭ5ˆpªè¡ eÓPÅínˆ÷‰ S*8ù ;¨J¯0x7ð^:ÓçZ‹\ã°RTó¤úŠÏ-LV¬ú®/ôúߥ(´¯S¥µK¿YR¨í%1hMXâæÏFîK5&¨ÂGïwõöjRß¶ ´ï¥ß(÷¼€VrA4h6«ò|| 8 1#D‹ÑmyD›"·ƒ¢cÔÔ}tgà Àmö&Ê´ó>Ÿ@HÕŽ¾vè‡:ôÚéŸçÖ yÇRe€Z·Ôš¥/QeÑ.݇D¾×-ßï&_óT%í7£‘æ@Ë‚–û"Ž]Cúë4ŸVWòð:]¯ë´Í–f8÷6DñbÑqÏ÷§‡…@]wv´U»£4>ø!rï•AïõÇ ƒ?·7»ù]´ÞÛÚqVÂd¾}ÁQ-úQö%Y˵ ?aººµ«ûzØÅ]©ûîñ~Ruß;FÑ÷•*f# }½I|(»•øoZ—§eÜG;…ª %%Èjž<ídæÙ¿< ø p8òn¸±Ò¬DÞiuœÉŠÐ¨oLÔw$NaÚN+^;¢B/Tûœ˜ Z=þ7O¿aaEï'}G(¾×–?Â÷¸ØŒ(ÀocrŸ!1hMÅ ÏÀ[Ü”ý/zëÿ Uñã3øsXLXØ ‹ñßAÚ&hµ¢¿õ[¥¿‡BeU!ªe˰‘@A¾Ò[öç[>«¾ƒ|õwþ.n‚#Ž ¢Å°<¢Í¡L°íheÎõ¥º²À¹²¢Ö,¿s1€7e"W6•.H':ͺ¦ÖïG¨& ¼ «: Óê–¼xÐÍ5í!¨ hÙž*Æ‘Îô!n:¹äq«f=r¿n ­Â£.ëpµ:i'},´¼Î-7qìäBD8} ð äýSլ߆(T˜RÁIƒÑÌ'茩§D;†„É"ÈúÃ2‚ì|DèS‹‚Z–¸ÿ¶øðA:£œ&Z)ªÞ¾âs•^…ÞÃ3TèAëwI§¨ö ºàBÚq=ø òíí§÷¯Ÿ»SÍéé+φ½mšŽÆ÷ÜPKX§½/Džû[?zëËÆÈÂLDhÚ ø)AìURª!Àç Ò:,¢ÛÑ€“ú#øvùåj‚hÐ*lVeÔØxð>bd&D‹ayD›ƒF Î+ˆ|äšMÍ·fõë>˜iAˆªV¤îç§4&QU¤L}ޝ^„¢‡"c¹w¥ÕUH;ÄŠ?ŽÂÕÒ¨eߊ˜µ\¿°ÆM¿FѬ­£PWÍï“}<¾ KêZ®Âªï]â ²:^Z;•yöW%J;:ž×gÿ׆cÇщï‡vàUÀU!çro¹ìûyp¤[>ßM[eà¡ùýˆªóN'UűÞoÃ÷—Z†Â1ÂÃWfxa¨ò%ü~«}§é”(aEÝ´u¹¢ÊY”Ðêq1Q€ iYªÃ•ý›x4°âNÈ3x{Æýô¿h›êÿ Uñã3øsXLXØ ‹Ñs@ à!ho¡¿õ[¥¿GCeU!ªe›À]È{á}Ä( L-F!íj(j]ˆ¼4t\’Ñ=¨Et4±”Ñ ªJb?Å[Þ üÞMF½Tañ.¢¸Uër'„ª4Av*"Ä ²ëk‚ZÔª°’Þ}G©|X‰»Žàú•Dç"×p)p·û½¢”Ú5eßðøV%0¬.ê~SKp8N‚/ˆµ‹µÍU ÙHxO…Žo›ìó=7ÔÖIï‹)Hp®ƒì"®²ѹàª\Êóñ‡EÕ í9üÊÑbèGù$dìŒ_ÙŠ|„ü„Çú2ëi{(#ˆBûÇÐÙX´á:o5ŠSEz†;X›6‹*Ѧ+nUXìµø uPe.ÑQÄKe)ðRàä^ÚD—"Ö¶ð|ADÏv»Wê˜@uÓöÓ“iäcµNm"ð¾ñótëX[,xx |7²±†Ý\æÖíƒNìT¸ˆ jT·2‹h…¨ËÄß¹) cA |auEè·úf›F&ž¨´6ó \ÖTMtËígò¤P1‹hsÐ\¿eÇü¨ëg?ÿêªHßb1ú‡ªÆŒ+áwýf‚€}I¨ dØjé»–Bà^©D ®UXÇxo"pì–€•MA¿e\ý³¢m«cÀúP…u¤»¹ ¢Å8ÊÍï.&ˆ^8 T A –1б{å<—%1)bâ&&GOôf„ƒguG ç/ §}à ý(¨k/üpuŠÊ‹ªçŽcÙ;;…¯ A4)×_Ò²FTÓÀ%IyYu,G¯¢Úß,–5³ˆ6‡ª’Øk»÷ò=ÞTѾúÎÞšXÊ誴ˆBq¥£f 0e~³PEc'Qõz3cCý$¶» ¢Åxpx!«¦EÇ7,¡UXÝ9ô[ó÷øÉÌi ¥_*ù.#Û{B…A?ïh8Œw¨ ‹þ‡À§É§qW‹èSÜvQÂdÖåN’$¬êÀvUB¬•Y‹|ÐÕ•¨Ihª,¨2QUpD‰"R4ÓÛOó¡ú÷ÄGMõ££†á$íÓ Zܪrê·(eÜh?U¢ªd¨"G®Ñlª3AÎb0²WPEcÙ¡Y°{§9$¶» ¢Åø­›ò ‚ÂÝ9÷‹³¨é¸T é=Ë[Öœw~Rõp‚õéȃšw …†³×q* N@]Wt¼ƒŸ¬\_ÚáÖ1d@ó0A¹MÏ]Úá&{n»8’rý%-«5YsZ.ð~Ï%H#âçÂÔq”eÑDæ£ÂéHè÷hD±\-öCrEàæû¹I_PYQZ_çê¢Bd”@ŽVØÎ°ñUGÒg!JxÕõþ4±NóüUÍR$7âãÝï² M+°QŠ(é ð°ÐöÝ@ÕuÄ[ŽšÌÂZŽ*Æ7eŒhœ§Ž¯p —‰RÊf)3F!œ&A§5nÞKîèU‡V…cëÕìú IDAT/]#£3èýST&èoùÓPè·žcMhî/ÛwHˆ4f™ Ú|Tx©Ú½@… ¿£®¹¿´¬ñ¡÷>_G:ÑûÒ*@Yî$Iª&?V%ļPÿ%:›Àm¼šÀY§)´Ž R×m%<6'Ì£À÷€›2œûÄ¢ýæìÕý+ªàФÒ~ˆQ‚DÖ¾§Àz¤\‚Üza%&§×Ô¶}(eíä–p5¬{Ú¤Kß…^…÷)ˆWÆÈs£ ª•²é)T=ÇMU±ÉÂiXxÕŽ‚Fiõ‡=Q~íŒüŸ*rÜvmk?Mv´ÊXDUI1„䔋vý2qd–2¾pØD&ˆRýÜ~¾Ðºšf§™RE_®¹  ÷PeTó4ô»yð\‚þNQB¦ßGªý…Ô¨eUöëÔMݤ~ÄŒ7A´ÙJpó÷#À[ë®DôåTMPî¿”‡C¿‡"Êè|>"˜äqßüÉçõàÏÞï<÷áË€·3Y Ì­°ãÔª~–ÔÒ%¼êzŽX7D ¤HKy‡QDø¼ÁMW—<žôãOÀ÷ºßêa¡‚ÿ‚{oØ[ŽšæSNáׇº´'å!]¼Éí÷f$‰»ŸVawŒ$T òs]†sש뢎ÏW¡+.a{\»,i(ÊÚéüB‰cTÆFí€àÙ-RÆ÷ôY\è–ß„¼‡#гð´ÐÍuHNV6!‘GßC¹wÜ|&¿ï“–‡C¿µOY…çI‚è|åà0­ŠÂy´¾{'ý.¯£Uˆ°]­¨â¾Ýio Bµ ðãû‡BFCË|‡ü¹¿\ö;äŸ_= Õ#®¾Q÷aØ»2M®C‘ô¹ˆÛ?Èg¹)cþ Ãè.´3¬ÓZÝOT(TÊÈ2Ê1—h5<é‡ÄïX«ð®ð»4ïƒ4¦§!é«©Îûb*“…Ó°ðª¿ó©ÃÚéέÖ×ÍLN¢^ÚÖú¬ê°‰Qd þw ÷)À÷‘þFx‡ž#,øå-'@úeÚÍLàCÜï_Ï ù^ ZHÕi'Z…ÖEHz’8¶÷Ó* Ž Ë8¥¢¯Ä*Û'®žGù8ÏBÞ?#éãw‚õYC (†·Í&ð ÒmCyOË\‹B½ÊTX ªaËWÔöNæþl73 „ÑvËÀ—‹¨£ʰ Yõ;C¿Ca5j9l­­bÈïÝØÎ”Fá ¨ãJÜqDù‹ðN&ˆ†aFÊ=~Ä[nG@u—Wa5,¨úÓ^À1Þ¾ ,Ц°IX7ÑZc:¨k¿2„¼³–YƒÕmVŸ>–ûßS‘qâ €=Hϧ«øcåU‰ÙL5 Ã0ŒâÌE‚lí |xŸ[ÿ àåÀ¥À‹ê©šÑæ!í¿ðà\·ÞÚ¿<{#–4}‹e¯1 qGþ1"8¼ ¸À­rãŒú^—æyQëÁy¥›?‚ÕzXx ¬t“Qœ#—ÿ?!{D Ã0 £8ŸÎDܤžJ,f ¬hpåÓDÍäóÀkñ¡OÃÚß(ÇQÀ?"cæ)y¬™¤ ¬³h ­1Ôµ_%°lªÀÙ´¼ä½ÊûEç')–5À0 Ã0Œâù}ŽØþ÷ˆÔ#ä‹0mt/DÚw8(b»µ¿aUñ+ä}r\Ý1 Ã0 £^"nhÀÛÊýÄ•ùb'*etŒÅˆ5h8;¡œµ¿aU0Q~¶+X—a†a]·ã9ë¢Ø  ²8ºõ2:ÃÅHû_Mò0§½WÇ´£1 Ã0 Ã0 ÃÈÅɈ²4Ò8Û•¿ƒÎ1ÚÇ+‘ö\‹D¯Lã-®ü˜5ÃÈÇqÀ…XLÃ0 Ã0Œ¾g$„þ¤& SëÜ>mS½ŒÎ°AþÖÓ3î3¸Öíci+Œ¬ ")l&€WÕ\Ã0 Ã0 £F€!ÃËrî{rpxÅõ2:Ãp%Òþß˹ï¡íÿÄŠëeô.§ ÷ÛÃHR£ÿFr†a†ÑÇü?¤S¸ Éo˜—¹ýO0ÞèÞˆ´ß $=K^>èö¿˜^a½ŒÞeø%fMïgÞ lί»"†a†aÔÃ>HЙ àĂǘ…$#ŸÎ©¨^FgØÉ—8_ð3ÛÜ1ÞYQ½ŒÞçqˆ%} p@Íu1:Ï‘wÆ+ë®H;°Áφa†‘Ì$0‘šQ!2œ&'€ÇýÞâ–ŸŽ$%ظ½Òší` 2.xÈý~?’;T6y¿ýö‰š¼Õ- œë–÷þRuežäÓÀYÀÀsk®‹Ñ9fkñ» 9‰ Ã0 Ãè36 Zé*§¯wôe¸‡êÛÿÛüFW³¹Ï!9]”Ñ[<›À¿'±1*†a†‘γ€— e0Ïû½x)’Öã oý\ZÇ3€·¥¦F;x!’L¤ýfyÛf#–‹!àeˆõü'Þöy´ö·–!c¾>Ó®Ê=Çjà1–u£?xŽ›ÿ$±”a†a}Ïþˆ ú§º+bÔÂR‚`F†ae¹y§<»îŠ´ ³ˆ†aF5è¸ÁÁZkaÔň›×Z Ã0z©À͈Gůk®‹a†a gAz£?ѱÄs뮈ÑÓ 'Õ] Ã0 Ã0 £,@„Ѻ+bÔÆÈ=°[Ý1z–).›ÏI)k†a†aô ÕèOnFÚÿ±uWÄèiÞ†Üg·ÑÍ0 Ã0 ÃèS¶ Dëö'¿@Úÿ™5×Ãèm¦#Bè"”†a†a}Î(Ò9\PwEŒZø¤ýO¨»"FÏóä^[,¯¹.Fµ¼x;°GÝ1 Ã0 £{xé.«»"F-|iÿÓk®‡Ñ|¹ß.¬»"F¥üi×ë®H»™Rw Ã0 £‡Ð„óæšÛŸh´ÜuµÖÂèÎ6Ç"5÷ s€#€íˆ@ÚÓ˜ j†aÕ±ÖÍÍ5·?YêæÔZ £_¸8x0VoUŒŠx&0¸XSoUÚÏ´º+`†a=„YDû›Ý|E­µ0ú‰ßÖ]£R^ìæWÔZ Ã0 Ã0ºŽ_!c{ެ»"F-lDڰÑuÌV!ïÇÕ\—Ž`®¹†a†Qãnn‚Hÿ±i÷µ÷atš]뮀Q˜cEÀMÀ5×¥#˜ j†aÕ±ÅÍgÔZ £l|¨Q7onNª»"F!~ü-ðŽº+Ò)L5 Ã0ŒêÐh©ók­…QšËñÁZkaô3cÀ,à_±wP72ü¸²îŠt D Ã0 £:&Ü| ÖZu`ŠŒºù<ðD)òÁšëb©˜ j†aÕ1俣µÖ¨sÍ5êf8 ؼxB½Õi ÀAÞ&§»ñßÙoþ‚ŒÕü·n p9ðààp¯üÁ¡¹a†aFǹé >­îŠç|¤íßUwEŒ¾çcȽø;ª3:u«07 ø&p*ppŠ[ŸT÷5À°[žëæßŽrˇ"y>•ë€g¿pó¼ìP`?Ã0 Ã0Œ¿r+Ò<(­ Ñs|iû×Ö]£ï™ Ü쎩àxÝ ÌÅqö×À»½õIuÿp)ð ä¿<ŠD²ÕéN¯ürDè>¸x^Î:~yw¼-ç~†a†aåA¤C±KÝ1:ÎEHÛŸXwE xÐW–næâøb!¾ø!0Ó«»ŽåŸF뙊ð_®rëV³cÎñD y"p5°_ŽúÍ@®Ëðøû†a†a´°éPÄuXŒÞå*¤íŸ]wE £bš.Ì¥q&â*|&’âàÀ“Ýò+hÍý»»›#B"ˆ¢él¯ÌÞòÏ·ã«Ý<' ï¾Èj†aF{˜t(6×]£nÀ¬FïÒda. ³Có〻÷à÷ÑjͽÛz ðj·n p",ߊŒ ;v¾¼7ÞšVÐ0 Ã0 #Ž%H‡ÂÒwô'÷ í¿GÍõ0Œ8¦–Ü¿©Â\·²Øl¥=·a†a}Â~ˆ òçº+bÔÂz¤ýç¦4Œ³ðcà½uWÄháläqiÝ1 Ã0 £»y2AÊ£¿0·l£É<‰ ;ì[s]Œ€C€CA†a†aæ9ˆ0ò“º+btœ¥HÛ?\wE #†/#÷èk®‡aü•ª’܆aF¿3俣‰¥Œ^Dó%ŽÔZ ÈçHÔÚç'Õ\ÃL5 Ã0Œª0A´Yäæ«k­…aÄó(pŽ[þ80¿Æº`‚¨a†aT… ¢ý‹ ¢F7ðßÀo€]€sk®K?cJ‡ ¢†a†Q æžÙ¿,tó5µÖÂ0’™Î¾|¬æºô3_n#ÈÁÚ·L«»†a†Ñ#˜U¬1žÑ-Ü ¼¢îJô1;Ï€{k®KíØ‹Ó0 Ã0ªÁ¬bý˸›Ï¬µ†a4SéÀ€5×¥vL5 Ã0Œj0×\Ã0 #‰W»ùk­EC0AÔ0 Ã0ªÁ,¢ýËF7Ÿ[k- #?ÇW3ê®HppðpYÍui&ˆ†aF5ìäæÖZ £¶ºùÔZkaù˜|8øHÍuéÎpó/Ûj¬‡a†a=ÆF$*å`Ý1:ÎqHÛ›•Ãè6žlv/¬¹.½ÎÛ{€}k®‡a†a=Ä "ˆlL+hô$OGÚÿWuWÄ0 ðväþ]…ä5ÚÇ@Ý0 Ã0 £·XŽt䨻"F-‰ ¢F÷2\ŽÜÃ?Ã\ÌacD Ã0 £<–C´¿Ùáæ–ŸÝèF&€Ó€‡g'ÕY£°¦a†a”ÇÑþf½›Ï©µ†Qœ•HŽËÇ߬¹.FŸ`QÃ0 Ã(¥n1 £Û¹ø7ÄBjTÃlàÀù˜Ëó$Ì"j†aå1A´¿Ùâæ³j­…au3 Øäý>8¶×R£cQÃ0 苈؟Œ»ùÌZkaÕ³GÌú…À €çt°.Mf&r-®­›¾³Õ1 Ã0 £_8qg»¸îе°;Òþ÷Ö]èˆàãˆuï ÛAîùßt²R f±xú^1"×h-6~<³ˆ†aFyFÜ|¨ÖZu¡ùcçÖZ Ã¨Ž `:béû0/´ý:$Zôã­Z#A®Ç0ÁÐÇ3ÝüÀ†:*e†aFïóD¤ãö¿uWĨ…HûÖ]è™À Ƚýõˆí·¸mOîd¥Ì#ÈõØÎu¿ŸQö8L€7 Ã0 £öE:wÔ]£L5z•ý‘ôDškÔç ný›;\§¦òGäzìÜü>¢Ü³mÀ÷°¸†a†a”d1ÒYUwEŒZ@ÚßÒ^½È+‘{{ 8À[†[oyG…Ÿ#×ã™Þº…¡2û" « àƒ©Uƒ±ô-†a†Qµ„ ×Z £.Lm CCÜ|/$ƉŽËÞ€¤ÏÙâ–}Ëô°xÀM›;Uñ.f_äzÞç~ Nt2p8ð'·þZ7jGk×\V»¹/|úÁ‹?tóïïëP½‹ ¢†aF¿2D:þ²vn}¶"\ŸínÝ\$‚ªº²™«fÿ°¦!nwF>f:íRñyVÑ*œÞçÖ=Ü<¿ýÌ“Ïïþy—½ø2fT¹ ¹V{K‘1’ýŒ ¢‹"¶M¾쇸쪕¹¯1AÔ0 Ãh2H$ÚA7%-ÏF’‰§-G œU’–ÂCݲ|Ö!Âìv·<âæëŽž."©ÂÛÖÆ· LÞ„Ôo=’ss“[ÞêÖ«À¯Ö­nd=r_ÍAÚÈfÉMùDè<8 Ø›èlk€? qþÜÜç¡zrÍBží)Ï÷|Äm~9°",íì¦Ç'Ôq="ú©/¼Fûëk> ø$p<’ ónZ…PåËõÀQˆðziçªÙHuó"¶}±*¯Dò¯nŒ(Ów˜ j†Ñ†hí›na›¸%-r“ ~ÃÅa7Ÿ•q¹]lD„¢µ¡eŸLÎÿ6Éi:¦»º¹ •*D‡‰ZW•K¯/´ª :Z§ÓZZ…ZªŠÄ­ïäœû­E„ÔuˆÐ:Žü—mÈÿQ«ôFÄÕr­Û¶ÖýÞè¶o •W!XÛ¨J^çŽßÏiaoOÄ3`o¾§›‡Ó€(‘`/*têôP…õ›‚¡»Ë\Ývq¿ýuóëìA1Ç9øI…õjÀÜ/#BæMÀÛÏ3YÙu­+óLÕC„Îß"ï×"ʸ-À‹e† ¢†aÝÀñHt½ $wÛUÀ•ÈG®]Ö£Dô…JY§Å¡õƒ×CÝ[ÇÝ”´¼2Ò–£Î*8qÍú¥;GCLŽ”8b§ºåa7÷§ynßÛæ»móÜ´¼ä!j'Žwà¤sÒ©ƒ('f"ÿc:RwURÌuëÔ¢µ¸Pí³±ƒVÁU­±£ˆàšÕz«Bóí®ìî1ç‰YŸ¶­ÝÌ'¸æ3‘öÑv&PÆÌqËCnÛ<eÑBäïÄâ5p;"tÞ Ü Ü…´I;Ù¸â>œRnˆÀŠº‹7­Ûþ"zWUnþqÏ}ðYà¥H€¢û‘{à,ä›6N‚ñ G'"×k5ð)·þ5ãj ,d°aF7 è툠¢l@„žß".AÚ±Ž›ÏA:Œ‹™,PF œE¾‘ñj7©à7B`Ù!èàgY6²3h5Jp]àm ïæÀ+ Ôç“ÀkÀ§RÊú XžUpM˜¢„£ùˆÂ}ˆhaªú@I㌓¶é3ÆþÛáq0Œ¼ñà¸×›ßãæÝ<þò $eÉ#BE¿ðXà+n¾xâ^z<ðoÀÙˆåoˆê= º‰ç—#Š©éÀÓ‘ D;þ©¾ª5³ˆ†atsMëÑn:ùð=¯ çÒT$¾Pé/ë.“d 4ÚÏz7=Xò8j‘}9pÅÇÔîíæwåÜO-„+ ž7 ÓÁU­±ó„Z\³Xo£„æ0þ¸Å<ÛHvÛ§†(Â:¤ãçÖ¶GYŒ× ç}ôvTZ}¿UíùÑtnž„XFß…ã×"Bç›÷Ír$¸ÔM5Õ± h°¢éˆÕýÓˆz)rÝŒ&ˆ†a4‹8†tð®vÓ»NèQÀΪvhãæã*& ”QgßaÄM¨»"mDÇ›6uÕ®j›?öÖ<ò£ãÃãÆû­À¹À÷ëèS‘{h@!òTLU`\þžþ¶Çb‚¨aF³Ñ÷tT:ˆ5ÀÅn2Œv ïpp&£3l'YX^•°Í¨ž­n>½ÖZÔË À¡ônZˆŸŠŒ%ívÔ3B‡/ …~/ˆYö½–"Ïï\à§Þúpdp &wûú}H@£­ô8&ˆ†a4µjTÉÔ0²¢ ©‰¥â™áæÝš’Å0|ô=g‰n §"ŸPo—ªòÛnF¼r¾|Ø×­?8½¢se"üéøø¹ˆÛ}Xx&^°œ]Q]¦{¹)/Û÷Þ{½IÝßïþ\QkÅQÃ0Œf£š÷ž×ŒDÝÉŠZ€ÔRbc‡^@*3KÕˉHʤ(F‘Àváqþ:_Åä¡Ißžß!î§ßþŽâ §9ˆÀ8— €šþž‡s½IGÝ0ANÙ²h„lÍÏ<ú½6aYÇß|‘`,ºÿ_ýûgØM~š£Ý‘1·šV興:>¸£ÌŸl&ˆ†a4ÕÌZòk£ÆÜ¼* at3MV4¸È-ŸÉ)·ÔÅô19Ž©‘×5€UÔ6€?!ã"¯ •Ù†¶Ò¼Êó„GµZVÁz“«ë:D8sç Œ#Ä “e¾·A„È_”8ˆŒ¶NýI£5? D Ã0Œ6c%£›Ñ€WÝ*Å0Òhz°"£¸q“Úî§ï /ïÄä<Ñ ¼}³°zo@Æ1D\çý^cÞ¤‚£¿NsWå~\”;¨F@ÜFà’ë³ÉßÚÔ{0&ˆ†a4cgÔIÙ`EÝ2¦Î0²Ðô`E*,ƸÒhØ·ç8¦F^×tEQÛµzú¨kªæ½]O <ªÕÒ"´÷)&ˆ†a4 VdÔIÙ`E&ˆ½DÓïgM£²¦Âc®uó¦§:êtÜ~OÈpSÒ‹†a5’”¾Å0ÚMÙ`EëÝ|^b)Ã蚬(Í"jt?:n¿'\sM5 Ãh6ê9–XÊ0ÚCÙ`E&ˆv請]@Óƒ-vó*-¢†Ñ6L5 Ãh63Ý|s­µèo¬ƒ^D»»Ï³Ñä€E~°"£71×\Ã0 £cÌróMµÖ¢¿éçºvº‹ ’&ˆF3äÍN®nnžä• ðà&à_ܺ1àÃÀïÿöòÊ/.þàŽuxhÛeÀ­î\Ï>€Xùn¾çÊšB“µcŒ¨Ñ,zÊ5×0 Ãh6oG" ~´îŠtˆ&vзÓßô ŠGµ<Óíû¹êªÓõ̾ œŠä|<Å­÷£üÎû½†ÀÚ¥îúãÀnù $o¤ò-à(·|(p½·íÛÀ;ÜòAà°ûÿuÈ3ð 77„{‘{z÷º+Á'º½¥îŠmã¤/¨»"†aFïóNä£óÏuW¤X½™lEîÁ" “ݾ߬´FÝÍÀ±À¯w{ëŸ \ Ü‚(>Ö{Û~\ ¼‚ÀKbœ`ìî,`•WþQw îô¶­&ÚB¾Ï—#JžK€ç¥þ³þàväžÞ·îŠDðU¤n¯ª»"FÛx=¤œî ÿbÃ0Œ¦ŸÒ·\ |xð#ànýˆ ¾‰¼·Ï5ÀW!òo½ >"ÖQåÙîxŠß!?x[ž þšŸü "þ~”øÏº› H¾ÀÙ©²b®¹“¹¹×~< ¾ø:"@\ƒÜS¾{åñˆå$àt·Sܱ7FlËjÝ>˜À u˜Va¶ŸirÀ"UÊ™knïÒS®¹6FÔ0 £Ù¨jkb©Þ ªƒÒA/pb5òsZ| ±L^–áÚAœ›|¡Ö:èÕ£‚ë‚ZkÑ,ÞüXüð·~ð€[>±Ê+Ë+kÈc½õ'¹ùɈ…Uù)ðÿ¼ßzËW¯s˾å ­ÂÕ»çê,wü¿¤þ³þ ÉÁŠTñÐD!Ù0 Ã0Œ.ãÓˆ€tVÝég"ã›ÎÞêÖ­öpËo$°H@0NkqGÄm?Ý-Ÿ†tö•‹€³½ß~ý[D»æ®¡µc÷sÄ:{µ›÷:"÷à®ö}¬Û÷ÆJkÔÌÍOCÆ^¼õÊ^ƒŒƒ¾xµ[7†Œ‹þ=2Þ9<ZÇ;ß œÚv¹[ðL·þÄíTÇB‡ëg¿Dîé§×]‘>ƒÔíuiº) Ýk±q÷†aF‡øòÑ9­æztë 7‹2ÁYövûšÕ¸z,·p=\†ÜÓÇÕ]‘>„ÔíÝiºéY:iã 뮈a†Ñû\ˆ|tNI+hü•nêTtwR<8ËNnß••ÖÈ»Ïëâ»È=}B݉à­HÝþµîŠ$`‘ÑËa‚¨a†Ñ1.E>:/¬»"]„uЫåfä<´À¾³Ü¾ãi £KørOŸ”V°NGêöź+ƒEF/ÏIH»îІa½ÏåÈGÇR'uQF ‚“'ýË‘Žà$0ÌMÀÏ]7篭›@\ÉÓÖUEøØyÏU¶=ªþoç‘íš¶‹+ÜùŽéÐùòð"¤nÿ“V°&,uQyŽCÚ8Kp¾ÆcQs Ã0šÍ 7ß’cëÈWKÙŽüDÄTôXiukÚI+š‚EÓàÌO,ðÄ*r’ç:‹H·q.“ïßn£iíuM_¼½Cç×ÿ>Ò¡óåaµ›/ªµñXdtÃ0 Ãè".A>ž/αϭÀ&d¼ÐéÀçT_µŽUèlº z?’C'ådÄBPUÝòÕBù+wž# žçn·ÿžËoA:ÔûÇl¾†­ÚˆtNwñ¶ïŒCQíw;’ûu=’æä»ˆ•å#9ΣÇù’Û~'­ãÑ’”·‹½)\ÇÓ‘hÅÛ×Äÿ$蜧w_Dù4¼?æÿû¿ÓþgÙöÈ{¾yÀ+×Z#.gUè,B\?×»é"`a¨|Ò5Lâ·ÿ>ikà@¤n¬»" Xdôr‹´ñ뮈a†Ñû‰škùfuä£êw¬²uËzÍ/u×& ?sû•V0†¼®½W´ùƒHÛù¸oºmÿŽt*ïuuT~ÖáŒh IDATè¶ÿ3A°¯ð5ž@R6lôŽu7´D–´óèqþ‹ Zé¼í'Óª„ïžü:>±ú½ æ+£ÒÎû#·îCˆ53íþMûŸeÛ#ïù¾î¶ x•;Ÿ’tMýs|Ç­û¨›&Å/Ÿt “XãÊ7ÑJ¿©ÛÃuW$‹Œ^œ#‘6þUÝ1 Ã0zŸG>:oʱuä›Õ‘ªkܱÊÖ-k]>Kv—Ѳé*òZT‡€"Ñ0·»}¿æm_Ïä빃À»xÀ-}ïuË·¹ßóAe‚ ’gÚy&€GËÊ4÷ûæÐ Ÿ[×ݸ(ê.w:¢LòÏýžŒçÝ€<3 bþ¿ÿ;í–m¼ç[XÈâ†Å]SÝÁ=€[^*ŸÖvQLqe·g([3úm®»"m¦ŸÒ™ j†atŒ ÷æØÇ:òÍêÈGÕ7îXeë–¥.’/FDYA4oÀ­Þònß¼uëkîó ®ç1Èõ¹F÷¹å¸ûWßB Ðh¨‡dޏâžì|ŒÖo^Öód¨K–ÿ™V_Ÿ2uËR—¤·¯vúò܃>z¯d vt'b‰¿ 6‚[V¾¤д »"i žêêúsDÈùåÆñ¥' eHÎÉ凜õ™@Ú}iË<üGö!äÞO#í–m¼çû>!õBdÜ“Ï%Ë5½‰xúa÷{9â®[–&*RV!Bèb体Ʊˆ×ÅÝÈØÊ)À+ÛV»j˜ë-Ÿ‹ÜŸöÖ½œV÷Þ²|y—œƒ´ýÓhõ2ªú|Iè3•GÑ`†a…8é”~.Ç>÷#ãg^‰$6[D¿æÖ]ü=âb{ ÁÇ]]s?@¼kn‹RÚy²X%6 –ÍRÊ…×­A>دErÁå±(åuÍÍû?“ê]¶neê‡Þ§¤Œá nÿ3Ò :¾܃D­\t’—yÛç!®Å+aý~·Z}÷FÜl@´æÌzÿ¦'Ëýû ÷?þ{w$Ç]ßyü-­VZiŸe=Y’%ÛcÌa nüü^ƒÕ¢ŸÅúl×;¾Þ×Ùê÷£Ñ÷óÁŠN‡÷L·’Èó™ú`Eóaù •#É6[#ú´pì?Õ;°@wbe|RÎãoÃnL]í‹orµ:ðS½çË0xU½q²~žê}&~üG°ÏÿÕäóx²[‰ˆˆ´O^ýÉ^£ ùÞºog­W¶VÊRM«Aô½áõ¿ÞäëõD¬¬ÏÂjIVIjÆDZñÓØÏS/XúwXó.¶@à`Í£DDDÚà ì®êázŠtÈ"ö3ØlóÖ24gÉ£Ûý›ñ»XiÅò³Øô$ç± ûùè¹V~jµ}/ ^UoÜ…V>ƒ·†Ç%Ÿ¶«[£ôŠˆÈ%Lÿt¤H£ØÏßl½køYÖÖ!RF­ÜÕ ¿•ñ=ED:¢¯® Ô4WD¤·Ñ4WÄí ëã-œCMs¥_´Ú_ºô?£¿yŸÓ¾hš«ÁŠDDzß vQ1Nr‘!Ò ­öQéŒMX_?°&¥ ѺS¶…õ©¾G«DûÛÙ°ÞRh)ÚDATD¤÷Íbc(ˆöƒ­Xÿ¤­T†³E*…šŠ¶ÓØ3ض+¬Õ<ª6QiÖ pðè°\‹Í¥ûhl@µj-ûÒÁôÖ7ðÖ×ï pë{üWÛÑB ÓD¥4DEDzß46hÑ6ƒô†Í$¡ÒƒåöŒ}éívw‹YÆ.²OcMO`ÁñV“y<,'Âz*û4™|@Ó-”ϧ%Ø܈](ŸÃîìÏ’„„9’fgré®$ ˜6¯öSýZõ<6òè*ö»8­7…%¯),z0=Ö‡Ââ¿3e¨õ¾ƒ…–B:éö{1ˆýý/-Q‘Þç=/ÁFê›Â.6|­‹÷ÖlÄ.Ú&¨ ·E}_³M£Î`Áî •“ªûE´›Œ¶7¥Þoœ$Іc'± øz–°€zx$Ú>¶Obø'Iš>>’ã¼Õø×8\YË…pü<Pg±Àz °^»5MRƒ<Ö‹aÿ¹p\:ð® ݶ«EÜ–=Øß°½a}5Vã¹±Êë—±¹-ï ˽Àýaû0Õ[¤ƒé`»¡weXö…÷¾’ä÷çIUÎ7ƒÝ¤÷§²Rhÿ[ÀZHm¦äAt]Ñ‘º> ü|çç¨ ¦¾ÎÚ—^÷Ã$Ú±‹®‰Ô2™±o"ãØfå"I¨ô`y2c_z»ÝMj±f¯Û£åò°ÞmoÇšÚ6súzlŠ…fl"¹xÿ ö}ñ0AâpÝ ^s<ÊS+ð*äVÚ g¬7a?{;© ™»°Ð·‹ú•+Xí£L›÷bµ‘ݸѶ«}ÝOT¯ŒÇÍÊWè튜$- vÖ9VÊgö?d•>tVATD¤÷}›ìû;X˜ñ€åëνL¾ð k›Nú…},Ýw1kði쟨[Á.h³‚dVÀL/­Ú°DòµW §¢Ç¾¯“ƒ¢tÒ&ìbuWXoÇ.Xw†Ç¾íû×? |£…÷œ&ù^Öêçg ¨#X ë‘°=Ž çݶ'HjÁâÀÛ­Ë ¨EíÀ>³ôzö¹úzûŒÓëf]ÄBÑѰÁj×ë±f°K-¼G7\¼û[ü#àºb‹S“ßø9OósÿJïÇþžÎÐͯ{ùŽŽˆˆoùàϱ¶0V«­‡IjËÊj û§+åsmXßÛ†sõ{•þW–ŸáØ Q·aå~*½ió{“?“Ñ>_&¢ç}^_måVxsrq{¤EÍìæÃö|xìÏͰv·tÓûtK„të åvIÿûíX`¾",yxSkÕˆŠˆHWt²FT¤¿ˆZ­yT>e¹ˆ¿TxßÖñÔ2Yeÿx´,ã|ɾ0ö&Ày÷{á4<ÝT&c»SÊò3<‰}[±Ïx xbXÒI¦E‚ÊÁå<¸¥·×Q=H¶‹j=‡µ,ˆÃâ\(÷<É× á8ßž å]ˆÎÓ«}•·`áôr’Õm$ƒÈmÃ~wA5¢""Ò% ¢R¿ðÎ Íž«×/âË`€ÊѤãX-Ń”å ®é¥Ú¿ðoÇïC'ùÀQ[g†ík€G…åàÑa½ƒ$Ø´Ã IðóÐ8íóe:zÞ—™èõ½>Šr;-`ã;äãá×€÷£Qéc[Í£äR²‘¤9×>l¾Cß¾‚¤iž83E2ðŒ×.<|?,÷‘= ´'<ú|†×…íãm8g™mÆ‚ÂeѲڵ’¾¯•éTœÏ‹:“Z¦ªìŸ‰ögÕĬ'»F̧‰É»ß§‘IcíTNYýókeÄâjau8<M çî–¸áàö°¤ cŸ·´ºåƒû¤·W©$¥ó6…µjDED¤+f°0¼ ¦‹Xó%oŽä–Ò¶‘„Êýa;~¼‹|sç½ ?‡MKqðÏañó·D€ç„í·†e  ˧ø8Ö>¿äѰ>>YºŒÊP™˜[±&vñs­Ì}»BvXª§Rû;QãÔ ß+o6š5q­Çã©×c¿gYvu®ømã×÷õš£ž%ir+å QsED¤ëÂúuŽ[ é÷s6lû ç¢íE’;ÙÞßf6lŸ¥²ïMzziÜz’þ>éþ?ñãíÀ•Ôo.·D2èaàP´ýÉܳcØ4;ã$µ#XíÆcLJå*àÉaI{öó—®1ÍkøðcØÀ[ë±0¶7,µœ# ¦ñò\ÏDǦû¦§‰û¿ebmÜFí€y•’äµÊ~:ZNQ»VÒ÷i:•êV±Ï¯•‹«׫ߡórz­¹~VúFÍ‘®òùC/7“ M?mo¦rÞ³v÷×ò‹xqp‘µµ± ØEšoÇx‰¤IWZV¿¬¬ðÛ'¼ ZÜtÐGCô)6cŸ½7KœÇíÄú_yÐÜF¾LwŠ$T"éCä‘o¡¼äcX ½ ž¾=‚}­Í†P° ¦Ï`AôÃXèÖ|xw´øã=Ñö8’¯jáý«Y ™r%nzبÓT†ÊtÀ<ƒðø9Í}Û»¼¦9m;D›¹ñÐmCa]†Ð,Q‘®ò’¡Ï·6†]œøöfìÂ;ž‡Í'úÞL2'ÛPØÞ–Vú^u‚‡ÜX0Ü9,;ÿ:<\n!é{Ó ±¦¨>'Üq*çˆóÇ'ƒ©²vÃ,6ÕÃmѾíX™;šÎÂRˬÖtgXïš'ïÛѱé~‡ƒTö§Œû¿ù¹ãÇçX$OQ;`žF­.epËÿ–¥ÿJëÒÓ´@õÑž;AMsED¤«|¢“52óaÉsl#< N`ï›Y[ëµg¾â dPâ}ºÒ²oz^6Âû§k²Z Ë>MAÜÌÓkl½FÖûçú±ñÔÓá¸ãTÏS´g”nò¯©•þŒ­ZÀæ1mÇ\¦iñ͇xz ‘,ç°æÞCØMŽåb‹S•šå®ÿ/š ñyMã¹QóÊš¢ÈÿW\Àþ?Ǻœkÿ¿q4¬³n$¨FTDDºÊ›Ù9ˆ7Á=SïÀ.óÁ.céOo²êá2«õR·„}&›°‹ïVšùõbSA=X$¯Yìïñ½18S–øïáõØ`e -Þ‚'^&£í8 fÌI²ÿG´"«+IÖhÏdßõÿç׿x¯)¬FTžS¨ˆˆt…O}q¢ÐRô¦¬æP­ T"•æ° :Fk!RM¥ÌaAb”Þ ¢~F°‘°ÁÊýJ¤¶‡u;ÿnúß ˆãT†Çx:žtÐŒ_Ó®þ¸ñà|Ó4>¯i<7j^Y-€|žSØÅ‰Ï?žÇ.FæéÝþ[eãwÝ[½ðŽØž ü$>ŸÑ†óOSN„ǾOµäÒ~Ãc¼æQÅòðx?6ò5ØÈë»±¹Qwc]P½›£x„Êþà­ôµ?O}Ú¡8<Î’LI”šñk.õÀN…Åk¶wïÁšëªFTDDºbGX7rÔïR’„Ç ’9$‡± ßÉx<=îå€,KTSªÇ©œoó`ØîÖˆ‡eÓ® 7„õ[÷²¶Æä>àaXçÃ{{-Ívc6Ïêþ°öÅçz|B•÷Ÿ!ù^ ¬ÇHÑà.R–ÚË=LÆýŸÏ„åî¯K÷­#™¿ìR|ÓÑÿÇxÍh*Õ¿3Ö÷ZŠ6Ré}Þ4÷µØ|ŠYµ’#Ô¯¥lÕ,I¨ó ޳dO5RkÒ¬Ñcµj¯&Y¦ý±‡goι•dÖz¦Hæê<m?–c¬&¦ßùÏÔ®Á®'žô ìûÿ=àÑr¬…óƒ}ßãpzUxìû&±Z-¨‚ýŸ eñ©t|dKùØ9Eï_loÇÓƒE¤ †Išp’L]5¶ÃsÞRÃ/ÏùôV>ÝÕ ö3ãS¥;ëåQ“¶ I߈Q+‚ÞãubñB(ˆŠˆô>¯ }ZXòXemm`üØ›µžM=ï}°\t3 'c‹_ËyüF*ƒ©ÕXÚÖW†m¢VP9Ž‘‡± `Ãì Û>ªa¯MÓ²»™±5µöímUžŠÎ‘7Ðg9¼«Íü 6Wi»ûtN‡å®*Ï“|¯¯" ¬»Hú`„ç÷å|ÏìçÁ§è9‡ýžøœ‚~3ÆûÖyí®7÷>xÞqŠÊ©€ZÕ‰iœåá.ƒép‡AŸë5+ z° ûÆY;7l§Íß¾ØÅ÷lT_Mï!T#*""]÷ì¢b•µáp6zì#ü¥ÿ¦¥øvñ9D¾š¨%’æhyL’„+£í½añÀ²“Úau™¤ùïq’ÚÔtx"{Èÿtó¸¬þ´^û“7`6[›¹ˆõK¾øT“çpooñõ­šÁBjµ  t¶cßëíaÙ}Ï·cŸ§f¹džÁÇd¬Eé0{ûö9lÏb¿ ÕNI7©¬Æ›Zæ‘n¶™V¯x»yÈŸÅ~gH>3þÓa=Gò9úƒi’ÏúlØž!¹a¨§—5[#*½O5¢""Òu Ào]ˆ‚-`Éyƒh£¦ÂR+¨ìÄ‚È^lØý=ØÀ»Ã¶ÅïáµW¬`ütXÎd¬O¥ö¦<Þí⣌Îyü8öó° ª^[ç7¼¨A¯ô› æ|šŠI’0¸Ö‹éép‡Aws$AÑ÷`™=XN…µúöVòAâªuòòV*EŒÜ ¢""Rg± ôŠ›ªãxXj…ÕA’æ¿J³Âë$I­W,=âoVZ¿¨Ï0§ó‰ÒŸgðG8w:ÌzTož:Œµ<¤^3X7çÏ£ÞÈÔílb,ñ°Òʼ¿yÄ#£ûMoF öó»Ê–~&®1÷q¼ß´Y‡5ç†äg’›4qmÿzìçr&Z|ÞxäÞôó3ØM2ŒÒû-àg°‘Æ¿TpYÚBATDDÊÀ/zkU¼e’ŽDšu #ÍñA•^…õgõÀ×L¸›ÇÇ}q½¿c7ûšO-“7Ì>ŒuÅèÆ`v·‡õ3»ð^]¡ *""eð0I͈Ô÷MàéØ@_w\é¯Õ>ØÁ÷‰[tx yÜ‚Ã[nĵãÞä:®}÷þºÜ|‰ûäN‡ããš}¯½ŒGg_ÅþGŒGËÉÔNþ8ýü8Â}º³Q³±±’4ï?L2ñaêµkI³;¬÷†rø|¯{°V6`ó0Ì[Z ¢""R}sX¤Kü"µ›£ÊJñ>…˜kh,ðe…;Ù9î‹;GïöQœÆ„kÖ(ÙaµV˜ÝK w?Q£lNa7U÷`ƒâí&ßÈäËØ÷ë÷éƒ ¢""""ýÈk†k%ýèŠ.@IÍ…¥Ñ0» àJ’×÷‘Ìc¼$ÌÞPåçÂûÁëQ’f¿GÂöqz¿kCDEDDDú÷YÛXh)DúßE’9¥¿Yå˜ ’pº«Ñ| ˜G(~ÎßB(ˆŠˆˆˆôï“–ž«VDºÏ<ú~Ñé% ¢""R>ãy4o H>ßîPÍ£DD Ò«Ã/‹ˆˆÄþ6'æK‹.ˆHIøÀ4iZDDDD¤IÄF€|CÑ) õ ‘ž¦Q)ïï6^h)DÊc©þ!""ÅQ‘2XëÁBK!"""m¡ *""eà¬o)´"""Ò ¢""R a=\h)DÊg]ÑÉ¢ *""e0Laó°‰H}¿€ ðõ±¢ """""""—†cAô¯‹.ˆˆHÕˆŠˆˆˆôŸóa­i\D¤')ˆŠˆˆˆôŸ™°+´""U(ˆŠˆˆˆôŸ•°ÞTh)DDªP‘2&=ED¤$&Ãú\¡¥©BATDDÊàzà pkÑ)‰I,„þKÑ)«k±@ï-º "%²-º""YT#*""ep!¬ÕßM$¿‹À\Ñ…É¢ *""eàÿ¯– -…ˆˆˆ´…‚¨ˆˆ”ÁÖ°>Sh)DDD¤-DED¤ <ˆNZ ‘r®@×y"ÒÃ6]‘6]é9À>àà갾؋õ< œëãÀ1à‘°ïDåí†çŸn^XpYDD2)ˆŠˆHÜ–~u°›nc1ì›ÆF ®·¯_k‰ÇÃ2ßž$ ›WWaŸ]3–±0úPcõ ð¾Ê^´G…õÁ" !"R‹‚¨ˆˆH±þð‘6œ'+œfí[ ûóî[Åp£û6c¡1‘YÁ2ý¼¿f]ί{8Ü<Ö÷c5è“À.`'°ØÖ;Ãþ­Àž°¤þ$gzÑû -…ˆH ¢"""ÅúbX«ÙÂBØDØ¿›¶¦Þ¾¡°€°²› ËtX|{Š$l>ˆ…ÐfGSÞ„…Ó=a}9ðn¬åÕ-”½hD,´""5(ˆŠˆˆg8¬€×¶x®Zu=VÛØÈ¾a,çÝ7ŒEç[ÀBc"³‚eúyÍjÓŸD~ç±~ÇqßãàÏI>Ã2ÚÖýÚl[Dú€‚¨ˆˆ”Á>¬?ßIàBÁei§Ñ°žoùšæ¦ÞÊ\«<Ö…–BD¤ ë-""eð)l™§]6 ëvQi~¢'±f¹ýtÓFDúŒjDED¤ v†u¿Õôy«y”tS?Ñ]‘zT#*""eà}ÞNZŠöó¦¹ ¢½£‚¨ˆHÏS‘^7†Õúà7ýÄ÷™-´óZ÷­…–BD¤Ï)ˆŠˆH¯ó9.´¡ÑÞ3‹œ;†ûéQéuDZŠÎh稹Ò>eožëó –y és ¢""Òë÷]Ð`E½©ìAô¿¯/º ""ÕhÔ\éu·†¥©F´7•=ˆ.‡õ¦BK!"RƒjDEDDŠ£yD{SÙƒèBXZ ‘DEDDŠãAT£æš·]€àBX—µå˜ÑáBK!"Rƒ‚¨ˆˆHqÆÃZAÔôJÝÖ‹…–¢yÞ4·¬AZD. ¢""Òë¶×»Š.HÙGtö¹úà&àvàûÀÀS£ã¸¸ øƒ°oxð]à;ÀUÑñ;€¯ÿÎõäÔs·÷„÷ú7Àïað{À_‡ã—Zw‹×$žÍy|Ög™þžNGÛYŸe­Ï«ÑÏÁolŒ×CÒgÒ›/¯ Û¯nŽŽÿ$É×õxàŽè¹O¿¶×asvÂÚïÉíXHý‡°î–»±ïKžïgµÏ²ÖÏWÖgYëójôsx6jîûs+"""""~ ÿ³É×÷bPpw`_ÛÓxM;ü ð<౩>Ücáøn¬f2žVæKÀW ‚³l ÛCÀ©èø“á¾<=wš¤68–þžìÁnÜ|ø™º_Y{¾/ûr[í³¬õó•õYÖú¼ŠúDD:F}DD¤×ù|³Í ¿¼x;6 ÌÿÎñšÛ€¿Àjî>öý[àºè˜8H½øD~ y¦œ ëó9Žm§Ûgß~›æã<ð1àUØ×ÃitH yIDAT?„r÷B,ˆ¿xux}-ëù2ž[ÍYÎÇ‘ôwœ¤2œu’oóÌïZí³«í]Å®·âk®¬Ï²ÖçUÔç """"rÉú9ìbþ‹M¾þÀ»ß¾L2·â<ÀBB\ƒ5<øð·aßi’Ú¿´çbýoÄj¯ÍY¶[°¯í9o§×aM_¼9ì;ìÛo¢r°¯œÄjïÏ¿:lß|!:þÓÀoDm’즹gH ø:Ö$úïú[–°ïË`½ƒ×a_ë›I>Ë!©é~õ?ËZŸ××)æs¹dý þ¡…sd…®v…çbµ¢…÷a_Ûjà5í´%µ¾ k–zð;T6'½ ë3{7ðKaß<Ö‡ö»X¿Ùô`E> Ñ=À;SÏ}%ì¿ ûƒÝ,¸d°¢tùºaö=9—óøÝXmû!à°7ìð ù?ËZŸWŸƒˆˆˆˆÈ%í‰X0¸«Åó¤/æÛ®Æšc ûã@V˯ÒZÿ×¢1Úo§mž'§k³ëúy¬ÉìjX|ࢠ`?“{ë("""""Ù®~@óMs»á©X8õ@²üà%ÔnÞù¼p|žþ¤½¨ƒè>ì{r(ã¹Ë±¹N|¯€OïVsØOõ¯ADDDDDúÌ3€báăÊq¬ùê5Ç?*s—Ê'õ=ž¤v¬öó¹ÀgIúŽ®bÓø¼‰Ê)ozŬŒGŠ.ˆˆˆˆˆˆtÏ8Öìö.’àrø;àe$&mÀÂÍ2ùÆiÆS°Ñ]—€³¡\ÏèàûuÒï¿ÒÁó?û~Ýüv“ ®ýü Ú÷ÙmÀšÑ¶Û8VÞézŠˆe]ýCDDD.£XØXiã9$éHæ$=ü%ðçØH¨—cƒÞ<ÒÆ÷݃վ¾ ˜Â¦ ¹ëóZ6«Ø×ó¯:tþ÷b[­’\'ýø3௨œÒì{úsØÈ¿cáñ–°^ÆnHÄöbµ­ÃÀÆhÿtXÒý‹‡±Á›¦±›3Àlj™ylTèØ ðbldâ4UŸˆˆˆˆHKö7OÆ„y66]ÊKI^ìXxø6âíW±Ð÷m`WÆñ‡Ij¾–±ÀqÅ5kЗwü7¬æìÀk±9"³Ê3¼øNô>þ^«ÀUÿÒ[¶„ ¾ó˜*ÏO`!k â·`ÁØ]Œ;ÕH®b9¹Uìsº HÿÈgøÃÞÇÏó‘ðüØ÷;~>^:á\ê=° zð3Žß™Q._Ne?™:æb´Õ§sÕÏÿPÆñ{¢ç»=?­ˆˆˆˆH_Ù‡ÕV» ß—ñšƒ5ŽÏÕö’†ÒÇgчjœÆñ_Á¦y¹ [é×<·Ê×Þ½ÏàX rŸϽ›ßó6ûrxþ¿';ˆ®b5® ѹ`ŸçÖœïãçùSìFÂ*ð¥èù—‘°—5ò4àQýûš5¥Ëì3ùÖø··¿Œ V•¶ûì7¥öOb7+Ò†°Ÿ_ ç}#vóãMÇoÃöú*ðÓÙ_¢ˆHñÔ4WDDÊà§°ÚÍ%¬–gkž8ƒ7³v`–—aM%ýØépì 6‡è"ÕmÄšDŽbAàÖ6×½ ›ÒeS86nšù‹çŸ ϧ]ÄÂÉ«±Ž:a GÏǦÃY| xex~޵µ¸«Ø×¶Œõ+Âù$V[7]Åj÷cµ‡×a_뇱õ8ì3¯÷>>¸ÓåXßÉelP RÇw²iî€bxxIÿݣṄm‘žôhàÝXóšµÓÀc5¹öýfË÷E¼.¼ßѾY,¬?kòülà9$ý ÏbA’¦¥éQ|7I³ÙO†mõÞ§Öykík§ÿÞã]áñÖZ´„5=~êED¤Aúã)""Ò~ƒÀÏcÍ#„ÕFŽc}ToÄú¨¾ wÇÂk²ú­¶ËX³ÑWb'Õ*º›±È×`}6Ÿ† Ú3žÿ:6ŸëïaMW›Uï}òXÀjL_ÔB9jñ[Ÿ#õJ¯%™Æ…ðþ·bõm¡L""""""]·ø¯X3átíçµU^óÊpÜ_v°\…õ™]ÆšÇ~‰Êà4Š5}>ÕX>^ãMR¯¾‡ÕŒ¾›dz—·F´Þûä©}_ø:ŽÑÞ÷Í5ŽÙ¼•¤_±×’~–Î5©ð¯/`áʃɷ€ÿl®óÚç„ãÿ¦“lÑ—ÏÂúHÆMWûÍŸa_ß/ç8v=VKú9,ˆ^®ï\ÑDDDDDD¯ÂšáÎa#¾>±×Þ€Ÿ»:P®vyÖŒx«Íü5§-øå ¾îràí/ŽˆˆˆˆˆHu“dŠ[Ï™5‡¤tß-Ø÷ãEDD¤Ÿm¨ˆˆˆˆä0Ut¤-Òƒ‰ˆHhÔ\‘b†õÙBK!NATD¤ DEDDŠåÿ‹/Z q ¢""]  *""R¬ñ°ž+´âDEDº@ATDD¤XëÂúb¡¥§ *"Ò¬HDD¤X>ÒîL¡¥è ëÝÀ•Àþ° ³Øç3‡Äy`:<ö}íªQöïÇl›Î'""DEDDŠ5ÖW7b}Eç€óÀ°œÃÂÖ2Vº_Ì¶Ø ìÀ¦Í+Ãzö ¶pþi’P:…×ÙÔ¾)’0;Grç±ZéuØÀQ«-”CDDêP)–7½øt¯›Â‚Ó PçIÂëB؞Âí ^§ÃzXÂ×9,ìž ûfSÇÖª©õ`¹ ضýñ6`gêq½yx8ˆÍ«z(”m›§u4l`5—ãѾa`",­ÒÀQ""¦ *""R¬-a}?ð]`#ª6Ca{#¼° µ f—u¡ŒéZÚÍXðlô:â p8J4†õaà!,T7cöÙŒ’„ÓѰo$Ú7Ž}–ñ¾É°žÀÂóB“e‘œDEDDŠåµ„_~%çkÖcj¦6avKØ çÇNb!v,ìÁBîf’ ;Î7^3Iðy°<œ Ûþøp<õ¸Ù™Ç*VK<ÕÂ9ž|+³ˆˆt‚¨ˆˆH±6†u#!í"Ià:ÙÞâdJ×Ò.†÷í·&¬£a­©tDD:LATDD¤X›Ãº—›ƒ.…¥•ÚÆ2Ј¹""]¢yDEDDŠ5Öç -…€æéQ‘bm ës…–B@ATD¤kDEDDŠå£æ*ˆOATD¤KÔGTDD¤X>j®šævÆflŠ›ËHæ;j†dJš$7z¹¿®ˆH_P)–×Â=•dŠ“y’€´¶çIÂÓÅî³'L` mÇBåeKú¹-™gZë<ð£°=Xë@iݺ¢ ""r‰û8ðò_³ L+دKÀY¬yïbØ^¦!¹޽ÖÂ~Íbx¿f6œ×Ï?Ó@¹|z—I’¹MÇÂö(º7aó›ú±aßpêØá°=ÛŒEàtX|¾SHæYõr^O2‚ñCÀ¾&ßODDrP¨ˆˆH±ÆÃú«Xø$ l[²)ìóð´ {`µæáÕki±:Be¸ì´i,(ŸÄBåéŒ%ý\Þf¶ãÀ;7?lk©EDd Q‘Þð~àË9]‡Õ`µˆ±Ä!¬Vo8ìÅþ×O`áu"<^ãµþš±p^?ÿxxßZ¼vuŠ$´Î†í9’&Ç3ѱÓaßÙÔ±gÃöttl'ÍŸÆ‚è¦:ÇŠˆH‹DEDDŠå5‰s ¼f {`µæáÕki7c!užÊpYvþYv£–YDä’¦ *""R,ŸJ­— º@|ûÙ™°ÞZh)DD.šGTDD¤X£aÝ5Šeça;O3di‚¨ˆˆH±º—kD/ÞWÕGñ‘Q)–Ï#ÚHQéõéõ)Ö`X¯¤ö¯vc#Ùº¸É¨äF±Ñt}äÙl ¡e’ùCgÛVêþuØõ=TpYDDú–‚¨ˆˆH± hover + grabbed) - key is pressed - key is released - modifier is pressed (e.g. may cause mouse pointer to change, giving a hit about what a grab event will do. There is a lot of behaviour possible and it can depend on the kind of diagrams that are created what has to be done. To organize the event sequences and keep some order in what the user is doing Tools are used. Tools define what has to happen (find a handle nearly the mouse cursor, move an item). Gaphas contains a set of default tools. Each tool is ment to deal with a special part of the view. A list of responsibilities is also defined here: :HoverTool: First thing a user wants to know is if the mouse cursor is over an item. The ``HoverTool`` makes that explicit. - Find a handle or item, if found, mark it as the ``hovered_item`` :HandleTool and ConnectHandleTool: Handles are an important means to interact with items. They are used to resize element, move lines and (in case of ``ConnectHandleTool``) establish connections between items. Handles are rendered on top of items so it makes sense to deal with them before you deal with items. - On click: find a handle, if found become the grabbed tool and set focus on the selected item. Deselected current selection based on modifier keys. - On motion: move the handle - On release: release grab and release the handle :ItemTool: Items are the elements that are actually providing any (visual) meaning to the diagram. ItemTool deals with moving them around. The tool makes sure the right subset of selected elements are moved (e.g. you don't want to move a nested item if its parent item is already moved, this gives funny visual effects) - On click: find an item, if found become the grabbed tool and set the item as focused. Some extra behaviour regarding multiple select is also done here. - On motion: move the selected items (only the ones that have no selected parent items) - On release: release grab and release item :RubberBandTool: If no handle or item is selected a rubberband selection is started. :PanTool and ZoomTool: Handy tools for moving the canvas around and zooming in and out. Convenience functionality, basically. :TextEditTool: An experimental tool for editing onscreen text. All tools mentioned above are linked in a ``ToolChain``. Only one tool can deal with a use event. There is one more tool, that has not been mentioned yet: :PlacementTool: A special tool to use for placing new items on the screen. As said, tools define *what* has to happen, they don't say how. Take for example finding a handle: on a normal element (a box or something) that would mean find the handle in one of the corners. On a line, however, that may also mean a not-yet existing handle in the middle of a line segment (there is a functionality that splits the line segment). The *how* is defined by so called aspects [#]_. Separating the *What* from the *How* ------------------------------------ The *what* is decided in a tool. Based on this the *how* logic can be applied to the item at hand. For example: if an item is clicked, it should be marked as the focused item. Same for dragging: if an item is dragged it should be updated based on the event information. It may even apply this to all other selected items. The how logic depends actually on the item it is applied to. Lines have different behaviours than boxes for example. In Gaphas this has been resolved by defining a generic methods. To put it simple: a generic method is a factory that returns a specific method (or instance of a class, as we do in gaphas) based on its parameters. The advantage is that more complex behaviour can be composed. Since the decision on what should happen is done in the tool, the aspect which is then used to work on the item ensures a certain behaviour is performed. .. image:: tools.png :width: 700 The diagram above shows the relation between tools and their aspects. Note that tools that delegate their behaviour to aspects have more than one aspects. The reason is that there are different concerns involved in defining what the tools should do. Typically ``ItemTool`` will be selecting the actual item and takes care of moving it around as well. ``HandleTool`` does similar things for handles. Big changes from Gaphas 0.4 tool include: * Tools can contain state and should be used for one view only. * Grabbing is done automatically for press-move-release event sequence. * The _What_ is separated from the _How_, leaving less tools and less overhead (like finding the item under the mouse pointer). .. [#] as opposed to versions < 0.5, where tools could be shared among multiple views. .. [#] not the AOP term. The term aspect is coming from a paper by Dick Riehe: The Tools and Materials metaphore . gaphas-0.7.2/doc/undo.txt000066400000000000000000000263231175456763600152700ustar00rootroot00000000000000Undo - implementing basic undo behaviour with Gaphas #################################################### This document describes a basic undo system and tests Gaphas' classes with this system. This document contains a set of test cases that is used to prove that it really works. See state.txt about how state is recorded. .. contents:: For this to work, some boilerplate has to be configured: >>> from gaphas import state >>> state.observers.clear() >>> state.subscribers.clear() >>> undo_list = [] >>> redo_list = [] >>> def undo_handler(event): ... undo_list.append(event) >>> state.observers.add(state.revert_handler) >>> state.subscribers.add(undo_handler) This simple undo function will revert all states collected in the undo_list: >>> def undo(): ... apply_me = list(undo_list) ... del undo_list[:] ... apply_me.reverse() ... for e in apply_me: ... state.saveapply(*e) ... redo_list[:] = undo_list[:] ... del undo_list[:] Undo functionality tests ======================== The following sections contain most of the basis unit tests for undo management. tree.py: Tree ------------- Tree has no observed methods. matrix.py: Matrix ----------------- Matrix is used by Item classes. >>> from gaphas.matrix import Matrix >>> m = Matrix() >>> m Matrix(1, 0, 0, 1, 0, 0) translate(tx, ty): >>> m.translate(12, 16) >>> m Matrix(1, 0, 0, 1, 12, 16) >>> undo() >>> m Matrix(1, 0, 0, 1, 0, 0) scale(sx, sy): >>> m.scale(1.5, 1.5) >>> m Matrix(1.5, 0, 0, 1.5, 0, 0) >>> undo() >>> m Matrix(1, 0, 0, 1, 0, 0) rotate(radians): >>> def matrix_approx(m): ... a = [] ... for i in tuple(m): ... if -1e-10 < i < 1e-10: i=0 ... a.append(i) ... return tuple(a) >>> m.rotate(0.5) >>> m Matrix(0.877583, 0.479426, -0.479426, 0.877583, 0, 0) >>> undo() >>> matrix_approx(m) (1.0, 0, 0, 1.0, 0, 0) Okay, nearly, close enough IMHO... >>> m = Matrix() >>> m.translate(12, 10) >>> m.scale(1.5, 1.5) >>> m.rotate(0.5) >>> m Matrix(1.31637, 0.719138, -0.719138, 1.31637, 12, 10) >>> m.invert() >>> m Matrix(0.585055, -0.319617, 0.319617, 0.585055, -10.2168, -2.01515) >>> undo() >>> matrix_approx(m) (1.0, 0, 0, 1.0, 0, 0) Again, rotate does not result in an exact match, but it's close enough. >>> undo_list [] canvas.py: Canvas ----------------- >>> from gaphas import Canvas, Item >>> canvas = Canvas() >>> canvas.get_all_items() [] >>> item = Item() >>> canvas.add(item) The ``request_update()`` method is observed: >>> len(undo_list) 1 >>> canvas.request_update(item) >>> len(undo_list) 2 On the canvas only ``add()`` and ``remove()`` are monitored: >>> canvas.get_all_items() # doctest: +ELLIPSIS [] >>> item.canvas is canvas True >>> undo() >>> canvas.get_all_items() [] >>> item.canvas is None True >>> canvas.add(item) >>> del undo_list[:] >>> canvas.remove(item) >>> canvas.get_all_items() [] >>> undo() >>> canvas.get_all_items() # doctest: +ELLIPSIS [] >>> undo_list [] Parent-child relationships are restored as well: TODO! >>> child = Item() >>> canvas.add(child, parent=item) >>> child.canvas is canvas True >>> canvas.get_parent(child) is item True >>> canvas.get_all_items() # doctest: +ELLIPSIS [, ] >>> undo() >>> child.canvas is None True >>> canvas.get_all_items() # doctest: +ELLIPSIS [] >>> child in canvas.get_all_items() False Now redo the previous undo action: >>> undo_list[:] = redo_list[:] >>> undo() >>> child.canvas is canvas True >>> canvas.get_parent(child) is item True >>> canvas.get_all_items() # doctest: +ELLIPSIS [, ] Remove also works when items are removed recursively (an item and it's children): >>> child = Item() >>> canvas.add(child, parent=item) >>> canvas.get_all_items() # doctest: +ELLIPSIS [, ] >>> del undo_list[:] >>> canvas.remove(item) >>> canvas.get_all_items() [] >>> undo() >>> canvas.get_all_items() # doctest: +ELLIPSIS [, ] >>> canvas.get_children(item) # doctest: +ELLIPSIS [] As well as the reparent() method: >>> canvas = Canvas() >>> class NameItem(Item): ... def __init__(self, name): ... super(NameItem, self).__init__() ... self.name = name ... def __repr__(self): ... return '<%s>' % self.name >>> ni1 = NameItem('a') >>> canvas.add(ni1) >>> ni2 = NameItem('b') >>> canvas.add(ni2) >>> ni3 = NameItem('c') >>> canvas.add(ni3, parent=ni1) >>> ni4 = NameItem('d') >>> canvas.add(ni4, parent=ni3) >>> canvas.get_all_items() [, , , ] >>> del undo_list[:] >>> canvas.reparent(ni3, parent=ni2) >>> canvas.get_all_items() [, , , ] >>> len(undo_list) 1 >>> undo() >>> canvas.get_all_items() [, , , ] Redo should work too: >>> undo_list[:] = redo_list[:] >>> undo() >>> canvas.get_all_items() [, , , ] Undo/redo a connection: see gaphas/tests/test_undo.py connector.py: Handle -------------------- Changing the Handle's position is reversible: >>> from gaphas import Handle >>> handle = Handle() >>> handle.pos = 10, 12 >>> handle.pos >>> undo() >>> handle.pos As are all other properties: >>> handle.connectable, handle.movable, handle.visible (False, True, True) >>> handle.connectable = True >>> handle.movable = False >>> handle.visible = False >>> handle.connectable, handle.movable, handle.visible (True, False, False) And now undo the whole lot at once: >>> undo() >>> handle.connectable, handle.movable, handle.visible (False, True, True) item.py: Item ------------- The basic Item properties are canvas and matrix. Canvas has been tested before, while testing the Canvas class. The Matrix has been tested in section matrix.py: Matrix. item.py: Element ---------------- An element has ``min_height`` and ``min_width`` properties. >>> from gaphas import Element >>> e = Element() >>> e.min_height, e.min_width (10, 10) >>> e.min_height, e.min_width = 30, 40 >>> e.min_height, e.min_width (30, 40) >>> undo() >>> e.min_height, e.min_width (10, 10) >>> canvas = Canvas() >>> canvas.add(e) >>> undo() >>> e.canvas item.py: Line ------------- A line has the following properties: ``line_width``, ``fuzziness``, ``orthogonal`` and ``horizontal``. Each one of then is observed for changes: >>> from gaphas import Line >>> from gaphas.segment import Segment >>> l = Line() Let's first add a segment to the line, to test orthogonal lines as well. >>> segment = Segment(l, None) >>> _ = segment.split_segment(0) >>> l.line_width, l.fuzziness, l.orthogonal, l.horizontal (2, 0, False, False) Now change the properties: >>> l.line_width = 4 >>> l.fuzziness = 2 >>> l.orthogonal = True >>> l.horizontal = True >>> l.line_width, l.fuzziness, l.orthogonal, l.horizontal (4, 2, True, True) And undo the changes: >>> undo() >>> l.line_width, l.fuzziness, l.orthogonal, l.horizontal (2, 0, False, False) In addition to those properties, line segments can be split and merged. >>> l.handles()[1].pos = 10, 10 >>> l.handles() [, ] This is our basis for further testing. >>> del undo_list[:] >>> Segment(l, None).split_segment(0) # doctest: +ELLIPSIS ([], []) >>> l.handles() [, , ] The opposite operation is performed with the merge_segment() method: >>> undo() >>> l.handles() [, ] Also creation and removal of connected lines is recorded and can be undone: >>> canvas = Canvas() >>> def real_connect(hitem, handle, item): ... def real_disconnect(): ... pass ... canvas.connect_item(hitem, handle, item, port=None, constraint=None, callback=real_disconnect) >>> b0 = Item() >>> canvas.add(b0) >>> b1 = Item() >>> canvas.add(b1) >>> l = Line() >>> canvas.add(l) >>> real_connect(l, l.handles()[0], b0) >>> real_connect(l, l.handles()[1], b1) >>> canvas.get_connection(l.handles()[0]) # doctest: +ELLIPSIS Connection(item=) >>> canvas.get_connection(l.handles()[1]) # doctest: +ELLIPSIS Connection(item=) Clear already collected undo data: >>> del undo_list[:] Now remove the line from the canvas: >>> canvas.remove(l) The handles are disconnected: >>> l.canvas >>> canvas.get_connection(l.handles()[0]) >>> canvas.get_connection(l.handles()[1]) Undoing the remove() action should put everything back in place again: >>> undo() >>> l.canvas # doctest: +ELLIPSIS >>> canvas.get_connection(l.handles()[0]) # doctest: +ELLIPSIS Connection(item=) >>> canvas.get_connection(l.handles()[1]) # doctest: +ELLIPSIS Connection(item=) solver.py: Variable ------------------- Variable's strength and value properties are observed: >>> from gaphas.solver import Variable >>> v = Variable() >>> v.value = 10 >>> v.strength = 100 >>> v Variable(10, 100) >>> undo() >>> v Variable(0, 20) solver.py: Solver ----------------- Solvers ``add_constraint()`` and ``remove_constraint()`` are observed. >>> from gaphas.solver import Solver >>> from gaphas.constraint import EquationConstraint >>> s = Solver() >>> a, b = Variable(1.0), Variable(2.0) >>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=a, b=b)) EquationConstraint(, a=Variable(1, 20), b=Variable(2, 20)) >>> list(s.constraints_with_variable(a)) [EquationConstraint(, a=Variable(1, 20), b=Variable(2, 20))] >>> undo() >>> list(s.constraints_with_variable(a)) [] >>> undo_list[:] = redo_list[:] >>> undo() >>> list(s.constraints_with_variable(a)) [EquationConstraint(, a=Variable(1, 20), b=Variable(2, 20))] gaphas-0.7.2/epydoc.conf000066400000000000000000000005631175456763600151450ustar00rootroot00000000000000[epydoc] name: Documentation Index url: ./index.html modules: gaphas verbosity: 1 # Extraction docformat: restructuredtext parse: yes introspect: yes exclude: .*\.tests.* inheritance: listed private: no imports: no include-log: no # HTML output output: html #target: doc/api/ #css: doc/style/epydoc.css top: gaphas frames: no sourcecode: yes gaphas-0.7.2/ez_setup.py000077500000000000000000000240551175456763600152300ustar00rootroot00000000000000#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c11" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) gaphas-0.7.2/gaphas/000077500000000000000000000000001175456763600142525ustar00rootroot00000000000000gaphas-0.7.2/gaphas/__init__.py000066400000000000000000000013331175456763600163630ustar00rootroot00000000000000""" Gaphas ====== Gaphor's Canvas. This module contains the application independant parts of Gaphor's Canvas. It can and may be used by others under the terms of the GNU LGPL licence. Notes ===== In py-cairo 1.8.0 (or 1.8.1, or 1.8.2) the multiplication order has been reverted. This causes bugs in Gaphas. Also a new method ``multiply()`` has been introduced. This method is used in Gaphas instead of the multiplier (``*``). In both the ``Canvas`` and ``View`` class a workaround is provided in case an older version of py-cairo is used. """ __version__ = "$Revision$" # $HeadURL$ from canvas import Canvas from connector import Handle from item import Item, Line, Element from view import View, GtkView # vi:sw=4:et:ai gaphas-0.7.2/gaphas/aspect.py000066400000000000000000000205121175456763600161030ustar00rootroot00000000000000""" Defines aspects for Items. Aspects form intermediate items between tools and items. Note: This module uses Phillip J. Eby's simplegeneric module. This module transforms the generic class (used as fall-back) to a generic function. In order to inherit from this class you should inherit from Class.default. The simplegeneric module is dispatching opnly based on the first argument. For Gaphas that's enough. """ import gtk.gdk from simplegeneric import generic from gaphas.item import Item, Element class ItemFinder(object): """ Find an item on the canvas. """ def __init__(self, view): self.view = view def get_item_at_point(self, pos): item, handle = self.view.get_handle_at_point(pos) return item or self.view.get_item_at_point(pos) Finder = generic(ItemFinder) class ItemSelection(object): """ A role for items. When dealing with selection. Behaviour can be overridden by applying the @aspect decorator to a subclass. """ def __init__(self, item, view): self.item = item self.view = view def select(self): """ Set selection on the view. """ self.view.focused_item = self.item def unselect(self): self.view.focused_item = None self.view.unselect_item(self.item) Selection = generic(ItemSelection) class ItemInMotion(object): """ Aspect for dealing with motion on an item. In this case the item is moved. """ def __init__(self, item, view): self.item = item self.view = view self.last_x, self.last_y = None, None def start_move(self, pos): self.last_x, self.last_y = pos def move(self, pos): """ Move the item. x and y are in view coordinates. """ item = self.item view = self.view v2i = view.get_matrix_v2i(item) x, y = pos dx, dy = x - self.last_x, y - self.last_y dx, dy = v2i.transform_distance(dx, dy) self.last_x, self.last_y = x, y item.matrix.translate(dx, dy) item.canvas.request_matrix_update(item) def stop_move(self): pass InMotion = generic(ItemInMotion) class ItemHandleFinder(object): """ Deals with the task of finding handles. """ def __init__(self, item, view): self.item = item self.view = view def get_handle_at_point(self, pos): return self.view.get_handle_at_point(pos) HandleFinder = generic(ItemHandleFinder) class ItemHandleSelection(object): """ Deal with selection of the handle. """ def __init__(self, item, handle, view): self.item = item self.handle = handle self.view = view def select(self): pass def unselect(self): pass HandleSelection = generic(ItemHandleSelection) @HandleSelection.when_type(Element) class ElementHandleSelection(ItemHandleSelection): CURSORS = ( gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_CORNER), gtk.gdk.Cursor(gtk.gdk.TOP_RIGHT_CORNER), gtk.gdk.Cursor(gtk.gdk.BOTTOM_RIGHT_CORNER), gtk.gdk.Cursor(gtk.gdk.BOTTOM_LEFT_CORNER) ) def select(self): index = self.item.handles().index(self.handle) if index < 4: self.view.window.set_cursor(self.CURSORS[index]) def unselect(self): from view import DEFAULT_CURSOR cursor = gtk.gdk.Cursor(DEFAULT_CURSOR) self.view.window.set_cursor(cursor) class ItemHandleInMotion(object): """ Move a handle (role is applied to the handle) """ GLUE_DISTANCE = 10 def __init__(self, item, handle, view): self.item = item self.handle = handle self.view = view self.last_x, self.last_y = None, None def start_move(self, pos): self.last_x, self.last_y = pos canvas = self.item.canvas cinfo = canvas.get_connection(self.handle) if cinfo: canvas.solver.remove_constraint(cinfo.constraint) def move(self, pos): item = self.item handle = self.handle view = self.view v2i = view.get_matrix_v2i(item) x, y = v2i.transform_point(*pos) self.handle.pos = (x, y) sink = self.glue(pos) # do not request matrix update as matrix recalculation will be # performed due to item normalization if required item.request_update(matrix=False) return sink def stop_move(self): pass def glue(self, pos, distance=GLUE_DISTANCE): """ Glue to an item near a specific point. Returns a ConnectionSink or None. """ item = self.item handle = self.handle view = self.view if not handle.connectable: return None connectable, port, glue_pos = \ view.get_port_at_point(pos, distance=distance, exclude=(item,)) # check if item and found item can be connected on closest port if port is not None: assert connectable is not None connector = Connector(self.item, self.handle) sink = ConnectionSink(connectable, port) if connector.allow(sink): # transform coordinates from view space to the item space and # update position of item's handle v2i = view.get_matrix_v2i(item).transform_point handle.pos = v2i(*glue_pos) return sink return None HandleInMotion = generic(ItemHandleInMotion) class ItemConnector(object): GLUE_DISTANCE = 10 # Glue distance in view points def __init__(self, item, handle): self.item = item self.handle = handle def allow(self, sink): return True def glue(self, sink): """ Glue the Connector handle on the sink's port. """ handle = self.handle item = self.item matrix = item.canvas.get_matrix_i2i(item, sink.item) pos = matrix.transform_point(*handle.pos) gluepos, dist = sink.port.glue(pos) matrix.invert() handle.pos = matrix.transform_point(*gluepos) def connect(self, sink): """ Connect the handle to a sink (item, port). Note that connect() also takes care of disconnecting in case a handle is reattached to another element. """ cinfo = self.item.canvas.get_connection(self.handle) # Already connected? disconnect first. if cinfo: self.disconnect() if not self.allow(sink): return self.glue(sink) self.connect_handle(sink) def connect_handle(self, sink, callback=None): """ Create constraint between handle of a line and port of connectable item. :Parameters: sink Connectable item and port. callback Function to be called on disconnection. """ canvas = self.item.canvas handle = self.handle item = self.item constraint = sink.port.constraint(canvas, item, handle, sink.item) canvas.connect_item(item, handle, sink.item, sink.port, constraint, callback=callback) def disconnect(self): """ Disconnect the handle from the attached element. """ self.item.canvas.disconnect_item(self.item, self.handle) Connector = generic(ItemConnector) class ItemConnectionSink(object): """ This role should be applied to items that is connected to. """ def __init__(self, item, port): self.item = item self.port = port def find_port(self, pos): """ Glue to the closest item on the canvas. If the item can connect, it returns a port. """ port = None max_dist = 10e6 for p in self.item.ports(): pg, d = p.glue(pos) if d >= max_dist: continue port = p max_dist = d return port ConnectionSink = generic(ItemConnectionSink) ## ## Painter aspects ## class ItemPaintFocused(object): """ Paints on top of all items, just for the focused item and only when it's hovered (see gaphas.painter.FocusedItemPainter) """ def __init__(self, item, view): self.item = item self.view = view def paint(self, context): pass PaintFocused = generic(ItemPaintFocused) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/canvas.py000066400000000000000000000754141175456763600161120ustar00rootroot00000000000000""" A Canvas owns a set of Items and acts as a container for both the items and a constraint solver. Connections =========== Getting Connection Information ============================== To get connected item to a handle:: c = canvas.get_connection(handle) if c is not None: print c.connected print c.port print c.constraint To get all connected items (i.e. items on both sides of a line):: classes = (i.connected for i in canvas.get_connections(item=line)) To get connecting items (i.e. all lines connected to a class):: lines = (c.item for c in canvas.get_connections(connected=item)) """ __version__ = "$Revision$" # $HeadURL$ from collections import namedtuple import logging from cairo import Matrix from gaphas import tree from gaphas import solver from gaphas import table from gaphas.decorators import nonrecursive, async, PRIORITY_HIGH_IDLE from state import observed, reversible_method, reversible_pair # # Information about two connected items # # - item: connecting item # - handle: handle of connecting item (points connected item) # - connected: connected item # - port: port of connected item # - constraint: optional connection constraint # - callback: optional disconnection callback # Connection = namedtuple('Connection', 'item handle connected port constraint callback') class ConnectionError(Exception): """ Exception raised when there is an error when connecting an items with each other. """ class Context(object): """ Context used for updating and drawing items in a drawing canvas. >>> c=Context(one=1,two='two') >>> c.one 1 >>> c.two 'two' >>> try: c.one = 2 ... except: 'got exc' 'got exc' """ def __init__(self, **kwargs): self.__dict__.update(**kwargs) def __setattr__(self, key, value): raise AttributeError, 'context is not writable' class Canvas(object): """ Container class for items. """ def __init__(self): self._tree = tree.Tree() self._solver = solver.Solver() self._connections = table.Table(Connection, range(4)) self._dirty_items = set() self._dirty_matrix_items = set() self._dirty_index = False self._registered_views = set() solver = property(lambda s: s._solver) @observed def add(self, item, parent=None, index=None): """ Add an item to the canvas. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> len(c._tree.nodes) 1 >>> i._canvas is c True """ assert item not in self._tree.nodes, 'Adding already added node %s' % item self._tree.add(item, parent, index) self._dirty_index = True self.update_matrix(item, parent) item._set_canvas(self) self.request_update(item) @observed def _remove(self, item): """ Remove is done in a separate, @observed, method so the undo system can restore removed items in the right order. """ item._set_canvas(None) self._tree.remove(item) self._update_views(removed_items=(item,)) self._dirty_items.discard(item) self._dirty_matrix_items.discard(item) def remove(self, item): """ Remove item from the canvas. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> c.remove(i) >>> c._tree.nodes [] >>> i._canvas """ for child in reversed(self.get_children(item)): self.remove(child) self.remove_connections_to_item(item) self._remove(item) reversible_pair(add, _remove, bind1={'parent': lambda self, item: self.get_parent(item), 'index': lambda self, item: self._tree.get_siblings(item).index(item) }) @observed def reparent(self, item, parent, index=None): """ Set new parent for an item. """ self._tree.reparent(item, parent, index) self._dirty_index = True reversible_method(reparent, reverse=reparent, bind={'parent': lambda self, item: self.get_parent(item), 'index': lambda self, item: self._tree.get_siblings(item).index(item) }) def get_all_items(self): """ Get a list of all items. >>> c = Canvas() >>> c.get_all_items() [] >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> c.get_all_items() # doctest: +ELLIPSIS [] """ return self._tree.nodes def get_root_items(self): """ Return the root items of the canvas. >>> c = Canvas() >>> c.get_all_items() [] >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> c.get_root_items() # doctest: +ELLIPSIS [] """ return self._tree.get_children(None) def get_parent(self, item): """ See `tree.Tree.get_parent()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> c.get_parent(i) >>> c.get_parent(ii) # doctest: +ELLIPSIS """ return self._tree.get_parent(item) def get_ancestors(self, item): """ See `tree.Tree.get_ancestors()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> iii = item.Item() >>> c.add(iii, ii) >>> list(c.get_ancestors(i)) [] >>> list(c.get_ancestors(ii)) # doctest: +ELLIPSIS [] >>> list(c.get_ancestors(iii)) # doctest: +ELLIPSIS [, ] """ return self._tree.get_ancestors(item) def get_children(self, item): """ See `tree.Tree.get_children()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> iii = item.Item() >>> c.add(iii, ii) >>> list(c.get_children(iii)) [] >>> list(c.get_children(ii)) # doctest: +ELLIPSIS [] >>> list(c.get_children(i)) # doctest: +ELLIPSIS [] """ return self._tree.get_children(item) def get_all_children(self, item): """ See `tree.Tree.get_all_children()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> iii = item.Item() >>> c.add(iii, ii) >>> list(c.get_all_children(iii)) [] >>> list(c.get_all_children(ii)) # doctest: +ELLIPSIS [] >>> list(c.get_all_children(i)) # doctest: +ELLIPSIS [, ] """ return self._tree.get_all_children(item) @observed def connect_item(self, item, handle, connected, port, constraint=None, callback=None): """ Create a connection between two items. The connection is registered and the constraint is added to the constraint solver. The pair (item, handle) should be unique and not yet connected. The callback is invoked when the connection is broken. :Parameters: item Connecting item (i.e. a line). handle Handle of connecting item. connected Connected item (i.e. a box). port Port of connected item. constraint Constraint to keep the connection in place. callback Function to be called on disconnection. ConnectionError is raised in case handle is already registered on a connection. """ if self.get_connection(handle): raise ConnectionError('Handle %r of item %r is already connected' % (handle, item)) self._connections.insert(item, handle, connected, port, constraint, callback) if constraint: self._solver.add_constraint(constraint) def disconnect_item(self, item, handle=None): """ Disconnect the connections of an item. If handle is not None, only the connection for that handle is disconnected. """ # disconnect on canvas level for cinfo in list(self._connections.query(item=item, handle=handle)): self._disconnect_item(*cinfo) @observed def _disconnect_item(self, item, handle, connected, port, constraint, callback): """ Perform the real disconnect. """ # Same arguments as connect_item, makes reverser easy if constraint: self._solver.remove_constraint(constraint) if callback: callback() self._connections.delete(item, handle, connected, port, constraint, callback) reversible_pair(connect_item, _disconnect_item) def remove_connections_to_item(self, item): """ Remove all connections (handles connected to and constraints) for a specific item (to and from the item). This is some brute force cleanup (e.g. if constraints are referenced by items, those references are not cleaned up). """ disconnect_item = self._disconnect_item # remove connections from this item for cinfo in list(self._connections.query(item=item)): disconnect_item(*cinfo) # remove constraints to this item for cinfo in list(self._connections.query(connected=item)): disconnect_item(*cinfo) @observed def reconnect_item(self, item, handle, constraint=None): """ Update an existing connection. This is mainly useful to provide a new constraint or callback to the connection. ``item`` and ``handle`` are the keys to the to-be-updated connection. >>> c = Canvas() >>> from gaphas import item >>> i = item.Line() >>> c.add(i) >>> ii = item.Line() >>> c.add(ii, i) >>> iii = item.Line() >>> c.add(iii, ii) We need a few constraints, because that's what we're updating: >>> from gaphas.constraint import EqualsConstraint >>> cons1 = EqualsConstraint(i.handles()[0].pos.x, i.handles()[0].pos.x) >>> cons2 = EqualsConstraint(i.handles()[0].pos.y, i.handles()[0].pos.y) >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0], cons1) >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS Connection(item=>> c.get_connection(i.handles()[0]).constraint is cons1 True >>> cons1 in c.solver.constraints True >>> c.reconnect_item(i, i.handles()[0], cons2, lambda: 0) >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS Connection(item=>> c.get_connection(i.handles()[0]).constraint is cons2 True >>> cons1 in c.solver.constraints False >>> cons2 in c.solver.constraints True An exception is raised if no connection exists: >>> c.reconnect_item(ii, ii.handles()[0], cons2, lambda: 0) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: No data available for item ... """ # checks: cinfo = self.get_connection(handle) if not cinfo: raise ValueError, 'No data available for item "%s" and handle "%s"' % (item, handle) if cinfo.constraint: self._solver.remove_constraint(cinfo.constraint) self._connections.delete(item=cinfo.item, handle=cinfo.handle) self._connections.insert(item, handle, cinfo.connected, cinfo.port, constraint, cinfo.callback) if constraint: self._solver.add_constraint(constraint) reversible_method(reconnect_item, reverse=reconnect_item, bind={'constraint': lambda self, item, handle: self.get_connection(handle).constraint }) def get_connection(self, handle): """ Get connection information for specified handle. >>> c = Canvas() >>> from gaphas.item import Line >>> line = Line() >>> from gaphas import item >>> i = item.Line() >>> c.add(i) >>> ii = item.Line() >>> c.add(ii) >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0]) >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS Connection(item=>> c.get_connection(i.handles()[1]) # doctest: +ELLIPSIS >>> c.get_connection(ii.handles()[0]) # doctest: +ELLIPSIS """ try: return self._connections.query(handle=handle).next() except StopIteration, ex: return None def get_connections(self, item=None, handle=None, connected=None, port=None): """ Return an iterator of connection information. The list contains (item, handle). As a result an item may be in the list more than once (depending on the number of handles that are connected). If ``item`` is connected to itself it will also appear in the list. >>> c = Canvas() >>> from gaphas import item >>> i = item.Line() >>> c.add(i) >>> ii = item.Line() >>> c.add(ii) >>> iii = item.Line() >>> c.add (iii) >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0], None) >>> list(c.get_connections(item=i)) # doctest: +ELLIPSIS [Connection(item=>> list(c.get_connections(connected=i)) [] >>> list(c.get_connections(connected=ii)) # doctest: +ELLIPSIS [Connection(item=>> c.connect_item(ii, ii.handles()[0], iii, iii.ports()[0], None) >>> list(c.get_connections(item=ii)) # doctest: +ELLIPSIS [Connection(item=>> list(c.get_connections(connected=iii)) # doctest: +ELLIPSIS [Connection(item=>> c = Canvas() >>> from gaphas import item >>> i1 = item.Line() >>> c.add(i1) >>> i2 = item.Line() >>> c.add(i2) >>> i3 = item.Line() >>> c.add (i3) >>> c.update() # ensure items are indexed >>> i1._canvas_index 0 >>> s = c.sort([i2, i3, i1]) >>> s[0] is i1 and s[1] is i2 and s[2] is i3 True """ return self._tree.sort(items, index_key='_canvas_index', reverse=reverse) def get_matrix_i2c(self, item, calculate=False): """ Get the Item to Canvas matrix for ``item``. item: The item who's item-to-canvas transformation matrix should be found calculate: True will allow this function to actually calculate it, in stead of raising an `AttributeError` when no matrix is present yet. Note that out-of-date matrices are not recalculated. """ if item._matrix_i2c is None or calculate: self.update_matrix(item) return item._matrix_i2c def get_matrix_c2i(self, item, calculate=False): """ Get the Canvas to Item matrix for ``item``. See `get_matrix_i2c()`. """ if item._matrix_c2i is None or calculate: self.update_matrix(item) return item._matrix_c2i def get_matrix_i2i(self, from_item, to_item, calculate=False): i2c = self.get_matrix_i2c(from_item, calculate) c2i = self.get_matrix_c2i(to_item, calculate) try: return i2c.multiply(c2i) except AttributeError: # Fall back to old behaviour return i2c * c2i @observed def request_update(self, item, update=True, matrix=True): """ Set an update request for the item. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> ii = item.Item() >>> c.add(i) >>> c.add(ii, i) >>> len(c._dirty_items) 0 >>> c.update_now() >>> len(c._dirty_items) 0 """ if update: self._dirty_items.add(item) if matrix: self._dirty_matrix_items.add(item) self.update() reversible_method(request_update, reverse=request_update) def request_matrix_update(self, item): """ Schedule only the matrix to be updated. """ self.request_update(item, update=False, matrix=True) def require_update(self): """ Returns ``True`` or ``False`` depending on if an update is needed. >>> c=Canvas() >>> c.require_update() False >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> c.require_update() False Since we're not in a GTK+ mainloop, the update is not scheduled asynchronous. Therefore ``require_update()`` returns ``False``. """ return bool(self._dirty_items) @async(single=True, priority=PRIORITY_HIGH_IDLE) def update(self): """ Update the canvas, if called from within a gtk-mainloop, the update job is scheduled as idle job. """ self.update_now() def _pre_update_items(self, items, cr): context_map = dict() c = Context(cairo=cr) for item in items: item.pre_update(c) def _post_update_items(self, items, cr): c = Context(cairo=cr) for item in items: item.post_update(c) def _extend_dirty_items(self, dirty_items): # item's can be marked dirty due to external constraints solving if self._dirty_items: dirty_items.extend(self._dirty_items) self._dirty_items.clear() dirty_items = self.sort(set(dirty_items), reverse=True) @nonrecursive def update_now(self): """ Peform an update of the items that requested an update. """ if self._dirty_index: self.update_index() self._dirty_index = False sort = self.sort extend_dirty_items = self._extend_dirty_items # perform update requests for parents of dirty items dirty_items = self._dirty_items for item in set(dirty_items): dirty_items.update(self._tree.get_ancestors(item)) # order the dirty items, so they are updated bottom to top dirty_items = sort(self._dirty_items, reverse=True) self._dirty_items.clear() try: cr = self._obtain_cairo_context() # allow programmers to perform tricks and hacks before item # full update (only called for items that requested a full update) self._pre_update_items(dirty_items, cr) # recalculate matrices dirty_matrix_items = self.update_matrices(self._dirty_matrix_items) self._dirty_matrix_items.clear() self.update_constraints(dirty_matrix_items) # no matrix can change during constraint solving assert not self._dirty_matrix_items, 'No matrices may have been marked dirty (%s)' % (self._dirty_matrix_items,) # item's can be marked dirty due to external constraints solving extend_dirty_items(dirty_items) assert not self._dirty_items, 'No items may have been marked dirty (%s)' % (self._dirty_items,) # normalize items, which changed after constraint solving; # store those items, whose matrices changed normalized_items = self._normalize(dirty_items) # recalculate matrices of normalized items dirty_matrix_items.update(self.update_matrices(normalized_items)) # ensure constraints are still true after normalization self._solver.solve() # item's can be marked dirty due to normalization and solving extend_dirty_items(dirty_items) assert not self._dirty_items, 'No items may have been marked dirty (%s)' % (self._dirty_items,) self._post_update_items(dirty_items, cr) except Exception, e: logging.error('Error while updating canvas', exc_info=e) assert len(self._dirty_items) == 0 and len(self._dirty_matrix_items) == 0, \ 'dirty: %s; matrix: %s' % (self._dirty_items, self._dirty_matrix_items) self._update_views(dirty_items, dirty_matrix_items) def update_matrices(self, items): """ Recalculate matrices of the items. Items' children matrices are recalculated, too. Return items, which matrices were recalculated. """ changed = set() for item in items: parent = self._tree.get_parent(item) if parent is not None and parent in items: # item's matrix will be updated thanks to parent's matrix # update continue self.update_matrix(item, parent) changed.add(item) changed_children = self.update_matrices(set(self.get_children(item))) changed.update(changed_children) return changed def update_matrix(self, item, parent=None): """ Update matrices of an item. """ try: orig_matrix_i2c = Matrix(*item._matrix_i2c) except: orig_matrix_i2c = None item._matrix_i2c = Matrix(*item.matrix) if parent is not None: try: item._matrix_i2c = item._matrix_i2c.multiply(parent._matrix_i2c) except AttributeError: # Fall back to old behaviour item._matrix_i2c *= parent._matrix_i2c if orig_matrix_i2c is None or orig_matrix_i2c != item._matrix_i2c: # calculate c2i matrix and view matrices item._matrix_c2i = Matrix(*item._matrix_i2c) item._matrix_c2i.invert() def update_constraints(self, items): """ Update constraints. Also variables may be marked as dirty before the constraint solver kicks in. """ # request solving of external constraints associated with dirty items request_resolve = self._solver.request_resolve for item in items: for p in item._canvas_projections: request_resolve(p[0], projections_only=True) request_resolve(p[1], projections_only=True) # solve all constraints self._solver.solve() def _normalize(self, items): """ Update handle positions of items, so the first handle is always located at (0, 0). Return those items, which matrices changed due to first handle movement. For example having an item >>> from gaphas.item import Element >>> c = Canvas() >>> e = Element() >>> c.add(e) >>> e.min_width = e.min_height = 0 >>> c.update_now() >>> e.handles() [, , , ] and moving its first handle a bit >>> e.handles()[0].pos.x += 1 >>> map(float, e.handles()[0].pos) [1.0, 0.0] After normalization >>> c._normalize([e]) # doctest: +ELLIPSIS set([]) >>> e.handles() [, , , ] """ dirty_matrix_items = set() for item in items: if item.normalize(): dirty_matrix_items.add(item) return dirty_matrix_items def update_index(self): """ Provide each item in the canvas with an index attribute. This makes for fast searching of items. """ self._tree.index_nodes('_canvas_index') def register_view(self, view): """ Register a view on this canvas. This method is called when setting a canvas on a view and should not be called directly from user code. """ self._registered_views.add(view) def unregister_view(self, view): """ Unregister a view on this canvas. This method is called when setting a canvas on a view and should not be called directly from user code. """ self._registered_views.discard(view) def _update_views(self, dirty_items=(), dirty_matrix_items=(), removed_items=()): """ Send an update notification to all registered views. """ for v in self._registered_views: v.request_update(dirty_items, dirty_matrix_items, removed_items) def _obtain_cairo_context(self): """ Try to obtain a Cairo context. This is a not-so-clean way to solve issues like calculating the bounding box for a piece of text (for that you'll need a CairoContext). The Cairo context is created by a View registered as view on this canvas. By lack of registered views, a PNG image surface is created that is used to create a context. >>> c = Canvas() >>> c.update_now() """ for view in self._registered_views: try: return view.window.cairo_create() except AttributeError: pass else: import cairo surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) return cairo.Context(surface) def __getstate__(self): """ Persist canvas. Dirty item sets and views are not saved. """ d = dict(self.__dict__) for n in ('_dirty_items', '_dirty_matrix_items', '_dirty_index', '_registered_views'): try: del d[n] except KeyError: pass return d def __setstate__(self, state): """ Load persisted state. Before loading the state, the constructor is called. """ self.__dict__.update(state) self._dirty_items = set(self._tree.nodes) self._dirty_matrix_items = set(self._tree.nodes) self._dirty_index = True self._registered_views = set() #self.update() def project(self, item, *points): """ Project item's points into canvas coordinate system. If there is only one point returned than projected point is returned. If there are more than one points, then tuple of projected points is returned. """ def reg(cp): item._canvas_projections.add(cp) return cp if len(points) == 1: return reg(CanvasProjection(points[0], item)) elif len(points) > 1: return tuple(reg(CanvasProjection(p, item)) for p in points) else: raise AttributeError('There should be at least one point specified') class VariableProjection(solver.Projection): """ Project a single `solver.Variable` to another space/coordinate system. The value has been set in the "other" coordinate system. A callback is executed when the value changes. It's a simple Variable-like class, following the Projection protocol: >>> def notify_me(val): ... print 'new value', val >>> p = VariableProjection('var placeholder', 3.0, callback=notify_me) >>> p.value 3.0 >>> p.value = 6.5 new value 6.5 """ def __init__(self, var, value, callback): self._var = var self._value = value self._callback = callback def _set_value(self, value): self._value = value self._callback(value) value = property(lambda s: s._value, _set_value) def variable(self): return self._var class CanvasProjection(object): """ Project a point as Canvas coordinates. Although this is a projection, it behaves like a tuple with two Variables (Projections). >>> canvas = Canvas() >>> from gaphas.item import Element >>> a = Element() >>> canvas.add(a) >>> a.matrix.translate(30, 2) >>> canvas.request_matrix_update(a) >>> canvas.update_now() >>> canvas.get_matrix_i2c(a) cairo.Matrix(1, 0, 0, 1, 30, 2) >>> p = CanvasProjection(a.handles()[2].pos, a) >>> a.handles()[2].pos >>> p[0].value 40.0 >>> p[1].value 12.0 >>> p[0].value = 63 >>> p._point When the variables are retrieved, new values are calculated. """ def __init__(self, point, item): self._point = point self._item = item def _on_change_x(self, value): item = self._item self._px = value self._point.x.value, self._point.y.value = item.canvas.get_matrix_c2i(item).transform_point(value, self._py) item.canvas.request_update(item, matrix=False) def _on_change_y(self, value): item = self._item self._py = value self._point.x.value, self._point.y.value = item.canvas.get_matrix_c2i(item).transform_point(self._px, value) item.canvas.request_update(item, matrix=False) def _get_value(self): """ Return two delegating variables. Each variable should contain a value attribute with the real value. """ item = self._item x, y = self._point.x, self._point.y self._px, self._py = item.canvas.get_matrix_i2c(item).transform_point(x, y) return self._px, self._py pos = property(lambda self: map(VariableProjection, self._point, self._get_value(), (self._on_change_x, self._on_change_y))) def __getitem__(self, key): # Note: we can not use bound methods as callbacks, since that will # cause pickle to fail. return self.pos[key] def __iter__(self): return iter(self.pos) # Additional tests in @observed methods __test__ = { 'Canvas.add': Canvas.add, 'Canvas.remove': Canvas.remove, 'Canvas.request_update': Canvas.request_update, } # vim:sw=4:et:ai gaphas-0.7.2/gaphas/connector.py000066400000000000000000000152401175456763600166200ustar00rootroot00000000000000""" Basic connectors such as Ports and Handles. """ __version__ = "$Revision: 2341 $" # $HeadURL: https://svn.devjavu.com/gaphor/gaphas/trunk/gaphas/item.py $ from gaphas.solver import solvable, WEAK, NORMAL, STRONG, VERY_STRONG from gaphas.state import observed, reversible_property from gaphas.geometry import distance_line_point, distance_point_point from gaphas.constraint import LineConstraint, PositionConstraint def deprecated(e): return e class Position(object): """ A point constructed of two `Variable`'s. >>> vp = Position((3, 5)) >>> vp.x, vp.y (Variable(3, 20), Variable(5, 20)) >>> vp.pos (Variable(3, 20), Variable(5, 20)) >>> vp[0], vp[1] (Variable(3, 20), Variable(5, 20)) """ _x = solvable(varname='_v_x') _y = solvable(varname='_v_y') def __init__(self, pos, strength=NORMAL): self._x, self._y = pos self._x.strength = strength self._y.strength = strength # _x is a Variable, therefore observed def _set_x(self, x): self._x = x x = property(lambda s: s._x, _set_x) # _y is a Variable, therefore observed def _set_y(self, y): self._y = y y = property(lambda s: s._y, _set_y) @observed def _set_pos(self, pos): """ Set handle position (Item coordinates). """ self.x, self.y = pos pos = property(lambda s: (s.x, s.y), _set_pos) def __str__(self): return '<%s object on (%g, %g)>' % (self.__class__.__name__, float(self._x), float(self._y)) __repr__ = __str__ def __getitem__(self, index): """ Shorthand for returning the x(0) or y(1) component of the point. >>> h = Position((3, 5)) >>> h[0] Variable(3, 20) >>> h[1] Variable(5, 20) """ return (self.x, self.y)[index] class Handle(object): """ Handles are used to support modifications of Items. If the handle is connected to an item, the ``connected_to`` property should refer to the item. A ``disconnect`` handler should be provided that handles all disconnect behaviour (e.g. clean up constraints and ``connected_to``). Note for those of you that use the Pickle module to persist a canvas: The property ``disconnect`` should contain a callable object (with __call__() method), so the pickle handler can also pickle that. Pickle is not capable of pickling ``instancemethod`` or ``function`` objects. """ def __init__(self, pos=(0, 0), strength=NORMAL, connectable=False, movable=True): self._pos = Position(pos, strength) self._connectable = connectable self._movable = movable self._visible = True def _set_pos(self, pos): """ Shortcut for ``handle.pos.pos = pos`` >>> h = Handle((10, 10)) >>> h.pos >>> h.pos = (20, 15) >>> h.pos """ self._pos.pos = pos pos = property(lambda s: s._pos, _set_pos) def _set_x(self, x): """ Shortcut for ``handle.pos.x = x`` """ self._pos.x = x def _get_x(self): return self._pos.x x = property(deprecated(_get_x), deprecated(_set_x)) def _set_y(self, y): """ Shortcut for ``handle.pos.y = y`` """ self._pos.y = y def _get_y(self): return self._pos.y y = property(deprecated(_get_y), deprecated(_set_y)) @observed def _set_connectable(self, connectable): self._connectable = connectable connectable = reversible_property(lambda s: s._connectable, _set_connectable) @observed def _set_movable(self, movable): self._movable = movable movable = reversible_property(lambda s: s._movable, _set_movable) @observed def _set_visible(self, visible): self._visible = visible visible = reversible_property(lambda s: s._visible, _set_visible) def __str__(self): return '<%s object on (%g, %g)>' % (self.__class__.__name__, float(self._pos.x), float(self._pos.y)) __repr__ = __str__ class Port(object): """ Port connectable part of an item. Item's handle connects to a port. """ def __init__(self): super(Port, self).__init__() self._connectable = True @observed def _set_connectable(self, connectable): self._connectable = connectable connectable = reversible_property(lambda s: s._connectable, _set_connectable) def glue(self, pos): """ Get glue point on the port and distance to the port. """ raise NotImplemented('Glue method not implemented') def constraint(self, canvas, item, handle, glue_item): """ Create connection constraint between item's handle and glue item. """ raise NotImplemented('Constraint method not implemented') class LinePort(Port): """ Port defined as a line between two handles. """ def __init__(self, start, end): super(LinePort, self).__init__() self.start = start self.end = end def glue(self, pos): """ Get glue point on the port and distance to the port. >>> p1, p2 = (0.0, 0.0), (100.0, 100.0) >>> port = LinePort(p1, p2) >>> port.glue((50, 50)) ((50.0, 50.0), 0.0) >>> port.glue((0, 10)) ((5.0, 5.0), 7.0710678118654755) """ d, pl = distance_line_point(self.start, self.end, pos) return pl, d def constraint(self, canvas, item, handle, glue_item): """ Create connection line constraint between item's handle and the port. """ line = canvas.project(glue_item, self.start, self.end) point = canvas.project(item, handle.pos) return LineConstraint(line, point) class PointPort(Port): """ Port defined as a point. """ def __init__(self, point): super(PointPort, self).__init__() self.point = point def glue(self, pos): """ Get glue point on the port and distance to the port. >>> h = Handle((10, 10)) >>> port = PointPort(h.pos) >>> port.glue((10, 0)) (, 10.0) """ d = distance_point_point(self.point, pos) return self.point, d def constraint(self, canvas, item, handle, glue_item): """ Return connection position constraint between item's handle and the port. """ origin = canvas.project(glue_item, self.point) point = canvas.project(item, handle.pos) c = PositionConstraint(origin, point) return c #PositionConstraint(origin, point) # vim: sw=4:et:ai gaphas-0.7.2/gaphas/constraint.py000066400000000000000000000411241175456763600170120ustar00rootroot00000000000000""" This module contains several flavors of constraint classes. Each has a method `Constraint.solve_for(name)` and a method `Constraint.mark_dirty(v)`. These methods are used by the constraint solver (`solver.Solver`) to set the variables. Variables should be of type `solver.Variable`. See classes' documentation below for constraints description and for examples of their usage. EqualsConstraint Make 'a' and 'b' equal. LessThanConstraint Ensure one variable stays smaller than the other. CenterConstraint Ensures a Variable is kept between two other variables. EquationConstraint Solve a linear equation. BalanceConstraint Keeps three variables in line, maintaining a specific ratio. LineConstraint Solves the equation where a line is connected to a line or side at a specific point. New constraint class should derive from Constraint class abstract class and implement `Constraint.solve_for(Variable)` method to update a variable with appropriate value. """ from __future__ import division import operator import math from solver import Projection __version__ = "$Revision$" # $HeadURL$ # is simple abs(x - y) > EPSILON enough for canvas needs? EPSILON = 1e-6 def _update(variable, value): if abs(variable.value - value) > EPSILON: variable.value = value class Constraint(object): """ Constraint base class. - _variables - list of all variables - _weakest - list of weakest variables """ disabled = False def __init__(self, *variables): """ Create new constraint, register all variables, and find weakest variables. """ self._variables = [] for v in variables: self._variables.append(v) self.create_weakest_list() # Used by the Solver for efficiency self._solver_has_projections = False def create_weakest_list(self): """ Create list of weakest variables. """ # strength = min([v.strength for v in self._variables]) strength = min(v.strength for v in self._variables) self._weakest = [v for v in self._variables if v.strength == strength] def variables(self): """ Return an iterator which iterates over the variables that are held by this constraint. """ return self._variables def weakest(self): """ Return the weakest variable. The weakest variable should be always as first element of Constraint._weakest list. """ return self._weakest[0] def mark_dirty(self, v): """ Mark variable v dirty and if possible move it to the end of Constraint._weakest list to maintain weakest variable invariants (see gaphas.solver module documentation). """ weakest = self.weakest() # Fast lane: if v is weakest: self._weakest.remove(v) self._weakest.append(v) return # Handle projected variables well: global Projection p = weakest while isinstance(weakest, Projection): weakest = weakest.variable() if v is weakest: self._weakest.remove(p) self._weakest.append(p) return def solve_for(self, var): """ Solve the constraint for a given variable. The variable itself is updated. """ raise NotImplemented class EqualsConstraint(Constraint): """ Constraint, which ensures that two arguments ``a`` and ``b`` are equal, for example >>> from solver import Variable >>> a, b = Variable(1.0), Variable(2.0) >>> eq = EqualsConstraint(a, b) >>> eq.solve_for(a) >>> a Variable(2, 20) >>> a.value = 10.8 >>> eq.solve_for(b) >>> b Variable(10.8, 20) """ def __init__(self, a=None, b=None, delta=0.0): super(EqualsConstraint, self).__init__(a, b) self.a = a self.b = b self._delta = delta def solve_for(self, var): assert var in (self.a, self.b) _update(*((var is self.a) and \ (self.a, self.b.value + self._delta) or \ (self.b, self.a.value + self._delta))) class CenterConstraint(Constraint): """ Simple Constraint, takes three arguments: 'a', 'b' and center. When solved, the constraint ensures 'center' is located in the middle of 'a' and 'b'. >>> from solver import Variable >>> a, b, center = Variable(1.0), Variable(3.0), Variable() >>> eq = CenterConstraint(a, b, center) >>> eq.solve_for(a) >>> a Variable(1, 20) >>> center Variable(2, 20) >>> a.value = 10 >>> eq.solve_for(b) >>> b Variable(3, 20) >>> center Variable(6.5, 20) """ def __init__(self, a=None, b=None, center=None): super(CenterConstraint, self).__init__(a, b, center) self.a = a self.b = b self.center = center def solve_for(self, var): assert var in (self.a, self.b, self.center) v = (self.a.value + self.b.value) / 2.0 _update(self.center, v) class LessThanConstraint(Constraint): """ Ensure ``smaller`` is less than ``bigger``. The variable that is passed as to-be-solved is left alone (cause it is the variable that has not been moved lately). Instead the other variable is solved. >>> from solver import Variable >>> a, b = Variable(3.0), Variable(2.0) >>> lt = LessThanConstraint(smaller=a, bigger=b) >>> lt.solve_for(a) >>> a, b (Variable(3, 20), Variable(3, 20)) >>> b.value = 0.8 >>> lt.solve_for(b) >>> a, b (Variable(0.8, 20), Variable(0.8, 20)) Also minimal delta between two values can be set >>> a, b = Variable(10.0), Variable(8.0) >>> lt = LessThanConstraint(smaller=a, bigger=b, delta=5) >>> lt.solve_for(a) >>> a, b (Variable(10, 20), Variable(15, 20)) """ def __init__(self, smaller=None, bigger=None, delta=0.0): super(LessThanConstraint, self).__init__(smaller, bigger) self.smaller = smaller self.bigger = bigger self.delta = delta def solve_for(self, var): if self.smaller.value > self.bigger.value - self.delta: if var is self.smaller: self.bigger.value = self.smaller.value + self.delta elif var is self.bigger: self.smaller.value = self.bigger.value - self.delta # Constants for the EquationConstraint ITERLIMIT = 1000 # iteration limit class EquationConstraint(Constraint): """ Equation solver using attributes and introspection. Takes a function, named arg value (opt.) and returns a Constraint object Calling EquationConstraint.solve_for will solve the equation for variable ``arg``, so that the outcome is 0. >>> from solver import Variable >>> a, b, c = Variable(), Variable(4), Variable(5) >>> cons = EquationConstraint(lambda a, b, c: a + b - c, a=a, b=b, c=c) >>> cons.solve_for(a) >>> a Variable(1, 20) >>> a.value = 3.4 >>> cons.solve_for(b) >>> b Variable(1.6, 20) From: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/303396 """ def __init__(self, f, **args): super(EquationConstraint, self).__init__(*args.values()) self._f = f self._args = {} # see important note on order of operations in __setattr__ below. for arg in f.func_code.co_varnames[0:f.func_code.co_argcount]: self._args[arg] = None self._set(**args) def __repr__(self): argstring = ', '.join(['%s=%s' % (arg, str(value)) for (arg, value) in self._args.items()]) if argstring: return 'EquationConstraint(%s, %s)' % (self._f.func_code.co_name, argstring) else: return 'EquationConstraint(%s)' % self._f.func_code.co_name def __getattr__(self, name): """ Used to extract function argument values. """ self._args[name] return self.solve_for(name) def __setattr__(self, name, value): """ Sets function argument values. """ # Note - once self._args is created, no new attributes can # be added to self.__dict__. This is a good thing as it throws # an exception if you try to assign to an arg which is inappropriate # for the function in the solver. if self.__dict__.has_key('_args'): if name in self._args: self._args[name] = value elif name in self.__dict__: self.__dict__[name] = value else: raise KeyError, name else: object.__setattr__(self, name, value) def _set(self, **args): """ Sets values of function arguments. """ for arg in args: self._args[arg] # raise exception if arg not in _args setattr(self, arg, args[arg]) def solve_for(self, var): """ Solve this constraint for the variable named 'arg' in the constraint. """ args = {} for nm, v in self._args.items(): args[nm] = v.value if v is var: arg = nm v = self._solve_for(arg, args) if var.value != v: var.value = v def _solve_for(self, arg, args): """ Newton's method solver """ #args = self._args close_runs = 10 # after getting close, do more passes if args[arg]: x0 = args[arg] else: x0 = 1 if x0 == 0: x1 = 1 else: x1 = x0*1.1 def f(x): """function to solve""" args[arg] = x return self._f(**args) fx0 = f(x0) n = 0 while 1: # Newton's method loop here fx1 = f(x1) if fx1 == 0 or x1 == x0: # managed to nail it exactly break if abs(fx1-fx0) < EPSILON: # very close close_flag = True if close_runs == 0: # been close several times break else: close_runs -= 1 # try some more else: close_flag = False if n > ITERLIMIT: print "Failed to converge; exceeded iteration limit" break slope = (fx1 - fx0) / (x1 - x0) if slope == 0: if close_flag: # we're close but have zero slope, finish break else: print 'Zero slope and not close enough to solution' break x2 = x0 - fx0 / slope # New 'x1' fx0 = fx1 x0 = x1 x1 = x2 n += 1 return x1 class BalanceConstraint(Constraint): """ Ensure that a variable ``v`` is between values specified by ``band`` and in distance proportional from ``band[0]``. Consider >>> from solver import Variable, WEAK >>> a, b, c = Variable(2.0), Variable(3.0), Variable(2.3, WEAK) >>> bc = BalanceConstraint(band=(a,b), v=c) >>> c.value = 2.4 >>> c Variable(2.4, 10) >>> bc.solve_for(c) >>> a, b, c (Variable(2, 20), Variable(3, 20), Variable(2.3, 10)) Band does not have to be ``band[0] < band[1]`` >>> a, b, c = Variable(3.0), Variable(2.0), Variable(2.45, WEAK) >>> bc = BalanceConstraint(band=(a,b), v=c) >>> c.value = 2.50 >>> c Variable(2.5, 10) >>> bc.solve_for(c) >>> a, b, c (Variable(3, 20), Variable(2, 20), Variable(2.45, 10)) """ def __init__(self, band=None, v=None, balance=None): super(BalanceConstraint, self).__init__(band[0], band[1], v) self.band = band self.balance = balance self.v = v if self.balance is None: self.update_balance() def update_balance(self): b1, b2 = self.band w = b2 - b1 if w != 0: self.balance = (self.v - b1) / w else: self.balance = 0 def solve_for(self, var): b1, b2 = self.band w = b2.value - b1.value value = b1.value + w * self.balance _update(var, value) class LineConstraint(Constraint): """ Ensure a point is kept on a line. Attributes: - _line: line defined by tuple ((x1, y1), (x2, y2)) - _point: point defined by tuple (x, y) """ def __init__(self, line, point): super(LineConstraint, self).__init__(line[0][0], line[0][1], line[1][0], line[1][1], point[0], point[1]) self._line = line self._point = point self.update_ratio() def update_ratio(self): """ >>> from gaphas.solver import Variable >>> line = (Variable(0), Variable(0)), (Variable(30), Variable(20)) >>> point = (Variable(15), Variable(4)) >>> lc = LineConstraint(line=line, point=point) >>> lc.update_ratio() >>> lc.ratio_x, lc.ratio_y (0.5, 0.20000000000000001) >>> line[1][0].value = 40 >>> line[1][1].value = 30 >>> lc.solve_for(point[0]) >>> lc.ratio_x, lc.ratio_y (0.5, 0.20000000000000001) >>> point (Variable(20, 20), Variable(6, 20)) """ sx, sy = self._line[0] ex, ey = self._line[1] px, py = self._point try: self.ratio_x = float(px.value - sx.value) / float(ex.value - sx.value) except ZeroDivisionError: self.ratio_x = 0.0 try: self.ratio_y = float(py.value - sy.value) / float(ey.value - sy.value) except ZeroDivisionError: self.ratio_y = 0.0 def solve_for(self, var=None): self._solve() def _solve(self): """ Solve the equation for the connected_handle. >>> from gaphas.solver import Variable >>> line = (Variable(0), Variable(0)), (Variable(30), Variable(20)) >>> point = (Variable(15), Variable(4)) >>> lc = LineConstraint(line=line, point=point) >>> lc.update_ratio() >>> lc.solve_for(point[0]) >>> point (Variable(15, 20), Variable(4, 20)) >>> line[1][0].value = 40 >>> line[1][1].value = 30 >>> lc.solve_for(point[0]) >>> point (Variable(20, 20), Variable(6, 20)) """ sx, sy = self._line[0] ex, ey = self._line[1] px, py = self._point x = sx.value + (ex.value - sx.value) * self.ratio_x y = sy.value + (ey.value - sy.value) * self.ratio_y _update(px, x) _update(py, y) class PositionConstraint(Constraint): """ Ensure that point is always in origin position. Attributes: - _origin: origin position - _point: point to be in origin position """ def __init__(self, origin, point): super(PositionConstraint, self).__init__(origin[0], origin[1], point[0], point[1]) self._origin = origin self._point = point def solve_for(self, var=None): """ Ensure that point's coordinates are the same as coordinates of the origin position. """ x, y = self._origin[0].value, self._origin[1].value _update(self._point[0], x) _update(self._point[1], y) class LineAlignConstraint(Constraint): """ Ensure a point is kept on a line in position specified by align and padding information. Align is specified as a number between 0 and 1, for example 0 keep point at one end of the line 1 keep point at other end of the line 0.5 keep point in the middle of the line Align can be adjusted with `delta` parameter, which specifies the padding of the point. :Attributes: _line Line defined by tuple ((x1, y1), (x2, y2)). _point Point defined by tuple (x, y). _align Align of point. _delta Padding of the align. """ def __init__(self, line, point, align=0.5, delta=0.0): super(LineAlignConstraint, self).__init__(line[0][0], line[0][1], line[1][0], line[1][1], point[0], point[1]) self._line = line self._point = point self._align = align self._delta = delta def solve_for(self, var=None): sx, sy = self._line[0] ex, ey = self._line[1] px, py = self._point a = math.atan2(ey.value - sy.value, ex.value - sx.value) x = sx.value + (ex.value - sx.value) * self._align + self._delta * math.cos(a) y = sy.value + (ey.value - sy.value) * self._align + self._delta * math.sin(a) _update(px, x) _update(py, y) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/decorators.py000066400000000000000000000126061175456763600167760ustar00rootroot00000000000000""" Custom decorators. """ __version__ = "$Revision$" # $HeadURL$ import threading import gobject from gobject import PRIORITY_HIGH, PRIORITY_HIGH_IDLE, PRIORITY_DEFAULT, \ PRIORITY_DEFAULT_IDLE, PRIORITY_LOW DEBUG_ASYNC = False class async(object): """ Instead of calling the function, schedule an idle handler at a given priority. This requires the async'ed method to be called from within the GTK main loop. Otherwise the method is executed directly. Note: the current implementation of async single mode only works for methods, not functions. Calling the async function from outside the gtk main loop will yield imediate execution: async just works on functions (as long as ``single=False``): >>> a = async()(lambda: 'Hi') >>> a() 'Hi' Simple method: >>> class A(object): ... @async(single=False, priority=gobject.PRIORITY_HIGH) ... def a(self): ... print 'idle-a', gobject.main_depth() Methods can also set single mode to True (the method is only scheduled one). >>> class B(object): ... @async(single=True) ... def b(self): ... print 'idle-b', gobject.main_depth() Also a timeout property can be provided: >>> class C(object): ... @async(timeout=50) ... def c1(self): ... print 'idle-c1', gobject.main_depth() ... @async(single=True, timeout=60) ... def c2(self): ... print 'idle-c2', gobject.main_depth() This is a helper function used to test classes A and B from within the GTK+ main loop: >>> def delayed(): ... print 'before' ... a = A() ... b = B() ... c = C() ... c.c1() ... c.c1() ... c.c2() ... c.c2() ... a.a() ... b.b() ... a.a() ... b.b() ... a.a() ... b.b() ... print 'after' ... gobject.timeout_add(100, gtk.main_quit) >>> gobject.timeout_add(1, delayed) > 0 # timeout id may vary True >>> import gtk >>> gtk.main() before after idle-a 1 idle-a 1 idle-a 1 idle-b 1 idle-c1 1 idle-c1 1 idle-c2 1 As you can see, although ``b.b()`` has been called three times, it's only executed once. """ def __init__(self, single=False, timeout=0, priority=gobject.PRIORITY_DEFAULT): self.single = single self.timeout = timeout self.priority = priority def source(self, func): timeout = self.timeout if timeout > 0: s = gobject.Timeout(timeout) else: s = gobject.Idle() s.set_callback(func) s.priority = self.priority return s def __call__(self, func): async_id = '_async_id_%s' % func.__name__ source = self.source def wrapper(*args, **kwargs): global getattr, setattr, delattr # execute directly if we're not in the main loop. if gobject.main_depth() == 0: return func(*args, **kwargs) elif not self.single: def async_wrapper(): if DEBUG_ASYNC: print 'async:', func, args, kwargs func(*args, **kwargs) source(async_wrapper).attach() else: # Idle handlers should be registered per instance holder = args[0] try: if getattr(holder, async_id): return except AttributeError, e: def async_wrapper(): if DEBUG_ASYNC: print 'async:', func, args, kwargs try: func(*args, **kwargs) finally: delattr(holder, async_id) return False setattr(holder, async_id, source(async_wrapper).attach()) return wrapper def nonrecursive(func): """ Enforce a function or method is not executed recursively: >>> class A(object): ... @nonrecursive ... def a(self, x=1): ... print x ... self.a(x+1) >>> A().a() 1 >>> A().a() 1 """ m = threading.Lock() def wrapper(*args, **kwargs): """ Decorate function with a mutex that prohibits recursice execution. """ if m.acquire(False): try: return func(*args, **kwargs) finally: m.release() return wrapper class recursive(object): """ This decorator limits the recursion for a specific function >>> class A(object): ... def __init__(self): self.r = 0 ... @recursive(10) ... def a(self, x=0): ... self.r += 1 ... self.a() >>> a = A() >>> a.a() >>> a.r 10 """ def __init__(self, limit=10000): self.limit = limit def __call__(self, func): def wrapper(*args, **kwargs): try: func._recursion_level += 1 except AttributeError: # _recursion_level not present func._recursion_level = 0 if func._recursion_level < self.limit: try: return func(*args, **kwargs) finally: func._recursion_level -= 1 return wrapper # vim:sw=4:et:ai gaphas-0.7.2/gaphas/examples.py000066400000000000000000000127571175456763600164560ustar00rootroot00000000000000""" Simple example items. These items are used in various tests. """ __version__ = "$Revision$" # $HeadURL$ from gaphas.item import Element, Item, NW, NE,SW, SE from gaphas.connector import Handle, PointPort, LinePort, Position from gaphas.solver import solvable, WEAK import tool from util import text_align, text_multiline, path_ellipse class Box(Element): """ A Box has 4 handles (for a start): NW +---+ NE SW +---+ SE """ def __init__(self, width=10, height=10): super(Box, self).__init__(width, height) def draw(self, context): c = context.cairo nw = self._handles[NW].pos c.rectangle(nw.x, nw.y, self.width, self.height) if context.hovered: c.set_source_rgba(.8,.8,1, .8) else: c.set_source_rgba(1,1,1, .8) c.fill_preserve() c.set_source_rgb(0,0,0.8) c.stroke() class PortoBox(Box): """ Box item with few falvours of port(o)s. Default box ports are disabled. Three, non-default connectable ports are created (represented by ``x`` on the picture). - point port on the east edge, movable with a handle - static point port in the middle of the south edge - line port from north-west to south east corner NW +--------+ NE |xx | | xx |x | xx | | xx| SW +--------+ SE x """ def __init__(self, width=10, height=10): super(PortoBox, self).__init__(width, height) # disable default ports for p in self._ports: p.connectable = False nw = self._handles[NW] sw = self._handles[SW] ne = self._handles[NE] se = self._handles[SE] # handle for movable port self._hm = Handle(strength=WEAK) self._hm.pos = width, height / 2.0 self._handles.append(self._hm) # movable port self._ports.append(PointPort(self._hm.pos)) # keep movable port at right edge self.constraint(vertical=(self._hm.pos, ne.pos), delta=10) self.constraint(above=(ne.pos, self._hm.pos)) self.constraint(above=(self._hm.pos, se.pos)) # static point port self._sport = PointPort(Position((width / 2.0, height))) l = sw.pos, se.pos self.constraint(line=(self._sport.point, l)) self._ports.append(self._sport) # line port self._lport = LinePort(nw.pos, se.pos) self._ports.append(self._lport) def draw(self, context): super(PortoBox, self).draw(context) c = context.cairo if context.hovered: c.set_source_rgba(.0, .8, 0, .8) else: c.set_source_rgba(.9, .0, .0, .8) # draw movable port x, y = self._hm.pos c.rectangle(x - 20 , y - 5, 20, 10) c.rectangle(x - 1 , y - 1, 2, 2) # draw static port x, y = self._sport.point c.rectangle(x - 2 , y - 2, 4, 4) c.fill_preserve() # draw line port x1, y1 = self._lport.start x2, y2 = self._lport.end c.move_to(x1, y1) c.line_to(x2, y2) c.set_source_rgb(0,0,0.8) c.stroke() class Text(Item): """ Simple item showing some text on the canvas. """ def __init__(self, text=None, plain=False, multiline=False, align_x=1, align_y=-1): super(Text, self).__init__() self.text = text is None and 'Hello' or text self.plain = plain self.multiline = multiline self.align_x = align_x self.align_y = align_y def draw(self, context): #print 'Text.draw', self cr = context.cairo if self.multiline: text_multiline(cr, 0, 0, self.text) elif self.plain: cr.show_text(self.text) else: text_align(cr, 0, 0, self.text, self.align_x, self.align_y) def point(self, pos): return 0 class FatLine(Item): """ Simple, vertical line with two handles. todo: rectangle port instead of line port would be nicer """ def __init__(self): super(FatLine, self).__init__() self._handles.extend((Handle(), Handle())) h1, h2 = self._handles self._ports.append(LinePort(h1.pos, h2.pos)) self.constraint(vertical=(h1.pos, h2.pos)) self.constraint(above=(h1.pos, h2.pos), delta=20) def _set_height(self, height): h1, h2 = self._handles h2.pos.y = height def _get_height(self): h1, h2 = self._handles return h2.pos.y height = property(_get_height, _set_height) def draw(self, context): cr = context.cairo cr.set_line_width(10) h1, h2 = self.handles() cr.move_to(0, 0) cr.line_to(0, self.height) cr.stroke() class Circle(Item): def __init__(self): super(Circle, self).__init__() self._handles.extend((Handle(), Handle())) def _set_radius(self, r): h1, h2 = self._handles h2.pos.x = r h2.pos.y = r def _get_radius(self): h1, h2 = self._handles p1, p2 = h1.pos, h2.pos return ((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2) ** 0.5 radius = property(_get_radius, _set_radius) def setup_canvas(self): super(Circle, self).setup_canvas() h1, h2 = self._handles h1.movable = False def draw(self, context): cr = context.cairo path_ellipse(cr, 0, 0, 2 * self.radius, 2 * self.radius) cr.stroke() # vim: sw=4:et:ai gaphas-0.7.2/gaphas/freehand.py000066400000000000000000000122301175456763600163760ustar00rootroot00000000000000""" Cairo context using Steve Hanov's freehand drawing code. # Crazyline. By Steve Hanov, 2008 # Released to the public domain. # The idea is to draw a curve, setting two control points at random # close to each side of the line. The longer the line, the sloppier # it's drawn. See: http://stevehanov.ca/blog/index.php?id=33 and http://stevehanov.ca/blog/index.php?id=93 """ from math import sqrt from random import Random from painter import Context class FreeHandCairoContext(object): KAPPA = 0.5522847498 def __init__(self, cr, sloppiness=0.5): """ Create context with given sloppiness. Range [0..2.0] gives acceptable results. * Draftsman: 0.0 * Artist: 0.25 * Cartoonist: 0.5 * Child: 1.0 * Drunk: 2.0 """ self.cr = cr self.sloppiness = sloppiness # In range 0.0 .. 2.0 def __getattr__(self, key): return getattr(self.cr, key) def line_to(self, x, y): cr = self.cr sloppiness = self.sloppiness from_x, from_y = cr.get_current_point() # calculate the length of the line. length = sqrt( (x-from_x)*(x-from_x) + (y-from_y)*(y-from_y)) # This offset determines how sloppy the line is drawn. It depends on # the length, but maxes out at 20. offset = length/10 * sloppiness if offset > 20: offset = 20 dev_x, dev_y = cr.user_to_device(x, y) rand = Random((from_x, from_y, dev_x, dev_y, length, offset)).random # Overshoot the destination a little, as one might if drawing with a pen. to_x = x + sloppiness * rand() * offset/4 to_y = y + sloppiness * rand() * offset/4 # t1 and t2 are coordinates of a line shifted under or to the right of # our original. t1_x = from_x + offset t1_y = from_y + offset t2_x = to_x + offset t2_y = to_y + offset # create a control point at random along our shifted line. r = rand() control1_x = t1_x + r * (t2_x-t1_x) control1_y = t1_y + r * (t2_y-t1_y) # now make t1 and t2 the coordinates of our line shifted above # and to the left of the original. t1_x = from_x - offset t2_x = to_x - offset t1_y = from_y - offset t2_y = to_y - offset # create a second control point at random along the shifted line. r = rand() control2_x = t1_x + r * (t2_x-t1_x) control2_y = t1_y + r * (t2_y-t1_y) # draw the line! cr.curve_to(control1_x, control1_y, control2_x, control2_y, to_x, to_y) def rel_line_to(self, dx, dy): cr = self.cr from_x, from_y = cr.get_current_point() self.line_to(from_x + dx, from_y + dy) def curve_to(self, x1, y1, x2, y2, x3, y3): cr = self.cr from_x, from_y = cr.get_current_point() dev_x, dev_y = cr.user_to_device(x3, y3) rand = Random((from_x, from_y, dev_x, dev_y, x1, y1, x2, y2, x3, y3)).random r = rand() c1_x = from_x + r * (x1-from_x) c1_y = from_y + r * (y1-from_y) r = rand() c2_x = x3 + r * (x2-x3) c2_y = y3 + r * (y2-y3) cr.curve_to(c1_x, c1_y, c2_x, c2_y, x3, y3) def rel_curve_to(self, dx1, dy1, dx2, dy2, dx3, dy3): cr = self.cr from_x, from_y = cr.get_current_point() self.curve_to(from_x+dx1, from_y+dy1, from_x+dx2, from_y+dy2, from_x+dx3, from_y+dy3) def corner_to(self, cx, cy, x, y): cr = self.cr from_x, from_y = cr.get_current_point() # calculate radius of the circle. radius1 = Math.sqrt( (cx-from_x)*(cx-from_x) + (cy-from_y)*(cy-from_y)); radius2 = Math.sqrt( (cx-x)*(cx-x) + (cy-y)*(cy-y)); dev_x, dev_y = cr.user_to_device(x, y) rand = Random((cx, cy, dev_x, dev_y, radius1, radius2)).random # place first control point c1_x = from_x + self.KAPPA * (cx - from_x) + rand() * sloppiness * radius1 / 2 c1_y = from_y + self.KAPPA * (cy - from_y) + rand() * sloppiness * radius1 / 2 # place second control point c2_x = x + self.KAPPA * (cx - x) + rand() * sloppiness * radius2 / 1.5 c2_y = y + self.KAPPA * (cy - y) + rand() * sloppiness * radius2 / 1.5 cr.curve_to(c1_x, c1_y, c2_x, c2_y, x3, y3) def rectangle(self, x, y, width, height): x1 = x + width y1 = y + height self.move_to(x, y) self.line_to(x1, y) self.line_to(x1, y1) self.line_to(x, y1) if self.sloppiness > 0.1: self.line_to(x, y) else: self.close_path() class FreeHandPainter(object): def __init__(self, subpainter, sloppiness=1.0, view=None): self.subpainter = subpainter self.view = view self.sloppiness = sloppiness def set_view(self, view): self.view = view self.subpainter.set_view(view) def paint(self, context): subcontext = Context(cairo=FreeHandCairoContext(context.cairo, self.sloppiness), items=context.items, area=context.area) self.subpainter.paint(subcontext) # vi:sw=4:et:ai gaphas-0.7.2/gaphas/geometry.py000066400000000000000000000420451175456763600164640ustar00rootroot00000000000000""" Geometry functions. Rectangle is a utility class for working with rectangles (unions and intersections). A point is represented as a tuple `(x, y)`. """ __version__ = "$Revision$" # $HeadURL$ from math import sqrt class Rectangle(object): """ Python Rectangle implementation. Rectangles can be added (union), substituted (intersection) and points and rectangles can be tested to be in the rectangle. >>> r1= Rectangle(1,1,5,5) >>> r2 = Rectangle(3,3,6,7) Test if two rectangles intersect: >>> if r1 - r2: 'yes' 'yes' >>> r1, r2 = Rectangle(1,2,3,4), Rectangle(1,2,3,4) >>> r1 == r2 True >>> r = Rectangle(-5, 3, 10, 8) >>> r.width = 2 >>> r Rectangle(-5, 3, 2, 8) >>> r = Rectangle(-5, 3, 10, 8) >>> r.height = 2 >>> r Rectangle(-5, 3, 10, 2) """ def __init__(self, x=0, y=0, width=None, height=None, x1=0, y1=0): if width is None: self.x = min(x, x1) self.width = abs(x1 - x) else: self.x = x self.width = width if height is None: self.y = min(y, y1) self.height = abs(y1 - y) else: self.y = y self.height = height def _set_x1(self, x1): """ """ width = x1 - self.x if width < 0: width = 0 self.width = width x1 = property(lambda s: s.x + s.width, _set_x1) def _set_y1(self, y1): """ """ height = y1 - self.y if height < 0: height = 0 self.height = height y1 = property(lambda s: s.y + s.height, _set_y1) def expand(self, delta): """ >>> r = Rectangle(-5, 3, 10, 8) >>> r.expand(5) >>> r Rectangle(-10, -2, 20, 18) """ self.x -= delta self.y -= delta self.width += delta * 2 self.height += delta * 2 def __repr__(self): """ >>> Rectangle(5,7,20,25) Rectangle(5, 7, 20, 25) """ if self: return '%s(%g, %g, %g, %g)' % (self.__class__.__name__, self.x, self.y, self.width, self.height) return '%s()' % self.__class__.__name__ def __iter__(self): """ >>> tuple(Rectangle(1,2,3,4)) (1, 2, 3, 4) """ return iter((self.x, self.y, self.width, self.height)) def __getitem__(self, index): """ >>> Rectangle(1,2,3,4)[1] 2 """ return (self.x, self.y, self.width, self.height)[index] def __nonzero__(self): """ >>> r=Rectangle(1,2,3,4) >>> if r: 'yes' 'yes' >>> r = Rectangle(1,1,0,0) >>> if r: 'no' """ return self.width > 0 and self.height > 0 def __eq__(self, other): return (type(self) is type(other)) \ and self.x == other.x \ and self.y == other.y \ and self.width == other.width \ and self.height == self.height def __add__(self, obj): """ Create a new Rectangle is the union of the current rectangle with another Rectangle, tuple `(x,y)` or tuple `(x, y, width, height)`. >>> r=Rectangle(5, 7, 20, 25) >>> r + (0, 0) Traceback (most recent call last): ... TypeError: Can only add Rectangle or tuple (x, y, width, height), not (0, 0). >>> r + (20, 30, 40, 50) Rectangle(5, 7, 55, 73) """ return Rectangle(self.x, self.y, self.width, self.height).__iadd__(obj) def __iadd__(self, obj): """ >>> r = Rectangle() >>> r += Rectangle(5, 7, 20, 25) >>> r += (0, 0, 30, 10) >>> r Rectangle(0, 0, 30, 32) >>> r += 'aap' Traceback (most recent call last): ... TypeError: Can only add Rectangle or tuple (x, y, width, height), not 'aap'. """ try: x, y, width, height = obj except ValueError: raise TypeError, "Can only add Rectangle or tuple (x, y, width, height), not %s." % repr(obj) x1, y1 = x + width, y + height if self: ox1, oy1 = self.x + self.width, self.y + self.height self.x = min(self.x, x) self.y = min(self.y, y) self.x1 = max(ox1, x1) self.y1 = max(oy1, y1) else: self.x, self.y, self.width, self.height = x, y, width, height return self def __sub__(self, obj): """ Create a new Rectangle is the union of the current rectangle with another Rectangle or tuple (x, y, width, height). >>> r = Rectangle(5, 7, 20, 25) >>> r - (20, 30, 40, 50) Rectangle(20, 30, 5, 2) >>> r - (30, 40, 40, 50) Rectangle() """ return Rectangle(self.x, self.y, self.width, self.height).__isub__(obj) def __isub__(self, obj): """ >>> r = Rectangle() >>> r -= Rectangle(5, 7, 20, 25) >>> r -= (0, 0, 30, 10) >>> r Rectangle(5, 7, 20, 3) >>> r -= 'aap' Traceback (most recent call last): ... TypeError: Can only substract Rectangle or tuple (x, y, width, height), not 'aap'. """ try: x, y, width, height = obj except ValueError: raise TypeError, "Can only substract Rectangle or tuple (x, y, width, height), not %s." % repr(obj) x1, y1 = x + width, y + height if self: ox1, oy1 = self.x + self.width, self.y + self.height self.x = max(self.x, x) self.y = max(self.y, y) self.x1 = min(ox1, x1) self.y1 = min(oy1, y1) else: self.x, self.y, self.width, self.height = x, y, width, height return self def __contains__(self, obj): """ Check if a point `(x, y)` in inside rectangle `(x, y, width, height)` or if a rectangle instance is inside with the rectangle. >>> r=Rectangle(10, 5, 12, 12) >>> (0, 0) in r False >>> (10, 6) in r True >>> (12, 12) in r True >>> (100, 4) in r False >>> (11, 6, 5, 5) in r True >>> (11, 6, 15, 15) in r False >>> Rectangle(11, 6, 5, 5) in r True >>> Rectangle(11, 6, 15, 15) in r False >>> 'aap' in r Traceback (most recent call last): ... TypeError: Should compare to Rectangle, tuple (x, y, width, height) or point (x, y), not 'aap'. """ try: x, y, width, height = obj x1, y1 = x + width, y + width except ValueError: # point try: x, y = obj x1, y1 = obj except ValueError: raise TypeError, "Should compare to Rectangle, tuple (x, y, width, height) or point (x, y), not %s." % repr(obj) return x >= self.x and x1 <= self.x1 and \ y >= self.y and y1 <= self.y1 def distance_point_point(point1, point2=(0., 0.)): """ Return the distance from point ``point1`` to ``point2``. >>> '%.3f' % distance_point_point((0,0), (1,1)) '1.414' """ dx = point1[0] - point2[0] dy = point1[1] - point2[1] return sqrt(dx*dx + dy*dy) def distance_point_point_fast(point1, point2): """ Return the distance from point ``point1`` to ``point2``. This version is faster than ``distance_point_point()``, but less precise. >>> distance_point_point_fast((0,0), (1,1)) 2 """ dx = point1[0] - point2[0] dy = point1[1] - point2[1] return abs(dx) + abs(dy) def distance_rectangle_point(rect, point): """ Return the distance (fast) from a rectangle ``(x, y, width, height)`` to a ``point``. >>> distance_rectangle_point(Rectangle(0, 0, 10, 10), (11, -1)) 2 >>> distance_rectangle_point((0, 0, 10, 10), (11, -1)) 2 >>> distance_rectangle_point((0, 0, 10, 10), (-1, 11)) 2 """ dx = dy = 0 px, py = point rx, ry, rw, rh = tuple(rect) if px < rx: dx = rx - px elif px > rx + rw: dx = px - (rx + rw) if py < ry: dy = ry - py elif py > ry + rh: dy = py - (ry + rh) return abs(dx) + abs(dy) def point_on_rectangle(rect, point, border=False): """ Return the point on which ``point`` can be projecten on the rectangle. ``border = True`` will make sure the point is bound to the border of the reactangle. Otherwise, if the point is in the rectangle, it's okay. >>> point_on_rectangle(Rectangle(0, 0, 10, 10), (11, -1)) (10, 0) >>> point_on_rectangle((0, 0, 10, 10), (5, 12)) (5, 10) >>> point_on_rectangle(Rectangle(0, 0, 10, 10), (12, 5)) (10, 5) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (3, 4)) (3, 4) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (0, 3)) (1, 3) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (4, 3)) (4, 3) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (4, 9), border=True) (4, 11) >>> point_on_rectangle((1, 1, 10, 10), (4, 6), border=True) (1, 6) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (5, 3), border=True) (5, 1) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (8, 4), border=True) (11, 4) >>> point_on_rectangle((1, 1, 10, 100), (5, 8), border=True) (1, 8) >>> point_on_rectangle((1, 1, 10, 100), (5, 98), border=True) (5, 101) """ px, py = point rx, ry, rw, rh = tuple(rect) x_inside = y_inside = False if px < rx: px = rx elif px > rx + rw: px = rx + rw elif border: x_inside = True if py < ry: py = ry elif py > ry + rh: py = ry + rh elif border: y_inside = True if x_inside and y_inside: # Find point on side closest to the point if min(abs(rx - px), abs(rx + rw - px)) > \ min(abs(ry - py), abs(ry + rh - py)): if py < ry + rh / 2.: py = ry else: py = ry + rh else: if px < rx + rw / 2.: px = rx else: px = rx + rw return px, py def distance_line_point(line_start, line_end, point): """ Calculate the distance of a ``point`` from a line. The line is marked by begin and end point ``line_start`` and ``line_end``. A tuple is returned containing the distance and point on the line. >>> distance_line_point((0., 0.), (2., 4.), point=(3., 4.)) (1.0, (2.0, 4.0)) >>> distance_line_point((0., 0.), (2., 4.), point=(-1., 0.)) (1.0, (0.0, 0.0)) >>> distance_line_point((0., 0.), (2., 4.), point=(1., 2.)) (0.0, (1.0, 2.0)) >>> d, p = distance_line_point((0., 0.), (2., 4.), point=(2., 2.)) >>> '%.3f' % d '0.894' >>> '(%.3f, %.3f)' % p '(1.200, 2.400)' """ # The original end point: true_line_end = line_end # "Move" the line, so it "starts" on (0, 0) line_end = line_end[0] - line_start[0], line_end[1] - line_start[1] point = point[0] - line_start[0], point[1] - line_start[1] line_len_sqr = line_end[0] * line_end[0] + line_end[1] * line_end[1] # Both points are very near each other. if line_len_sqr < 0.0001: return distance_point_point(point), line_start projlen = (line_end[0] * point[0] + line_end[1] * point[1]) / line_len_sqr if projlen < 0.0: # Closest point is the start of the line. return distance_point_point(point), line_start elif projlen > 1.0: # Point has a projection after the line_end. return distance_point_point(point, line_end), true_line_end else: # Projection is on the line. multiply the line_end with the projlen # factor to obtain the point on the line. proj = line_end[0] * projlen, line_end[1] * projlen return distance_point_point((proj[0] - point[0], proj[1] - point[1])),\ (line_start[0] + proj[0], line_start[1] + proj[1]) def intersect_line_line(line1_start, line1_end, line2_start, line2_end): """ Find the point where the lines (segments) defined by ``(line1_start, line1_end)`` and ``(line2_start, line2_end)`` intersect. If no intersecion occurs, ``None`` is returned. >>> intersect_line_line((3, 0), (8, 10), (0, 0), (10, 10)) (6, 6) >>> intersect_line_line((0, 0), (10, 10), (3, 0), (8, 10)) (6, 6) >>> intersect_line_line((0, 0), (10, 10), (8, 10), (3, 0)) (6, 6) >>> intersect_line_line((8, 10), (3, 0), (0, 0), (10, 10)) (6, 6) >>> intersect_line_line((0, 0), (0, 10), (3, 0), (8, 10)) >>> intersect_line_line((0, 0), (0, 10), (3, 0), (3, 10)) Ticket #168: >>> intersect_line_line((478.0, 117.0), (478.0, 166.0), (527.5, 141.5), (336.5, 139.5)) (478.5, 141.48167539267016) >>> intersect_line_line((527.5, 141.5), (336.5, 139.5), (478.0, 117.0), (478.0, 166.0)) (478.5, 141.48167539267016) This is a Python translation of the ``lines_intersect`` routine written by Mukesh Prasad. """ # # This function computes whether two line segments, # respectively joining the input points (x1,y1) -- (x2,y2) # and the input points (x3,y3) -- (x4,y4) intersect. # If the lines intersect, the output variables x, y are # set to coordinates of the point of intersection. # # All values are in integers. The returned value is rounded # to the nearest integer point. # # If non-integral grid points are relevant, the function # can easily be transformed by substituting floating point # calculations instead of integer calculations. # # Entry # x1, y1, x2, y2 Coordinates of endpoints of one segment. # x3, y3, x4, y4 Coordinates of endpoints of other segment. # # Exit # x, y Coordinates of intersection point. # # The value returned by the function is one of: # # DONT_INTERSECT 0 # DO_INTERSECT 1 # COLLINEAR 2 # # Error condititions: # # Depending upon the possible ranges, and particularly on 16-bit # computers, care should be taken to protect from overflow. # # In the following code, 'long' values have been used for this # purpose, instead of 'int'. # x1, y1 = line1_start x2, y2 = line1_end x3, y3 = line2_start x4, y4 = line2_end #long a1, a2, b1, b2, c1, c2; /* Coefficients of line eqns. */ #long r1, r2, r3, r4; /* 'Sign' values */ #long denom, offset, num; /* Intermediate values */ # Compute a1, b1, c1, where line joining points 1 and 2 # is "a1 x + b1 y + c1 = 0". a1 = y2 - y1 b1 = x1 - x2 c1 = x2 * y1 - x1 * y2 # Compute r3 and r4. r3 = a1 * x3 + b1 * y3 + c1 r4 = a1 * x4 + b1 * y4 + c1 # Check signs of r3 and r4. If both point 3 and point 4 lie on # same side of line 1, the line segments do not intersect. if r3 and r4 and (r3 * r4) >= 0: return None # ( DONT_INTERSECT ) # Compute a2, b2, c2 a2 = y4 - y3 b2 = x3 - x4 c2 = x4 * y3 - x3 * y4 # Compute r1 and r2 r1 = a2 * x1 + b2 * y1 + c2 r2 = a2 * x2 + b2 * y2 + c2 # Check signs of r1 and r2. If both point 1 and point 2 lie # on same side of second line segment, the line segments do # not intersect. if r1 and r2 and (r1 * r2) >= 0: #SAME_SIGNS( r1, r2 )) return None # ( DONT_INTERSECT ) # Line segments intersect: compute intersection point. denom = a1 * b2 - a2 * b1 if not denom: return None # ( COLLINEAR ) offset = abs(denom) / 2 # The denom/2 is to get rounding instead of truncating. It # is added or subtracted to the numerator, depending upon the # sign of the numerator. num = b1 * c2 - b2 * c1 x = ( (num < 0) and (num - offset) or (num + offset) ) / denom num = a2 * c1 - a1 * c2 y = ( (num < 0) and (num - offset) or (num + offset) ) / denom return x, y def rectangle_contains(inner, outer): """ Returns True if ``inner`` rect is contained in ``outer`` rect. """ ix, iy, iw, ih = inner ox, oy, ow, oh = outer return ox <= ix and oy <= iy and ox + ow >= ix + iw and oy + oh >= iy + ih def rectangle_intersects(recta, rectb): """ Return True if ``recta`` and ``rectb`` intersect. >>> rectangle_intersects((5,5,20, 20), (10, 10, 1, 1)) True >>> rectangle_intersects((40, 30, 10, 1), (1, 1, 1, 1)) False """ ax, ay, aw, ah = recta bx, by, bw, bh = rectb return ax <= bx + bw and ax + aw >= bx and ay <= by + bh and ay + ah >= by def rectangle_clip(recta, rectb): """ Return the clipped rectangle of ``recta`` and ``rectb``. If they do not intersect, ``None`` is returned. >>> rectangle_clip((0, 0, 20, 20), (10, 10, 20, 20)) (10, 10, 10, 10) """ ax, ay, aw, ah = recta bx, by, bw, bh = rectb x = max(ax, bx) y = max(ay, by) w = min(ax +aw, bx + bw) - x h = min(ay +ah, by + bh) - y if w < 0 or h < 0: return None return (x, y, w, h) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/guide.py000066400000000000000000000203121175456763600157170ustar00rootroot00000000000000""" Module implements guides when moving items and handles around. """ from simplegeneric import generic from gaphas.aspect import InMotion, HandleInMotion, PaintFocused from gaphas.aspect import ItemInMotion, ItemHandleInMotion, ItemPaintFocused from gaphas.connector import Handle from gaphas.item import Item, Element, Line, SE class ItemGuide(object): """ Get edges on an item, on which we can align the items. """ def __init__(self, item): self.item = item def horizontal(self): """ Return horizontal edges (on y axis) """ return () def vertical(self): """ Return vertical edges (on x axis) """ return () Guide = generic(ItemGuide) @Guide.when_type(Element) class ElementGuide(ItemGuide): def horizontal(self): y = self.item.height return (0, y/2, y) def vertical(self): x = self.item.width return (0, x/2, x) @Guide.when_type(Line) class LineGuide(ItemGuide): """ Support guides for orthogonal lines. """ def horizontal(self): line = self.item if line.orthogonal: if line.horizontal: for i, h in enumerate(line.handles()): if i % 2 == 1: yield h.pos.y else: for i, h in enumerate(line.handles()): if i % 2 == 0 and i > 0: yield h.pos.y def vertical(self): line = self.item if line.orthogonal: if line.horizontal: for i, h in enumerate(line.handles()): if i % 2 == 0 and i > 0: yield h.pos.x else: for i, h in enumerate(line.handles()): if i % 2 == 1: yield h.pos.x class Guides(object): def __init__(self, v, h): self.v = v self.h = h def vertical(self): return self.v def horizontal(self): return self.h class GuideMixin(object): """ Helper methods for guides. """ MARGIN = 2 def find_vertical_guides(self, item_vedges, pdx, height, excluded_items): view = self.view item = self.item i2v = self.view.get_matrix_i2v margin = self.MARGIN items = [] for x in item_vedges: items.append(view.get_items_in_rectangle((x - margin, 0, margin*2, height))) try: guides = map(Guide, reduce(set.union, map(set, items)) - excluded_items) except TypeError: guides = [] vedges = set() for g in guides: for x in g.vertical(): vedges.add(i2v(g.item).transform_point(x, 0)[0]) dx, edges_x = self.find_closest(item_vedges, vedges) return dx, edges_x def find_horizontal_guides(self, item_hedges, pdy, width, excluded_items): view = self.view item = self.item i2v = self.view.get_matrix_i2v margin = self.MARGIN items = [] for y in item_hedges: items.append(view.get_items_in_rectangle((0, y - margin, width, margin*2))) try: guides = map(Guide, reduce(set.union, map(set, items)) - excluded_items) except TypeError: guides = [] # Translate edges to canvas or view coordinates hedges = set() for g in guides: for y in g.horizontal(): hedges.add(i2v(g.item).transform_point(0, y)[1]) dy, edges_y = self.find_closest(item_hedges, hedges) return dy, edges_y def get_excluded_items(self): """ Get a set of items excluded from guide calculation. """ item = self.item view = self.view excluded_items = set(view.canvas.get_all_children(item)) excluded_items.add(item) excluded_items.update(view.selected_items) return excluded_items def get_view_dimensions(self): try: allocation = self.view.allocation except AttributeError, e: return 0, 0 return allocation.width, allocation.height def queue_draw_guides(self): view = self.view try: guides = view.guides except AttributeError: return w, h = self.get_view_dimensions() for x in guides.vertical(): view.queue_draw_area(x-1, 0, x+2, h) for y in guides.horizontal(): view.queue_draw_area(0, y-1, w, y+2) def find_closest(self, item_edges, edges): delta = 0 min_d = 1000 closest = [] for e in edges: for ie in item_edges: d = abs(e - ie) if d < min_d: min_d = d delta = e - ie closest = [e] elif d == min_d: closest.append(e) if min_d <= self.MARGIN: return delta, closest else: return 0, () @InMotion.when_type(Item) class GuidedItemInMotion(GuideMixin, ItemInMotion): """ Move the item, lock position on any element that's located at the same location. """ def move(self, pos): item = self.item view = self.view transform = view.get_matrix_i2v(item).transform_point w, h = self.get_view_dimensions() px, py = pos pdx, pdy = px - self.last_x, py - self.last_y excluded_items = self.get_excluded_items() item_guide = Guide(item) item_vedges = [transform(x, 0)[0] + pdx for x in item_guide.vertical()] dx, edges_x = self.find_vertical_guides(item_vedges, pdx, h, excluded_items) item_hedges = [transform(0, y)[1] + pdy for y in item_guide.horizontal()] dy, edges_y = self.find_horizontal_guides(item_hedges, pdy, w, excluded_items) newpos = px + dx, py + dy # Call super class, with new position super(GuidedItemInMotion, self).move(newpos) self.queue_draw_guides() view.guides = Guides(edges_x, edges_y) self.queue_draw_guides() def stop_move(self): self.queue_draw_guides() try: del self.view.guides except AttributeError: # No problem if guides do not exist. pass @HandleInMotion.when_type(Item) class GuidedItemHandleInMotion(GuideMixin, ItemHandleInMotion): def move(self, pos): sink = super(GuidedItemHandleInMotion, self).move(pos) if not sink: item = self.item handle = self.handle view = self.view x, y = pos v2i = view.get_matrix_v2i(item) excluded_items = self.get_excluded_items() w, h = self.get_view_dimensions() dx, edges_x = self.find_vertical_guides((x,), 0, h, excluded_items) dy, edges_y = self.find_horizontal_guides((y,), 0, w, excluded_items) newpos = x + dx, y + dy x, y = v2i.transform_point(*newpos) self.handle.pos = (x, y) #super(GuidedItemHandleInMotion, self).move(newpos) self.queue_draw_guides() view.guides = Guides(edges_x, edges_y) self.queue_draw_guides() item.request_update() def stop_move(self): self.queue_draw_guides() try: del self.view.guides except AttributeError: # No problem if guides do not exist. pass @PaintFocused.when_type(Item) class GuidePainter(ItemPaintFocused): def paint(self, context): try: guides = self.view.guides except AttributeError: return cr = context.cairo view = self.view allocation = view.allocation w, h = allocation.width, allocation.height cr.save() try: cr.set_line_width(1) cr.set_source_rgba(0.0, 0.0, 1.0, 0.6) for g in guides.vertical(): cr.move_to(g, 0) cr.line_to(g, h) cr.stroke() for g in guides.horizontal(): cr.move_to(0, g) cr.line_to(w, g) cr.stroke() finally: cr.restore() # vim:sw=4:et:ai gaphas-0.7.2/gaphas/item.py000066400000000000000000000533461175456763600155750ustar00rootroot00000000000000""" Basic items. """ __version__ = "$Revision$" # $HeadURL$ from math import atan2 from weakref import WeakKeyDictionary try: # python 3.0 (better be prepared) from weakref import WeakSet except ImportError: from weakset import WeakSet from matrix import Matrix from geometry import distance_line_point, distance_rectangle_point from connector import Handle, LinePort from solver import solvable, WEAK, NORMAL, STRONG, VERY_STRONG from constraint import EqualsConstraint, LessThanConstraint, LineConstraint, LineAlignConstraint from state import observed, reversible_method, reversible_pair, reversible_property class Item(object): """ Base class (or interface) for items on a canvas.Canvas. Attributes: - matrix: item's transformation matrix - canvas: canvas, which owns an item - constraints: list of item constraints, automatically registered when the item is added to a canvas; may be extended in subclasses Private: - _canvas: canvas, which owns an item - _handles: list of handles owned by an item - _ports: list of ports, connectable areas of an item - _matrix_i2c: item to canvas coordinates matrix - _matrix_c2i: canvas to item coordinates matrix - _matrix_i2v: item to view coordinates matrices - _matrix_v2i: view to item coordinates matrices - _sort_key: used to sort items - _canvas_projections: used to sort items """ def __init__(self): self._canvas = None self._matrix = Matrix() self._handles = [] self._constraints = [] self._ports = [] # used by gaphas.canvas.Canvas to hold conversion matrices self._matrix_i2c = None self._matrix_c2i = None # used by gaphas.view.GtkView to hold item 2 view matrices (view=key) self._matrix_i2v = WeakKeyDictionary() self._matrix_v2i = WeakKeyDictionary() self._canvas_projections = WeakSet() @observed def _set_canvas(self, canvas): """ Set the canvas. Should only be called from Canvas.add and Canvas.remove(). """ assert not canvas or not self._canvas or self._canvas is canvas if self._canvas: self.teardown_canvas() self._canvas = canvas if canvas: self.setup_canvas() canvas = reversible_property(lambda s: s._canvas, _set_canvas, doc="Canvas owning this item") constraints = property(lambda s: s._constraints, doc="Item constraints") def setup_canvas(self): """ Called when the canvas is set for the item. This method can be used to create constraints. """ add = self.canvas.solver.add_constraint for c in self._constraints: add(c) def teardown_canvas(self): """ Called when the canvas is unset for the item. This method can be used to dispose constraints. """ self.canvas.disconnect_item(self) remove = self.canvas.solver.remove_constraint for c in self._constraints: remove(c) @observed def _set_matrix(self, matrix): """ Set the conversion matrix (parent -> item) """ if not isinstance(matrix, Matrix): matrix = Matrix(*matrix) self._matrix = matrix matrix = reversible_property(lambda s: s._matrix, _set_matrix) def request_update(self, update=True, matrix=True): if self._canvas: self._canvas.request_update(self, update=update, matrix=matrix) def pre_update(self, context): """ Perform any changes before item update here, for example: - change matrix - move handles Gaphas does not guarantee that any canvas invariant is valid at this point (i.e. constraints are not solved, first handle is not in position (0, 0), etc). """ pass def post_update(self, context): """ Method called after item update. If some variables should be used during drawing or in another update, then they should be calculated in post method. Changing matrix or moving handles programmatically is really not advised to be performed here. All canvas invariants are true. """ pass def normalize(self): """ Update handle positions of the item, so the first handle is always located at (0, 0). Note that, since this method basically does some housekeeping during the update phase, there's no need to keep track of the changes. Alternative implementation can also be created, e.g. set (0, 0) in the center of a circle or change it depending on the location of a rotation point. Returns ``True`` if some updates have been done, ``False`` otherwise. See ``canvas._normalize()`` for tests. """ updated = False handles = self._handles if handles: x, y = map(float, handles[0].pos) if x: self.matrix.translate(x, 0) updated = True for h in handles: h.pos.x -= x if y: self.matrix.translate(0, y) updated = True for h in handles: h.pos.y -= y return updated def draw(self, context): """ Render the item to a canvas view. Context contains the following attributes: - cairo: the Cairo Context use this one to draw - view: the view that is to be rendered to - selected, focused, hovered, dropzone: view state of items (True/False) - draw_all: a request to draw everything, for bounding box calculations """ pass def handles(self): """ Return a list of handles owned by the item. """ return self._handles def ports(self): """ Return list of ports. """ return self._ports def point(self, pos): """ Get the distance from a point (``x``, ``y``) to the item. ``x`` and ``y`` are in item coordinates. """ pass def constraint(self, horizontal=None, vertical=None, left_of=None, above=None, line=None, delta=0.0, align=None): """ Utility (factory) method to create item's internal constraint between two positions or between a position and a line. Position is a tuple of coordinates, i.e. ``(2, 4)``. Line is a tuple of positions, i.e. ``((2, 3), (4, 2))``. This method shall not be used to create constraints between two different items. Created constraint is returned. :Parameters: horizontal=(p1, p2) Keep positions ``p1`` and ``p2`` aligned horizontally. vertical=(p1, p2) Keep positions ``p1`` and ``p2`` aligned vertically. left_of=(p1, p2) Keep position ``p1`` on the left side of position ``p2``. above=(p1, p2) Keep position ``p1`` above position ``p2``. line=(p, l) Keep position ``p`` on line ``l``. """ cc = None # created constraint if horizontal: p1, p2 = horizontal cc = EqualsConstraint(p1[1], p2[1], delta) elif vertical: p1, p2 = vertical cc = EqualsConstraint(p1[0], p2[0], delta) elif left_of: p1, p2 = left_of cc = LessThanConstraint(p1[0], p2[0], delta) elif above: p1, p2 = above cc = LessThanConstraint(p1[1], p2[1], delta) elif line: pos, l = line if align is None: cc = LineConstraint(line=l, point=pos) else: cc = LineAlignConstraint(line=l, point=pos, align=align, delta=delta) else: raise ValueError('Constraint incorrectly specified') assert cc is not None self._constraints.append(cc) return cc def __getstate__(self): """ Persist all, but calculated values (``_matrix_?2?``). """ d = dict(self.__dict__) for n in ('_matrix_i2c', '_matrix_c2i', '_matrix_i2v', '_matrix_v2i'): try: del d[n] except KeyError: pass d['_canvas_projections'] = tuple(self._canvas_projections) return d def __setstate__(self, state): """ Set state. No ``__init__()`` is called. """ for n in ('_matrix_i2c', '_matrix_c2i'): setattr(self, n, None) for n in ('_matrix_i2v', '_matrix_v2i'): setattr(self, n, WeakKeyDictionary()) self.__dict__.update(state) self._canvas_projections = WeakSet(state['_canvas_projections']) [ NW, NE, SE, SW ] = xrange(4) class Element(Item): """ An Element has 4 handles (for a start):: NW +---+ NE | | SW +---+ SE """ def __init__(self, width=10, height=10): super(Element, self).__init__() self._handles = [ h(strength=VERY_STRONG) for h in [Handle]*4 ] handles = self._handles h_nw = handles[NW] h_ne = handles[NE] h_sw = handles[SW] h_se = handles[SE] # edge of element define default element ports self._ports = [ LinePort(h_nw.pos, h_ne.pos), LinePort(h_ne.pos, h_se.pos), LinePort(h_se.pos, h_sw.pos), LinePort(h_sw.pos, h_nw.pos) ] # setup constraints self.constraint(horizontal=(h_nw.pos, h_ne.pos)) self.constraint(horizontal=(h_se.pos, h_sw.pos)) self.constraint(vertical=(h_nw.pos, h_sw.pos)) self.constraint(vertical=(h_se.pos, h_ne.pos)) # create minimal size constraints self._c_min_w = self.constraint(left_of=(h_nw.pos, h_se.pos), delta=10) self._c_min_h = self.constraint(above=(h_nw.pos, h_se.pos), delta=10) # set width/height when minimal size constraints exist self.width = width self.height = height def setup_canvas(self): super(Element, self).setup_canvas() # Trigger solver to honour width/height by SE handle pos self._handles[SE].pos.x.dirty() self._handles[SE].pos.y.dirty() def _set_width(self, width): """ >>> b=Element() >>> b.width = 20 >>> b.width 20.0 >>> b._handles[NW].pos.x Variable(0, 40) >>> b._handles[SE].pos.x Variable(20, 40) """ if width < self.min_width: width = self.min_width h = self._handles h[SE].pos.x = h[NW].pos.x + width def _get_width(self): """ Width of the box, calculated as the distance from the left and right handle. """ h = self._handles return float(h[SE].pos.x) - float(h[NW].pos.x) width = property(_get_width, _set_width) def _set_height(self, height): """ >>> b=Element() >>> b.height = 20 >>> b.height 20.0 >>> b.height = 2 >>> b.height 10.0 >>> b._handles[NW].pos.y Variable(0, 40) >>> b._handles[SE].pos.y Variable(10, 40) """ if height < self.min_height: height = self.min_height h = self._handles h[SE].pos.y = h[NW].pos.y + height def _get_height(self): """ Height. """ h = self._handles return float(h[SE].pos.y) - float(h[NW].pos.y) height = property(_get_height, _set_height) @observed def _set_min_width(self, min_width): """ Set minimal width. """ if min_width < 0: raise ValueError, 'Minimal width cannot be less than 0' self._c_min_w.delta = min_width if self.canvas: self.canvas.solver.request_resolve_constraint(self._c_min_w) min_width = reversible_property(lambda s: s._c_min_w.delta, _set_min_width) @observed def _set_min_height(self, min_height): """ Set minimal height. """ if min_height < 0: raise ValueError, 'Minimal height cannot be less than 0' self._c_min_h.delta = min_height if self.canvas: self.canvas.solver.request_resolve_constraint(self._c_min_h) min_height = reversible_property(lambda s: s._c_min_h.delta, _set_min_height) def point(self, pos): """ Distance from the point (x, y) to the item. >>> e = Element() >>> e.point((20, 10)) 10.0 """ h = self._handles pnw, pse = h[NW].pos, h[SE].pos return distance_rectangle_point(map(float, (pnw.x, pnw.y, pse.x, pse.y)), pos) class Line(Item): """ A Line item. Properties: - fuzziness (0.0..n): an extra margin that should be taken into account when calculating the distance from the line (using point()). - orthogonal (bool): wherther or not the line should be orthogonal (only straight angles) - horizontal: first line segment is horizontal - line_width: width of the line to be drawn This line also supports arrow heads on both the begin and end of the line. These are drawn with the methods draw_head(context) and draw_tail(context). The coordinate system is altered so the methods do not have to know about the angle of the line segment (e.g. drawing a line from (10, 10) via (0, 0) to (10, -10) will draw an arrow point). """ def __init__(self): super(Line, self).__init__() self._handles = [Handle(connectable=True), Handle((10, 10), connectable=True)] self._ports = [] self._update_ports() self._line_width = 2 self._fuzziness = 0 self._orthogonal_constraints = [] self._horizontal = False self._head_angle = self._tail_angle = 0 @observed def _set_line_width(self, line_width): self._line_width = line_width line_width = reversible_property(lambda s: s._line_width, _set_line_width) @observed def _set_fuzziness(self, fuzziness): self._fuzziness = fuzziness fuzziness = reversible_property(lambda s: s._fuzziness, _set_fuzziness) def _update_orthogonal_constraints(self, orthogonal): """ Update the constraints required to maintain the orthogonal line. The actual constraints attribute (``_orthogonal_constraints``) is observed, so the undo system will update the contents properly """ if not self.canvas: self._orthogonal_constraints = orthogonal and [ None ] or [] return for c in self._orthogonal_constraints: self.canvas.solver.remove_constraint(c) del self._orthogonal_constraints[:] if not orthogonal: return h = self._handles #if len(h) < 3: # self.split_segment(0) eq = EqualsConstraint #lambda a, b: a - b add = self.canvas.solver.add_constraint cons = [] rest = self._horizontal and 1 or 0 for pos, (h0, h1) in enumerate(zip(h, h[1:])): p0 = h0.pos p1 = h1.pos if pos % 2 == rest: # odd cons.append(add(eq(a=p0.x, b=p1.x))) else: cons.append(add(eq(a=p0.y, b=p1.y))) self.canvas.solver.request_resolve(p1.x) self.canvas.solver.request_resolve(p1.y) self._set_orthogonal_constraints(cons) self.request_update() @observed def _set_orthogonal_constraints(self, orthogonal_constraints): """ Setter for the constraints maintained. Required for the undo system. """ self._orthogonal_constraints = orthogonal_constraints reversible_property(lambda s: s._orthogonal_constraints, _set_orthogonal_constraints) @observed def _set_orthogonal(self, orthogonal): """ >>> a = Line() >>> a.orthogonal False """ if orthogonal and len(self.handles()) < 3: raise ValueError, "Can't set orthogonal line with less than 3 handles" self._update_orthogonal_constraints(orthogonal) orthogonal = reversible_property(lambda s: bool(s._orthogonal_constraints), _set_orthogonal) @observed def _inner_set_horizontal(self, horizontal): self._horizontal = horizontal reversible_method(_inner_set_horizontal, _inner_set_horizontal, {'horizontal': lambda horizontal: not horizontal }) def _set_horizontal(self, horizontal): """ >>> line = Line() >>> line.horizontal False >>> line.horizontal = False >>> line.horizontal False """ self._inner_set_horizontal(horizontal) self._update_orthogonal_constraints(self.orthogonal) horizontal = reversible_property(lambda s: s._horizontal, _set_horizontal) def setup_canvas(self): """ Setup constraints. In this case orthogonal. """ super(Line, self).setup_canvas() self._update_orthogonal_constraints(self.orthogonal) def teardown_canvas(self): """ Remove constraints created in setup_canvas(). """ super(Line, self).teardown_canvas() for c in self._orthogonal_constraints: self.canvas.solver.remove_constraint(c) @observed def _reversible_insert_handle(self, index, handle): self._handles.insert(index, handle) @observed def _reversible_remove_handle(self, handle): self._handles.remove(handle) reversible_pair(_reversible_insert_handle, _reversible_remove_handle, \ bind1={'index': lambda self, handle: self._handles.index(handle)}) @observed def _reversible_insert_port(self, index, port): self._ports.insert(index, port) @observed def _reversible_remove_port(self, port): self._ports.remove(port) reversible_pair(_reversible_insert_port, _reversible_remove_port, \ bind1={'index': lambda self, port: self._ports.index(port)}) def _create_handle(self, pos, strength=WEAK): return Handle(pos, strength=strength) def _create_port(self, p1, p2): return LinePort(p1, p2) def _update_ports(self): """ Update line ports. """ assert len(self._handles) >= 2, 'Not enough segments' self._ports = [] handles = self._handles for h1, h2 in zip(handles[:-1], handles[1:]): self._ports.append(self._create_port(h1.pos, h2.pos)) def opposite(self, handle): """ Given the handle of one end of the line, return the other end. """ handles = self._handles if handle is handles[0]: return handles[-1] elif handle is handles[-1]: return handles[0] else: raise KeyError('Handle is not an end handle') def post_update(self, context): """ """ super(Line, self).post_update(context) h0, h1 = self._handles[:2] p0, p1 = h0.pos, h1.pos self._head_angle = atan2(p1.y - p0.y, p1.x - p0.x) h1, h0 = self._handles[-2:] p1, p0 = h1.pos, h0.pos self._tail_angle = atan2(p1.y - p0.y, p1.x - p0.x) def closest_segment(self, pos): """ Obtain a tuple (distance, point_on_line, segment). Distance is the distance from point to the closest line segment Point_on_line is the reflection of the point on the line. Segment is the line segment closest to (x, y) >>> a = Line() >>> a.closest_segment((4, 5)) (0.70710678118654757, (4.5, 4.5), 0) """ h = self._handles hpos = map(getattr, h, ['pos'] * len(h)) # create a list of (distance, point_on_line) tuples: distances = map(distance_line_point, hpos[:-1], hpos[1:], [pos] * (len(hpos) - 1)) distances, pols = zip(*distances) return reduce(min, zip(distances, pols, range(len(distances)))) def point(self, pos): """ >>> a = Line() >>> a.handles()[1].pos = 25, 5 >>> a._handles.append(a._create_handle((30, 30))) >>> a.point((-1, 0)) 1.0 >>> '%.3f' % a.point((5, 4)) '2.942' >>> '%.3f' % a.point((29, 29)) '0.784' """ distance, point, segment = self.closest_segment(pos) return max(0, distance - self.fuzziness) def draw_head(self, context): """ Default head drawer: move cursor to the first handle. """ context.cairo.move_to(0, 0) def draw_tail(self, context): """ Default tail drawer: draw line to the last handle. """ context.cairo.line_to(0, 0) def draw(self, context): """ Draw the line itself. See Item.draw(context). """ def draw_line_end(pos, angle, draw): cr = context.cairo cr.save() try: cr.translate(*pos) cr.rotate(angle) draw(context) finally: cr.restore() cr = context.cairo cr.set_line_width(self.line_width) draw_line_end(self._handles[0].pos, self._head_angle, self.draw_head) for h in self._handles[1:-1]: cr.line_to(*h.pos) draw_line_end(self._handles[-1].pos, self._tail_angle, self.draw_tail) cr.stroke() ### debug code to draw line ports ### cr.set_line_width(1) ### cr.set_source_rgb(1.0, 0.0, 0.0) ### for p in self.ports(): ### cr.move_to(*p.start) ### cr.line_to(*p.end) ### cr.stroke() __test__ = { 'Line._set_orthogonal': Line._set_orthogonal, } # vim: sw=4:et:ai gaphas-0.7.2/gaphas/matrix.py000066400000000000000000000047511175456763600161370ustar00rootroot00000000000000""" Some Gaphor specific updates to the canvas. This is done by setting the correct properties on gaphas' modules. Matrix ------ Small utility class wrapping cairo.Matrix. The `Matrix` class adds state preservation capabilities. """ __version__ = "$Revision$" # $HeadURL$ import cairo from state import observed, reversible_method class Matrix(object): """ Matrix wrapper. This version sends @observed messages on state changes >>> cairo.Matrix() cairo.Matrix(1, 0, 0, 1, 0, 0) >>> Matrix() Matrix(1, 0, 0, 1, 0, 0) """ def __init__(self, xx=1.0, yx=0.0, xy=0.0, yy=1.0, x0=0.0, y0=0.0): self._matrix = cairo.Matrix(xx, yx, xy, yy, x0, y0) @staticmethod def init_rotate(radians): return cairo.Matrix.init_rotate(radians) @observed def invert(self): return self._matrix.invert() @observed def rotate(self, radians): return self._matrix.rotate(radians) @observed def scale(self, sx, sy): return self._matrix.scale(sx, sy) @observed def translate(self, tx, ty): self._matrix.translate(tx, ty) @observed def multiply(self, m): return self._matrix.multiply(m) reversible_method(invert, invert) reversible_method(rotate, rotate, { 'radians': lambda radians: -radians }) reversible_method(scale, scale, { 'sx': lambda sx: 1/sx, 'sy': lambda sy: 1/sy }) reversible_method(translate, translate, { 'tx': lambda tx: -tx, 'ty': lambda ty: -ty }) def transform_distance(self, dx, dy): self._matrix.transform_distance(dx, dy) def transform_point(self, x, y): self._matrix.transform_point(x, y) def __eq__(self, other): return self._matrix.__eq__(other) def __ne__(self, other): return self._matrix.__ne__(other) def __le__(self, other): return self._matrix.__le__(other) def __lt__(self, other): return self._matrix.__lt__(other) def __ge__(self, other): return self._matrix.__ge__(other) def __gt__(self, other): return self._matrix.__gt__(other) def __getitem__(self, val): return self._matrix.__getitem__(val) @observed def __mul__(self, other): return self._matrix.__mul__(other) @observed def __rmul__(self, other): return self._matrix.__rmul__(other) def __repr__(self): return 'Matrix(%g, %g, %g, %g, %g, %g)' % tuple(self._matrix) # vim:sw=4:et gaphas-0.7.2/gaphas/painter.py000066400000000000000000000252051175456763600162720ustar00rootroot00000000000000""" The painter module provides different painters for parts of the canvas. Painters can be swapped in and out. Each painter takes care of a layer in the canvas (such as grid, items and handles). """ __version__ = "$Revision$" # $HeadURL$ from cairo import Matrix, ANTIALIAS_NONE, LINE_JOIN_ROUND from gaphas.canvas import Context from gaphas.geometry import Rectangle from gaphas.item import Line from gaphas.aspect import PaintFocused DEBUG_DRAW_BOUNDING_BOX = False # The tolerance for Cairo. Bigger values increase speed and reduce accuracy # (default: 0.1) TOLERANCE = 0.8 class Painter(object): """ Painter interface. """ def __init__(self, view=None): self.view = view def set_view(self, view): self.view = view def paint(self, context): """ Do the paint action (called from the View). """ pass class PainterChain(Painter): """ Chain up a set of painters. like ToolChain. """ def __init__(self, view=None): super(PainterChain, self).__init__(view) self._painters = [] def set_view(self, view): self.view = view for painter in self._painters: painter.set_view(self.view) def append(self, painter): """ Add a painter to the list of painters. """ self._painters.append(painter) painter.set_view(self.view) return self def prepend(self, painter): """ Add a painter to the beginning of the list of painters. """ self._painters.insert(0, painter) def paint(self, context): """ See Painter.paint(). """ for painter in self._painters: painter.paint(context) class DrawContext(Context): """ Special context for draw()'ing the item. The draw-context contains stuff like the cairo context and properties like selected and focused. """ deprecated = False def __init__(self, **kwargs): super(DrawContext, self).__init__(**kwargs) class ItemPainter(Painter): draw_all = False def _draw_item(self, item, cairo, area=None): view = self.view cairo.save() try: cairo.set_matrix(view.matrix) cairo.transform(view.canvas.get_matrix_i2c(item)) item.draw(DrawContext(painter=self, cairo=cairo, _area=area, _item=item, selected=(item in view.selected_items), focused=(item is view.focused_item), hovered=(item is view.hovered_item), dropzone=(item is view.dropzone_item), draw_all=self.draw_all)) finally: cairo.restore() def _draw_items(self, items, cairo, area=None): """ Draw the items. """ for item in items: #if not area or area - view.get_item_bounding_box(item): self._draw_item(item, cairo, area=area) if DEBUG_DRAW_BOUNDING_BOX: self._draw_bounds(item, cairo) def _draw_bounds(self, item, cairo): view = self.view try: b = view.get_item_bounding_box(item) except KeyError: pass # No bounding box right now.. else: cairo.save() cairo.identity_matrix() cairo.set_source_rgb(.8, 0, 0) cairo.set_line_width(1.0) cairo.rectangle(*b) cairo.stroke() cairo.restore() def paint(self, context): cairo = context.cairo cairo.set_tolerance(TOLERANCE) cairo.set_line_join(LINE_JOIN_ROUND) self._draw_items(context.items, cairo, context.area) class CairoBoundingBoxContext(object): """ Delegate all calls to the wrapped CairoBoundingBoxContext, intercept ``stroke()``, ``fill()`` and a few others so the bounding box of the item involved can be calculated. """ def __init__(self, cairo): self._cairo = cairo self._bounds = None # a Rectangle object def __getattr__(self, key): return getattr(self._cairo, key) def get_bounds(self): """ Return the bounding box. """ return self._bounds or Rectangle() def _update_bounds(self, bounds): if bounds: if not self._bounds: self._bounds = bounds else: self._bounds += bounds def _extents(self, extents_func, line_width=False): """ Calculate the bounding box for a given drawing operation. if ``line_width`` is True, the current line-width is taken into account. """ cr = self._cairo cr.save() cr.identity_matrix() x0, y0, x1, y1 = extents_func() b = Rectangle(x0, y0, x1=x1, y1=y1) cr.restore() if b and line_width: # Do this after the restore(), so we can get the proper width. lw = cr.get_line_width()/2 d = cr.user_to_device_distance(lw, lw) b.expand(d[0]+d[1]) self._update_bounds(b) return b def fill(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.fill_extents) cr.fill() def fill_preserve(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.fill_extents) def stroke(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.stroke_extents, line_width=True) cr.stroke() def stroke_preserve(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.stroke_extents, line_width=True) def show_text(self, utf8, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: x, y = cr.get_current_point() e = cr.text_extents(utf8) x0, y0 = cr.user_to_device(x+e[0], y+e[1]) x1, y1 = cr.user_to_device(x+e[0]+e[2], y+e[1]+e[3]) b = Rectangle(x0, y0, x1=x1, y1=y1) self._update_bounds(b) cr.show_text(utf8) class BoundingBoxPainter(ItemPainter): """ This specific case of an ItemPainter is used to calculate the bounding boxes (in canvas coordinates) for the items. """ draw_all = True def _draw_item(self, item, cairo, area=None): cairo = CairoBoundingBoxContext(cairo) super(BoundingBoxPainter, self)._draw_item(item, cairo) bounds = cairo.get_bounds() # Update bounding box with handles. view = self.view i2v = view.get_matrix_i2v(item).transform_point for h in item.handles(): cx, cy = i2v(*h.pos) bounds += (cx - 5, cy - 5, 9, 9) bounds.expand(1) view.set_item_bounding_box(item, bounds) def _draw_items(self, items, cairo, area=None): """ Draw the items. """ for item in items: self._draw_item(item, cairo) def paint(self, context): self._draw_items(context.items, context.cairo) class HandlePainter(Painter): """ Draw handles of items that are marked as selected in the view. """ def _draw_handles(self, item, cairo, opacity=None, inner=False): """ Draw handles for an item. The handles are drawn in non-antialiased mode for clearity. """ view = self.view cairo.save() i2v = view.get_matrix_i2v(item) if not opacity: opacity = (item is view.focused_item) and .7 or .4 cairo.set_line_width(1) get_connection = view.canvas.get_connection for h in item.handles(): if not h.visible: continue # connected and not being moved, see HandleTool.on_button_press if get_connection(h): r, g, b = 1, 0, 0 # connected but being moved, see HandleTool.on_button_press elif get_connection(h): r, g, b = 1, 0.6, 0 elif h.movable: r, g, b = 0, 1, 0 else: r, g, b = 0, 0, 1 cairo.identity_matrix() cairo.set_antialias(ANTIALIAS_NONE) cairo.translate(*i2v.transform_point(*h.pos)) cairo.rectangle(-4, -4, 8, 8) if inner: cairo.rectangle(-3, -3, 6, 6) cairo.set_source_rgba(r, g, b, opacity) cairo.fill_preserve() if h.connectable: cairo.move_to(-2, -2) cairo.line_to(2, 3) cairo.move_to(2, -2) cairo.line_to(-2, 3) cairo.set_source_rgba(r/4., g/4., b/4., opacity*1.3) cairo.stroke() cairo.restore() def paint(self, context): view = self.view canvas = view.canvas cairo = context.cairo # Order matters here: for item in canvas.sort(view.selected_items): self._draw_handles(item, cairo) # Draw nice opaque handles when hovering an item: item = view.hovered_item if item and item not in view.selected_items: self._draw_handles(item, cairo, opacity=.25) item = view.dropzone_item if item and item not in view.selected_items: self._draw_handles(item, cairo, opacity=.25, inner=True) class ToolPainter(Painter): """ ToolPainter allows the Tool defined on a view to do some special drawing. """ def paint(self, context): view = self.view cairo = context.cairo if view.tool: cairo.save() cairo.identity_matrix() view.tool.draw(context) cairo.restore() class FocusedItemPainter(Painter): """ This painter allows for drawing on top off all other layers for the focused item. """ def paint(self, context): view = self.view item = view.hovered_item if item and item is view.focused_item: PaintFocused(item, view).paint(context) def DefaultPainter(view=None): """ Default painter, containing item, handle and tool painters. """ return PainterChain(view). \ append(ItemPainter()). \ append(HandlePainter()). \ append(FocusedItemPainter()). \ append(ToolPainter()) # vim: sw=4:et:ai gaphas-0.7.2/gaphas/picklers.py000066400000000000000000000014451175456763600164440ustar00rootroot00000000000000""" Some extra picklers needed to gracefully dump and load a canvas. """ import copy_reg # Allow instancemethod to be pickled: import new def construct_instancemethod(funcname, self, clazz): func = getattr(clazz, funcname) return new.instancemethod(func, self, clazz) def reduce_instancemethod(im): return construct_instancemethod, (im.im_func.__name__, im.im_self, im.im_class) copy_reg.pickle(new.instancemethod, reduce_instancemethod, construct_instancemethod) # Allow cairo.Matrix to be pickled: import cairo def construct_cairo_matrix(*args): return cairo.Matrix(*args) def reduce_cairo_matrix(m): return construct_cairo_matrix, tuple(m) copy_reg.pickle(cairo.Matrix, reduce_cairo_matrix, construct_cairo_matrix) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/quadtree.py000066400000000000000000000272471175456763600164520ustar00rootroot00000000000000""" Quadtree ======== A quadtree is a tree data structure in which each internal node has up to four children. Quadtrees are most often used to partition a two dimensional space by recursively subdividing it into four quadrants or regions. The regions may be square or rectangular, or may have arbitrary shapes. This data structure was named a quadtree by Raphael Finkel and J.L. Bentley in 1974. A similar partitioning is also known as a Q-tree. All forms of Quadtrees share some common features: * They decompose space into adaptable cells. * Each cell (or bucket) has a maximum capacity. When maximum capacity is reached, the bucket splits. * The tree directory follows the spatial decomposition of the Quadtree. (From Wikipedia, the free encyclopedia) """ __version__ = "$Revision$" # $HeadURL$ import operator from geometry import rectangle_contains, rectangle_intersects, rectangle_clip class Quadtree(object): """ The Quad-tree. Rectangles use the same scheme throughout Gaphas: (x, y, width, height). >>> qtree = Quadtree((0, 0, 100, 100)) >>> for i in range(20): ... qtree.add('%d' % i, ((i * 4) % 90, (i * 10) % 90, 10, 10)) >>> len(qtree) 20 >>> qtree.dump() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE <....QuadtreeBucket object at 0x...> (0, 0, 100, 100) 11 (44, 20, 10, 10) 12 (48, 30, 10, 10) <....QuadtreeBucket object at 0x...> (0, 0, 50.0, 50.0) 0 (0, 0, 10, 10) 1 (4, 10, 10, 10) 10 (40, 10, 10, 10) 2 (8, 20, 10, 10) 3 (12, 30, 10, 10) 4 (16, 40, 10, 10) 9 (36, 0, 10, 10) <....QuadtreeBucket object at 0x...> (50.0, 0, 50.0, 50.0) 13 (52, 40, 10, 10) 18 (72, 0, 10, 10) 19 (76, 10, 10, 10) <....QuadtreeBucket object at 0x...> (0, 50.0, 50.0, 50.0) 5 (20, 50, 10, 10) 6 (24, 60, 10, 10) 7 (28, 70, 10, 10) 8 (32, 80, 10, 10) <....QuadtreeBucket object at 0x...> (50.0, 50.0, 50.0, 50.0) 14 (56, 50, 10, 10) 15 (60, 60, 10, 10) 16 (64, 70, 10, 10) 17 (68, 80, 10, 10) Find all items in the tree: >>> sorted(qtree.find_inside((0, 0, 100, 100))) ['0', '1', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '2', '3', '4', '5', '6', '7', '8', '9'] Or just the items in a section of the tree: >>> sorted(qtree.find_inside((40, 40, 40, 40))) ['13', '14', '15', '16'] >>> sorted([qtree.get_bounds(item) for item in qtree.find_inside((40, 40, 40, 40))]) [(52, 40, 10, 10), (56, 50, 10, 10), (60, 60, 10, 10), (64, 70, 10, 10)] >>> sorted(qtree.find_intersect((40, 40, 20, 20))) ['12', '13', '14', '15'] >>> sorted([qtree.get_bounds(item) for item in qtree.find_intersect((40, 40, 20, 20))]) [(48, 30, 10, 10), (52, 40, 10, 10), (56, 50, 10, 10), (60, 60, 10, 10)] >>> qtree.rebuild() """ def __init__(self, bounds=(0, 0, 0, 0), capacity=10): """ Create a new Quadtree instance. Bounds is the boundries of the quadtree. this is fixed and do not change depending on the contents. Capacity defines the number of elements in one tree bucket (default: 10) """ self._capacity = capacity self._bucket = QuadtreeBucket(bounds, capacity) # Easy lookup item->(bounds, data, clipped bounds) mapping self._ids = dict() bounds = property(lambda s: s._bucket.bounds) def resize(self, bounds): """ Resize the tree. The tree structure is rebuild. """ self._bucket = QuadtreeBucket(bounds, self._capacity) self.rebuild() def get_soft_bounds(self): """ Calculate the size of all items in the tree. This size may be beyond the limits of the tree itself. Returns a tuple (x, y, width, height). >>> qtree = Quadtree() >>> qtree.add('1', (10, 20, 30, 40)) >>> qtree.add('2', (20, 30, 40, 10)) >>> qtree.bounds (0, 0, 0, 0) >>> qtree.soft_bounds (10, 20, 50, 40) Quadtree's bounding box is not adjusted: >>> qtree.bounds (0, 0, 0, 0) """ x_y_w_h = zip(*map(operator.getitem, self._ids.itervalues(), [0] * len(self._ids))) if not x_y_w_h: return 0, 0, 0, 0 x0 = min(x_y_w_h[0]) y0 = min(x_y_w_h[1]) add = operator.add x1 = max(map(add, x_y_w_h[0], x_y_w_h[2])) y1 = max(map(add, x_y_w_h[1], x_y_w_h[3])) return (x0, y0, x1 - x0, y1 - y0) soft_bounds = property(get_soft_bounds) def add(self, item, bounds, data=None): """ Add an item to the tree. If an item already exists, its bounds are updated and the item is moved to the right bucket. Data can be used to add some extra info to the item """ # Clip item bounds to fit in top-level bucket # Keep original bounds in _ids, for reference clipped_bounds = rectangle_clip(bounds, self._bucket.bounds) if item in self._ids: old_clip = self._ids[item][2] if old_clip: bucket = self._bucket.find_bucket(old_clip) assert item in bucket.items # Fast lane, if item moved just a little it may still reside # in the same bucket. We do not need to search from top-level. if bucket and clipped_bounds and \ rectangle_contains(clipped_bounds, bucket.bounds): bucket.update(item, clipped_bounds) self._ids[item] = (bounds, data, clipped_bounds) return elif bucket: bucket.remove(item) if clipped_bounds: self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds) self._ids[item] = (bounds, data, clipped_bounds) def remove(self, item): """ Remove an item from the tree. """ bounds, data, clipped_bounds = self._ids[item] del self._ids[item] if clipped_bounds: self._bucket.find_bucket(clipped_bounds).remove(item) def clear(self): """ Remove all items from the tree. """ self._bucket.clear() self._ids.clear() def rebuild(self): """ Rebuild the tree structure. """ # Clean bucket and items: self._bucket.clear() for item, (bounds, data, _) in dict(self._ids).iteritems(): clipped_bounds = rectangle_clip(bounds, self._bucket.bounds) if clipped_bounds: self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds) self._ids[item] = (bounds, data, clipped_bounds) def get_bounds(self, item): """ Return the bounding box for the given item. """ return self._ids[item][0] def get_data(self, item): """ Return the data for the given item, None if no data was provided. """ return self._ids[item][1] def get_clipped_bounds(self, item): """ Return the bounding box for the given item. The bounding box is clipped on the boundries of the tree (provided on construction or with resize()). """ return self._ids[item][2] def find_inside(self, rect): """ Find all items in the given rectangle (x, y, with, height). Returns a set. """ return set(self._bucket.find(rect, method=rectangle_contains)) def find_intersect(self, rect): """ Find all items that intersect with the given rectangle (x, y, width, height). Returns a set. """ return set(self._bucket.find(rect, method=rectangle_intersects)) def __len__(self): """ Return number of items in tree. """ return len(self._ids) def __contains__(self, item): """ Check if an item is in tree. """ return item in self._ids def dump(self): """ Print structure to stdout. """ self._bucket.dump() class QuadtreeBucket(object): """ A node in a Quadtree structure. """ def __init__(self, bounds, capacity): """ Set bounding box for the node as (x, y, width, height). """ self.bounds = bounds self.capacity = capacity self.items = {} self._buckets = [] def add(self, item, bounds): """ Add an item to the quadtree. The bucket is split when nessecary. Items are otherwise added to this bucket, not some sub-bucket. """ assert rectangle_contains(bounds, self.bounds) # create new subnodes if threshold is reached if not self._buckets and len(self.items) >= self.capacity: x, y, w, h = self.bounds rw, rh = w / 2., h / 2. cx, cy = x + rw, y + rh self._buckets = [QuadtreeBucket((x, y, rw, rh), self.capacity), QuadtreeBucket((cx, y, rw, rh), self.capacity), QuadtreeBucket((x, cy, rw, rh), self.capacity), QuadtreeBucket((cx, cy, rw, rh), self.capacity)] # Add items to subnodes items = self.items.items() self.items.clear() for i, b in items: self.find_bucket(b).add(i, b) self.find_bucket(bounds).add(item, bounds) else: self.items[item] = bounds def remove(self, item): """ Remove an item from the quadtree bucket. The item should be contained by *this* bucket (not a sub-bucket). """ del self.items[item] def update(self, item, new_bounds): """ Update the position of an item within the current bucket. The item should live in the current bucket, but may be placed in a sub-bucket. """ assert item in self.items self.remove(item) self.find_bucket(new_bounds).add(item, new_bounds) def find_bucket(self, bounds): """ Find the bucket that holds a bounding box. This method should be used to find a bucket that fits, before add() or remove() is called. """ if self._buckets: sx, sy, sw, sh = self.bounds cx, cy = sx + sw / 2., sy + sh / 2. x, y, w, h = bounds index = 0 if x >= cx: index += 1 elif x + w > cx: return self if y >= cy: index += 2 elif y + h > cy: return self return self._buckets[index].find_bucket(bounds) return self def find(self, rect, method): """ Find all items in the given rectangle (x, y, with, height). Method can be either the contains or intersects function. Returns an iterator. """ if rectangle_intersects(rect, self.bounds): for item, bounds in self.items.iteritems(): if method(bounds, rect): yield item for bucket in self._buckets: for item in bucket.find(rect, method=method): yield item def clear(self): """ Clear the bucket, including sub-buckets. """ del self._buckets[:] self.items.clear() def dump(self, indent=''): print indent, self, self.bounds indent += ' ' for item, bounds in sorted(self.items.iteritems()): print indent, item, bounds for bucket in self._buckets: bucket.dump(indent) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/segment.py000066400000000000000000000205531175456763600162730ustar00rootroot00000000000000""" Allow for easily adding segments to lines. """ from cairo import Matrix, ANTIALIAS_NONE from simplegeneric import generic from gaphas.geometry import distance_point_point_fast, distance_line_point from gaphas.item import Line from gaphas.aspect import HandleFinder, HandleSelection, PaintFocused from gaphas.aspect import ConnectionSink from gaphas.aspect import ItemHandleFinder, ItemHandleSelection, ItemPaintFocused @generic class Segment(object): def __init__(self, item, view): raise TypeError @Segment.when_type(Line) class LineSegment(object): def __init__(self, item, view): self.item = item self.view = view def split(self, pos): item = self.item handles = item.handles() x, y = self.view.get_matrix_v2i(item).transform_point(*pos) for h1, h2 in zip(handles, handles[1:]): xp = (h1.pos.x + h2.pos.x) / 2 yp = (h1.pos.y + h2.pos.y) / 2 if distance_point_point_fast((x,y), (xp, yp)) <= 4: segment = handles.index(h1) handles, ports = self.split_segment(segment) return handles and handles[0] def split_segment(self, segment, count=2): """ Split one item segment into ``count`` equal pieces. Two lists are returned - list of created handles - list of created ports :Parameters: segment Segment number to split (starting from zero). count Amount of new segments to be created (minimum 2). """ item = self.item if segment < 0 or segment >= len(item.ports()): raise ValueError('Incorrect segment') if count < 2: raise ValueError('Incorrect count of segments') def do_split(segment, count): handles = item.handles() p0 = handles[segment].pos p1 = handles[segment + 1].pos dx, dy = p1.x - p0.x, p1.y - p0.y new_h = item._create_handle((p0.x + dx / count, p0.y + dy / count)) item._reversible_insert_handle(segment + 1, new_h) p0 = item._create_port(p0, new_h.pos) p1 = item._create_port(new_h.pos, p1) item._reversible_remove_port(item.ports()[segment]) item._reversible_insert_port(segment, p0) item._reversible_insert_port(segment + 1, p1) if count > 2: do_split(segment + 1, count - 1) do_split(segment, count) # force orthogonal constraints to be recreated item._update_orthogonal_constraints(item.orthogonal) self._recreate_constraints() handles = item.handles()[segment + 1:segment + count] ports = item.ports()[segment:segment + count - 1] return handles, ports def merge_segment(self, segment, count=2): """ Merge two (or more) item segments. Tuple of two lists is returned, list of deleted handles and list of deleted ports. :Parameters: segment Segment number to start merging from (starting from zero). count Amount of segments to be merged (minimum 2). """ item = self.item if len(item.ports()) < 2: raise ValueError('Cannot merge item with one segment') if segment < 0 or segment >= len(item.ports()): raise ValueError('Incorrect segment') if count < 2 or segment + count > len(item.ports()): raise ValueError('Incorrect count of segments') # remove handle and ports which share position with handle deleted_handles = item.handles()[segment + 1:segment + count] deleted_ports = item.ports()[segment:segment + count] for h in deleted_handles: item._reversible_remove_handle(h) for p in deleted_ports: item._reversible_remove_port(p) # create new port, which replaces old ports destroyed due to # deleted handle p1 = item.handles()[segment].pos p2 = item.handles()[segment + 1].pos port = item._create_port(p1, p2) item._reversible_insert_port(segment, port) # force orthogonal constraints to be recreated item._update_orthogonal_constraints(item.orthogonal) self._recreate_constraints() return deleted_handles, deleted_ports def _recreate_constraints(self): """ Create connection constraints between connecting lines and an item. :Parameters: connected Connected item. """ connected = self.item def find_port(line, handle, item): #port = None #max_dist = sys.maxint canvas = item.canvas ix, iy = canvas.get_matrix_i2i(line, item).transform_point(*handle.pos) # find the port using item's coordinates sink = ConnectionSink(item, None) return sink.find_port((ix, iy)) if not connected.canvas: # No canvas, no constraints return canvas = connected.canvas for cinfo in list(canvas.get_connections(connected=connected)): item, handle = cinfo.item, cinfo.handle port = find_port(item, handle, connected) constraint = port.constraint(canvas, item, handle, connected) cinfo = canvas.get_connection(handle) canvas.reconnect_item(item, handle, constraint=constraint) @HandleFinder.when_type(Line) class SegmentHandleFinder(ItemHandleFinder): """ Find a handle on a line, create a new one if the mouse is located between two handles. The position aligns with the points drawn by the SegmentPainter. """ def get_handle_at_point(self, pos): view = self.view item = view.hovered_item handle = None if self.item is view.focused_item: try: segment = Segment(self.item, self.view) except TypeError: pass else: handle = segment.split(pos) if not handle: item, handle = super(SegmentHandleFinder, self).get_handle_at_point(pos) return item, handle @HandleSelection.when_type(Line) class SegmentHandleSelection(ItemHandleSelection): """ In addition to the default behaviour, merge segments if the handle is released. """ def unselect(self): item = self.item handle = self.handle handles = item.handles() # don't merge using first or last handle if handles[0] is handle or handles[-1] is handle: return True handle_index = handles.index(handle) segment = handle_index - 1 # cannot merge starting from last segment if segment == len(item.ports()) - 1: segment =- 1 assert segment >= 0 and segment < len(item.ports()) - 1 before = handles[handle_index - 1] after = handles[handle_index + 1] d, p = distance_line_point(before.pos, after.pos, handle.pos) if d < 2: assert len(self.view.canvas.solver._marked_cons) == 0 Segment(item, self.view).merge_segment(segment) if handle: item.request_update() @PaintFocused.when_type(Line) class LineSegmentPainter(ItemPaintFocused): """ This painter draws pseudo-hanldes on gaphas.item.Line objects. Each line can be split by dragging those points, which will result in a new handle. ConnectHandleTool take care of performing the user interaction required for this feature. """ def paint(self, context): view = self.view item = view.hovered_item if item and item is view.focused_item: cr = context.cairo h = item.handles() for h1, h2 in zip(h[:-1], h[1:]): p1, p2 = h1.pos, h2.pos cx = (p1.x + p2.x) / 2 cy = (p1.y + p2.y) / 2 cr.save() cr.identity_matrix() m = Matrix(*view.get_matrix_i2v(item)) cr.set_antialias(ANTIALIAS_NONE) cr.translate(*m.transform_point(cx, cy)) cr.rectangle(-3, -3, 6, 6) cr.set_source_rgba(0, 0.5, 0, .4) cr.fill_preserve() cr.set_source_rgba(.25, .25, .25, .6) cr.set_line_width(1) cr.stroke() cr.restore() # vim:sw=4:et:ai gaphas-0.7.2/gaphas/solver.py000066400000000000000000000455041175456763600161460ustar00rootroot00000000000000""" Constraint solver allows to define constraint between two or more different variables and keep this constraint always true when one or more of the constrained variables change. For example, one may want to keep two variables always equal. Variables change and at some point of time we want to make all constraints valid again. This process is called solving constraints. Gaphas' solver allows to define constraints between Variable instances. Constraint classes are defined in `gaphas.constraint` module. How It Works ------------ Every constraint contains list of variables and has to be registered in solver object. Variables change (`Variable.dirty()`, `Solver.request_resolve()` methods) and their constraints are marked by solver as dirty. To solve constraints, solver loops through dirty constraints and asks constraint for a variable (called weakest variable), which - has the lowest strength - or if there are many variables with the same, lowest strength value return first unchanged variable with lowest strength - or if there is no unchanged, then return the first changed with the lowest strength (weakest variable invariants defined above) Having weakest variable (`constraint.Constraint.weakest()` method) every constraint is being asked to solve itself (`constraint.Constraint.solve_for()` method) changing appropriate variables to make the constraint valid again. """ from __future__ import division __version__ = "$Revision$" # $HeadURL$ from operator import isCallable from state import observed, reversible_pair, reversible_property # epsilon for float comparison # is simple abs(x - y) > EPSILON enough for canvas needs? EPSILON = 1e-6 # Variable Strengths: VERY_WEAK = 0 WEAK = 10 NORMAL = 20 STRONG = 30 VERY_STRONG = 40 REQUIRED = 100 class Variable(object): """ Representation of a variable in the constraint solver. Each Variable has a @value and a @strength. Ina constraint the weakest variables are changed. You can even do some calculating with it. The Variable always represents a float variable. """ def __init__(self, value=0.0, strength=NORMAL): self._value = float(value) self._strength = strength # These variables are set by the Solver: self._solver = None self._constraints = set() @observed def _set_strength(self, strength): self._strength = strength for c in self._constraints: c.create_weakest_list() strength = reversible_property(lambda s: s._strength, _set_strength) def dirty(self): """ Mark the variable dirty in both the constraint solver and attached constraints. Variables are marked dirty also during constraints solving to solve all dependent constraints, i.e. two equals constraints between 3 variables. """ solver = self._solver if not solver: return solver.request_resolve(self) @observed def set_value(self, value): oldval = self._value if abs(oldval - value) > EPSILON: #print id(self), oldval, value self._value = float(value) self.dirty() value = reversible_property(lambda s: s._value, set_value) def __str__(self): return 'Variable(%g, %d)' % (self._value, self._strength) __repr__ = __str__ def __float__(self): return float(self._value) def __eq__(self, other): """ >>> Variable(5) == 5 True >>> Variable(5) == 4 False >>> Variable(5) != 5 False """ return abs(self._value - other) < EPSILON def __ne__(self, other): """ >>> Variable(5) != 4 True >>> Variable(5) != 5 False """ return abs(self._value - other) > EPSILON def __gt__(self, other): """ >>> Variable(5) > 4 True >>> Variable(5) > 5 False """ return self._value.__gt__(float(other)) def __lt__(self, other): """ >>> Variable(5) < 4 False >>> Variable(5) < 6 True """ return self._value.__lt__(float(other)) def __ge__(self, other): """ >>> Variable(5) >= 5 True """ return self._value.__ge__(float(other)) def __le__(self, other): """ >>> Variable(5) <= 5 True """ return self._value.__le__(float(other)) def __add__(self, other): """ >>> Variable(5) + 4 9.0 """ return self._value.__add__(float(other)) def __sub__(self, other): """ >>> Variable(5) - 4 1.0 >>> Variable(5) - Variable(4) 1.0 """ return self._value.__sub__(float(other)) def __mul__(self, other): """ >>> Variable(5) * 4 20.0 >>> Variable(5) * Variable(4) 20.0 """ return self._value.__mul__(float(other)) def __floordiv__(self, other): """ >>> Variable(21) // 4 5.0 >>> Variable(21) // Variable(4) 5.0 """ return self._value.__floordiv__(float(other)) def __mod__(self, other): """ >>> Variable(5) % 4 1.0 >>> Variable(5) % Variable(4) 1.0 """ return self._value.__mod__(float(other)) def __divmod__(self, other): """ >>> divmod(Variable(21), 4) (5.0, 1.0) >>> divmod(Variable(21), Variable(4)) (5.0, 1.0) """ return self._value.__divmod__(float(other)) def __pow__(self, other): """ >>> pow(Variable(5), 4) 625.0 >>> pow(Variable(5), Variable(4)) 625.0 """ return self._value.__pow__(float(other)) def __div__(self, other): """ >>> Variable(5) / 4. 1.25 >>> Variable(5) / Variable(4) 1.25 """ return self._value.__div__(float(other)) def __truediv__(self, other): """ >>> Variable(5.) / 4 1.25 >>> 10 / Variable(5.) 2.0 """ return self._value.__truediv__(float(other)) # .. And the other way around: def __radd__(self, other): """ >>> 4 + Variable(5) 9.0 >>> Variable(4) + Variable(5) 9.0 """ return self._value.__radd__(float(other)) def __rsub__(self, other): """ >>> 6 - Variable(5) 1.0 """ return self._value.__rsub__(other) def __rmul__(self, other): """ >>> 4 * Variable(5) 20.0 """ return self._value.__rmul__(other) def __rfloordiv__(self, other): """ >>> 21 // Variable(4) 5.0 """ return self._value.__rfloordiv__(other) def __rmod__(self, other): """ >>> 5 % Variable(4) 1.0 """ return self._value.__rmod__(other) def __rdivmod__(self, other): """ >>> divmod(21, Variable(4)) (5.0, 1.0) """ return self._value.__rdivmod__(other) def __rpow__(self, other): """ >>> pow(4, Variable(5)) 1024.0 """ return self._value.__rpow__(other) def __rdiv__(self, other): """ >>> 5 / Variable(4.) 1.25 """ return self._value.__rdiv__(other) def __rtruediv__(self, other): """ >>> 5. / Variable(4) 1.25 """ return self._value.__rtruediv__(other) class Projection(object): """ Projections are used to convert values from one space to another, e.g. from Canvas to Item space or visa versa. In order to be a Projection the ``value`` and ``strength`` properties should be implemented and a method named ``variable()`` should be present. Projections should inherit from this class. Projections may be nested. This default implementation projects a variable to it's own: >>> v = Variable(4.0) >>> v Variable(4, 20) >>> p = Projection(v) >>> p.value 4.0 >>> p.value = -1 >>> p.value -1.0 >>> v.value -1.0 >>> p.strength 20 >>> p.variable() Variable(-1, 20) """ def __init__(self, var): self._var = var def _set_value(self, value): self._var.value = value value = property(lambda s: s._var.value, _set_value) strength = property(lambda s: s._var.strength) def variable(self): """ Return the variable owned by the projection. """ return self._var def __float__(self): return float(self.variable()._value) def __str__(self): return '%s(%s)' % (self.__class__.__name__, self.variable()) __repr__ = __str__ class Solver(object): """ Solve constraints. A constraint should have accompanying variables. """ def __init__(self): # a dict of constraint -> name/variable mappings self._constraints = set() self._marked_cons = [] self._solving = False constraints = property(lambda s: s._constraints) def request_resolve(self, variable, projections_only=False): """ Mark a variable as "dirty". This means it it solved the next time the constraints are resolved. If projections_only is set to True, only constraints using the variable through a Projection instance (e.i. variable itself is not in `constraint.Constraint.variables()`) are marked. Example: >>> from constraint import EquationConstraint >>> a, b, c = Variable(1.0), Variable(2.0), Variable(3.0) >>> s = Solver() >>> c_eq = EquationConstraint(lambda a,b: a+b, a=a, b=b) >>> s.add_constraint(c_eq) EquationConstraint(, a=Variable(1, 20), b=Variable(2, 20)) >>> c_eq._weakest [Variable(1, 20), Variable(2, 20)] >>> s._marked_cons [EquationConstraint(, a=Variable(1, 20), b=Variable(2, 20))] >>> a.value=5.0 >>> c_eq.weakest() Variable(2, 20) >>> b.value=2.0 >>> c_eq.weakest() Variable(2, 20) >>> a.value=5.0 >>> c_eq.weakest() Variable(2, 20) """ # Peel of Projections: while isinstance(variable, Projection): variable = variable.variable() for c in variable._constraints: if not projections_only or c._solver_has_projections: if not self._solving: if c in self._marked_cons: self._marked_cons.remove(c) c.mark_dirty(variable) self._marked_cons.append(c) else: c.mark_dirty(variable) self._marked_cons.append(c) if self._marked_cons.count(c) > 100: raise JuggleError, 'Variable juggling detected, constraint %s resolved %d times out of %d' % (c, self._marked_cons.count(c), len(self._marked_cons)) @observed def add_constraint(self, constraint): """ Add a constraint. The actual constraint is returned, so the constraint can be removed later on. Example: >>> from constraint import EquationConstraint >>> s = Solver() >>> a, b = Variable(), Variable(2.0) >>> s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b)) EquationConstraint(, a=Variable(0, 20), b=Variable(2, 20)) >>> len(s._constraints) 1 >>> a.value 0.0 >>> b.value 2.0 >>> len(s._constraints) 1 """ assert constraint, 'No constraint (%s)' % (constraint,) self._constraints.add(constraint) self._marked_cons.append(constraint) constraint._solver_has_projections = False for v in constraint.variables(): while isinstance(v, Projection): v = v.variable() constraint._solver_has_projections = True v._constraints.add(constraint) v._solver = self #print 'added constraint', constraint return constraint @observed def remove_constraint(self, constraint): """ Remove a constraint from the solver >>> from constraint import EquationConstraint >>> s = Solver() >>> a, b = Variable(), Variable(2.0) >>> c = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b)) >>> c EquationConstraint(, a=Variable(0, 20), b=Variable(2, 20)) >>> s.remove_constraint(c) >>> s._marked_cons [] >>> s._constraints set([]) Removing a constraint twice has no effect: >>> s.remove_constraint(c) """ assert constraint, 'No constraint (%s)' % (constraint,) for v in constraint.variables(): while isinstance(v, Projection): v = v.variable() v._constraints.discard(constraint) self._constraints.discard(constraint) while constraint in self._marked_cons: self._marked_cons.remove(constraint) reversible_pair(add_constraint, remove_constraint) def request_resolve_constraint(self, c): """ Request resolving a constraint. """ self._marked_cons.append(c) def constraints_with_variable(self, *variables): """ Return an iterator of constraints that work with variable. The variable in question should be exposed by the constraints `constraint.Constraint.variables()` method. >>> from constraint import EquationConstraint >>> s = Solver() >>> a, b, c = Variable(), Variable(2.0), Variable(4.0) >>> eq_a_b = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b)) >>> eq_a_b EquationConstraint(, a=Variable(0, 20), b=Variable(2, 20)) >>> eq_a_c = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=c)) >>> eq_a_c EquationConstraint(, a=Variable(0, 20), b=Variable(4, 20)) And now for some testing: >>> eq_a_b in s.constraints_with_variable(a) True >>> eq_a_c in s.constraints_with_variable(a) True >>> eq_a_b in s.constraints_with_variable(a, b) True >>> eq_a_c in s.constraints_with_variable(a, b) False Using another variable with the same value does not work: >>> d = Variable(2.0) >>> eq_a_b in s.constraints_with_variable(a, d) False This also works for projections: >>> eq_pr_a_b = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=Projection(a), b=Projection(b))) >>> eq_pr_a_b # doctest: +ELLIPSIS EquationConstraint(, a=Projection(Variable(0, 20)), b=Projection(Variable(2, 20))) >>> eq_pr_a_b in s.constraints_with_variable(a, b) True >>> eq_pr_a_b in s.constraints_with_variable(a, c) False >>> eq_pr_a_b in s.constraints_with_variable(a, d) False """ # Use a copy of the original set, so constraints may be # deleted in the meantime. variables = set(variables) for c in set(self._constraints): if variables.issubset(set(c.variables())): yield c elif c._solver_has_projections: found = True for v in c.variables(): if v in variables: continue while isinstance(v, Projection): v = v.variable() if v in variables: break else: found = False if not found: break # quit for loop, variable not in constraint else: # All iteration have completed succesfully, # so all variables are in the constraint yield c def solve(self): """ Example: >>> from constraint import EquationConstraint >>> a, b, c = Variable(1.0), Variable(2.0), Variable(3.0) >>> s = Solver() >>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=a, b=b)) EquationConstraint(, a=Variable(1, 20), b=Variable(2, 20)) >>> a.value = 5.0 >>> s.solve() >>> len(s._marked_cons) 0 >>> b._value -5.0 >>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=b, b=c)) EquationConstraint(, a=Variable(-5, 20), b=Variable(3, 20)) >>> len(s._constraints) 2 >>> len(s._marked_cons) 1 >>> b._value -5.0 >>> s.solve() >>> b._value -3.0 >>> a.value = 10 >>> s.solve() >>> c._value 10.0 """ marked_cons = self._marked_cons try: self._solving = True # Solve each constraint. Using a counter makes it # possible to also solve constraints that are marked as # a result of other variabled being solved. n = 0 while n < len(marked_cons): c = marked_cons[n] if not c.disabled: wvar = c.weakest() c.solve_for(wvar) n += 1 self._marked_cons = [] finally: self._solving = False class solvable(object): """ Easy-to-use drop Variable descriptor. >>> class A(object): ... x = solvable(varname='_v_x') ... y = solvable(STRONG) ... def __init__(self): ... self.x = 12 >>> a = A() >>> a.x Variable(12, 20) >>> a._v_x Variable(12, 20) >>> a.x = 3 >>> a.x Variable(3, 20) >>> a.y Variable(0, 30) """ def __init__(self, strength=NORMAL, varname=None): self._strength = strength self._varname = varname or '_variable_%x' % id(self) def __get__(self, obj, class_=None): if not obj: return self try: return getattr(obj, self._varname) except AttributeError: setattr(obj, self._varname, Variable(strength=self._strength)) return getattr(obj, self._varname) def __set__(self, obj, value): try: getattr(obj, self._varname).value = float(value) except AttributeError: v = Variable(strength=self._strength) setattr(obj, self._varname, v) v.value = value class JuggleError(AssertionError): """ Variable juggling exception. Raised when constraint's variables are marking each other dirty forever. """ __test__ = { 'Solver.add_constraint': Solver.add_constraint, 'Solver.remove_constraint': Solver.remove_constraint, } # vim:sw=4:et:ai gaphas-0.7.2/gaphas/state.py000066400000000000000000000176371175456763600157620ustar00rootroot00000000000000""" This module is the central point where Gaphas' classes report their state changes. Invokations of method and state changing properties are emited to all functions (or bound methods) registered in the 'observers' set. Use `observers.add()` and `observers.remove()` to add/remove handlers. This module also contains a second layer: a state inverser. Instead of emiting the invoked method, it emits a signal (callable, \*\*kwargs) that can be applied to revert the state of the object to the point before the method invokation. For this to work the revert_handler has to be added to the observers set:: gaphas.state.observers.add(gaphas.state.revert_handler) """ import types, inspect import threading from decorator import decorator # This string is added to each docstring in order to denote is's observed #OBSERVED_DOCSTRING = \ # '\n\n This method is @observed. See gaphas.state for extra info.\n' # Tell @observed to dispatch invokation messages by default # May be changed (but be sure to do that right at the start of your # application,otherwise you have no idea what's enabled and what's not!) DISPATCH_BY_DEFAULT = True # Add/remove methods from this subscribers list. # Subscribers should have signature method(event) where event is a # Event has the form: (func, keywords) # Since most events originate from methods, it's save to call # saveapply(func, keywords) for those functions subscribers = set() # Subscribe to low-level change events: observers = set() # Perform locking (should be per thread?). mutex = threading.Lock() def observed(func): """ Simple observer, dispatches events to functions registered in the observers list. On the function an ``__observer__`` property is set, which references to the observer decorator. This is nessesary, since the event handlers expect the outer most function to be returned (that's what they see). Also note that the events are dispatched *before* the function is invoked. This is an important feature, esp. for the reverter code. """ def wrapper(func, *args, **kwargs): o = func.__observer__ acquired = mutex.acquire(False) try: if acquired: dispatch((o, args, kwargs), queue=observers) return func(*args, **kwargs) finally: if acquired: mutex.release() dec = decorator(wrapper)(func) func.__observer__ = dec return dec def dispatch(event, queue): """ Dispatch an event to a queue of event handlers. Event handlers should have signature: handler(event). >>> def handler(event): ... print 'event handled', event >>> observers.add(handler) >>> @observed ... def callme(): ... pass >>> callme() # doctest: +ELLIPSIS event handled (, (), {}) >>> class Callme(object): ... @observed ... def callme(self): ... pass >>> Callme().callme() # doctest: +ELLIPSIS event handled (>> observers.remove(handler) >>> callme() """ for s in queue: s(event) _reverse = dict() def reversible_function(func, reverse, bind={}): """ Straight forward reversible method, if func is invoked, reverse is dispatched with bind as arguments. """ global _reverse func = getfunction(func) _reverse[func] = (reverse, inspect.getargspec(reverse), bind) reversible_method = reversible_function def reversible_pair(func1, func2, bind1={}, bind2={}): """ Treat a pair of functions (func1 and func2) as each others inverse operation. bind1 provides arguments that can overrule the default values (or add additional values). bind2 does the same for func2. See `revert_handler()` for doctesting. """ global _reverse # We need the function, since that's what's in the events func1 = getfunction(func1) func2 = getfunction(func2) _reverse[func1] = (func2, inspect.getargspec(func2), bind2) _reverse[func2] = (func1, inspect.getargspec(func1), bind1) def reversible_property(fget=None, fset=None, fdel=None, doc=None, bind={}): """ Replacement for the property descriptor. In addition to creating a property instance, the property is registered as reversible and reverse events can be send out when changes occur. Cave eat: we can't handle both fset and fdel in the proper way. Therefore fdel should somehow invoke fset. (persinally, I hardly use fdel) See revert_handler() for doctesting. """ # given fset, read the value argument name (second arg) and create a # bind {value: lambda self: fget(self)} # TODO! handle fdel if fset: spec = inspect.getargspec(fset) argnames = spec[0] assert len(argnames) == 2 argself, argvalue = argnames func = getfunction(fset) b = { argvalue: eval("lambda %(self)s: fget(%(self)s)" % {'self': argself }, {'fget': fget}) } b.update(bind) _reverse[func] = (func, spec, b) return property(fget=fget, fset=fset, fdel=fdel, doc=doc) def revert_handler(event): """ Event handler, generates undoable statements and puts them on the subscribers queue. First thing to do is to actually enable the revert_handler: >>> observers.add(revert_handler) First let's define our simple list: >>> class SList(object): ... def __init__(self): ... self.l = list() ... def add(self, node, before=None): ... if before: self.l.insert(self.l.index(before), node) ... else: self.l.append(node) ... add = observed(add) ... @observed ... def remove(self, node): ... self.l.remove(self.l.index(node)) >>> sl = SList() >>> sl.add(10) >>> sl.l [10] >>> sl.add(11) >>> sl.l [10, 11] >>> sl.add(12, before=11) >>> sl.l [10, 12, 11] It works, so let's add some reversible stuff: >>> reversible_pair(SList.add, SList.remove, \ bind1={'before': lambda self, node: self.l[self.l.index(node)+1] }) >>> def handler(event): ... print 'handle', event >>> subscribers.add(handler) >>> sl.add(20) # doctest: +ELLIPSIS handle (>> class PropTest(object): ... def __init__(self): self._a = 0 ... @observed ... def _set_a(self, value): self._a = value ... a = reversible_property(lambda s: s._a, _set_a) >>> pt = PropTest() >>> pt.a 0 >>> pt.a = 10 # doctest: +ELLIPSIS handle (, {'self': , 'value': 0}) >>> subscribers.remove(handler) """ global _reverse func, args, kwargs = event spec = inspect.getargspec(func) reverse, revspec, bind = _reverse.get(func, (None, None, {})) if not reverse: return kw = dict(kwargs) kw.update(dict(zip(spec[0], args))) for arg, binding in bind.iteritems(): kw[arg] = saveapply(binding, kw) argnames = list(revspec[0]) if spec[1]: argnames.append(revspec[1]) if spec[2]: argnames.append(revspec[2]) kwargs = {} for arg in argnames: kwargs[arg] = kw.get(arg) dispatch((reverse, kwargs), queue=subscribers) def saveapply(func, kw): """ Do apply a set of keywords to a method or function. The function names should be known at meta-level, since arguments are applied as func(\*\*kwargs). """ spec = inspect.getargspec(func) argnames = list(spec[0]) if spec[1]: argnames.append(spec[1]) if spec[2]: argnames.append(spec[2]) kwargs = {} for arg in argnames: kwargs[arg] = kw.get(arg) return func(**kwargs) def getfunction(func): """ Return the function associated with a class method. """ if isinstance(func, types.UnboundMethodType): return func.im_func return func # vim:sw=4:et:ai gaphas-0.7.2/gaphas/table.py000066400000000000000000000132111175456763600157110ustar00rootroot00000000000000""" Table is a storage class that can be used to store information, like one would in a database table, with indexes on the desired "columns." """ class Table(object): """ A Table structure with indexing. Optimized for lookups. """ def __init__(self, columns, indexes): """ Create a new Store instance with columns and indexes: >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (2,)) """ fields = columns._fields self._type = columns self._indexes = tuple(fields[i] for i in indexes) # create data structure, which acts as cache index = {} for n in fields: index[n] = dict() self._index = index columns = property(lambda s: s._type) def insert(self, *values): """ Add a set of values to the store. >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (1, 2,)) >>> s.insert('a', 'b', 'c') >>> s.insert(1, 2, 3) The number of values should match the number of columns defined at construction time. >>> s.insert('x', 'z') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Number of arguments doesn't match the number of columns (2 != 3) """ if len(values) != len(self._type._fields): raise ValueError, "Number of arguments doesn't match the number of columns (%d != %d)" % (len(values), len(self._type._fields)) # Add value to index entries index = self._index data = self._type._make(values) for n in self._indexes: v = getattr(data, n) if v in index[n]: index[n][v].add(data) else: index[n][v] = set([data]) def delete(self, *_row, **kv): """ Remove value from the table. Either a complete set may be given or just one entry in "column=value" style. >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (0, 1,)) >>> s.insert('a', 'b', 'c') >>> s.insert(1, 2, 3) >>> s.insert('a', 'v', 'd') >>> list(s.query(foo='a')) [C(foo='a', bar='b', baz='c'), C(foo='a', bar='v', baz='d')] >>> s.delete('a', 'b', 'c') >>> list(s.query(foo='a')) [C(foo='a', bar='v', baz='d')] Query style: >>> s.insert('a', 'b', 'c') >>> list(s.query(foo='a')) [C(foo='a', bar='b', baz='c'), C(foo='a', bar='v', baz='d')] >>> s.delete(foo='a') >>> list(s.query(foo='a')) [] >>> list(s.query(foo=1)) [C(foo=1, bar=2, baz=3)] Delete a non existent value: >>> s.delete(foo='notPresent') Cannot provide both a row and a query value: >>> s.delete(('x', 'z'), foo=1) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Should either provide a row or a query statement, not both """ fields = self._type._fields if _row and kv: raise ValueError, "Should either provide a row or a query statement, not both" if _row: assert len(_row) == len(fields) kv = dict(zip(self._indexes, _row)) rows = list(self.query(**kv)) index = self._index for row in rows: for i, n in enumerate(self._indexes): v = row[i] if v in index[n]: index[n][v].remove(row) if len(index[n][v]) == 0: del index[n][v] def query(self, **kv): """ Get rows (tuples) for each key defined. An iterator is returned. >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (0, 1,)) >>> s.insert('a', 'b', 'c') >>> s.insert(1, 2, 3) >>> s.insert('a', 'v', 'd') >>> list(s.query(foo='a')) [C(foo='a', bar='b', baz='c'), C(foo='a', bar='v', baz='d')] >>> list(s.query(foo='a', bar='v')) [C(foo='a', bar='v', baz='d')] >>> list(s.query(foo='a', bar='q')) [] >>> list(s.query(bar=2)) [C(foo=1, bar=2, baz=3)] >>> list(s.query(foo=42)) [] >>> list(s.query(invalid_column_name=42)) # doctest: +ELLIPSIS Traceback (most recent call last): ... KeyError: "Invalid column 'invalid_column_name'" >>> list(s.query(baz=42)) # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: Column 'baz' is not indexed """ index = self._index bad = set(kv.keys()) - set(self._type._fields) if len(bad) == 1: raise KeyError("Invalid column '%s'" % bad.pop()) elif len(bad) > 1: raise KeyError("Invalid columns '%s'" % str(tuple(bad))) bad = set(kv.keys()) - set(self._indexes) if len(bad) == 1: raise AttributeError("Column '%s' is not indexed" % bad.pop()) elif len(bad) > 1: raise AttributeError("Columns %s are not indexed" % str(tuple(bad))) r = iter([]) items = tuple((n, v) for n, v in kv.items() if v is not None) if all(v in index[n] for n, v in items): rows = (index[n][v] for n, v in items) try: r = iter(reduce(set.intersection, rows)) except TypeError, ex: pass return r # vi:sw=4:et:ai gaphas-0.7.2/gaphas/tests/000077500000000000000000000000001175456763600154145ustar00rootroot00000000000000gaphas-0.7.2/gaphas/tests/__init__.py000066400000000000000000000000001175456763600175130ustar00rootroot00000000000000gaphas-0.7.2/gaphas/tests/test_aspect.py000066400000000000000000000024051175456763600203050ustar00rootroot00000000000000""" Generic gaphas item tests. """ import unittest from gaphas.item import Item from gaphas.aspect import * from gaphas.canvas import Canvas from gaphas.view import View class AspectTestCase(unittest.TestCase): """ Test aspects for items. """ def setUp(self): self.canvas = Canvas() self.view = View(self.canvas) def test_selection_select(self): """ Test the Selection role methods """ view = self.view item = Item() self.canvas.add(item) selection = Selection(item, view) assert item not in view.selected_items selection.select() assert item in view.selected_items assert item is view.focused_item selection.unselect() assert item not in view.selected_items assert None is view.focused_item def test_selection_move(self): """ Test the Selection role methods """ view = self.view item = Item() self.canvas.add(item) inmotion = InMotion(item, view) self.assertEquals((1, 0, 0, 1, 0, 0), tuple(item.matrix)) inmotion.start_move((0, 0)) inmotion.move((12, 26)) self.assertEquals((1, 0, 0, 1, 12, 26), tuple(item.matrix)) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_canvas.py000066400000000000000000000233311175456763600203020ustar00rootroot00000000000000import unittest from gaphas.canvas import Canvas, ConnectionError from gaphas.examples import Box from gaphas.item import Line, Handle from gaphas.constraint import BalanceConstraint, EqualsConstraint import cairo class MatricesTestCase(unittest.TestCase): def test_update_matrices(self): """Test updating of matrices""" c = Canvas() i = Box() ii = Box() c.add(i) c.add(ii, i) i.matrix = (1.0, 0.0, 0.0, 1.0, 5.0, 0.0) ii.matrix = (1.0, 0.0, 0.0, 1.0, 0.0, 8.0) updated = c.update_matrices([i]) self.assertEquals(i._matrix_i2c, cairo.Matrix(1, 0, 0, 1, 5, 0)) self.assertEquals(ii._matrix_i2c, cairo.Matrix(1, 0, 0, 1, 5, 8)) def test_reparent(self): c = Canvas() b1 = Box() b2 = Box() c.add(b1) c.add(b2, b1) c.reparent(b2, None) # fixme: what about multiple constraints for a handle? # what about 1d projection? def count(i): return len(list(i)) class CanvasTestCase(unittest.TestCase): def test_connect_item(self): b1 = Box() b2 = Box() l = Line() c = Canvas() c.add(b1) c.add(b2) c.add(l) c.connect_item(l, l.handles()[0], b1, b1.ports()[0]) assert count(c.get_connections(handle=l.handles()[0])) == 1 # Add the same self.assertRaises(ConnectionError, c.connect_item, l, l.handles()[0], b1, b1.ports()[0]) assert count(c.get_connections(handle=l.handles()[0])) == 1 # Same item, different port #c.connect_item(l, l.handles()[0], b1, b1.ports()[-1]) #assert count(c.get_connections(handle=l.handles()[0])) == 1 # Different item #c.connect_item(l, l.handles()[0], b2, b2.ports()[0]) #assert count(c.get_connections(handle=l.handles()[0])) == 1 def test_disconnect_item_with_callback(self): b1 = Box() b2 = Box() l = Line() c = Canvas() c.add(b1) c.add(b2) c.add(l) events = [] def callback(): events.append('called') c.connect_item(l, l.handles()[0], b1, b1.ports()[0], callback=callback) assert count(c.get_connections(handle=l.handles()[0])) == 1 c.disconnect_item(l, l.handles()[0]) assert count(c.get_connections(handle=l.handles()[0])) == 0 assert events == ['called'] def test_disconnect_item_with_constraint(self): b1 = Box() b2 = Box() l = Line() c = Canvas() c.add(b1) c.add(b2) c.add(l) cons = b1.ports()[0].constraint(c, l, l.handles()[0], b1) c.connect_item(l, l.handles()[0], b1, b1.ports()[0], constraint=cons) assert count(c.get_connections(handle=l.handles()[0])) == 1 ncons = len(c.solver.constraints) assert ncons == 13 c.disconnect_item(l, l.handles()[0]) assert count(c.get_connections(handle=l.handles()[0])) == 0 assert len(c.solver.constraints) == 12 def test_disconnect_item_by_deleting_element(self): b1 = Box() b2 = Box() l = Line() c = Canvas() c.add(b1) c.add(b2) c.add(l) events = [] def callback(): events.append('called') c.connect_item(l, l.handles()[0], b1, b1.ports()[0], callback=callback) assert count(c.get_connections(handle=l.handles()[0])) == 1 c.remove(b1) assert count(c.get_connections(handle=l.handles()[0])) == 0 assert events == ['called'] def test_disconnect_item_with_constraint_by_deleting_element(self): b1 = Box() b2 = Box() l = Line() c = Canvas() c.add(b1) c.add(b2) c.add(l) cons = b1.ports()[0].constraint(c, l, l.handles()[0], b1) c.connect_item(l, l.handles()[0], b1, b1.ports()[0], constraint=cons) assert count(c.get_connections(handle=l.handles()[0])) == 1 ncons = len(c.solver.constraints) assert ncons == 13 c.remove(b1) assert count(c.get_connections(handle=l.handles()[0])) == 0 self.assertEquals(6, len(c.solver.constraints)) class ConstraintProjectionTestCase(unittest.TestCase): def test_line_projection(self): """Test projection with line's handle on element's side""" line = Line() line.matrix.translate(15, 50) h1, h2 = line.handles() h1.x, h1.y = 0, 0 h2.x, h2.y = 20, 20 box = Box() box.matrix.translate(10, 10) box.width = 40 box.height = 20 h_nw, h_ne, h_se, h_sw = box.handles() canvas = Canvas() canvas.add(line) canvas.add(box) # move line's second handle on box side h2.x, h2.y = 5, -20 ### bc = BalanceConstraint(band=(h_sw.x, h_se.x), v=h2.x, balance=0.25) ### canvas.projector(bc, x={h_sw.x: box, h_se.x: box, h2.x: line}) ### canvas._solver.add_constraint(bc) ### ### eq = EqualsConstraint(a=h_se.y, b=h2.y) ### canvas.projector(eq, y={h_se.y: box, h2.y: line}) ### canvas._solver.add_constraint(eq) ### ### box.request_update() ### line.request_update() ### ### canvas.update() ### ### box.width = 60 ### box.height = 30 ### ### canvas.update() ### ### # expect h2.x to be moved due to balance constraint ### self.assertEquals(10, h2.x) ### self.assertEquals(-10, h2.y) class CanvasConstraintTestCase(unittest.TestCase): def test_remove_connected_item(self): """Test adding canvas constraint""" canvas = Canvas() from gaphas.aspect import Connector, ConnectionSink l1 = Line() canvas.add(l1) b1 = Box() canvas.add(b1) number_cons1 = len(canvas.solver.constraints) b2 = Box() canvas.add(b2) number_cons2 = len(canvas.solver.constraints) conn = Connector(l1, l1.handles()[0]) sink = ConnectionSink(b1, b1.ports()[0]) conn.connect(sink) assert canvas.get_connection(l1.handles()[0]) conn = Connector(l1, l1.handles()[1]) sink = ConnectionSink(b2, b2.ports()[0]) conn.connect(sink) assert canvas.get_connection(l1.handles()[1]) self.assertEquals(number_cons2 + 2, len(canvas.solver.constraints)) canvas.remove(b1) # Expecting a class + line connected at one end only self.assertEquals(number_cons1 + 1, len(canvas.solver.constraints)) ### def test_adding_constraint(self): ### """Test adding canvas constraint""" ### canvas = Canvas() ### ### l1 = Line() ### canvas.add(l1) ### ### h1, h2 = l1.handles() ### h = Handle() ### ### eq1 = EqualsConstraint(h1.x, h.x) ### canvas.add_canvas_constraint(l1, h1, eq1) ### self.assertTrue(l1 in cons) ### self.assertTrue(h1 in cons[l1]) ### self.assertTrue(eq1 in cons[l1][h1]) ### ### l2 = Line() ### canvas.add(l2) ### ### h1, h2 = l2.handles() ### h = Handle() ### ### eq2 = EqualsConstraint(h1.x, h.x) ### canvas.add_canvas_constraint(l2, h1, eq2) ### self.assertTrue(l2 in cons) ### self.assertTrue(h1 in cons[l2]) ### self.assertTrue(eq2 in cons[l2][h1]) ### ### ### def test_adding_constraint_ex(self): ### """Test adding canvas constraint for non-existing item""" ### canvas = Canvas() ### l1 = Line() ### h1, h2 = l1.handles() ### h = Handle() ### ### eq = EqualsConstraint(h1.x, h.x) ### self.assertRaises(ValueError, canvas.add_canvas_constraint, l1, h1, eq) ### ### ### def test_removing_constraint(self): ### """Test removing canvas constraint""" ### canvas = Canvas() ### cons = canvas._canvas_constraints ### ### l1 = Line() ### canvas.add(l1) ### ### h1, h2 = l1.handles() ### h = Handle() ### ### eq1 = EqualsConstraint(h1.x, h.x) ### canvas.add_canvas_constraint(l1, h1, eq1) ### ### # test preconditions ### assert l1 in cons ### assert h1 in cons[l1] ### assert eq1 in cons[l1][h1] ### ### canvas.remove_canvas_constraint(l1, h1, eq1) ### self.assertTrue(l1 in cons) ### self.assertTrue(h1 in cons[l1]) ### self.assertFalse(eq1 in cons[l1][h1]) ### ### eq1 = EqualsConstraint(h1.x, h.x) ### eq2 = EqualsConstraint(h1.y, h.y) ### canvas.add_canvas_constraint(l1, h1, eq1) ### canvas.add_canvas_constraint(l1, h1, eq2) ### ### # test preconditions ### assert l1 in cons ### assert h1 in cons[l1] ### assert eq1 in cons[l1][h1] ### assert eq2 in cons[l1][h1] ### ### canvas.remove_canvas_constraint(l1, h1) ### ### self.assertTrue(l1 in cons) ### self.assertTrue(h1 in cons[l1]) ### self.assertFalse(eq1 in cons[l1][h1]) ### self.assertFalse(eq2 in cons[l1][h1]) ### ### ### def test_fetching_constraints(self): ### """Test fetching canvas constraints""" ### canvas = Canvas() ### cons = canvas._canvas_constraints ### ### l1 = Line() ### canvas.add(l1) ### ### h1, h2 = l1.handles() ### h = Handle() ### ### eq1 = EqualsConstraint(h1.x, h.x) ### eq2 = EqualsConstraint(h1.y, h.y) ### canvas.add_canvas_constraint(l1, h1, eq1) ### canvas.add_canvas_constraint(l1, h1, eq2) ### ### # test preconditions ### assert l1 in cons ### assert h1 in cons[l1] ### assert eq1 in cons[l1][h1] ### assert eq2 in cons[l1][h1] ### ### self.assertTrue(eq1 in canvas.canvas_constraints(l1)) ### self.assertTrue(eq2 in canvas.canvas_constraints(l1)) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_connector.py000066400000000000000000000010701175456763600210150ustar00rootroot00000000000000 import unittest from gaphas.connector import Position, Handle class PositionTestCase(unittest.TestCase): def test_position(self): pos = Position((0, 0)) self.assertEquals(0, pos.x) self.assertEquals(0, pos.y) def test_position(self): pos = Position((1, 2)) self.assertEquals(1, pos.x) self.assertEquals(2, pos.y) class HandleTestCase(unittest.TestCase): def test_handle_x_y(self): h = Handle() self.assertEquals(0.0, h.x) self.assertEquals(0.0, h.y) # vim: sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_constraints.py000066400000000000000000000042551175456763600214020ustar00rootroot00000000000000import unittest from gaphas.solver import Variable from gaphas.constraint import PositionConstraint, LineAlignConstraint class PositionTestCase(unittest.TestCase): def test_pos_constraint(self): """Test position constraint""" x1, y1 = Variable(10), Variable(11) x2, y2 = Variable(12), Variable(13) pc = PositionConstraint(origin=(x1, y1), point=(x2, y2)) pc.solve_for() # origin shall remain the same self.assertEquals(10, x1) self.assertEquals(11, y1) # point shall be moved to origin self.assertEquals(10, x2) self.assertEquals(11, y2) # change just x of origin x1.value = 15 pc.solve_for() self.assertEquals(15, x2) # change just y of origin y1.value = 14 pc.solve_for() self.assertEquals(14, y2) class LineAlignConstraintTestCase(unittest.TestCase): """ Line align constraint test case. """ def test_delta(self): """Test line align delta """ line = (Variable(0), Variable(0)), (Variable(30), Variable(20)) point = (Variable(15), Variable(10)) lc = LineAlignConstraint(line=line, point=point, align=0.5, delta=5) lc.solve_for() self.assertAlmostEqual(19.16, point[0].value, 2) self.assertAlmostEqual(12.77, point[1].value, 2) line[1][0].value = 40 line[1][1].value = 30 lc.solve_for() self.assertAlmostEqual(24.00, point[0].value, 2) self.assertAlmostEqual(18.00, point[1].value, 2) def test_delta_below_zero(self): """Test line align with delta below zero """ line = (Variable(0), Variable(0)), (Variable(30), Variable(20)) point = (Variable(15), Variable(10)) lc = LineAlignConstraint(line=line, point=point, align=0.5, delta=-5) lc.solve_for() self.assertAlmostEqual(10.84, point[0].value, 2) self.assertAlmostEqual(7.23, point[1].value, 2) line[1][0].value = 40 line[1][1].value = 30 lc.solve_for() self.assertAlmostEqual(16.0, point[0].value, 2) self.assertAlmostEqual(12.00, point[1].value, 2) # vim: sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_element.py000066400000000000000000000054071175456763600204640ustar00rootroot00000000000000from os import getenv import unittest from gaphas.canvas import Canvas from gaphas.examples import Box from gaphas.item import NW, NE, SE, SW class ElementTestCase(unittest.TestCase): def test_creation_with_size(self): """ Test if initial size holds when added to a canvas. """ canvas = Canvas() box = Box(150, 153) assert box.width == 150, box.width assert box.height == 153, box.height assert box.handles()[SE].pos.x == 150, box.handles()[SE].pos.x assert box.handles()[SE].pos.y == 153, box.handles()[SE].pos.y canvas.add(box) assert box.width == 150, box.width assert box.height == 153, box.height assert box.handles()[SE].pos.x == 150, box.handles()[SE].pos.x assert box.handles()[SE].pos.y == 153, box.handles()[SE].pos.y def test_resize_se(self): """ Test resizing of element by dragging it SE handle. """ canvas = Canvas() box = Box() handles = box.handles() canvas.add(box) h_nw, h_ne, h_se, h_sw = handles assert h_nw is handles[NW] assert h_ne is handles[NE] assert h_sw is handles[SW] assert h_se is handles[SE] # to see how many solver was called: # GAPHAS_TEST_COUNT=3 nosetests -s --with-prof --profile-restrict=gaphas gaphas/tests/test_element.py | grep -e '\' -e dirty count = getenv('GAPHAS_TEST_COUNT') if count: count = int(count) else: count = 1 for i in range(count): h_se.pos.x += 100 # h.se.{x,y} = 10, now h_se.pos.y += 100 box.request_update() canvas.update() self.assertEquals(110 * count, h_se.pos.x) # h_se changed above, should remain the same self.assertEquals(110 * count, float(h_se.pos.y)) self.assertEquals(110 * count, float(h_ne.pos.x)) self.assertEquals(110 * count, float(h_sw.pos.y)) def test_minimal_se(self): """ Test resizing of element by dragging it SE handle. """ canvas = Canvas() box = Box() handles = box.handles() canvas.add(box) h_nw, h_ne, h_se, h_sw = handles assert h_nw is handles[NW] assert h_ne is handles[NE] assert h_sw is handles[SW] assert h_se is handles[SE] h_se.pos.x -= 20 # h.se.{x,y} == -10 h_se.pos.y -= 20 assert h_se.pos.x == h_se.pos.y == -10 box.request_update() canvas.update() self.assertEquals(10, h_se.pos.x) # h_se changed above, should be 10 self.assertEquals(10, h_se.pos.y) self.assertEquals(10, h_ne.pos.x) self.assertEquals(10, h_sw.pos.y) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_freehand.py000066400000000000000000000030651175456763600206050ustar00rootroot00000000000000 import unittest from gaphas.freehand import FreeHandCairoContext import cairo class PseudoFile(object): def __init__(self): self.data = '' def write(self, data): print data self.data = self.data + data class FreeHandCairoContextTest(unittest.TestCase): def setUp(self): pass def test_drawing_lines(self): f = PseudoFile() surface = cairo.SVGSurface('freehand-drawing-lines.svg', 100, 100) cr = FreeHandCairoContext(cairo.Context(surface)) cr.set_line_width(2) cr.move_to(20, 20) cr.line_to(20, 80) cr.line_to(80, 80) cr.line_to(80, 20) cr.stroke() cr.show_page() def test_drawing_rectangle(self): surface = cairo.SVGSurface('freehand-drawing-rectangle.svg', 100, 100) cr = FreeHandCairoContext(cairo.Context(surface)) cr.set_line_width(2) cr.rectangle(20, 20, 60, 60) cr.stroke() cr.show_page() DRAWING_LINES_OUTPUT = """ """ # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_guide.py000066400000000000000000000115771175456763600201350ustar00rootroot00000000000000 import unittest import gtk from gaphas.guide import * from gaphas.canvas import Canvas from gaphas.view import GtkView from gaphas.item import Element, Line class GuideTestCase(unittest.TestCase): def setUp(self): self.canvas = Canvas() self.view = GtkView(self.canvas) self.window = gtk.Window() self.window.add(self.view) self.window.show_all() def tearDown(self): self.window.destroy() def test_find_closest(self): """ test find closest method. """ set1 = [0, 10, 20] set2 = [2, 15, 30] guider = GuidedItemInMotion(Element(), self.view) d, closest = guider.find_closest(set1, set2) self.assertEquals(2.0, d) self.assertEquals([2.0], closest) def test_element_guide(self): e1 = Element() self.assertEquals(10, e1.width) self.assertEquals(10, e1.height) guides = Guide(e1).horizontal() self.assertEquals(0.0, guides[0]) self.assertEquals(5.0, guides[1]) self.assertEquals(10.0, guides[2]) guides = Guide(e1).vertical() self.assertEquals(0.0, guides[0]) self.assertEquals(5.0, guides[1]) self.assertEquals(10.0, guides[2]) def test_line_guide(self): c = Canvas() l = Line() c.add(l) l.handles().append(l._create_handle((20, 20))) l.handles().append(l._create_handle((30, 30))) l.handles().append(l._create_handle((40, 40))) l.orthogonal = True c.update_now() guides = list(Guide(l).horizontal()) self.assertEquals(2, len(guides)) self.assertEquals(10.0, guides[0]) self.assertEquals(40.0, guides[1]) guides = list(Guide(l).vertical()) self.assertEquals(2, len(guides)) self.assertEquals(00.0, guides[0]) self.assertEquals(20.0, guides[1]) def test_line_guide_horizontal(self): c = Canvas() l = Line() c.add(l) l.handles().append(l._create_handle((20, 20))) l.handles().append(l._create_handle((30, 30))) l.handles().append(l._create_handle((40, 40))) l.horizontal = True l.orthogonal = True c.update_now() guides = list(Guide(l).horizontal()) self.assertEquals(2, len(guides)) self.assertEquals(0.0, guides[0]) self.assertEquals(20.0, guides[1]) guides = list(Guide(l).horizontal()) self.assertEquals(2, len(guides)) self.assertEquals(0.0, guides[0]) self.assertEquals(20.0, guides[1]) def test_guide_item_in_motion(self): e1 = Element() e2 = Element() e3 = Element() canvas = self.canvas canvas.add(e1) canvas.add(e2) canvas.add(e3) self.assertEquals(0, e1.matrix[4]) self.assertEquals(0, e1.matrix[5]) e2.matrix.translate(40, 40) e2.request_update() self.assertEquals(40, e2.matrix[4]) self.assertEquals(40, e2.matrix[5]) guider = GuidedItemInMotion(e3, self.view) guider.start_move((0, 0)) self.assertEquals(0, e3.matrix[4]) self.assertEquals(0, e3.matrix[5]) # Moved back to guided lines: for d in range(0, 3): print 'move to', d guider.move((d, d)) self.assertEquals(0, e3.matrix[4]) self.assertEquals(0, e3.matrix[5]) for d in range(3, 5): print 'move to', d guider.move((d, d)) self.assertEquals(5, e3.matrix[4]) self.assertEquals(5, e3.matrix[5]) guider.move((20, 20)) self.assertEquals(20, e3.matrix[4]) self.assertEquals(20, e3.matrix[5]) def test_guide_item_in_motion_2(self): e1 = Element() e2 = Element() e3 = Element() canvas = self.canvas canvas.add(e1) canvas.add(e2) canvas.add(e3) self.assertEquals(0, e1.matrix[4]) self.assertEquals(0, e1.matrix[5]) e2.matrix.translate(40, 40) e2.request_update() self.assertEquals(40, e2.matrix[4]) self.assertEquals(40, e2.matrix[5]) guider = GuidedItemInMotion(e3, self.view) guider.start_move((3, 3)) self.assertEquals(0, e3.matrix[4]) self.assertEquals(0, e3.matrix[5]) # Moved back to guided lines: for y in range(4, 6): print 'move to', y guider.move((3, y)) self.assertEquals(0, e3.matrix[4]) self.assertEquals(0, e3.matrix[5]) for y in range(6, 9): print 'move to', y guider.move((3, y)) self.assertEquals(0, e3.matrix[4]) self.assertEquals(5, e3.matrix[5]) # Take into account initial cursor offset of (3, 3) guider.move((20, 23)) self.assertEquals(17, e3.matrix[4]) self.assertEquals(20, e3.matrix[5]) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_item.py000066400000000000000000000055341175456763600177720ustar00rootroot00000000000000""" Generic gaphas item tests. """ import unittest from gaphas.item import Item from gaphas.constraint import LineAlignConstraint, LineConstraint, \ EqualsConstraint, LessThanConstraint from gaphas.solver import Variable class ItemConstraintTestCase(unittest.TestCase): """ Item constraint creation tests. The test check functionality of `Item.constraint` method, not constraints themselves. """ def test_line_constraint(self): """ Test line creation constraint. """ item = Item() pos = Variable(1), Variable(2) line = (Variable(3), Variable(4)), (Variable(5), Variable(6)) item.constraint(line=(pos, line)) self.assertEquals(1, len(item._constraints)) c = item._constraints[0] self.assertTrue(isinstance(c, LineConstraint)) self.assertEquals((1, 2), c._point) self.assertEquals(((3, 4), (5, 6)), c._line) def test_horizontal_constraint(self): """ Test horizontal constraint creation. """ item = Item() p1 = Variable(1), Variable(2) p2 = Variable(3), Variable(4) item.constraint(horizontal=(p1, p2)) self.assertEquals(1, len(item._constraints)) c = item._constraints[0] self.assertTrue(isinstance(c, EqualsConstraint)) # expect constraint on y-axis self.assertEquals(2, c.a) self.assertEquals(4, c.b) def test_vertical_constraint(self): """ Test vertical constraint creation. """ item = Item() p1 = Variable(1), Variable(2) p2 = Variable(3), Variable(4) item.constraint(vertical=(p1, p2)) self.assertEquals(1, len(item._constraints)) c = item._constraints[0] self.assertTrue(isinstance(c, EqualsConstraint)) # expect constraint on x-axis self.assertEquals(1, c.a) self.assertEquals(3, c.b) def test_left_of_constraint(self): """ Test "less than" constraint (horizontal) creation. """ item = Item() p1 = Variable(1), Variable(2) p2 = Variable(3), Variable(4) item.constraint(left_of=(p1, p2)) self.assertEquals(1, len(item._constraints)) c = item._constraints[0] self.assertTrue(isinstance(c, LessThanConstraint)) self.assertEquals(1, c.smaller) self.assertEquals(3, c.bigger) def test_above_constraint(self): """ Test "less than" constraint (vertical) creation. """ item = Item() p1 = Variable(1), Variable(2) p2 = Variable(3), Variable(4) item.constraint(above=(p1, p2)) self.assertEquals(1, len(item._constraints)) c = item._constraints[0] self.assertTrue(isinstance(c, LessThanConstraint)) self.assertEquals(2, c.smaller) self.assertEquals(4, c.bigger) gaphas-0.7.2/gaphas/tests/test_line.py000066400000000000000000000052021175456763600177530ustar00rootroot00000000000000 import unittest from gaphas.item import Line from gaphas.canvas import Canvas from gaphas import state from gaphas.segment import Segment undo_list = [] redo_list = [] def undo_handler(event): undo_list.append(event) def undo(): apply_me = list(undo_list) del undo_list[:] apply_me.reverse() for e in apply_me: state.saveapply(*e) redo_list[:] = undo_list[:] del undo_list[:] class TestCaseBase(unittest.TestCase): """ Abstract test case class with undo support. """ def setUp(self): state.observers.add(state.revert_handler) state.subscribers.add(undo_handler) def tearDown(self): state.observers.remove(state.revert_handler) state.subscribers.remove(undo_handler) class LineTestCase(TestCaseBase): """ Basic line item tests. """ def test_initial_ports(self): """Test initial ports amount """ line = Line() self.assertEquals(1, len(line.ports())) def test_orthogonal_horizontal_undo(self): """Test orthogonal line constraints bug (#107) """ canvas = Canvas() line = Line() canvas.add(line) assert not line.horizontal assert len(canvas.solver._constraints) == 0 segment = Segment(line, None) segment.split_segment(0) line.orthogonal = True self.assertEquals(2, len(canvas.solver._constraints)) after_ortho = set(canvas.solver._constraints) del undo_list[:] line.horizontal = True self.assertEquals(2, len(canvas.solver._constraints)) undo() self.assertFalse(line.horizontal) self.assertEquals(2, len(canvas.solver._constraints)) line.horizontal = True self.assertTrue(line.horizontal) self.assertEquals(2, len(canvas.solver._constraints)) def test_orthogonal_line_undo(self): """Test orthogonal line undo """ canvas = Canvas() line = Line() canvas.add(line) segment = Segment(line, None) segment.split_segment(0) # start with no orthogonal constraints assert len(canvas.solver._constraints) == 0 line.orthogonal = True # check orthogonal constraints self.assertEquals(2, len(canvas.solver._constraints)) self.assertEquals(3, len(line.handles())) undo() self.assertFalse(line.orthogonal) self.assertEquals(0, len(canvas.solver._constraints)) self.assertEquals(2, len(line.handles())) # vim:sw=4:et gaphas-0.7.2/gaphas/tests/test_pickle.py000066400000000000000000000121231175456763600202730ustar00rootroot00000000000000 import unittest import pickle from gaphas.canvas import Canvas from gaphas.examples import Box from gaphas.item import Item, Element, Line from gaphas.view import View, GtkView # Ensure extra pickle reducers/reconstructors are loaded: import gaphas.picklers class MyPickler(pickle.Pickler): def save(self, obj): print 'saving obj', obj, type(obj) try: return pickle.Pickler.save(self, obj) except pickle.PicklingError, e: print 'Error while pickling', obj, self.dispatch.get(type(obj)) raise e class my_disconnect(object): """ Disconnect object should be located at top-level, so the pickle code can find it. """ def __call__(self): pass def create_canvas(): canvas = Canvas() box = Box() canvas.add(box) box.matrix.translate(100, 50) box.matrix.rotate(50) box2 = Box() canvas.add(box2, parent=box) line = Line() line.handles()[0].visible = False line.handles()[0].connected_to = box line.handles()[0].disconnect = my_disconnect() line.handles()[0].connection_data = 1 canvas.add(line) canvas.update() return canvas class PickleTestCase(unittest.TestCase): def test_pickle_element(self): item = Element() pickled = pickle.dumps(item) i2 = pickle.loads(pickled) assert i2 assert len(i2.handles()) == 4 def test_pickle_line(self): item = Line() pickled = pickle.dumps(item) i2 = pickle.loads(pickled) assert i2 assert len(i2.handles()) == 2 def test_pickle(self): canvas = create_canvas() pickled = pickle.dumps(canvas) c2 = pickle.loads(pickled) assert type(canvas._tree.nodes[0]) is Box assert type(canvas._tree.nodes[1]) is Box assert type(canvas._tree.nodes[2]) is Line def test_pickle_connect(self): """ Persist a connection. """ canvas = Canvas() box = Box() canvas.add(box) box2 = Box() canvas.add(box2, parent=box) line = Line() line.handles()[0].visible = False line.handles()[0].connected_to = box line.handles()[0].disconnect = my_disconnect() line.handles()[0].connection_data = 1 canvas.add(line) pickled = pickle.dumps(canvas) c2 = pickle.loads(pickled) assert type(canvas._tree.nodes[0]) is Box assert type(canvas._tree.nodes[1]) is Box assert type(canvas._tree.nodes[2]) is Line assert c2.solver line2 = c2._tree.nodes[2] h = line2.handles()[0] assert h.visible == False assert h.connected_to is c2._tree.nodes[0] # connection_data and disconnect have not been persisted assert h.connection_data == 1, h.connection_data assert h.disconnect, h.disconnect assert callable(h.disconnect) assert h.disconnect() is None, h.disconnect() def test_pickle_with_view(self): canvas = create_canvas() pickled = pickle.dumps(canvas) c2 = pickle.loads(pickled) view = View(canvas=c2) import cairo surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) cr = cairo.Context(surface) view.update_bounding_box(cr) cr.show_page() surface.flush() surface.finish() def test_pickle_with_gtk_view(self): canvas = create_canvas() pickled = pickle.dumps(canvas) c2 = pickle.loads(pickled) import gtk win = gtk.Window() view = GtkView(canvas=c2) win.add(view) view.show() win.show() view.update() def test_pickle_with_gtk_view_with_connection(self): canvas = create_canvas() box = canvas._tree.nodes[0] assert isinstance(box, Box) line = canvas._tree.nodes[2] assert isinstance(line, Line) view = GtkView(canvas=canvas) # from gaphas.tool import ConnectHandleTool # handle_tool = ConnectHandleTool() # handle_tool.connect(view, line, line.handles()[0], (40, 0)) # assert line.handles()[0].connected_to is box, line.handles()[0].connected_to # assert line.handles()[0].connection_data # assert line.handles()[0].disconnect # assert isinstance(line.handles()[0].disconnect, object), line.handles()[0].disconnect import StringIO f = StringIO.StringIO() pickler = MyPickler(f) pickler.dump(canvas) pickled = f.getvalue() c2 = pickle.loads(pickled) import gtk win = gtk.Window() view = GtkView(canvas=c2) win.add(view) view.show() win.show() view.update() def test_pickle_demo(self): import demo canvas = demo.create_canvas() pickled = pickle.dumps(canvas) c2 = pickle.loads(pickled) import gtk win = gtk.Window() view = GtkView(canvas=c2) win.add(view) view.show() win.show() view.update() if __name__ == '__main__': unittest.main() # vim: sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_quadtree.py000066400000000000000000000073341175456763600206460ustar00rootroot00000000000000 import unittest from gaphas.quadtree import Quadtree class QuadtreeTestCase(unittest.TestCase): def test_lookups(self): qtree = Quadtree((0, 0, 100, 100)) for i in range(100, 10): for j in range(100, 10): qtree.add("%dx%d" % (i, j), (i, j, 10, 10)) for i in range(100, 10): for j in range(100, 10): assert qtree.find_intersect((i+1, j+1, 1, 1)) == ['%dx%d' % (i, j)], \ qtree.find_intersect((i+1, j+1, 1, 1)) def test_with_rectangles(self): from gaphas.geometry import Rectangle qtree = Quadtree((0, 0, 100, 100)) for i in range(0, 100, 10): for j in range(0, 100, 10): qtree.add("%dx%d" % (i, j), Rectangle(i, j, 10, 10)) assert len(qtree._ids) == 100, len(qtree._ids) for i in range(100, 10): for j in range(100, 10): assert qtree.find_intersect((i+1, j+1, 1, 1)) == ['%dx%d' % (i, j)], \ qtree.find_intersect((i+1, j+1, 1, 1)) def test_moving_items(self): qtree = Quadtree((0, 0, 100, 100), capacity=10) for i in range(0, 100, 10): for j in range(0, 100, 10): qtree.add("%dx%d" % (i, j), (i, j, 10, 10)) assert len(qtree._ids) == 100, len(qtree._ids) assert qtree._bucket._buckets, qtree._bucket._buckets for i in range(4): assert qtree._bucket._buckets[i]._buckets for j in range(4): assert not qtree._bucket._buckets[i]._buckets[j]._buckets # Check contents: # First sub-level contains 9 items. second level contains 4 items # ==> 4 * (9 + (4 * 4)) = 100 assert len(qtree._bucket.items) == 0, qtree._bucket.items for i in range(4): assert len(qtree._bucket._buckets[i].items) == 9 for item, bounds in qtree._bucket._buckets[i].items.iteritems(): assert qtree._bucket.find_bucket(bounds) is qtree._bucket._buckets[i] for j in range(4): assert len(qtree._bucket._buckets[i]._buckets[j].items) == 4 assert qtree.get_bounds('0x0') # Now move item '0x0' to the center of the first quadrant (20, 20) qtree.add('0x0', (20, 20, 10, 10)) assert len(qtree._bucket.items) == 0 assert len(qtree._bucket._buckets[0]._buckets[0].items) == 3, \ qtree._bucket._buckets[0]._buckets[0].items assert len(qtree._bucket._buckets[0].items) == 10, \ qtree._bucket._buckets[0].items # Now move item '0x0' to the second quadrant (70, 20) qtree.add('0x0', (70, 20, 10, 10)) assert len(qtree._bucket.items) == 0 assert len(qtree._bucket._buckets[0]._buckets[0].items) == 3, \ qtree._bucket._buckets[0]._buckets[0].items assert len(qtree._bucket._buckets[0].items) == 9, \ qtree._bucket._buckets[0].items assert len(qtree._bucket._buckets[1].items) == 10, \ qtree._bucket._buckets[1].items def test_get_data(self): """ Extra data may be added to a node: """ qtree = Quadtree((0, 0, 100, 100)) for i in range(0, 100, 10): for j in range(0, 100, 10): qtree.add("%dx%d" % (i, j), (i, j, 10, 10), i+j) for i in range(0, 100, 10): for j in range(0, 100, 10): assert i+j == qtree.get_data("%dx%d" % (i, j)) def test_clipped_bounds(self): qtree = Quadtree((0, 0, 100, 100), capacity=10) qtree.add(1, (-100, -100, 120, 120)) self.assertEquals((0, 0, 20, 20), qtree.get_clipped_bounds(1)) if __name__ == '__main__': unittest.main() # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_segment.py000066400000000000000000000360701175456763600204750ustar00rootroot00000000000000""" Generic gaphas item tests. """ import unittest from gaphas.item import Item, Line from gaphas.segment import * from gaphas.canvas import Canvas from gaphas.view import View from gaphas import state class SegmentTestCase(unittest.TestCase): """ Test aspects for items. """ def setUp(self): self.canvas = Canvas() self.view = View(self.canvas) def test_segment_fails_for_item(self): """ Test if Segment aspect can be applied to Item """ item = Item() try: s = Segment(item, self.view) print item, 'segment aspect:', s except TypeError, e: print 'TypeError', e else: assert False, 'Should not be reached' def test_segment(self): """ """ view = self.view line = Line() self.canvas.add(line) segment = Segment(line, self.view) self.assertEquals(2, len(line.handles())) segment.split((5, 5)) self.assertEquals(3, len(line.handles())) from gaphas.tests.test_tool import simple_canvas undo_list = [] redo_list = [] def undo_handler(event): undo_list.append(event) def undo(): apply_me = list(undo_list) del undo_list[:] apply_me.reverse() for e in apply_me: state.saveapply(*e) redo_list[:] = undo_list[:] del undo_list[:] class TestCaseBase(unittest.TestCase): """ Abstract test case class with undo support. """ def setUp(self): state.observers.add(state.revert_handler) state.subscribers.add(undo_handler) simple_canvas(self) def tearDown(self): state.observers.remove(state.revert_handler) state.subscribers.remove(undo_handler) class LineSplitTestCase(TestCaseBase): """ Tests for line splitting. """ def test_split_single(self): """Test single line splitting """ # we start with two handles and one port, after split 3 handles are # expected and 2 ports assert len(self.line.handles()) == 2 assert len(self.line.ports()) == 1 old_port = self.line.ports()[0] h1, h2 = self.line.handles() self.assertEquals(h1.pos, old_port.start) self.assertEquals(h2.pos, old_port.end) segment = Segment(self.line, self.view) handles, ports = segment.split_segment(0) handle = handles[0] self.assertEquals(1, len(handles)) self.assertEquals((50, 50), handle.pos.pos) self.assertEquals(3, len(self.line.handles())) self.assertEquals(2, len(self.line.ports())) # new handle is between old handles self.assertEquals(handle, self.line.handles()[1]) # and old port is deleted self.assertTrue(old_port not in self.line.ports()) # check ports order p1, p2 = self.line.ports() h1, h2, h3 = self.line.handles() self.assertEquals(h1.pos, p1.start) self.assertEquals(h2.pos, p1.end) self.assertEquals(h2.pos, p2.start) self.assertEquals(h3.pos, p2.end) def test_split_multiple(self): """Test multiple line splitting """ self.line.handles()[1].pos = (20, 16) handles = self.line.handles() old_ports = self.line.ports()[:] # start with two handles, split into 4 segments - 3 new handles to # be expected assert len(handles) == 2 assert len(old_ports) == 1 segment = Segment(self.line, self.view) handles, ports = segment.split_segment(0, count=4) self.assertEquals(3, len(handles)) h1, h2, h3 = handles self.assertEquals((5, 4), h1.pos.pos) self.assertEquals((10, 8), h2.pos.pos) self.assertEquals((15, 12), h3.pos.pos) # new handles between old handles self.assertEquals(5, len(self.line.handles())) self.assertEquals(h1, self.line.handles()[1]) self.assertEquals(h2, self.line.handles()[2]) self.assertEquals(h3, self.line.handles()[3]) self.assertEquals(4, len(self.line.ports())) # and old port is deleted self.assertTrue(old_ports[0] not in self.line.ports()) # check ports order p1, p2, p3, p4 = self.line.ports() h1, h2, h3, h4, h5 = self.line.handles() self.assertEquals(h1.pos, p1.start) self.assertEquals(h2.pos, p1.end) self.assertEquals(h2.pos, p2.start) self.assertEquals(h3.pos, p2.end) self.assertEquals(h3.pos, p3.start) self.assertEquals(h4.pos, p3.end) self.assertEquals(h4.pos, p4.start) self.assertEquals(h5.pos, p4.end) def test_ports_after_split(self): """Test ports removal after split """ self.line.handles()[1].pos = (20, 16) segment = Segment(self.line, self.view) segment.split_segment(0) handles = self.line.handles() old_ports = self.line.ports()[:] # start with 3 handles and two ports assert len(handles) == 3 assert len(old_ports) == 2 # do split of first segment again # first port should be deleted, but 2nd one should remain untouched segment.split_segment(0) self.assertFalse(old_ports[0] in self.line.ports()) self.assertEquals(old_ports[1], self.line.ports()[2]) def test_constraints_after_split(self): """Test if constraints are recreated after line split """ # connect line2 to self.line line2 = Line() self.canvas.add(line2) head = line2.handles()[0] self.tool.connect(line2, head, (25, 25)) cinfo = self.canvas.get_connection(head) self.assertEquals(self.line, cinfo.connected) Segment(self.line, self.view).split_segment(0) assert len(self.line.handles()) == 3 h1, h2, h3 = self.line.handles() cinfo = self.canvas.get_connection(head) # connection shall be reconstrained between 1st and 2nd handle self.assertEquals(h1.pos, cinfo.constraint._line[0]._point) self.assertEquals(h2.pos, cinfo.constraint._line[1]._point) def test_split_undo(self): """Test line splitting undo """ self.line.handles()[1].pos = (20, 0) # we start with two handles and one port, after split 3 handles and # 2 ports are expected assert len(self.line.handles()) == 2 assert len(self.line.ports()) == 1 segment = Segment(self.line, self.view) segment.split_segment(0) assert len(self.line.handles()) == 3 assert len(self.line.ports()) == 2 # after undo, 2 handles and 1 port are expected again undo() self.assertEquals(2, len(self.line.handles())) self.assertEquals(1, len(self.line.ports())) def test_orthogonal_line_split(self): """Test orthogonal line splitting """ # start with no orthogonal constraints assert len(self.line._orthogonal_constraints) == 0 segment = Segment(self.line, None) segment.split_segment(0) self.line.orthogonal = True # check orthogonal constraints self.assertEquals(2, len(self.line._orthogonal_constraints)) self.assertEquals(3, len(self.line.handles())) Segment(self.line, self.view).split_segment(0) # 3 handles and 2 ports are expected # 2 constraints keep the self.line orthogonal self.assertEquals(3, len(self.line._orthogonal_constraints)) self.assertEquals(4, len(self.line.handles())) self.assertEquals(3, len(self.line.ports())) def test_params_errors(self): """Test parameter error exceptions """ line = Line() segment = Segment(line, self.view) # there is only 1 segment self.assertRaises(ValueError, segment.split_segment, -1) line = Line() segment = Segment(line, self.view) self.assertRaises(ValueError, segment.split_segment, 1) line = Line() # can't split into one or less segment :) segment = Segment(line, self.view) self.assertRaises(ValueError, segment.split_segment, 0, 1) class LineMergeTestCase(TestCaseBase): """ Tests for line merging. """ def test_merge_first_single(self): """Test single line merging starting from 1st segment """ self.line.handles()[1].pos = (20, 0) segment = Segment(self.line, self.view) segment.split_segment(0) # we start with 3 handles and 2 ports, after merging 2 handles and # 1 port are expected assert len(self.line.handles()) == 3 assert len(self.line.ports()) == 2 old_ports = self.line.ports()[:] segment = Segment(self.line, self.view) handles, ports = segment.merge_segment(0) # deleted handles and ports self.assertEquals(1, len(handles)) self.assertEquals(2, len(ports)) # handles and ports left after segment merging self.assertEquals(2, len(self.line.handles())) self.assertEquals(1, len(self.line.ports())) self.assertTrue(handles[0] not in self.line.handles()) self.assertTrue(ports[0] not in self.line.ports()) self.assertTrue(ports[1] not in self.line.ports()) # old ports are completely removed as they are replaced by new one # port self.assertEquals(old_ports, ports) # finally, created port shall span between first and last handle port = self.line.ports()[0] self.assertEquals((0, 0), port.start.pos) self.assertEquals((20, 0), port.end.pos) def test_constraints_after_merge(self): """Test if constraints are recreated after line merge """ # connect line2 to self.line line2 = Line() self.canvas.add(line2) head = line2.handles()[0] #conn = Connector(line2, head) #sink = conn.glue((25, 25)) #assert sink is not None #conn.connect(sink) self.tool.connect(line2, head, (25, 25)) cinfo = self.canvas.get_connection(head) self.assertEquals(self.line, cinfo.connected) segment = Segment(self.line, self.view) segment.split_segment(0) assert len(self.line.handles()) == 3 c1 = cinfo.constraint segment.merge_segment(0) assert len(self.line.handles()) == 2 h1, h2 = self.line.handles() # connection shall be reconstrained between 1st and 2nd handle cinfo = self.canvas.get_connection(head) self.assertEquals(cinfo.constraint._line[0]._point, h1.pos) self.assertEquals(cinfo.constraint._line[1]._point, h2.pos) self.assertFalse(c1 == cinfo.constraint) def test_merge_multiple(self): """Test multiple line merge """ self.line.handles()[1].pos = (20, 16) segment = Segment(self.line, self.view) segment.split_segment(0, count=3) # start with 4 handles and 3 ports, merge 3 segments assert len(self.line.handles()) == 4 assert len(self.line.ports()) == 3 print self.line.handles() handles, ports = segment.merge_segment(0, count=3) self.assertEquals(2, len(handles)) self.assertEquals(3, len(ports)) self.assertEquals(2, len(self.line.handles())) self.assertEquals(1, len(self.line.ports())) self.assertTrue(not set(handles).intersection(set(self.line.handles()))) self.assertTrue(not set(ports).intersection(set(self.line.ports()))) # finally, created port shall span between first and last handle port = self.line.ports()[0] self.assertEquals((0, 0), port.start.pos) self.assertEquals((20, 16), port.end.pos) def test_merge_undo(self): """Test line merging undo """ self.line.handles()[1].pos = (20, 0) segment = Segment(self.line, self.view) # split for merging segment.split_segment(0) assert len(self.line.handles()) == 3 assert len(self.line.ports()) == 2 # clear undo stack before merging del undo_list[:] # merge with empty undo stack segment.merge_segment(0) assert len(self.line.handles()) == 2 assert len(self.line.ports()) == 1 # after merge undo, 3 handles and 2 ports are expected again undo() self.assertEquals(3, len(self.line.handles())) self.assertEquals(2, len(self.line.ports())) def test_orthogonal_line_merge(self): """Test orthogonal line merging """ self.assertEquals(12, len(self.canvas.solver._constraints)) self.line.handles()[-1].pos = 100, 100 segment = Segment(self.line, self.view) # prepare the self.line for merging segment.split_segment(0) segment.split_segment(0) self.line.orthogonal = True self.assertEquals(12 + 3, len(self.canvas.solver._constraints)) self.assertEquals(4, len(self.line.handles())) self.assertEquals(3, len(self.line.ports())) # test the merging segment.merge_segment(0) self.assertEquals(12 + 2, len(self.canvas.solver._constraints)) self.assertEquals(3, len(self.line.handles())) self.assertEquals(2, len(self.line.ports())) def test_params_errors(self): """Test parameter error exceptions """ line = Line() self.canvas.add(line) segment = Segment(line, self.view) segment.split_segment(0) # no segment -1 self.assertRaises(ValueError, segment.merge_segment, -1) line = Line() self.canvas.add(line) segment = Segment(line, self.view) segment.split_segment(0) # no segment no 2 self.assertRaises(ValueError, segment.merge_segment, 2) line = Line() self.canvas.add(line) segment = Segment(line, self.view) segment.split_segment(0) # can't merge one or less segments :) self.assertRaises(ValueError, segment.merge_segment, 0, 1) line = Line() self.canvas.add(line) self.assertEquals(2, len(line.handles())) segment = Segment(line, self.view) # can't merge line with one segment self.assertRaises(ValueError, segment.merge_segment, 0) line = Line() self.canvas.add(line) segment = Segment(line, self.view) segment.split_segment(0) # 2 segments: no 0 and 1. cannot merge as there are no segments # after segment no 1 self.assertRaises(ValueError, segment.merge_segment, 1) line = Line() self.canvas.add(line) segment = Segment(line, self.view) segment.split_segment(0) # 2 segments: no 0 and 1. cannot merge 3 segments as there are no 3 # segments self.assertRaises(ValueError, segment.merge_segment, 0, 3) class SegmentHandlesTest(unittest.TestCase): def setUp(self): self.canvas = Canvas() self.line = Line() self.canvas.add(self.line) self.view = View(self.canvas) def testHandleFinder(self): finder = HandleFinder(self.line, self.view) assert type(finder) is SegmentHandleFinder, type(finder) # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_solver.py000066400000000000000000000101351175456763600203370ustar00rootroot00000000000000""" Unit tests for Gaphas' solver. """ import unittest from timeit import Timer from gaphas.solver import Solver, Variable from gaphas.constraint import EquationConstraint, EqualsConstraint, \ LessThanConstraint SETUP = """ from gaphas.solver import Solver, Variable from gaphas.constraint import EqualsConstraint, LessThanConstraint solver = Solver() v1, v2, v3 = Variable(1.0), Variable(2.0), Variable(3.0) c_eq = EqualsConstraint(v1, v2) solver.add_constraint(c_eq) """ # Timeit constants REPEAT = 30 NUMBER = 1000 class WeakestVariableTestCase(unittest.TestCase): """ Test weakest variable calculation. """ def test_weakest_list(self): """Test weakest list""" solver = Solver() a = Variable(1, 30) b = Variable(2, 10) c = Variable(3, 10) c_eq = EquationConstraint(lambda a, b, c: a + b + c, a=a, b=b, c=c) solver.add_constraint(c_eq) # because of kwargs above we cannot test by the order of arguments self.assertTrue(b in c_eq._weakest) self.assertTrue(c in c_eq._weakest) def test_weakest_list_order(self): """Test weakest list order""" solver = Solver() a = Variable(1, 30) b = Variable(2, 10) c = Variable(3, 10) c_eq = EquationConstraint(lambda a, b, c: a + b + c, a=a, b=b, c=c) solver.add_constraint(c_eq) weakest = [el for el in c_eq._weakest] a.value = 4 self.assertEqual(c_eq._weakest, weakest) # does not change if non-weak variable changed b.value = 5 self.assertEqual(c_eq.weakest(), c) b.value = 6 self.assertEqual(c_eq.weakest(), c) # b changed above, now change a - all weakest variables changed # return the oldest changed variable then c.value = 6 self.assertEqual(c_eq.weakest(), b) b.value = 6 self.assertEqual(c_eq.weakest(), c) def test_strength_change(self): """Test strength change""" solver = Solver() a = Variable(1, 30) b = Variable(2, 10) c = Variable(3, 10) c_eq = EquationConstraint(lambda a, b, c: a + b + c, a=a, b=b, c=c) solver.add_constraint(c_eq) b.strength = 9 self.assertEqual(c_eq._weakest, [b]) class SizeTestCase(unittest.TestCase): """ Test size related constraints, i.e. minimal size. """ def test_min_size(self): """Test minimal size constraint""" solver = Solver() v1 = Variable(0) v2 = Variable(10) v3 = Variable(10) c1 = EqualsConstraint(a=v2, b=v3) c2 = LessThanConstraint(smaller=v1, bigger=v3, delta=10) solver.add_constraint(c1) solver.add_constraint(c2) # check everyting is ok on start solver.solve() self.assertEquals(0, v1) self.assertEquals(10, v2) self.assertEquals(10, v3) # change v1 to 2, after solve it should be 0 again due to LT # constraint v1.value = 2 solver.solve() self.assertEquals(0, v1) self.assertEquals(10, v2) self.assertEquals(10, v3) # change v3 to 20, after solve v2 will follow thanks to EQ # constraint v3.value = 20 solver.solve() self.assertEquals(0, v1) self.assertEquals(20, v2) self.assertEquals(20, v3) # change v3 to 0, after solve it shoul be 10 due to LT.delta = 10, # v2 should also be 10 due to EQ constraint v3.value = 0 solver.solve() self.assertEquals(0, v1) self.assertEquals(10, v2) self.assertEquals(10, v3) class SolverSpeedTestCase(unittest.TestCase): """ Solver speed tests. """ def _test_speed_run_weakest(self): """ Speed test for weakest variable. """ results = Timer(setup=SETUP, stmt=""" v1.value = 5.0 c_eq.weakest()""").repeat(repeat=REPEAT, number=NUMBER) # Print the average of the best 10 runs: results.sort() print '[Avg: %gms]' % ((sum(results[:10]) / 10) * 1000) if __name__ == '__main__': unittest.main() # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_state.py000066400000000000000000000013661175456763600201530ustar00rootroot00000000000000import unittest from gaphas.state import reversible_pair, observed, _reverse class SList(object): def __init__(self): self.l = list() def add(self, node, before=None): if before: self.l.insert(self.l.index(before), node) else: self.l.append(node) add = observed(add) @observed def remove(self, node): self.l.remove(self.l.index(node)) class StateTestCase(unittest.TestCase): def test_adding_pair(self): """Test adding reversible pair """ reversible_pair(SList.add, SList.remove, \ bind1={'before': lambda self, node: self.l[self.l.index(node)+1] }) self.assertTrue(SList.add.im_func in _reverse) self.assertTrue(SList.remove.im_func in _reverse) gaphas-0.7.2/gaphas/tests/test_tool.py000066400000000000000000000176461175456763600200200ustar00rootroot00000000000000""" Test all the tools provided by gaphas. """ import unittest from gaphas.tool import ConnectHandleTool from gaphas.canvas import Canvas from gaphas.examples import Box from gaphas.item import Item, Element, Line from gaphas.view import View, GtkView from gaphas.constraint import LineConstraint from gaphas.canvas import Context from gaphas import state from gaphas.aspect import Connector, ConnectionSink Event = Context def simple_canvas(self): """ This decorator adds view, canvas and handle connection tool to a test case. Two boxes and a line are added to the canvas as well. """ self.canvas = Canvas() self.box1 = Box() self.canvas.add(self.box1) self.box1.matrix.translate(100, 50) self.box1.width = 40 self.box1.height = 40 self.box1.request_update() self.box2 = Box() self.canvas.add(self.box2) self.box2.matrix.translate(100, 150) self.box2.width = 50 self.box2.height = 50 self.box2.request_update() self.line = Line() self.head = self.line.handles()[0] self.tail = self.line.handles()[-1] self.tail.pos = 100, 100 self.canvas.add(self.line) self.canvas.update_now() self.view = GtkView() self.view.canvas = self.canvas import gtk win = gtk.Window() win.add(self.view) self.view.show() self.view.update() win.show() self.tool = ConnectHandleTool(self.view) class ConnectHandleToolGlueTestCase(unittest.TestCase): """ Test handle connection tool glue method. """ def setUp(self): simple_canvas(self) def test_item_and_port_glue(self): """Test glue operation to an item and its ports""" ports = self.box1.ports() # glue to port nw-ne sink = self.tool.glue(self.line, self.head, (120, 50)) self.assertEquals(sink.item, self.box1) self.assertEquals(ports[0], sink.port) # glue to port ne-se sink = self.tool.glue(self.line, self.head, (140, 70)) self.assertEquals(sink.item, self.box1) self.assertEquals(ports[1], sink.port) # glue to port se-sw sink = self.tool.glue(self.line, self.head, (120, 90)) self.assertEquals(sink.item, self.box1) self.assertEquals(ports[2], sink.port) # glue to port sw-nw sink = self.tool.glue(self.line, self.head, (100, 70)) self.assertEquals(sink.item, self.box1) self.assertEquals(ports[3], sink.port) def test_failed_glue(self): """Test glue from too far distance""" sink = self.tool.glue(self.line, self.head, (90, 50)) self.assertTrue(sink is None) # def test_glue_call_can_glue_once(self): # """Test if glue method calls can glue once only # # Box has 4 ports. Every port is examined once per # ConnectHandleTool.glue method call. The purpose of this test is to # assure that ConnectHandleTool.can_glue is called once (for the # found port), it cannot be called four times (once for every port). # """ # # # count ConnectHandleTool.can_glue calls # class Tool(ConnectHandleTool): # def __init__(self, *args): # super(Tool, self).__init__(*args) # self._calls = 0 # # def can_glue(self, *args): # self._calls += 1 # return True # # tool = Tool(self.view) # item, port = tool.glue(self.line, self.head, (120, 50)) # assert item and port # self.assertEquals(1, tool._calls) # def test_glue_cannot_glue(self): # """Test if glue method respects ConnectHandleTool.can_glue method""" # # class Tool(ConnectHandleTool): # def can_glue(self, *args): # return False # # tool = Tool(self.view) # item, port = tool.glue(self.line, self.head, (120, 50)) # self.assertTrue(item is None, item) # self.assertTrue(port is None, port) def test_glue_no_port_no_can_glue(self): """Test if glue method does not call ConnectHandleTool.can_glue method when port is not found""" class Tool(ConnectHandleTool): def __init__(self, *args): super(Tool, self).__init__(*args) self._calls = 0 def can_glue(self, *args): self._calls += 1 tool = Tool(self.view) # at 300, 50 there should be no item sink = tool.glue(self.line, self.head, (300, 50)) assert sink is None self.assertEquals(0, tool._calls) class ConnectHandleToolConnectTestCase(unittest.TestCase): def setUp(self): simple_canvas(self) def _get_line(self): line = Line() head = line.handles()[0] self.canvas.add(line) return line, head def test_connect(self): """Test connection to an item""" line, head = self._get_line() self.tool.connect(line, head, (120, 50)) cinfo = self.canvas.get_connection(head) self.assertTrue(cinfo is not None) self.assertEquals(self.box1, cinfo.connected) self.assertTrue(cinfo.port is self.box1.ports()[0], 'port %s' % cinfo.port) self.assertTrue(isinstance(cinfo.constraint, LineConstraint)) # No default callback defined: self.assertTrue(cinfo.callback is None) line, head = self._get_line() self.tool.connect(line, head, (90, 50)) cinfo2 = self.canvas.get_connection(head) self.assertTrue(cinfo is not cinfo2, cinfo2) self.assertTrue(cinfo2 is None, cinfo2) def test_reconnect_another(self): """Test reconnection to another item""" line, head = self._get_line() self.tool.connect(line, head, (120, 50)) cinfo = self.canvas.get_connection(head) assert cinfo is not None item = cinfo.connected port = cinfo.port constraint = cinfo.constraint assert item == self.box1 assert port == self.box1.ports()[0] assert item != self.box2 # connect to box2, handle's connected item and connection data # should differ self.tool.connect(line, head, (120, 150)) cinfo = self.canvas.get_connection(head) assert cinfo is not None self.assertEqual(self.box2, cinfo.connected) self.assertEqual(self.box2.ports()[0], cinfo.port) # old connection does not exist self.assertNotEqual(item, cinfo.connected) self.assertNotEqual(constraint, cinfo.constraint) def test_reconnect_same(self): """Test reconnection to same item""" line, head = self._get_line() self.tool.connect(line, head, (120, 50)) cinfo = self.canvas.get_connection(head) assert cinfo is not None item = cinfo.connected port = cinfo.port constraint = cinfo.constraint assert item == self.box1 assert item != self.box2 # connect to box1 again, handle's connected item and port should be # the same but connection constraint will differ connected = self.tool.connect(line, head, (120, 50)) cinfo = self.canvas.get_connection(head) assert cinfo is not None self.assertEqual(self.box1, cinfo.connected) self.assertEqual(self.box1.ports()[0], cinfo.port) self.assertNotEqual(constraint, cinfo.constraint) def xtest_find_port(self): """Test finding a port """ line, head = self._get_line() p1, p2, p3, p4 = self.box1.ports() head.pos = 110, 50 port = self.tool.find_port(line, head, self.box1) self.assertEquals(p1, port) head.pos = 140, 60 port = self.tool.find_port(line, head, self.box1) self.assertEquals(p2, port) head.pos = 110, 95 port = self.tool.find_port(line, head, self.box1) self.assertEquals(p3, port) head.pos = 100, 55 port = self.tool.find_port(line, head, self.box1) self.assertEquals(p4, port) # vim: sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_tree.py000066400000000000000000000106471175456763600177740ustar00rootroot00000000000000 import unittest from gaphas.tree import Tree class TreeTestCase(unittest.TestCase): def test_add(self): """ Test creating node trees. """ tree = Tree() n1 = 'n1' n2 = 'n2' n3 = 'n3' tree.add(n1) assert len(tree._nodes) == 1 assert len(tree._children) == 2 assert len(tree._children[None]) == 1 assert len(tree._children[n1]) == 0 tree.add(n2) tree.add(n3, parent=n1) assert len(tree._nodes) == 3 assert len(tree._children) == 4 assert len(tree._children[None]) == 2 assert len(tree._children[n1]) == 1 assert len(tree._children[n2]) == 0 assert len(tree._children[n2]) == 0 assert tree._nodes == [n1, n3, n2] n4 = 'n4' tree.add(n4, parent=n3) assert tree._nodes == [n1, n3, n4, n2] n5 = 'n5' tree.add(n5, parent=n3) assert tree._nodes == [n1, n3, n4, n5, n2] n6 = 'n6' tree.add(n6, parent=n2) assert tree._nodes == [n1, n3, n4, n5, n2, n6] n7 = 'n7' tree.add(n7, parent=n1) assert len(tree._children) == 8 assert tree._nodes == [n1, n3, n4, n5, n7, n2, n6] assert tree.get_parent(n7) is n1 assert tree.get_parent(n6) is n2 assert tree.get_parent(n5) is n3 assert tree.get_parent(n4) is n3 assert tree.get_parent(n3) is n1 assert tree.get_parent(n2) is None assert tree.get_parent(n1) is None def test_add_on_index(self): tree = Tree() n1 = 'n1' n2 = 'n2' n3 = 'n3' n4 = 'n4' n5 = 'n5' tree.add(n1) tree.add(n2) tree.add(n3, index=1) assert tree.get_children(None) == [n1, n3, n2], tree.get_children(None) assert tree.nodes == [n1, n3, n2], tree.nodes tree.add(n4, parent=n3) tree.add(n5, parent=n3, index=0) assert tree.get_children(None) == [n1, n3, n2], tree.get_children(None) assert tree.nodes == [n1, n3, n5, n4, n2], tree.nodes assert tree.get_children(n3) == [n5, n4], tree.get_children(n3) def test_remove(self): """ Test removal of nodes. """ tree = Tree() n1 = 'n1' n2 = 'n2' n3 = 'n3' n4 = 'n4' n5 = 'n5' tree.add(n1) tree.add(n2) tree.add(n3, parent=n1) tree.add(n4, parent=n3) tree.add(n5, parent=n4) assert tree._nodes == [n1, n3, n4, n5, n2] all_ch = list(tree.get_all_children(n1)) assert all_ch == [ n3, n4, n5 ], all_ch tree.remove(n4) assert tree._nodes == [n1, n3, n2] tree.remove(n1) assert len(tree._children) == 2 assert tree._children[None] == [n2] assert tree._children[n2] == [] assert tree._nodes == [n2] def test_siblings(self): tree = Tree() n1 = 'n1' n2 = 'n2' n3 = 'n3' tree.add(n1) tree.add(n2) tree.add(n3) assert tree.get_next_sibling(n1) is n2 assert tree.get_next_sibling(n2) is n3 try: tree.get_next_sibling(n3) except IndexError: pass # okay else: raise AssertionError, 'Index should be out of range, not %s' % tree.get_next_sibling(n3) assert tree.get_previous_sibling(n3) is n2 assert tree.get_previous_sibling(n2) is n1 try: tree.get_previous_sibling(n1) except IndexError: pass # okay else: raise AssertionError, 'Index should be out of range, not %s' % tree.get_previous_sibling(n1) def test_reparent(self): tree = Tree() n1 = 'n1' n2 = 'n2' n3 = 'n3' n4 = 'n4' n5 = 'n5' tree.add(n1) tree.add(n2) tree.add(n3) tree.add(n4, parent=n2) tree.add(n5, parent=n4) assert tree.nodes == [n1, n2, n4, n5, n3], tree.nodes tree.reparent(n4, parent=n1, index=0) assert tree.nodes == [n1, n4, n5, n2, n3], tree.nodes assert tree.get_children(n2) == [], tree.get_children(n2) assert tree.get_children(n1) == [n4], tree.get_children(n1) assert tree.get_children(n4) == [n5], tree.get_children(n4) tree.reparent(n4, parent=None, index=0) assert tree.nodes == [n4, n5, n1, n2, n3], tree.nodes # vi:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_undo.py000066400000000000000000000054251175456763600200000ustar00rootroot00000000000000 import unittest from gaphas import state state.observers.clear() state.subscribers.clear() undo_list = [] redo_list = [] def undo_handler(event): undo_list.append(event) def undo(): apply_me = list(undo_list) del undo_list[:] apply_me.reverse() for e in apply_me: state.saveapply(*e) redo_list[:] = undo_list[:] del undo_list[:] from gaphas.canvas import Canvas from gaphas.examples import Box from gaphas.item import Line from gaphas.aspect import Connector, ConnectionSink class UndoTestCase(unittest.TestCase): def setUp(self): state.observers.add(state.revert_handler) state.subscribers.add(undo_handler) def shutDown(self): state.observers.remove(state.revert_handler) state.subscribers.remove(undo_handler) def testUndoOnDeletedElement(self): b1 = Box() b2 = Box() line = Line() canvas = Canvas() canvas.add(b1) self.assertEquals(6, len(canvas.solver.constraints)) canvas.add(b2) self.assertEquals(12, len(canvas.solver.constraints)) canvas.add(line) sink = ConnectionSink(b1, b1.ports()[0]) connector = Connector(line, line.handles()[0]) connector.connect(sink) sink = ConnectionSink(b2, b2.ports()[0]) connector = Connector(line, line.handles()[-1]) connector.connect(sink) self.assertEquals(14, len(canvas.solver.constraints)) self.assertEquals(2, len(list(canvas.get_connections(item=line)))) del undo_list[:] # Here disconnect is not invoked! canvas.remove(b2) self.assertEquals(7, len(canvas.solver.constraints)) self.assertEquals(1, len(list(canvas.get_connections(item=line)))) cinfo = canvas.get_connection(line.handles()[0]) self.assertEquals(b1, cinfo.connected) cinfo = canvas.get_connection(line.handles()[-1]) self.assertEquals(None, cinfo) self.assertEquals([], list(canvas.solver.constraints_with_variable(line.handles()[-1].pos.x))) self.assertEquals([], list(canvas.solver.constraints_with_variable(line.handles()[-1].pos.y))) undo() self.assertEquals(14, len(canvas.solver.constraints)) self.assertEquals(2, len(list(canvas.get_connections(item=line)))) cinfo = canvas.get_connection(line.handles()[0]) self.assertEquals(b1, cinfo.connected) cinfo = canvas.get_connection(line.handles()[-1]) self.assertEquals(b2, cinfo.connected) # self.assertEquals(list(canvas.solver.constraints_with_variable(line.handles()[-1].pos.x))) # self.assertTrue(list(canvas.solver.constraints_with_variable(line.handles()[-1].pos.y))) if __name__ == '__main__': unittest.main() # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tests/test_view.py000066400000000000000000000137561175456763600200130ustar00rootroot00000000000000""" Test cases for the View class. """ import unittest import gtk from gaphas.view import View, GtkView from gaphas.canvas import Canvas, Context from gaphas.item import Line from gaphas.examples import Box from gaphas.tool import HoverTool class ViewTestCase(unittest.TestCase): def test_bounding_box_calculations(self): """ A view created before and after the canvas is populated should contain the same data. """ canvas = Canvas() window1 = gtk.Window(gtk.WINDOW_TOPLEVEL) view1 = GtkView(canvas=canvas) window1.add(view1) view1.realize() window1.show_all() box = Box() box.matrix = (1.0, 0.0, 0.0, 1, 10,10) canvas.add(box) line = Line() line.fyzzyness = 1 line.handles()[1].pos = (30, 30) #line.split_segment(0, 3) line.matrix.translate(30, 60) canvas.add(line) window2 = gtk.Window(gtk.WINDOW_TOPLEVEL) view2 = GtkView(canvas=canvas) window2.add(view2) window2.show_all() # Process pending (expose) events, which cause the canvas to be drawn. while gtk.events_pending(): gtk.main_iteration() try: assert view2.get_item_bounding_box(box) assert view1.get_item_bounding_box(box) assert view1.get_item_bounding_box(box) == view2.get_item_bounding_box(box), '%s != %s' % (view1.get_item_bounding_box(box), view2.get_item_bounding_box(box)) assert view1.get_item_bounding_box(line) == view2.get_item_bounding_box(line), '%s != %s' % (view1.get_item_bounding_box(line), view2.get_item_bounding_box(line)) finally: window1.destroy() window2.destroy() def test_get_item_at_point(self): """ Hover tool only reacts on motion-notify events """ canvas = Canvas() view = GtkView(canvas) window = gtk.Window(gtk.WINDOW_TOPLEVEL) window.add(view) window.show_all() box = Box() canvas.add(box) # No gtk main loop, so updates occur instantly assert not canvas.require_update() box.width = 50 box.height = 50 # Process pending (expose) events, which cause the canvas to be drawn. while gtk.events_pending(): gtk.main_iteration() assert len(view._qtree._ids) == 1 assert not view._qtree._bucket.bounds == (0, 0, 0, 0), view._qtree._bucket.bounds assert view.get_item_at_point((10, 10)) is box assert view.get_item_at_point((60, 10)) is None window.destroy() def test_item_removal(self): canvas = Canvas() view = GtkView(canvas) window = gtk.Window(gtk.WINDOW_TOPLEVEL) window.add(view) window.show_all() box = Box() canvas.add(box) # No gtk main loop, so updates occur instantly assert not canvas.require_update() # Process pending (expose) events, which cause the canvas to be drawn. while gtk.events_pending(): gtk.main_iteration() assert len(canvas.get_all_items()) == len(view._qtree) view.focused_item = box canvas.remove(box) assert len(canvas.get_all_items()) == 0 assert len(view._qtree) == 0 window.destroy() def test_view_registration(self): canvas = Canvas() # Simple views do not register on the canvas view = View(canvas) assert len(canvas._registered_views) == 0 box = Box() canvas.add(box) # By default no complex updating/calculations are done: assert not box._matrix_i2v.has_key(view) assert not box._matrix_v2i.has_key(view) # GTK view does register for updates though view = GtkView(canvas) assert len(canvas._registered_views) == 1 # No entry, since GtkView is not realized and has no window assert not box._matrix_i2v.has_key(view) assert not box._matrix_v2i.has_key(view) window = gtk.Window(gtk.WINDOW_TOPLEVEL) window.add(view) window.show_all() # Now everything is realized and updated assert box._matrix_i2v.has_key(view) assert box._matrix_v2i.has_key(view) view.canvas = None assert len(canvas._registered_views) == 0 assert not box._matrix_i2v.has_key(view) assert not box._matrix_v2i.has_key(view) view.canvas = canvas assert len(canvas._registered_views) == 1 assert box._matrix_i2v.has_key(view) assert box._matrix_v2i.has_key(view) def test_view_registration_2(self): """ Test view registration and destroy when view is destroyed. """ canvas = Canvas() view = GtkView(canvas) window = gtk.Window(gtk.WINDOW_TOPLEVEL) window.add(view) window.show_all() box = Box() canvas.add(box) assert hasattr(box, '_matrix_i2v') assert hasattr(box, '_matrix_v2i') assert box._matrix_i2v[view] assert box._matrix_v2i[view] assert len(canvas._registered_views) == 1 assert view in canvas._registered_views window.destroy() assert len(canvas._registered_views) == 0 assert not box._matrix_i2v.has_key(view) assert not box._matrix_v2i.has_key(view) def test_scroll_adjustments_signal(self): def handler(self, hadj, vadj): self.handled = True sc = gtk.ScrolledWindow() view = GtkView(Canvas()) view.connect('set-scroll-adjustments', handler) sc.add(view) assert view.handled def test_scroll_adjustments(self): sc = gtk.ScrolledWindow() view = GtkView(Canvas()) sc.add(view) print sc.get_hadjustment(), view.hadjustment assert sc.get_hadjustment() is view.hadjustment assert sc.get_vadjustment() is view.vadjustment if __name__ == '__main__': unittest.main() # vim:sw=4:et:ai gaphas-0.7.2/gaphas/tool.py000066400000000000000000000577301175456763600156150ustar00rootroot00000000000000""" Tools provide interactive behavior to a `View` by handling specific events sent by view. Some of implemented tools are `HoverTool` make the item under the mouse cursor the "hovered item" `ItemTool` handle selection and movement of items `HandleTool` handle selection and movement of handles `RubberbandTool` for rubber band selection of multiple items `PanTool` for easily moving the canvas around `PlacementTool` for placing items on the canvas The tools are chained with `ToolChain` class (it is a tool as well), which allows to combine functionality provided by different tools. Tools can handle events in different ways - event can be ignored - tool can handle the event (obviously) """ __version__ = "$Revision$" # $HeadURL$ import sys import cairo import gtk from gaphas.canvas import Context from gaphas.geometry import Rectangle from gaphas.geometry import distance_point_point_fast, distance_line_point from gaphas.item import Line from gaphas.aspect import Finder, Selection, InMotion, \ HandleFinder, HandleSelection, HandleInMotion, \ Connector DEBUG_TOOL_CHAIN = False Event = Context class Tool(object): """ Base class for a tool. This class A word on click events: Mouse (pointer) button click. A button press is normally followed by a button release. Double and triple clicks should work together with the button methods. A single click is emited as: on_button_press on_button_release In case of a double click: on_button_press (x 2) on_double_click on_button_release In case of a tripple click: on_button_press (x 3) on_triple_click on_button_release """ # Custom events: GRAB = -100 UNGRAB = -101 # Map GDK events to tool methods EVENT_HANDLERS = { gtk.gdk.BUTTON_PRESS: 'on_button_press', gtk.gdk.BUTTON_RELEASE: 'on_button_release', gtk.gdk._2BUTTON_PRESS: 'on_double_click', gtk.gdk._3BUTTON_PRESS: 'on_triple_click', gtk.gdk.MOTION_NOTIFY: 'on_motion_notify', gtk.gdk.KEY_PRESS: 'on_key_press', gtk.gdk.KEY_RELEASE: 'on_key_release', gtk.gdk.SCROLL: 'on_scroll', # Custom events: GRAB: 'on_grab', UNGRAB: 'on_ungrab', } # Those events force the tool to release the grabbed tool. FORCE_UNGRAB_EVENTS = (gtk.gdk._2BUTTON_PRESS, gtk.gdk._3BUTTON_PRESS) def __init__(self, view=None): self.view = view def set_view(self, view): self.view = view def _dispatch(self, event): """ Deal with the event. The event is dispatched to a specific handler for the event type. """ handler = self.EVENT_HANDLERS.get(event.type) #print event.type, handler if handler: try: h = getattr(self, handler) except AttributeError: pass # No handler else: return bool(h(event)) return False def handle(self, event): return self._dispatch(event) def draw(self, context): """ Some tools (such as Rubberband selection) may need to draw something on the canvas. This can be done through the draw() method. This is called after all items are drawn. The context contains the following fields: - context: the render context (contains context.view and context.cairo) - cairo: the Cairo drawing context """ pass class ToolChain(Tool): """ A ToolChain can be used to chain tools together, for example HoverTool, HandleTool, SelectionTool. The grabbed item is bypassed in case a double or tripple click event is received. Should make sure this doesn't end up in dangling states. """ def __init__(self, view=None): super(ToolChain, self).__init__(view) self._tools = [] self._grabbed_tool = None def set_view(self, view): self.view = view for tool in self._tools: tool.set_view(self.view) def append(self, tool): """ Append a tool to the chain. Self is returned. """ self._tools.append(tool) tool.view = self.view return self def grab(self, tool): if not self._grabbed_tool: if DEBUG_TOOL_CHAIN: print 'Grab tool', tool # Send grab event event = Event(type=Tool.GRAB) tool.handle(event) self._grabbed_tool = tool def ungrab(self, tool): if self._grabbed_tool is tool: if DEBUG_TOOL_CHAIN: print 'UNgrab tool', self._grabbed_tool # Send ungrab event event = Event(type=Tool.UNGRAB) tool.handle(event) self._grabbed_tool = None def validate_grabbed_tool(self, event): """ Check if it's valid to have a grabbed tool on an event. If not the grabbed tool will be released. """ if event.type in self.FORCE_UNGRAB_EVENTS: self.ungrab(self._grabbed_tool) def handle(self, event): """ Handle the event by calling each tool until the event is handled or grabbed. If a tool is returning True on a button press event, the motion and button release events are also passed to this """ handler = self.EVENT_HANDLERS.get(event.type) self.validate_grabbed_tool(event) if self._grabbed_tool and handler: try: return self._grabbed_tool.handle(event) finally: if event.type == gtk.gdk.BUTTON_RELEASE: self.ungrab(self._grabbed_tool) else: for tool in self._tools: if DEBUG_TOOL_CHAIN: print 'tool', tool rt = tool.handle(event) if rt: if event.type == gtk.gdk.BUTTON_PRESS: self.view.grab_focus() self.grab(tool) return rt def draw(self, context): if self._grabbed_tool: self._grabbed_tool.draw(context) class HoverTool(Tool): """ Make the item under the mouse cursor the "hovered item". """ def on_motion_notify(self, event): view = self.view pos = event.x, event.y view.hovered_item = Finder(view).get_item_at_point(pos) class ItemTool(Tool): """ ItemTool does selection and dragging of items. On a button click, the currently "hovered item" is selected. If CTRL or SHIFT are pressed, already selected items remain selected. The last selected item gets the focus (e.g. receives key press events). The roles used are Selection (select, unselect) and InMotion (move). """ def __init__(self, view=None, buttons=(1,)): super(ItemTool, self).__init__(view) self._buttons = buttons self._movable_items = set() def get_item(self): return self.view.hovered_item def movable_items(self): """ Filter the items that should eventually be moved. Returns InMotion aspects for the items. """ view = self.view get_ancestors = view.canvas.get_ancestors selected_items = set(view.selected_items) for item in selected_items: # Do not move subitems of selected items if not set(get_ancestors(item)).intersection(selected_items): yield InMotion(item, view) def on_button_press(self, event): ### TODO: make keys configurable view = self.view item = self.get_item() if event.button not in self._buttons: return False # Deselect all items unless CTRL or SHIFT is pressed # or the item is already selected. if not (event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK) or item in view.selected_items): del view.selected_items if item: if view.hovered_item in view.selected_items and \ event.state & gtk.gdk.CONTROL_MASK: selection = Selection(item, view) selection.unselect() else: selection = Selection(item, view) selection.select() self._movable_items.clear() return True def on_button_release(self, event): if event.button not in self._buttons: return False for inmotion in self._movable_items: inmotion.stop_move() self._movable_items.clear() return True def on_motion_notify(self, event): """ Normally do nothing. If a button is pressed move the items around. """ if event.state & gtk.gdk.BUTTON_PRESS_MASK: if not self._movable_items: self._movable_items = set(self.movable_items()) for inmotion in self._movable_items: inmotion.start_move((event.x, event.y)) for inmotion in self._movable_items: inmotion.move((event.x, event.y)) return True class HandleTool(Tool): """ Tool for moving handles around. By default this tool does not provide connecting handles to another item (see `ConnectHandleTool`). """ def __init__(self, view=None): super(HandleTool, self).__init__(view) self.grabbed_handle = None self.grabbed_item = None self.motion_handle = None def grab_handle(self, item, handle): """ Grab a specific handle. This can be used from the PlacementTool to set the state of the handle tool. """ assert item is None and handle is None or handle in item.handles() self.grabbed_item = item self.grabbed_handle = handle selection = HandleSelection(item, handle, self.view) selection.select() def ungrab_handle(self): """ Reset grabbed_handle and grabbed_item. """ item = self.grabbed_item handle = self.grabbed_handle self.grabbed_handle = None self.grabbed_item = None if handle: selection = HandleSelection(item, handle, self.view) selection.unselect() def on_button_press(self, event): """ Handle button press events. If the (mouse) button is pressed on top of a Handle (item.Handle), that handle is grabbed and can be dragged around. """ view = self.view #item, handle = view.get_handle_at_point((event.x, event.y)) #if not handle and view.hovered_item item, handle = HandleFinder(view.hovered_item, view).get_handle_at_point((event.x, event.y)) if handle: # Deselect all items unless CTRL or SHIFT is pressed # or the item is already selected. ### TODO: duplicate from ItemTool if not (event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK) or view.hovered_item in view.selected_items): del view.selected_items ###/ view.hovered_item = item view.focused_item = item self.motion_handle = None self.grab_handle(item, handle) return True def on_button_release(self, event): """ Release a grabbed handle. """ # queue extra redraw to make sure the item is drawn properly grabbed_handle, grabbed_item = self.grabbed_handle, self.grabbed_item if self.motion_handle: self.motion_handle.stop_move() self.motion_handle = None self.ungrab_handle() if grabbed_handle: grabbed_item.request_update() return True def on_motion_notify(self, event): """ Handle motion events. If a handle is grabbed: drag it around, else, if the pointer is over a handle, make the owning item the hovered-item. """ view = self.view if self.grabbed_handle and event.state & gtk.gdk.BUTTON_PRESS_MASK: canvas = view.canvas item = self.grabbed_item handle = self.grabbed_handle pos = event.x, event.y if not self.motion_handle: self.motion_handle = HandleInMotion(item, handle, self.view) self.motion_handle.start_move(pos) self.motion_handle.move(pos) return True class RubberbandTool(Tool): def __init__(self, view=None): super(RubberbandTool, self).__init__(view) self.x0, self.y0, self.x1, self.y1 = 0, 0, 0, 0 def on_button_press(self, event): self.x0, self.y0 = event.x, event.y self.x1, self.y1 = event.x, event.y return True def on_button_release(self, event): self.queue_draw(self.view) x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1 self.view.select_in_rectangle((min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0))) return True def on_motion_notify(self, event): if event.state & gtk.gdk.BUTTON_PRESS_MASK: view = self.view self.queue_draw(view) self.x1, self.y1 = event.x, event.y self.queue_draw(view) return True def queue_draw(self, view): x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1 view.queue_draw_area(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0)) def draw(self, context): cr = context.cairo x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1 cr.set_line_width(1.0) cr.set_source_rgba(.5, .5, .7, .6) cr.rectangle(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0)) cr.fill() PAN_MASK = gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK | gtk.gdk.CONTROL_MASK PAN_VALUE = 0 class PanTool(Tool): """ Captures drag events with the middle mouse button and uses them to translate the canvas within the view. Trumps the ZoomTool, so should be placed later in the ToolChain. """ def __init__(self, view=None): super(PanTool, self).__init__(view) self.x0, self.y0 = 0, 0 self.speed = 10 def on_button_press(self, event): if not event.state & PAN_MASK == PAN_VALUE: return False if event.button == 2: self.x0, self.y0 = event.x, event.y return True def on_button_release(self, event): self.x0, self.y0 = event.x, event.y return True def on_motion_notify(self, event): if event.state & gtk.gdk.BUTTON2_MASK: view = self.view self.x1, self.y1 = event.x, event.y dx = self.x1 - self.x0 dy = self.y1 - self.y0 view._matrix.translate(dx/view._matrix[0], dy/view._matrix[3]) # Make sure everything's updated view.request_update((), view._canvas.get_all_items()) self.x0 = self.x1 self.y0 = self.y1 return True def on_scroll(self, event): # Ensure no modifiers if not event.state & PAN_MASK == PAN_VALUE: return False view = self.view direction = event.direction gdk = gtk.gdk if direction == gdk.SCROLL_LEFT: view._matrix.translate(self.speed/view._matrix[0], 0) elif direction == gdk.SCROLL_RIGHT: view._matrix.translate(-self.speed/view._matrix[0], 0) elif direction == gdk.SCROLL_UP: view._matrix.translate(0, self.speed/view._matrix[3]) elif direction == gdk.SCROLL_DOWN: view._matrix.translate(0, -self.speed/view._matrix[3]) view.request_update((), view._canvas.get_all_items()) return True ZOOM_MASK = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK ZOOM_VALUE = gtk.gdk.CONTROL_MASK class ZoomTool(Tool): """ Tool for zooming using either of two techniques: - ctrl + middle-mouse dragging in the up-down direction. - ctrl + mouse-wheeel """ def __init__(self, view=None): super(ZoomTool, self).__init__(view) self.x0, self.y0 = 0, 0 self.lastdiff = 0; def on_button_press(self, event): if event.button == 2 \ and event.state & ZOOM_MASK == ZOOM_VALUE: self.x0 = event.x self.y0 = event.y self.lastdiff = 0 return True def on_button_release(self, event): self.lastdiff = 0 return True def on_motion_notify(self, event): if event.state & ZOOM_MASK == ZOOM_VALUE \ and event.state & gtk.gdk.BUTTON2_MASK: view = self.view dy = event.y - self.y0 sx = view._matrix[0] sy = view._matrix[3] ox = (view._matrix[4] - self.x0) / sx oy = (view._matrix[5] - self.y0) / sy if abs(dy - self.lastdiff) > 20: if dy - self.lastdiff < 0: factor = 1./0.9 else: factor = 0.9 m = view.matrix m.translate(-ox, -oy) m.scale(factor, factor) m.translate(+ox, +oy) # Make sure everything's updated view.request_update((), view._canvas.get_all_items()) self.lastdiff = dy; return True def on_scroll(self, event): if event.state & gtk.gdk.CONTROL_MASK: view = self.view sx = view._matrix[0] sy = view._matrix[3] ox = (view._matrix[4] - event.x) / sx oy = (view._matrix[5] - event.y) / sy factor = 0.9 if event.direction == gtk.gdk.SCROLL_UP: factor = 1. / factor view._matrix.translate(-ox, -oy) view._matrix.scale(factor, factor) view._matrix.translate(+ox, +oy) # Make sure everything's updated view.request_update((), view._canvas.get_all_items()) return True class PlacementTool(Tool): def __init__(self, view, factory, handle_tool, handle_index): super(PlacementTool, self).__init__(view) self._factory = factory self.handle_tool = handle_tool handle_tool.set_view(view) self._handle_index = handle_index self._new_item = None self.grabbed_handle = None #handle_tool = property(lambda s: s._handle_tool, doc="Handle tool") handle_index = property(lambda s: s._handle_index, doc="Index of handle to be used by handle_tool") new_item = property(lambda s: s._new_item, doc="The newly created item") def on_button_press(self, event): view = self.view canvas = view.canvas new_item = self._create_item((event.x, event.y)) # Enforce matrix update, as a good matrix is required for the handle # positioning: canvas.get_matrix_i2c(new_item, calculate=True) self._new_item = new_item view.focused_item = new_item h = new_item.handles()[self._handle_index] if h.movable: self.handle_tool.grab_handle(new_item, h) self.grabbed_handle = h return True def _create_item(self, pos, **kw): view = self.view item = self._factory(**kw) x, y = view.get_matrix_v2i(item).transform_point(*pos) item.matrix.translate(x, y) return item def on_button_release(self, event): if self.grabbed_handle: self.handle_tool.on_button_release(event) self.grabbed_handle = None self._new_item = None return True def on_motion_notify(self, event): if self.grabbed_handle: return self.handle_tool.on_motion_notify(event) else: # act as if the event is handled if we have a new item return bool(self._new_item) class TextEditTool(Tool): """ Demo of a text edit tool (just displays a text edit box at the cursor position. """ def __init__(self, view=None): super(TextEditTool, self).__init__(view) def on_double_click(self, event): """ Create a popup window with some editable text. """ window = gtk.Window() window.set_property('decorated', False) window.set_resize_mode(gtk.RESIZE_IMMEDIATE) #window.set_modal(True) window.set_parent_window(self.view.window) buffer = gtk.TextBuffer() text_view = gtk.TextView() text_view.set_buffer(buffer) text_view.show() window.add(text_view) window.size_allocate(gtk.gdk.Rectangle(int(event.x), int(event.y), 50, 50)) #window.move(int(event.x), int(event.y)) cursor_pos = self.view.get_toplevel().get_screen().get_display().get_pointer() window.move(cursor_pos[1], cursor_pos[2]) window.connect('focus-out-event', self._on_focus_out_event, buffer) text_view.connect('key-press-event', self._on_key_press_event, buffer) #text_view.set_size_request(50, 50) window.show() #text_view.grab_focus() #window.set_uposition(event.x, event.y) #window.focus return True def _on_key_press_event(self, widget, event, buffer): #if event.keyval == gtk.keysyms.Return: #print 'Enter!' #widget.get_toplevel().destroy() if event.keyval == gtk.keysyms.Escape: #print 'Escape!' widget.get_toplevel().destroy() def _on_focus_out_event(self, widget, event, buffer): #print 'focus out!', buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter()) widget.destroy() class ConnectHandleTool(HandleTool): """ Tool for connecting two items. There are two items involved. Handle of connecting item (usually a line) is being dragged by an user towards another item (item in short). Port of an item is found by the tool and connection is established by creating a constraint between line's handle and item's port. """ def glue(self, item, handle, vpos): """ Perform a small glue action to ensure the handle is at a proper location for connecting. """ if self.motion_handle: return self.motion_handle.glue(vpos) else: return HandleInMotion(item, handle, self.view).glue(vpos) def connect(self, item, handle, vpos): """ Connect a handle of a item to connectable item. Connectable item is found by `ConnectHandleTool.glue` method. :Parameters: item Connecting item. handle Handle of connecting item. vpos Position to connect to (or near at least) """ connector = Connector(item, handle) # find connectable item and its port sink = self.glue(item, handle, vpos) # no new connectable item, then diconnect and exit if sink: connector.connect(sink) else: cinfo = item.canvas.get_connection(handle) if cinfo: connector.disconnect() def on_button_release(self, event): view = self.view item = self.grabbed_item handle = self.grabbed_handle try: if handle and handle.connectable: self.connect(item, handle, (event.x, event.y)) finally: return super(ConnectHandleTool, self).on_button_release(event) # def on_motion_notify(self, event): # super(ConnectHandleTool, self).on_motion_notify(event) # handle = self.grabbed_handle # if handle and event.state & gtk.gdk.BUTTON_PRESS_MASK: # if handle.connectable: # self.glue(self.grabbed_item, handle, (event.x, event.y)) # # return True def DefaultTool(view=None): """ The default tool chain build from HoverTool, ItemTool and HandleTool. """ return ToolChain(view). \ append(HoverTool()). \ append(ConnectHandleTool()). \ append(PanTool()). \ append(ZoomTool()). \ append(ItemTool()). \ append(TextEditTool()). \ append(RubberbandTool()) # vim: sw=4:et:ai gaphas-0.7.2/gaphas/tree.py000066400000000000000000000241431175456763600155670ustar00rootroot00000000000000""" Simple class containing the tree structure for the canvas items. """ __version__ = "$Revision$" # $HeadURL$ from operator import attrgetter class Tree(object): """ A Tree structure. Nodes are stores in a depth-first order. ``None`` is the root node. @invariant: len(self._children) == len(self._nodes) + 1 """ def __init__(self): # List of nodes in the tree, sorted in the order they ought to be # rendered self._nodes = [] # Per entry a list of children is maintained. self._children = { None: [] } # For easy and fast lookups, also maintain a child -> parent mapping self._parents = { } nodes = property(lambda s: list(s._nodes)) def get_parent(self, node): """ Return the parent item of ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.get_parent('n2') 'n1' """ return self._parents.get(node) def get_children(self, node): """ Return all child objects of ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_children('n1') ['n2', 'n3'] >>> tree.get_children('n2') [] """ return self._children[node] def get_siblings(self, node): """ Get all siblings of ``node``, including ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_siblings('n2') ['n2', 'n3'] """ parent = self.get_parent(node) return self._children[parent] def get_next_sibling(self, node): """ Return the node on the same level after ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_next_sibling('n2') 'n3' >>> tree.get_next_sibling('n3') # doctest: +ELLIPSIS Traceback (most recent call last): ... IndexError: list index out of range """ parent = self.get_parent(node) siblings = self._children[parent] return siblings[siblings.index(node) + 1] def get_previous_sibling(self, node): """ Return the node on the same level before ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_previous_sibling('n3') 'n2' >>> tree.get_previous_sibling('n2') # doctest: +ELLIPSIS Traceback (most recent call last): ... IndexError: list index out of range """ parent = self.get_parent(node) siblings = self._children[parent] index = siblings.index(node) - 1 if index < 0: raise IndexError('list index out of range') return siblings[index] def get_all_children(self, node): """ Iterate all children (and children of children and so forth) >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n2') >>> tree.get_children('n1') ['n2'] >>> tree.get_all_children('n1') # doctest: +ELLIPSIS >>> list(tree.get_all_children('n1')) ['n2', 'n3'] """ children = self.get_children(node) for c in children: yield c for cc in self.get_all_children(c): yield cc def get_ancestors(self, node): """ Iterate all parents and parents of parents, etc. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n2') >>> tree.get_parent('n3') 'n2' >>> tree.get_ancestors('n3') # doctest: +ELLIPSIS >>> list(tree.get_ancestors('n3')) ['n2', 'n1'] >>> list(tree.get_ancestors('n1')) [] """ parent = self.get_parent(node) while parent: yield parent parent = self.get_parent(parent) def index_nodes(self, index_key): """ Provide each item in the tree with an index attribute. This makes for fast sorting of items. >>> class A(object): ... def __init__(self, n): ... self.n = n ... def __repr__(self): ... return self.n >>> t = Tree() >>> a = A('a') >>> t.add(a) >>> t.add(A('b')) >>> t.add(A('c'), parent=a) >>> t.nodes [a, c, b] >>> t.index_nodes('my_key') >>> t.nodes[0].my_key, t.nodes[1].my_key, t.nodes[2].my_key (0, 1, 2) For sorting, see ``sort()``. """ nodes = self.nodes lnodes = len(nodes) map(setattr, nodes, [index_key] * lnodes, xrange(lnodes)) def sort(self, nodes, index_key, reverse=False): """ Sort a set (or list) of nodes. >>> class A(object): ... def __init__(self, n): ... self.n = n ... def __repr__(self): ... return self.n >>> t = Tree() >>> a = A('a') >>> t.add(a) >>> t.add(A('b')) >>> t.add(A('c'), parent=a) >>> t.nodes # the series from Tree.index_nodes [a, c, b] >>> t.index_nodes('my_key') >>> selection = (t.nodes[2], t.nodes[1]) >>> t.sort(selection, index_key='my_key') [c, b] """ if index_key: return sorted(nodes, key=attrgetter(index_key), reverse=reverse) else: raise NotImplemented('index_key should be provided.') def _add_to_nodes(self, node, parent, index=None): """ Helper method to place nodes on the right location in the nodes list Called only from add() and reparent() """ nodes = self._nodes siblings = self._children[parent] try: atnode = siblings[index] except (TypeError, IndexError): index = len(siblings) #self._add_to_nodes(node, parent) if parent: try: next_uncle = self.get_next_sibling(parent) except IndexError: # parent has no younger brothers.. # place it before the next uncle of grant_parent: return self._add_to_nodes(node, self.get_parent(parent)) else: nodes.insert(nodes.index(next_uncle), node) else: # append to root node: nodes.append(node) else: nodes.insert(nodes.index(atnode), node) def _add(self, node, parent=None, index=None): """ Helper method for both add() and reparent(). """ assert node not in self._nodes siblings = self._children[parent] self._add_to_nodes(node, parent, index) # Fix parent-child and child-parent relationship try: siblings.insert(index, node) except TypeError: siblings.append(node) # Create new entry for it's own children: if parent: self._parents[node] = parent def add(self, node, parent=None, index=None): """ Add node to the tree. parent is the parent node, which may be None if the item should be added to the root item. For usage, see the unit tests. """ self._add(node, parent, index) self._children[node] = [] def _remove(self, node): # Remove from parent item self.get_siblings(node).remove(node) # Remove data entries: del self._children[node] self._nodes.remove(node) try: del self._parents[node] except KeyError: pass def remove(self, node): """ Remove ``node`` from the tree. For usage, see the unit tests. """ # First remove children: for c in reversed(list(self._children[node])): self.remove(c) self._remove(node) def _reparent_nodes(self, node, parent): """ Helper for reparent(). The _children and _parent trees can be left intact as far as children of the reparented node are concerned. Only the position in the _nodes list changes. """ self._nodes.remove(node) self._add_to_nodes(node, parent) for c in self._children[node]: self._reparent_nodes(c, node) def reparent(self, node, parent, index=None): """ Set new parent for a ``node``. ``Parent`` can be ``None``, indicating it's added to the top. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.nodes ['n1', 'n2', 'n3'] >>> tree.reparent('n2', 'n3') >>> tree.get_parent('n2') 'n3' >>> tree.get_children('n3') ['n2'] >>> tree.nodes ['n1', 'n3', 'n2'] If a node contains children, those are also moved: >>> tree.add('n4') >>> tree.nodes ['n1', 'n3', 'n2', 'n4'] >>> tree.reparent('n1', 'n4') >>> tree.get_parent('n1') 'n4' >>> list(tree.get_all_children('n4')) ['n1', 'n3', 'n2'] >>> tree.nodes ['n4', 'n1', 'n3', 'n2'] """ if parent is self.get_parent(node): return # Remove all node references: old_parent = self.get_parent(node) self._children[old_parent].remove(node) self._nodes.remove(node) if old_parent: del self._parents[node] self._add(node, parent, index) # reorganize children in nodes list for c in self._children[node]: self._reparent_nodes(c, node) # vi: sw=4:et:ai gaphas-0.7.2/gaphas/util.py000066400000000000000000000071261175456763600156070ustar00rootroot00000000000000""" Helper functions and classes for Cairo (drawing engine used by the canvas). """ __version__ = "$Revision$" # $HeadURL$ from math import pi import cairo def text_extents(cr, text, font=None, multiline=False, padding=1): """ Simple way to determine the size of a piece of text. """ if not text: return 0, 0 if font: cr.save() text_set_font(cr, font) if multiline: width, height = 0, 0 for line in text.split('\n'): x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(line) width = max(width, w) height += h + padding else: x_bear, y_bear, width, height, x_adv, y_adv = cr.text_extents(text) #width, height = width + x_bearing, height + y_bearing if font: cr.restore() return width, height def text_center(cr, x, y, text): text_align(cr, x, y, text, align_x=0, align_y=0) def text_align(cr, x, y, text, align_x=0, align_y=0, padding_x=0, padding_y=0): """ Draw text relative to (x, y). x, y - coordinates text - text to print (utf8) align_x - -1 (top), 0 (middle), 1 (bottom) align_y - -1 (left), 0 (center), 1 (right) padding_x - padding (extra offset), always > 0 padding_y - padding (extra offset), always > 0 """ if not text: return x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text) if align_x == 0: x = 0.5 - (w / 2 + x_bear) + x elif align_x < 0: x = - (w + x_bear) + x - padding_x else: x = x + padding_x if align_y == 0: y = 0.5 - (h / 2 + y_bear) + y elif align_y < 0: y = - (h + y_bear) + y - padding_y else: y = -y_bear + y + padding_y cr.move_to(x, y) cr.show_text(text) def text_multiline(cr, x, y, text, padding=1): """ Draw a string of text with embedded newlines. cr - cairo context x - leftmost x y - topmost y text - text to draw padding - additional padding between lines. """ if not text: return #cr.move_to(x, y) for line in text.split('\n'): x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text) y += h cr.move_to(x, y) cr.show_text(line) def text_underline(cr, x, y, text, offset=1.5): """ Draw text with underline. """ x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text) cr.move_to(x, y - y_bear) cr.show_text(text) cr.move_to(x, y - y_bear + offset) cr.set_line_width(1.0) cr.rel_line_to(x_adv, 0) cr.stroke() def text_set_font(cr, font): """ Set the font from a string. E.g. 'sans 10' or 'sans italic bold 12' only restriction is that the font name should be the first option and the font size as last argument """ font = font.split() cr.select_font_face(font[0], 'italic' in font and cairo.FONT_SLANT_ITALIC \ or cairo.FONT_SLANT_NORMAL, 'bold' in font and cairo.FONT_WEIGHT_BOLD \ or cairo.FONT_WEIGHT_NORMAL) cr.set_font_size(float(font[-1])) def path_ellipse (cr, x, y, width, height, angle=0): """ Draw an ellipse. x - center x y - center y width - width of ellipse (in x direction when angle=0) height - height of ellipse (in y direction when angle=0) angle - angle in radians to rotate, clockwise """ cr.save() cr.translate (x, y) cr.rotate (angle) cr.scale (width / 2.0, height / 2.0) cr.move_to(1.0, 0.0) cr.arc (0.0, 0.0, 1.0, 0.0, 2.0 * pi) cr.restore() # vim:sw=4:et gaphas-0.7.2/gaphas/view.py000066400000000000000000000647761175456763600156220ustar00rootroot00000000000000""" This module contains everything to display a Canvas on a screen. """ __version__ = "$Revision$" # $HeadURL$ import gobject import gtk from cairo import Matrix from canvas import Context from geometry import Rectangle from quadtree import Quadtree from tool import DefaultTool from painter import DefaultPainter, BoundingBoxPainter from decorators import async, PRIORITY_HIGH_IDLE from decorators import nonrecursive # Handy debug flag for drawing bounding boxes around the items. DEBUG_DRAW_BOUNDING_BOX = False DEBUG_DRAW_QUADTREE = False # The default cursor (use in case of a cursor reset) DEFAULT_CURSOR = gtk.gdk.LEFT_PTR class View(object): """ View class for gaphas.Canvas objects. """ def __init__(self, canvas=None): self._matrix = Matrix() self._painter = DefaultPainter(self) self._bounding_box_painter = BoundingBoxPainter(self) # Handling selections. ### TODO: Move this to a context? self._selected_items = set() self._focused_item = None self._hovered_item = None self._dropzone_item = None ###/ self._qtree = Quadtree() self._bounds = Rectangle(0, 0, 0, 0) self._canvas = None if canvas: self._set_canvas(canvas) matrix = property(lambda s: s._matrix, doc="Canvas to view transformation matrix") def _set_canvas(self, canvas): """ Use view.canvas = my_canvas to set the canvas to be rendered in the view. """ if self._canvas: self._qtree.clear() self._selected_items.clear() self._focused_item = None self._hovered_item = None self._dropzone_item = None self._canvas = canvas canvas = property(lambda s: s._canvas, _set_canvas) def emit(self, *args, **kwargs): """ Placeholder method for signal emission functionality. """ pass def queue_draw_item(self, *items): """ Placeholder for item redraw queueing. """ pass def select_item(self, item): """ Select an item. This adds @item to the set of selected items. """ self.queue_draw_item(item) if item not in self._selected_items: self._selected_items.add(item) self.emit('selection-changed', self._selected_items) def unselect_item(self, item): """ Unselect an item. """ self.queue_draw_item(item) if item in self._selected_items: self._selected_items.discard(item) self.emit('selection-changed', self._selected_items) def select_all(self): for item in self.canvas.get_all_items(): self.select_item(item) def unselect_all(self): """ Clearing the selected_item also clears the focused_item. """ self.queue_draw_item(*self._selected_items) self._selected_items.clear() self.focused_item = None self.emit('selection-changed', self._selected_items) selected_items = property(lambda s: s._selected_items, select_item, unselect_all, "Items selected by the view") def _set_focused_item(self, item): """ Set the focused item, this item is also added to the selected_items set. """ if not item is self._focused_item: self.queue_draw_item(self._focused_item, item) if item: self.select_item(item) if item is not self._focused_item: self._focused_item = item self.emit('focus-changed', item) def _del_focused_item(self): """ Items that loose focus remain selected. """ self._set_focused_item(None) focused_item = property(lambda s: s._focused_item, _set_focused_item, _del_focused_item, "The item with focus (receives key events a.o.)") def _set_hovered_item(self, item): """ Set the hovered item. """ if item is not self._hovered_item: self.queue_draw_item(self._hovered_item, item) self._hovered_item = item self.emit('hover-changed', item) def _del_hovered_item(self): """ Unset the hovered item. """ self._set_hovered_item(None) hovered_item = property(lambda s: s._hovered_item, _set_hovered_item, _del_hovered_item, "The item directly under the mouse pointer") def _set_dropzone_item(self, item): """ Set dropzone item. """ if item is not self._dropzone_item: self.queue_draw_item(self._dropzone_item, item) self._dropzone_item = item self.emit('dropzone-changed', item) def _del_dropzone_item(self): """ Unset dropzone item. """ self._set_dropzone_item(None) dropzone_item = property(lambda s: s._dropzone_item, _set_dropzone_item, _del_dropzone_item, 'The item which can group other items') def _set_painter(self, painter): """ Set the painter to use. Painters should implement painter.Painter. """ self._painter = painter painter.set_view(self) self.emit('painter-changed') painter = property(lambda s: s._painter, _set_painter) def _set_bounding_box_painter(self, painter): """ Set the painter to use for bounding box calculations. """ self._bounding_box_painter = painter painter.set_view(self) self.emit('painter-changed') bounding_box_painter = property(lambda s: s._bounding_box_painter, _set_bounding_box_painter) def get_item_at_point(self, pos, selected=True): """ Return the topmost item located at ``pos`` (x, y). Parameters: - selected: if False returns first non-selected item """ items = self._qtree.find_intersect((pos[0], pos[1], 1, 1)) for item in self._canvas.sort(items, reverse=True): if not selected and item in self.selected_items: continue # skip selected items v2i = self.get_matrix_v2i(item) ix, iy = v2i.transform_point(*pos) if item.point((ix, iy)) < 0.5: return item return None def get_handle_at_point(self, pos, distance=6): """ Look for a handle at ``pos`` and return the tuple (item, handle). """ def find(item): """ Find item's handle at pos """ v2i = self.get_matrix_v2i(item) d = v2i.transform_distance(distance, 0)[0] x, y = v2i.transform_point(*pos) for h in item.handles(): if not h.movable: continue hx, hy = h.pos if -d < (hx - x) < d and -d < (hy - y) < d: return h # The focused item is the prefered item for handle grabbing if self.focused_item: h = find(self.focused_item) if h: return self.focused_item, h # then try hovered item if self.hovered_item: h = find(self.hovered_item) if h: return self.hovered_item, h # Last try all items, checking the bounding box first x, y = pos items = self.get_items_in_rectangle((x - distance, y - distance, distance * 2, distance * 2), reverse=True) found_item, found_h = None, None for item in items: h = find(item) if h: return item, h return None, None def get_port_at_point(self, vpos, distance=10, exclude=None): """ Find item with port closest to specified position. List of items to be ignored can be specified with `exclude` parameter. Tuple is returned - found item - closest, connectable port - closest point on found port (in view coordinates) :Parameters: vpos Position specified in view coordinates. distance Max distance from point to a port (default 10) exclude Set of items to ignore. """ v2i = self.get_matrix_v2i vx, vy = vpos max_dist = distance port = None glue_pos = None item = None rect = (vx - distance, vy - distance, distance * 2, distance * 2) items = self.get_items_in_rectangle(rect, reverse=True) for i in items: if i in exclude: continue for p in i.ports(): if not p.connectable: continue ix, iy = v2i(i).transform_point(vx, vy) pg, d = p.glue((ix, iy)) if d >= max_dist: continue item = i port = p # transform coordinates from connectable item space to view # space i2v = self.get_matrix_i2v(i).transform_point glue_pos = i2v(*pg) return item, port, glue_pos def get_items_in_rectangle(self, rect, intersect=True, reverse=False): """ Return the items in the rectangle 'rect'. Items are automatically sorted in canvas' processing order. """ if intersect: items = self._qtree.find_intersect(rect) else: items = self._qtree.find_inside(rect) return self._canvas.sort(items, reverse=reverse) def select_in_rectangle(self, rect): """ Select all items who have their bounding box within the rectangle @rect. """ items = self._qtree.find_inside(rect) map(self.select_item, items) def zoom(self, factor): """ Zoom in/out by factor @factor. """ # TODO: should the scale factor be clipped? self._matrix.scale(factor, factor) # Make sure everything's updated #map(self.update_matrix, self._canvas.get_all_items()) self.request_update((), self._canvas.get_all_items()) def set_item_bounding_box(self, item, bounds): """ Update the bounding box of the item. ``bounds`` is in view coordinates. Coordinates are calculated back to item coordinates, so matrix-only updates can occur. """ v2i = self.get_matrix_v2i(item).transform_point ix0, iy0 = v2i(bounds.x, bounds.y) ix1, iy1 = v2i(bounds.x1, bounds.y1) self._qtree.add(item=item, bounds=bounds, data=Rectangle(ix0, iy0, x1=ix1, y1=iy1)) def get_item_bounding_box(self, item): """ Get the bounding box for the item, in view coordinates. """ return self._qtree.get_bounds(item) bounding_box = property(lambda s: s._bounds) def update_bounding_box(self, cr, items=None): """ Update the bounding boxes of the canvas items for this view, in canvas coordinates. """ painter = self._bounding_box_painter if items is None: items = self.canvas.get_all_items() # The painter calls set_item_bounding_box() for each rendered item. painter.paint(Context(cairo=cr, items=items, area=None)) # Update the view's bounding box with the rest of the items self._bounds = Rectangle(*self._qtree.soft_bounds) def paint(self, cr): self._painter.paint(Context(cairo=cr, items=self.canvas.get_all_items(), area=None)) def get_matrix_i2v(self, item): """ Get Item to View matrix for ``item``. """ if self not in item._matrix_i2v: self.update_matrix(item) return item._matrix_i2v[self] def get_matrix_v2i(self, item): """ Get View to Item matrix for ``item``. """ if self not in item._matrix_v2i: self.update_matrix(item) return item._matrix_v2i[self] def update_matrix(self, item): """ Update item matrices related to view. """ try: i2v = item._matrix_i2c.multiply(self._matrix) except AttributeError: # Fall back to old behaviour i2v = item._matrix_i2c * self._matrix item._matrix_i2v[self] = i2v v2i = Matrix(*i2v) v2i.invert() item._matrix_v2i[self] = v2i def _clear_matrices(self): """ Clear registered data in Item's _matrix{i2c|v2i} attributes. """ for item in self.canvas.get_all_items(): try: del item._matrix_i2v[self] del item._matrix_v2i[self] except KeyError: pass class GtkView(gtk.DrawingArea, View): # NOTE: Inherit from GTK+ class first, otherwise BusErrors may occur! """ GTK+ widget for rendering a canvas.Canvas to a screen. The view uses Tools from `tool.py` to handle events and Painters from `painter.py` to draw. Both are configurable. The widget already contains adjustment objects (`hadjustment`, `vadjustment`) to be used for scrollbars. This view registers itself on the canvas, so it will receive update events. """ # Just defined a name to make GTK register this class. __gtype_name__ = 'GaphasView' # Signals: emited after the change takes effect. __gsignals__ = { 'set-scroll-adjustments': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gtk.Adjustment, gtk.Adjustment)), 'dropzone-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), 'hover-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), 'focus-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), 'selection-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), 'tool-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'painter-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) } def __init__(self, canvas=None, hadjustment=None, vadjustment=None): gtk.DrawingArea.__init__(self) self._dirty_items = set() self._dirty_matrix_items = set() View.__init__(self, canvas) self.set_flags(gtk.CAN_FOCUS) self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.KEY_PRESS_MASK | gtk.gdk.KEY_RELEASE_MASK | gtk.gdk.SCROLL_MASK) self._hadjustment = None self._vadjustment = None self._hadjustment_handler_id = None self._vadjustment_handler_id = None self.emit('set-scroll-adjustments', hadjustment, vadjustment) self._set_tool(DefaultTool()) # Set background to white. self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#FFF')) def emit(self, *args, **kwargs): """ Delegate signal emissions to the DrawingArea (=GTK+) """ gtk.DrawingArea.emit(self, *args, **kwargs) def _set_canvas(self, canvas): """ Use view.canvas = my_canvas to set the canvas to be rendered in the view. This extends the behaviour of View.canvas. The view is also registered. """ if self._canvas: self._clear_matrices() self._canvas.unregister_view(self) super(GtkView, self)._set_canvas(canvas) if self._canvas: self._canvas.register_view(self) self.request_update(self._canvas.get_all_items()) self.queue_draw_refresh() canvas = property(lambda s: s._canvas, _set_canvas) def _set_tool(self, tool): """ Set the tool to use. Tools should implement tool.Tool. """ self._tool = tool tool.set_view(self) self.emit('tool-changed') tool = property(lambda s: s._tool, _set_tool) hadjustment = property(lambda s: s._hadjustment) vadjustment = property(lambda s: s._vadjustment) def do_set_scroll_adjustments(self, hadjustment, vadjustment): if self._hadjustment_handler_id: self._hadjustment.disconnect(self._hadjustment_handler_id) self._hadjustment_handler_id = None if self._vadjustment_handler_id: self._vadjustment.disconnect(self._vadjustment_handler_id) self._vadjustment_handler_id = None self._hadjustment = hadjustment or gtk.Adjustment() self._vadjustment = vadjustment or gtk.Adjustment() self._hadjustment_handler_id = \ self._hadjustment.connect('value-changed', self.on_adjustment_changed) self._vadjustment_handler_id = \ self._vadjustment.connect('value-changed', self.on_adjustment_changed) self.update_adjustments() def zoom(self, factor): """ Zoom in/out by factor ``factor``. """ super(GtkView, self).zoom(factor) self.queue_draw_refresh() @async(single=True) def update_adjustments(self, allocation=None): if not allocation: allocation = self.allocation hadjustment = self._hadjustment vadjustment = self._vadjustment # canvas limits (in view coordinates) c = Rectangle(*self._qtree.soft_bounds) # view limits v = Rectangle(0, 0, allocation.width, allocation.height) # union of these limits gives scrollbar limits if v in c: u = c else: u = c + v # set lower limits hadjustment.lower, vadjustment.lower = u.x, u.y # set upper limits hadjustment.upper, vadjustment.upper = u.x1, u.y1 # set page size aw, ah = allocation.width, allocation.height hadjustment.page_size = aw vadjustment.page_size = ah # set increments hadjustment.page_increment = aw hadjustment.step_increment = aw / 10 vadjustment.page_increment = ah vadjustment.step_increment = ah / 10 # set position if v.x != hadjustment.value or v.y != vadjustment.value: hadjustment.value, vadjustment.value = v.x, v.y def queue_draw_item(self, *items): """ Like ``DrawingArea.queue_draw_area``, but use the bounds of the item as update areas. Of course with a pythonic flavor: update any number of items at once. TODO: Should we also create a (sorted) list of items that need redrawal? """ get_bounds = self._qtree.get_bounds items = filter(None, items) try: # create a copy, otherwise we'll change the original rectangle bounds = Rectangle(*get_bounds(items[0])) for item in items[1:]: bounds += get_bounds(item) self.queue_draw_area(*bounds) except IndexError: pass except KeyError: pass # No bounds calculated yet? bummer. def queue_draw_area(self, x, y, w, h): """ Wrap draw_area to convert all values to ints. """ try: super(GtkView, self).queue_draw_area(int(x), int(y), int(w+1), int(h+1)) except OverflowError: # Okay, now the zoom factor is very large or something a = self.allocation super(GtkView, self).queue_draw_area(0, 0, a.width, a.height) def queue_draw_refresh(self): """ Redraw the entire view. """ a = self.allocation super(GtkView, self).queue_draw_area(0, 0, a.width, a.height) def request_update(self, items, matrix_only_items=(), removed_items=()): """ Request update for items. Items will get a full update treatment, while ``matrix_only_items`` will only have their bounding box recalculated. """ if items: self._dirty_items.update(items) if matrix_only_items: self._dirty_matrix_items.update(matrix_only_items) # Remove removed items: if removed_items: self._dirty_items.difference_update(removed_items) self.queue_draw_item(*removed_items) for item in removed_items: self._qtree.remove(item) self.selected_items.discard(item) if self.focused_item in removed_items: self.focused_item = None if self.hovered_item in removed_items: self.hovered_item = None if self.dropzone_item in removed_items: self.dropzone_item = None self.update() @async(single=True, priority=PRIORITY_HIGH_IDLE) def update(self): """ Update view status according to the items updated by the canvas. """ if not self.window: return dirty_items = self._dirty_items dirty_matrix_items = self._dirty_matrix_items try: self.queue_draw_item(*dirty_items) # Mark old bb section for update self.queue_draw_item(*dirty_matrix_items) for i in dirty_matrix_items: if i not in self._qtree: dirty_items.add(i) self.update_matrix(i) continue self.update_matrix(i) if i not in dirty_items: # Only matrix has changed, so calculate new bb based # on quadtree data (= bb in item coordinates). bounds = self._qtree.get_data(i) i2v = self.get_matrix_i2v(i).transform_point x0, y0 = i2v(bounds.x, bounds.y) x1, y1 = i2v(bounds.x1, bounds.y1) vbounds = Rectangle(x0, y0, x1=x1, y1=y1) self._qtree.add(i, vbounds, bounds) self.queue_draw_item(*dirty_matrix_items) # Request bb recalculation for all 'really' dirty items self.update_bounding_box(set(dirty_items)) finally: self._dirty_items.clear() self._dirty_matrix_items.clear() @async(single=False) def update_bounding_box(self, items): """ Update bounding box is not necessary. """ cr = self.window.cairo_create() cr.save() cr.rectangle(0, 0, 0, 0) cr.clip() try: super(GtkView, self).update_bounding_box(cr, items) finally: cr.restore() self.queue_draw_item(*items) self.update_adjustments() @nonrecursive def do_size_allocate(self, allocation): """ Allocate the widget size ``(x, y, width, height)``. """ gtk.DrawingArea.do_size_allocate(self, allocation) self.update_adjustments(allocation) self._qtree.resize((0, 0, allocation.width, allocation.height)) def do_realize(self): gtk.DrawingArea.do_realize(self) # Ensure updates are propagated self._canvas.register_view(self) if self._canvas: self.request_update(self._canvas.get_all_items()) def do_unrealize(self): if self.canvas: # Although Item._matrix_{i2v|v2i} keys are automatically removed # (weak refs), better do it explicitly to be sure. self._clear_matrices() self._qtree.clear() self._dirty_items.clear() self._dirty_matrix_items.clear() self._canvas.unregister_view(self) gtk.DrawingArea.do_unrealize(self) def do_expose_event(self, event): """ Render canvas to the screen. """ if not self._canvas: return area = event.area x, y, w, h = area.x, area.y, area.width, area.height cr = self.window.cairo_create() # Draw no more than nessesary. cr.rectangle(x, y, w, h) cr.clip() area = Rectangle(x, y, width=w, height=h) self._painter.paint(Context(cairo=cr, items=self.get_items_in_rectangle(area), area=area)) if DEBUG_DRAW_BOUNDING_BOX: cr.save() cr.identity_matrix() cr.set_source_rgb(0, .8, 0) cr.set_line_width(1.0) b = self._bounds cr.rectangle(b[0], b[1], b[2], b[3]) cr.stroke() cr.restore() # Draw Quadtree structure if DEBUG_DRAW_QUADTREE: def draw_qtree_bucket(bucket): cr.rectangle(*bucket.bounds) cr.stroke() for b in bucket._buckets: draw_qtree_bucket(b) cr.set_source_rgb(0, 0, .8) cr.set_line_width(1.0) draw_qtree_bucket(self._qtree._bucket) return False def do_event(self, event): """ Handle GDK events. Events are delegated to a `tool.Tool`. """ if self._tool: return self._tool.handle(event) and True or False return False def on_adjustment_changed(self, adj): """ Change the transformation matrix of the view to reflect the value of the x/y adjustment (scrollbar). """ if adj.value == 0.0: return # Can not use self._matrix.translate( - adj.value , 0) here, since # the translate method effectively does a m * self._matrix, which # will result in the translation being multiplied by the orig. matrix m = Matrix() if adj is self._hadjustment: m.translate( - adj.value, 0) elif adj is self._vadjustment: m.translate(0, - adj.value) self._matrix *= m # Force recalculation of the bounding boxes: self.request_update((), self._canvas.get_all_items()) self.queue_draw_refresh() # Set a signal to set adjustments. This way a ScrolledWindow can set its own # Adjustment objects on the View. Otherwise a warning is shown: # # GtkWarning: gtk_scrolled_window_add(): cannot add non scrollable widget # use gtk_scrolled_window_add_with_viewport() instead GtkView.set_set_scroll_adjustments_signal("set-scroll-adjustments") # vim: sw=4:et:ai gaphas-0.7.2/gaphas/weakset.py000066400000000000000000000122341175456763600162710ustar00rootroot00000000000000""" Backport of the Python 3.0 weakref.WeakSet() class. Note that, since it's shamelessly copied from the Python 3.0 distribution, this file is licensed under the Python Software Foundation License, version 2. """ from _weakref import ref __all__ = ['WeakSet'] class WeakSet: def __init__(self, data=None): self.data = set() def _remove(item, selfref=ref(self)): self = selfref() if self is not None: self.data.discard(item) self._remove = _remove if data is not None: self.update(data) def __iter__(self): for itemref in self.data: item = itemref() if item is not None: yield item def __len__(self): return sum(x() is not None for x in self.data) def __contains__(self, item): """ >>> class C(object): pass >>> a = C() >>> b = C() >>> ws = WeakSet((a, b)) >>> a in ws True >>> a = C() >>> a in ws False """ return ref(item) in self.data def __reduce__(self): return (self.__class__, (list(self),), getattr(self, '__dict__', None)) def add(self, item): self.data.add(ref(item, self._remove)) def clear(self): """ >>> class C(object): pass >>> s = C(), C() >>> ws = WeakSet(s) >>> list(ws) # doctest: +ELLIPSIS [, ] >>> ws.clear() >>> list(ws) [] """ self.data.clear() def copy(self): return self.__class__(self) def pop(self): """ >>> class C(object): pass >>> a, b = C(), C() >>> ws = WeakSet((a, b)) >>> len(ws) 2 >>> ws.pop() # doctest: +ELLIPSIS >>> len(ws) 1 """ while True: try: itemref = self.data.pop() except KeyError: raise KeyError('pop from empty WeakSet') item = itemref() if item is not None: return item def remove(self, item): self.data.remove(ref(item)) def discard(self, item): self.data.discard(ref(item)) def update(self, other): if isinstance(other, self.__class__): self.data.update(other.data) else: for element in other: self.add(element) def __ior__(self, other): self.update(other) return self # Helper functions for simple delegating methods. def _apply(self, other, method): if not isinstance(other, self.__class__): other = self.__class__(other) newdata = method(other.data) newset = self.__class__() newset.data = newdata return newset def difference(self, other): return self._apply(other, self.data.difference) __sub__ = difference def difference_update(self, other): if self is other: self.data.clear() else: self.data.difference_update(ref(item) for item in other) def __isub__(self, other): if self is other: self.data.clear() else: self.data.difference_update(ref(item) for item in other) return self def intersection(self, other): return self._apply(other, self.data.intersection) __and__ = intersection def intersection_update(self, other): self.data.intersection_update(ref(item) for item in other) def __iand__(self, other): self.data.intersection_update(ref(item) for item in other) return self def issubset(self, other): return self.data.issubset(ref(item) for item in other) __lt__ = issubset def __le__(self, other): return self.data <= set(ref(item) for item in other) def issuperset(self, other): return self.data.issuperset(ref(item) for item in other) __gt__ = issuperset def __ge__(self, other): return self.data >= set(ref(item) for item in other) def __eq__(self, other): """ >>> class C(object): pass >>> a, b = C(), C() >>> ws1 = WeakSet((a, b)) >>> ws2 = WeakSet((a, b)) >>> ws1 == ws2 True >>> ws1 == WeakSet((a, )) False """ return self.data == set(ref(item) for item in other) def symmetric_difference(self, other): return self._apply(other, self.data.symmetric_difference) __xor__ = symmetric_difference def symmetric_difference_update(self, other): if self is other: self.data.clear() else: self.data.symmetric_difference_update(ref(item) for item in other) def __ixor__(self, other): if self is other: self.data.clear() else: self.data.symmetric_difference_update(ref(item) for item in other) return self def union(self, other): return self._apply(other, self.data.union) __or__ = union def isdisjoint(self, other): return len(self.intersection(other)) == 0 # vim:sw=4:et:ai gaphas-0.7.2/pylintrc000066400000000000000000000234201175456763600145770ustar00rootroot00000000000000# lint Python modules using external checkers. # # $Revision$ # $HeadURL$ # # This is the main checker controling the other ones and the reports # generation. It is itself both a raw checker and an astng checker in order # to: # * handle message activation / deactivation at the module level # * handle some basic but necessary stats'data (number of classes, methods...) # # This checker also defines the following reports: # * R0001: Total errors / warnings # * R0002: % errors / warnings by module # * R0003: Messages # * R0004: Global evaluation [MASTER] # Profiled execution. profile=no # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore=CVS ignore=gaphas/tests # Pickle collected data for later comparisons. persistent=yes # Set the cache size for astng objects. cache-size=500 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [REPORTS] # Tells wether to display a full report or only the messages reports=yes # Use HTML as output format instead of text html=no # Use a parseable text output format, so your favorite text editor will be able # to jump to the line corresponding to a message. parseable=no # Colorizes text output using ansi escape codes color=no # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Python expression which should return a note less than 10 (10 is the highest # note).You have access to the variables errors warning, statement which # respectivly contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (R0004). comment=no # Include message's id in output include-ids=no # checks for : # * doc strings # * modules / classes / functions / methods / arguments / variables name # * number of arguments, local variables, branchs, returns and statements in # functions, methods # * required module attributes # * dangerous default values as arguments # * redefinition of function / method / class # * uses of the global statement # # This checker also defines the following reports: # * R0101: Statistics by type [BASIC] # Enable / disable this checker enable-basic=yes # Required attributes for module, separated by a comma required-attributes=__version__ # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=__.*__ # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{0,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{0,30}$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{0,30}$ # Regular expression which should only match correct variable names variable-rgx=[a-z_][a-z0-9_]{0,30}$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input # try to find bugs in the code using type inference # [TYPECHECK] # Enable / disable this checker enable-typecheck=yes # Tells wether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # When zope mode is activated, consider the acquired-members option to ignore # access to some undefined attributes. zope=no # List of members which are usually get through zope's acquisition mecanism and # so shouldn't trigger E0201 when accessed (need zope=yes to be considered. acquired-members=REQUEST,acl_users,aq_parent # checks for # * unused variables / imports # * undefined variables # * redefinition of variable from builtins or from an outer scope # * use of variable before assigment # [VARIABLES] # Enable / disable this checker enable-variables=yes # Tells wether we should check for unused import in __init__ files. init-import=no # A regular expression matching names used for dummy variables (i.e. not used). dummy-variables-rgx=_|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # checks for : # * methods without self as first argument # * overriden methods signature # * access only to existant members via self # * attributes not defined in the __init__ method # * supported interfaces implementation # * unreachable code # [CLASSES] # Enable / disable this checker enable-classes=yes # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # checks for sign of poor/misdesign: # * number of methods, attributes, local variables... # * size, complexity of functions, methods # [DESIGN] # Enable / disable this checker enable-design=yes # Maximum number of arguments for function / method max-args=5 # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branchs=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # checks for # * external modules dependencies # * relative / wildcard imports # * cyclic imports # * uses of deprecated modules # # This checker also defines the following reports: # * R0401: External dependencies # * R0402: Modules dependencies graph [IMPORTS] # Enable / disable this checker enable-imports=yes # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report R0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report R0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report R0402 must # not be disabled) int-import-graph= # checks for usage of new style capabilities on old style classes and # other new/old styles conflicts problems # * use of property, __slots__, super # * "super" usage # * raising a new style class as exception # [NEWSTYLE] # Enable / disable this checker enable-newstyle=yes # checks for # * excepts without exception filter # * string exceptions # [EXCEPTIONS] # Enable / disable this checker enable-exceptions=yes # checks for : # * unauthorized constructions # * strict indentation # * line length # * use of <> instead of != # [FORMAT] # Enable / disable this checker enable-format=yes # Maximum number of characters on a single line. max-line-length=80 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # checks for: # * warning notes in the code like FIXME, XXX # * PEP 263: source code with non ascii character but no encoding declaration # [MISCELLANEOUS] # Enable / disable this checker enable-miscellaneous=yes # List of note tags to take in consideration, separated by a comma. Default to # FIXME, XXX, TODO notes=FIXME,XXX,TODO # does not check anything but gives some raw metrics : # * total number of lines # * total number of code lines # * total number of docstring lines # * total number of comments lines # * total number of empty lines # # This checker also defines the following reports: # * R0701: Raw metrics [METRICS] # Enable / disable this checker enable-metrics=yes # checks for similarities and duplicated code. This computation may be # memory / CPU intensive, so you should disable it if you experiments some # problems. # # This checker also defines the following reports: # * R0801: Duplication [SIMILARITIES] # Enable / disable this checker enable-similarities=yes # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes gaphas-0.7.2/setup.cfg000066400000000000000000000002731175456763600146320ustar00rootroot00000000000000[egg_info] #tag_build = .dev #tag_svn_revision = 1 [nosetests] with-doctest=1 doctest-extension=.txt tests=gaphas,doc verbosity=2 detailed-errors=1 with-coverage=1 cover-package=gaphas gaphas-0.7.2/setup.py000066400000000000000000000040541175456763600145240ustar00rootroot00000000000000"""\ Gaphas is a MVC canvas that uses Cairo_ for rendering. One of the nicer things of this widget is that the user (model) is not bothered with bounding box calculations: this is all done through Cairo. Some more features: - Each item has it's own separate coordinate space (easy when items are rotated). - Items on the canvas can be connected to each other. Connections are maintained by a linear constraint solver. - Multiple views on one Canvas. - What is drawn is determined by Painters. Multiple painters can be used and painters can be chained. - User interaction is handled by Tools. Tools can be chained. - Versatile undo/redo system GTK+ and PyGTK_ are required. .. _Cairo: http://cairographics.org/ .. _PyGTK: http://www.pygtk.org/ """ VERSION = '0.7.2' from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages from distutils.cmd import Command setup( name='gaphas', version=VERSION, description='Gaphas is a GTK+ based diagramming widget', long_description=__doc__, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: X11 Applications :: GTK', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', ], keywords='', author="Arjan J. Molenaar", author_email='arjanmol@users.sourceforge.net', url='http://gaphor.sourceforge.net', #download_url='http://cheeseshop.python.org/', license='GNU Library General Public License (LGPL, see COPYING)', packages=find_packages(exclude=['ez_setup']), setup_requires = [ 'nose >= 0.10.4', 'setuptools-git >= 0.3.4' ], install_requires=[ 'decorator >= 3.0.0', 'simplegeneric >= 0.6', # 'PyGTK >= 2.8.0', # 'cairo >= 1.8.2' ], zip_safe=False, package_data={ # -*- package_data: -*- }, entry_points = { }, test_suite = 'nose.collector', )