pax_global_header00006660000000000000000000000064125212275730014520gustar00rootroot0000000000000052 comment=a81b9c916e1a65f7e8390c25ca5c601a4021baab pyepr-0.9.3/000077500000000000000000000000001252122757300126705ustar00rootroot00000000000000pyepr-0.9.3/.gitattributes000066400000000000000000000000631252122757300155620ustar00rootroot00000000000000*.py ident *.pyx ident *.px[di] ident *.txt ident pyepr-0.9.3/.gitignore000066400000000000000000000000321252122757300146530ustar00rootroot00000000000000SciTEDirectory.properties pyepr-0.9.3/.travis.yml000066400000000000000000000007041252122757300150020ustar00rootroot00000000000000language: python python: - 2.6 - 2.7 - "2.7_with_system_site_packages" - 3.2 - "3.2_with_system_site_packages" - 3.3 - 3.4 before_install: - sudo apt-get update -qq - sudo apt-get install -qq libepr-api2-dev - pip install -r requirements.txt --use-mirrors - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -U unittest2 --use-mirrors; fi install: python setup.py build_ext --inplace script: make PYTHON=python check pyepr-0.9.3/LICENSE.txt000066400000000000000000001045131252122757300145170ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them 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 prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . pyepr-0.9.3/MANIFEST.in000066400000000000000000000006151252122757300144300ustar00rootroot00000000000000include MANIFEST.in include LICENSE.txt include requirements.txt recursive-include LICENSES *.txt recursive-include tests *.py recursive-include src *.pyx *.pxd *.c recursive-include epr-api-src *.c *.h recursive-include doc *.txt *.py Makefile make.bat *.png recursive-include doc/_templates *.html recursive-include doc/pydoctheme * recursive-include doc/html * recursive-exclude doc/_build * pyepr-0.9.3/Makefile000066400000000000000000000045111252122757300143310ustar00rootroot00000000000000#!/usr/bin/make -f # -*- coding: utf-8 -*- # Copyright (C) 2011-2014, Antonio Valentino # # This file is part of PyEPR. # # PyEPR is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyEPR is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyEPR. If not, see . PYTHON = python3 CYTHON = cython3 TEST_DATSET_URL = "http://earth.esa.int/services/sample_products/meris/LRC/L2/MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1.gz" TEST_DATSET = tests/MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1 EPRAPIROOT = ../epr-api .PHONY: default ext cythonize sdist eprsrc fullsdist doc clean distclean \ check debug data upload default: ext ext: src/epr.pyx $(PYTHON) setup.py build_ext --inplace cythonize: src/epr.c src/epr.c: src/epr.pyx $(CYTHON) src/epr.pyx sdist: doc cythonize $(PYTHON) setup.py sdist epr-api-src: mkdir -p epr-api-src cp $(EPRAPIROOT)/src/*.[ch] epr-api-src LICENSES/epr-api.txt: mkdir LICENSES cp $(EPRAPIROOT)/LICENSE.txt LICENSES/epr-api.txt eprsrc: epr-api-src LICENSES/epr-api.txt fullsdist: doc cythonize eprsrc $(PYTHON) setup.py sdist upload: doc cythonize eprsrc $(PYTHON) setup.py sdist upload -s -i 24B76CFE doc: $(MAKE) -C doc html clean: $(PYTHON) setup.py clean --all $(RM) -r build dist pyepr.egg-info $(RM) -r $$(find doc -name __pycache__) $$(find tests -name __pycache__) $(RM) MANIFEST src/*.c src/*.o *.so $(RM) tests/*.py[co] doc/sphinxext/*.py[co] README.html $(MAKE) -C doc clean find . -name '*~' -delete distclean: clean $(RM) $(TEST_DATSET) $(RM) -r doc/html $(RM) -r LICENSES epr-api-src $(MAKE) -C tests -f checksetup.mak distclean check: ext $(TEST_DATSET) env PYTHONPATH=. $(PYTHON) tests/test_all.py --verbose debug: $(PYTHON) setup.py build_ext --inplace --debug data: $(TEST_DATSET) $(TEST_DATSET): wget -P tests $(TEST_DATSET_URL) gunzip $@ pyepr-0.9.3/README.txt000066400000000000000000000065101252122757300143700ustar00rootroot00000000000000================================= ENVISAT Product Reader Python API ================================= :HomePage: http://avalentino.github.io/pyepr :Author: Antonio Valentino :Contact: antonio.valentino@tiscali.it :Copyright: 2011-2015, Antonio Valentino :Version: 0.9.3 Introduction ============ PyEPR_ provides Python_ bindings for the ENVISAT Product Reader C API (`EPR API`_) for reading satellite data from ENVISAT_ ESA_ (European Space Agency) mission. PyEPR_, as well as the `EPR API`_ for C, supports ENVISAT_ MERIS, AATSR Level 1B and Level 2 and also ASAR data products. It provides access to the data either on a geophysical (decoded, ready-to-use pixel samples) or on a raw data layer. The raw data access makes it possible to read any data field contained in a product file. .. _PyEPR: https://github.com/avalentino/pyepr .. _Python: https://www.python.org .. _`EPR API`: https://github.com/bcdev/epr-api .. _ENVISAT: https://envisat.esa.int .. _ESA: https://earth.esa.int Requirements ============ In order to use PyEPR it is needed that the following software are correctly installed and configured: * Python2_ >= 2.6 or Python3_ >= 3.1 * numpy_ >= 1.5.0 * `EPR API`_ >= 2.2 (optional, since PyEPR 0.7 the source tar-ball comes with a copy of the PER C API sources) * a reasonably updated C compiler (build only) * Cython_ >= 0.15 (build only) * unittest2_ (only required for Python < 2.7) .. _Python2: Python_ .. _Python3: Python_ .. _numpy: http://www.numpy.org .. _gcc: http://gcc.gnu.org .. _Cython: http://cython.org .. _unittest2: https://pypi.python.org/pypi/unittest2 Download ======== Official source tarballs can be downloaded form PyPi_: https://pypi.python.org/pypi/pyepr The source code of the development versions is available on the GitHub_ project page https://github.com/avalentino/pyepr To clone the git_ repository the following command can be used:: $ git clone https://github.com/avalentino/pyepr.git .. _PyPi: https://pypi.python.org/pypi .. _GitHub: https://github.com .. _git: http://git-scm.com Installation ============ The easier way to install PyEPR_ is using tools like pip_ or easy_install_:: $ pip install pyepr or:: $ pip install -U --prefix= PyEPR_ can be installed from the source tar.ball using the following command:: $ python setup.py install To install PyEPR_ in a non-standard path:: $ python setup.py install --prefix= .. _pip: https://pypi.python.org/pypi/pip .. _easy_install: https://pypi.python.org/pypi/setuptools#using-setuptools-and-easyinstall License ======= Copyright (C) 2011-2015 Antonio Valentino PyEPR is free software: you can redistribute it and/or modify it under the terms of the `GNU General Public License`_ as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PyEPR is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with PyEPR. If not, see . .. _`GNU General Public License`: http://www.gnu.org/licenses/gpl-3.0.html pyepr-0.9.3/doc/000077500000000000000000000000001252122757300134355ustar00rootroot00000000000000pyepr-0.9.3/doc/Makefile000066400000000000000000000151161252122757300151010ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) html @echo @echo "Build finished. The HTML pages are in html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyEPR.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyEPR.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PyEPR" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyEPR" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." pyepr-0.9.3/doc/NEWS.txt000066400000000000000000000275141252122757300147630ustar00rootroot00000000000000Change history ============== PyEPR 0.9.3 (02/05/2015) ------------------------ * Fix PyEprExtension class in setup.py (closes :issue:`11`) * Updated internal EPR API version PyEPR 0.9.2 (08/03/2015) ------------------------ * Improved string representation of fields in case of :data:`E_TID_STRING` data type. Now bytes are decoded and represented as Python strings. * New tutorial :doc:`gdal_export_example` * Improved "Installation" and "Testing" sections of the user manual PyEPR 0.9.1 (27/02/2015) ------------------------ * Fix source distribution (missing EPR API C sources) PyEPR 0.9 (27/02/2015) ---------------------- * basic support for update mode: products can now be opened in update mode ('rb+') and it is possible to call :meth:`epr.Field.set_elem` and :meth:`epr.Field.set_elems` methods to set :class:`epr.Field` elements changing the contents of the :class:`epr.Product` on disk. This feature is not available in the EPR C API. * new functions/methods and properties: - :attr:`epr.Record.index` property: returns the index of the :class:`epr.Record` within the :class:`epr.Dataset` - :attr:`epr.Band.dataset` property: returns the source :class:`epr.Dataset` object containing the raw data used to create the :class:`epr.Band`\ ’s pixel values - :attr:`epr.Band._field_index` and :attr:`epr.Band._elem_index` properties: return the :class:`epr.Field` index (within the :class:`epr.Record`) and the element index (within the :class:`epr.Field`) containing the raw data used to create the :class:`epr.Band`\ ’s pixel values - :attr:`epr.Record.dataset_name` property: returns the name of the :class:`epr.Dataset` from which the :class:`Record` has bee read - :attr:`epr.Record.tot_size` and :attr:`epr.Field.tot_size` properties: return the total size in bytes of the :class:`epr.Record` and :class:`epr.Field` respectively - :func:`epr.get_numpy_dtype` function: retrieves the numpy_ data type corresponding to the specified EPR type ID - added support for some low level feature: the *_magic* private attribute stores the identifier of EPR C stricture, the :meth:`epr.Record.get_offset` returns the offset in bytes of the :class:`epr.Record` within the file, and the :meth:`epr.Field.get_offset` method returns the :clasS:`epr.Field` offset within the :class:`epr.Record` * improved functions/methods: - :meth:`epr.Field.get_elems` now also handles :data:`epr.E_TID_STRING` and :data:`epr.E_TID_TIME` data types - improved :func:`epr.get_data_type_size`, :func:`epr.data_type_id_to_str`, :func:`epr.get_scaling_method_name` and :func:`epr.get_sample_model_name` functions that are now defined using the cython `cpdef` directive - the :meth:`epr.Field.get_elems` method has been re-written to remove loops and unnecessary data copy - now generator expressions are used to implement `__iter__` special methods * the *index* parameter of the :meth:`epr.Dataset.read_record` method is now optional (defaults to zero) * the deprecated `__revision__` variable has been removed * declarations of the EPR C API have been moved to the new :file:`epr.pyd` * the `const_char` and `const_void` definitions have been dropped, no longer necessary with cython_ >= 0.19 * minimum required version for cython_ is now 0.19 * the :file:`setup.py` script has been completely rewritten to be more "pip_ friendly". The new script uses setuptools_ if available and functions that use numpy_ are evaluated lazily so to give a chance to pip_ and setuptools_ to install dependencies, numpy_, before they are actually used. This should make PyEPR "pip-installable" even on system there numpy_ is not already installed. * the :file:`test` directory has been renamed into :file:`tests` * the test suite now has a :func:`setUpModule` function that automatically downloads the ENVISAT test data required for test execution. The download only happens if the test dataset is not already available. * tests can now be run using the :file:`setup.py` script:: $ python3 setup.py test * enable continuous integration and testing in for Windows_ using AppVeyor_ (32bit only) * status badges for `AppVeyor CI `_ and PyPI_ added to the HTML doc index .. _pip: https://pip.pypa.io .. _setuptools: https://bitbucket.org/pypa/setuptools .. _numpy: http://www.numpy.org .. _Windows: http://windows.microsoft.com .. _AppVeyor: http://www.appveyor.com .. _PyPI: https://pypi.python.org/pypi/pyepr PyEPR 0.8.2 (03/08/2014) ------------------------ * fixed segfault caused by incorrect access to :attr:`epr.Dataset.description` string in case of closed products * fixed a memory leak in :class:`epr.Raster` (closes :issue:`10`) * the size parameters (*src_width* and *src_height*) in :meth:`epr.Band.create_compatible_raster` are now optional. By default a :class:`epr.Raster` with the same size of the scene is created * the test suite have been improved * improved the :doc:`NDVI computation example ` * updates sphinx config * small clarification in the :ref:`installation` section of the :doc:`usermanual`. * EPR C API (version bundled with the official source tar-ball) - in case of error always free resources before setting the error code. This avoids error shadowing in some cases. - fixed a bug that caused reading of the incorrect portion of data in case of mirrored annotation datasets (closes :issue:`9`) - fixed a bug that caused incorrect data sub-sampling in case of mirrored datasets PyEPR 0.8.1 (07/09/2013) ------------------------ * fixed an important bug in the error checking code introduced in previous release (closes :issue:`8`) * fixed the NDVI example * no more display link URL in footnotes of the PDF User Manual PyEPR 0.8 (07/09/2013) ---------------------- * now the :class:`epr.Product` objects have a :meth:`epr.Product.close` method that can be used to explicitly close products without relying on the garbage collector behaviour (closes :issue:`7`) * new :attr:`epr.Product.closed` (read-only) attribute that can be used to check if a :class:`epr.Product` has been closed * the :class:`Product` class now supports context management so they can be used in ``with`` statements * added entries for :data:`epr.__version__` and :data:`epr.__revision__` in the reference manual * the :data:`epr.__revision__` module attribute is now deprecated * some *cythonization* warnings have been fixed * several small improvements to the documentation PyEPR 0.7.1 (19/08/2013) ------------------------ * fixed potential issues with conversion from python strings to ``char*`` * new snapshot of the EPR C API sources (2.3dev): - the size of the record tables has been fixed - the EPR_NUM_PRODUCT_TABLES has been fixed - fixed a missing prototype - several GCC warnings has been silenced - additional checks on return codes - now and error is raised when an invalid flag name is used * better factorization of Python 3 specific code * use the *CLOUD* flag instead of *BRIGHT* in unit tests * added function/method signature to all doc-strings for better interactive help * several improvements to the documentation: - updated the :file:`README.txt` file to mention EPR C API sourced inclusion in the PyEPR 0.7 (and lates) source tar-ball - small fix in the installation instructions: the pip_ tool does not have a "--prefix" parameter - always use the python3 syntax for the *print* function in all examples in the documentation - links to older (and dev) versions of the documentation have been added in the man page of the HTML doc - removed *date* form the doc meta-data. The documentation build date is reported in the front page of the LaTeX (PDF) doc and, starting from this release, in the footer of the HTML doc. - the Ohloh_ widget has been added in the sidebar of the HTML doc - improved the regexp for detecting the SW version in the :file`setup.py` script - formatting .. _Ohloh: https://www.openhub.net PyEPR 0.7 (04/08/2013) ---------------------- * more detailed error messages in case of open failures * new sphinx theme for the HTML documentation * `Travis-CI`_ has been set-up for the project * now the source tar-ball also includes a copy of the EPR C API sources so that no external C library is required to build PyEPR. This features also makes it easier to install PyEPR using pip_. The user can still guild PyEPR against a system version of the ERP-API library simply using the :option:`--epr-api-src` option of the :file:`setup.py` script with "None"" as value. The ERP C API included in the source tar-ball is version *2.3dev-pyepr062*, a development and patched version that allows the following enhancements. - support for ERS products in ENVISAT format - support for ASAR products generated with the new ASAR SW version 6.02 (ref. doc. PO-RS-MDA-GS-2009_4/C - fix incorrect reading of "incident_angle" bands (closes :issue:`6`). The issue is in the EPR C API. .. _`Travis-CI`: https://travis-ci.org/avalentino/pyepr PyEPR 0.6.1 (26/04/2012) ------------------------ * fix compatibility with cython_ 0.16 * added a new option to the setup script (:option:`--epr-api-src`) to build PyEPR using the EPR-API C sources PyEPR 0.6 (12/08/2011) ---------------------- * full support for `Python 3`_ * improved code highligh in the documentation * depend from cython >= 0.13 instead of cython >= 0.14.1. Cythonizing :file:`epr.pyx` with `Python 3`_ requires cython >= 0.15 PyEPR 0.5 (25/04/2011) ---------------------- * stop using :c:func:`PyFile_AsFile` that is no more available in `Python 3`_ * now documentation uses intersphinx_ capabilities * code examples added to documentation * tutorials added to documentation * the LICENSE.txt file is now included in the source distribution * the cython_ construct ``with nogil`` is now used instead of calling :c:func:`Py_BEGIN_ALLOW_THREADS` and :c:func:`Py_END_ALLOW_THREADS` directly * dropped old versions of cython_; now cython_ 0.14.1 or newer is required * suppressed several constness related warnings .. _`Python 3`: https://docs.python.org/3 .. _intersphinx: http://sphinx-doc.org/latest/ext/intersphinx.html .. _cython: http://cython.org PyEPR 0.4 (10/04/2011) ---------------------- * fixed a bug in the :meth:`epr.Product.__str__`, :meth:`Dataset.__str__` and :meth:`erp.Band.__repr__` methods (bad formatting) * fixed :meth:`epr.Field.get_elems` method for char and uchar data types * implemented :meth:`epr.Product.read_bitmask_raster`, now the :class:`epr.Product` API is complete * fixed segfault in :meth:`epr.Field.get_unit` method when the field has no unit * a smaller dataset is now used for unit tests * a new tutorial section has been added to the user documentation PyEPR 0.3 (01/04/2011) ---------------------- * version string of the EPR C API is now exposed as module attribute :data:`epr.EPR_C_API_VERSION` * implemented ``__repr__``, ``__str__``, ``__eq__``, ``__ne__`` and ``__iter__`` special methods * added utility methods (not included in the C API) like: - :meth:`epr.Record.get_field_names` - :meth:`epr.Record.fields` - :meth:`epr.Dataset.records` - :meth:`epr.Product.get_dataset_names` - :meth:`epr.Product.get_band_names` - :meth:`epr.Product.datasets` - :meth:`epr.Product.bands` * fixed a logic error that caused empty messages in custom EPR exceptions PyEPR 0.2 (20/03/2011) ---------------------- * sphinx_ documentation added * added docstrings to all method and classes * renamed some method and parameter in order to avoid redundancies and have a more *pythonic* API * in case of null pointers a :exc:`epr.EPRValueError` is raised * improved C library shutdown management * introduced some utility methods to :class:`epr.Product` and :class:`epr.Record` classes .. _sphinx: http://sphinx-doc.org PyEPR 0.1 (09/03/2011) ---------------------- Initial release pyepr-0.9.3/doc/_static/000077500000000000000000000000001252122757300150635ustar00rootroot00000000000000pyepr-0.9.3/doc/_static/empty.txt000066400000000000000000000000001252122757300167500ustar00rootroot00000000000000pyepr-0.9.3/doc/_templates/000077500000000000000000000000001252122757300155725ustar00rootroot00000000000000pyepr-0.9.3/doc/_templates/appveyor.html000066400000000000000000000003031252122757300203210ustar00rootroot00000000000000

AppVeyor status page

pyepr-0.9.3/doc/_templates/ohloh.html000066400000000000000000000001721252122757300175710ustar00rootroot00000000000000
pyepr-0.9.3/doc/_templates/pypi.html000066400000000000000000000012461252122757300174440ustar00rootroot00000000000000

Latest Version

Supported Python versions

License

Downloads

Wheel Status

pyepr-0.9.3/doc/_templates/travis-ci.html000066400000000000000000000002371252122757300203630ustar00rootroot00000000000000

travis-ci status page

pyepr-0.9.3/doc/bands_example.txt000066400000000000000000000115221252122757300170010ustar00rootroot00000000000000Exporting band data ------------------- .. index:: ENVISAT, band, raster This tutorial shows how to convert ENVISAT_ raster information from dataset and generate flat binary rasters using PyEPR_. The program generates as many raster as the dataset specified in input. The example code (:download:`examples/write_bands.py`) is a direct translation of the C sample program `write_bands.c`_ bundled with the EPR API distribution. The program is invoked as follows: .. code-block:: sh $ python write_bands.py \ \ [ ... ] .. _ENVISAT: https://envisat.esa.int .. _PyEPR: https://github.com/avalentino/pyepr .. _`write_bands.c`: https://github.com/bcdev/epr-api/blob/master/src/examples/write_bands.c Import section ~~~~~~~~~~~~~~ To use the Python_ EPR API one have to import :mod:`epr` module. At first import time the underlaying C library is opportunely initialized. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 1-15 The main program ~~~~~~~~~~~~~~~~ The main program in quite simple (this is just an example). .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 55- It performs some basic command line arguments handling and then open the input product. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 97-99 Finally the core function (:func:`write_raw_image`) is called on each band specified on the command: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 100-101 The :func:`write_raw_image` core function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The core function is :func:`write_raw_image`. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :pyobject: write_raw_image It generates a flat binary file with data of a single band whose name is specified as input parameter. First the output file name is computed using the :mod:`os` module. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 23-25 Then the desired band is retrieved using the :meth:`epr.Product.get_band` method and some of its parameters are loaded in to local variables: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 26-27 Band data are accessed by means of a :class:`epr.Raster` object. .. seealso:: :func:`epr.Band.read_as_array` The :meth:`epr.Band.create_compatible_raster` is a facility method that allows to instantiate a :class:`epr.Raster` object with a data type compatible with the band data: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 32-34 Then data are read using the :meth:`epr.Band.read_raster` method: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 36-37 Then the output file object is created (in binary mode of course) .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 39 and data are copied to the output file one line at time .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bands.py :language: python :lines: 41-42 Please note that it has been used :data:`epr.Raster.data` attribute of the :class:`epr.Raster` objects that exposes :class:`epr.Raster` data with the powerful :class:`numpy.ndarray` interface. .. note:: copying one line at time is not the best way to perform the task in Python_. It has been done just to mimic the original C code: .. code-block:: c out_stream = fopen(image_file_path, "wb"); if (out_stream == NULL) { printf("Error: can't open '%s'\n", image_file_path); return 3; } for (y = 0; y < (uint)raster->raster_height; ++y) { numwritten = fwrite(epr_get_raster_line_addr(raster, y), raster->elem_size, raster->raster_width, out_stream); if (numwritten != raster->raster_width) { printf("Error: can't write to %s\n", image_file_path); return 4; } } fclose(out_stream); A by far more pythonic_ solution would be:: raster.data.tofile(out_stream) .. _Python: https://www.python.org .. _pythonic: http://www.cafepy.com/article/be_pythonic .. raw:: latex \clearpage pyepr-0.9.3/doc/bitmask_example.txt000066400000000000000000000070171252122757300173500ustar00rootroot00000000000000Exporting bitmasks ------------------- .. index:: bitmask, ENVISAT This tutorial shows how to generate bit masks from ENVISAT_ flags information as "raw" image using PyEPR_. The example code (:download:`examples/write_bitmask.py`) is a direct translation of the C sample program `write_bitmask.c`_ bundled with the EPR API distribution. The program is invoked as follows: .. code-block:: sh $ python write_bitmask.py \ .. _ENVISAT: https://envisat.esa.int .. _PyEPR: https://github.com/avalentino/pyepr .. _`write_bitmask.c`: https://github.com/bcdev/epr-api/blob/master/src/examples/write_bitmask.c The :download:`examples/write_bitmask.py` code consists in a single function that also includes command line arguments handling: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bitmask.py :language: python .. index:: EPR-API, module pair: epr; module In order to use the Python_ EPR API the :mod:`epr` module is imported: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bitmask.py :lines: 27 .. index:: product, open pair: with; statement As usual the ENVISAT_ product is opened using the :func:`epr.open` function that returns an :class:`epr.Product` instance. In this case the :func:`epr.open` is used together with a ``with`` statement so that the :class:`epr.Product` instance is closed automatically when the program exits the ``with`` block. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bitmask.py :language: python :lines: 51-52 .. index:: product Scene size parameters are retrieved form the :class:`epr.Product` object using the :meth:`epr.Product.get_scene_width` and :meth:`epr.Product.get_scene_height` methods: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bitmask.py :language: python :lines: 55-56 The EPR API allows to manage data by means of :class:`epr.Raster` objects, so the function :func:`epr.create_bitmask_raster`, specific for bitmasks, is used to create a :class:`epr.Raster` instance. .. seealso:: :func:`epr.create_raster` Data are actually read using the :meth:`epr.Product.read_bitmask_raster` method of the :class:`epr.Product` class: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bitmask.py :language: python :lines: 63 The :meth:`epr.Product.read_bitmask_raster` method receives in input the *bm_expr* parameter that is set via command line: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bitmask.py :language: python :lines: 48 *bm_expr* is a string that define the logical expression for the definition of the bit-mask. In a bit-mask expression, any number of the flag-names (found in the DDDB) can be composed with “(”, ”)”, “NOT”, “AND”, “OR”. .. index:: AND, OR, NOT, DDDB Valid bit-mask expression are for example:: flags.LAND OR flags.CLOUD or:: NOT flags.WATER AND flags.TURBID_S Finally data are written to disk as a flat binary file using the :meth:`numpy.ndarray.tofile` method of the :data:`epr.Raster.data` attribute of the :class:`epr.Raster` objects that exposes data via the :class:`numpy.ndarray` interface: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_bitmask.py :language: python :lines: 65-66 .. _Python: https://www.python.org .. _ENVISAT: https://envisat.esa.int .. raw:: latex \clearpage pyepr-0.9.3/doc/conf.py000066400000000000000000000266441252122757300147500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # PyEPR documentation build configuration file, created by # sphinx-quickstart on Sat Jul 26 10:12:56 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('sphinxext')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ #'sphinx.ext.autodoc', #'sphinx.ext.autosummary', #'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', #'sphinx.ext.coverage', 'sphinx.ext.pngmath', #'sphinx.ext.jsmath', #'sphinx.ext.mathjax', #'sphinx.ext.graphviz', #'sphinx.ext.inheritance_diagram', #'sphinx.ext.refcounting', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.extlinks', 'ipython_console_highlighting', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.txt' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'PyEPR' copyright = u'2011-2015, Antonio Valentino' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.9.3' # The full version, including alpha/beta/rc tags. release = version + '.dev0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'sphinxext', '**/empty.txt'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Extensions configuration -------------------------------------------------- # Autodoc configuration #autoclass_content = 'both' #autodoc_default_flags = ['members', 'undoc-members', 'show-inheritance'] # #,'inherited-members'] # Auto summary generation #autosummary_generate = ['reference'] # Enable todo list reporting todo_include_todos = True # External links configuration extlinks = { 'issue': ('https://github.com/avalentino/pyepr/issues/%s', 'gh-'), } # Intersphinx intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'numpy': ('http://docs.scipy.org/doc/numpy', None), } # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'pydoctheme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { 'collapsiblesidebar': True, } # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['.'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { 'index': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html', 'ohloh.html', 'pypi.html', 'travis-ci.html', 'appveyor.html'], } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'PyEPRdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). 'papersize': 'a4paper', # The font size ('10pt', '11pt' or '12pt'). 'pointsize': '12pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'pyepr.tex', u'PyEPR Documentation', u'Antonio Valentino', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = 'footnote' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. latex_domain_indices = False # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pyepr', u'PyEPR Documentation', [u'Antonio Valentino'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'PyEPR', u'PyEPR Documentation', u'Antonio Valentino', 'PyEPR', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = u'Antonio Valentino' epub_publisher = epub_author epub_copyright = copyright # The basename for the epub file. It defaults to the project name. #epub_basename = u'PyEPR' # The HTML theme for the epub output. Since the default themes are not optimized # for small screen space, using the same theme for HTML and epub output is # usually not wise. This defaults to 'epub', a theme designed to save visual # space. #epub_theme = 'epub' # The language of the text. It defaults to the language option # or en if the language is not set. #epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. #epub_identifier = '' # A unique identification for the text. #epub_uid = '' # A tuple containing the cover image and cover page html template filenames. #epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. #epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 # Allow duplicate toc entries. #epub_tocdup = True # Choose between 'default' and 'includehidden'. #epub_tocscope = 'default' # Fix unsupported image types using the PIL. #epub_fix_images = False # Scale large images. #epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. #epub_show_urls = 'inline' # If false, no index is generated. #epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'https://docs.python.org/': None} pyepr-0.9.3/doc/examples/000077500000000000000000000000001252122757300152535ustar00rootroot00000000000000pyepr-0.9.3/doc/examples/export_gdalvrt.py000077500000000000000000000134171252122757300207020ustar00rootroot00000000000000#!/usr/bin/env python3 import os import epr from osgeo import gdal epr_to_gdal_type = { epr.E_TID_UNKNOWN: gdal.GDT_Unknown, epr.E_TID_UCHAR: gdal.GDT_Byte, epr.E_TID_CHAR: gdal.GDT_Byte, epr.E_TID_USHORT: gdal.GDT_UInt16, epr.E_TID_SHORT: gdal.GDT_Int16, epr.E_TID_UINT: gdal.GDT_UInt32, epr.E_TID_INT: gdal.GDT_Int32, epr.E_TID_FLOAT: gdal.GDT_Float32, epr.E_TID_DOUBLE: gdal.GDT_Float64, #epr.E_TID_STRING: gdal.GDT_Unknown, #epr.E_TID_SPARE: gdal.GDT_Unknown, #epr.E_TID_TIME: gdal.GDT_Unknown, } def epr2gdal_band(band, vrt): product = band.product dataset = band.dataset record = dataset.read_record(0) field = record.get_field_at(band._field_index - 1) ysize = product.get_scene_height() xsize = product.get_scene_width() if isinstance(vrt, gdal.Dataset): if (vrt.RasterYSize, vrt.RasterXSize) != (ysize, xsize): raise ValueError('dataset size do not match') gdal_ds = vrt elif os.path.exists(vrt): gdal_ds = gdal.Open(vrt, gdal.GA_Update) if gdal_ds is None: raise RuntimeError('unable to open "{}"'.format(vrt)) driver = gdal_ds.GetDriver() if driver.ShortName != 'VRT': raise TypeError('unexpected GDAL driver ({}). ' 'VRT driver expected'.format(driver.ShortName)) else: driver = gdal.GetDriverByName('VRT') if driver is None: raise RuntimeError('unable to get driver "VRT"') gdal_ds = driver.Create(vrt, xsize, ysize, 0) if gdal_ds is None: raise RuntimeError('unable to create "{}" dataset'.format(vrt)) filename = os.pathsep.join(product.file_path.split('/')) # denormalize offset = dataset.get_dsd().ds_offset + field.get_offset() line_offset = record.tot_size pixel_offset = epr.get_data_type_size(field.get_type()) if band.sample_model == epr.E_SMOD_1OF2: pixel_offset *= 2 elif band.sample_model == epr.E_SMOD_2OF2: offset += pixel_offset pixel_offset *= 2 options = [ 'subClass=VRTRawRasterBand', 'SourceFilename={}'.format(filename), 'ImageOffset={}'.format(offset), 'LineOffset={}'.format(line_offset), 'PixelOffset={}'.format(pixel_offset), 'ByteOrder=MSB', ] gtype = epr_to_gdal_type[field.get_type()] ret = gdal_ds.AddBand(gtype, options=options) if ret != gdal.CE_None: raise RuntimeError( 'unable to add VRTRawRasterBand to "{}"'.format(vrt)) gdal_band = gdal_ds.GetRasterBand(gdal_ds.RasterCount) gdal_band.SetDescription(band.description) metadata = { 'name': band.get_name(), 'dataset_name': dataset.get_name(), 'dataset_description': dataset.description, 'lines_mirrored': str(band.lines_mirrored), 'sample_model': epr.get_sample_model_name(band.sample_model), 'scaling_factor': str(band.scaling_factor), 'scaling_offset': str(band.scaling_offset), 'scaling_method': epr.get_scaling_method_name(band.scaling_method), 'spectr_band_index': str(band.spectr_band_index), 'unit': band.unit if band.unit else '', 'bm_expr': band.bm_expr if band.bm_expr else '', } gdal_band.SetMetadata(metadata) return gdal_ds def epr2gdal(product, vrt, overwrite_existing=False): if isinstance(product, str): filename = product product = epr.open(filename) ysize = product.get_scene_height() xsize = product.get_scene_width() if os.path.exists(vrt) and not overwrite_existing: raise ValueError('unable to create "{0}". Already exists'.format(vrt)) driver = gdal.GetDriverByName('VRT') if driver is None: raise RuntimeError('unable to get driver "VRT"') gdal_ds = driver.Create(vrt, xsize, ysize, 0) if gdal_ds is None: raise RuntimeError('unable to create "{}" dataset'.format(vrt)) metadata = { 'id_string': product.id_string, 'meris_iodd_version': str(product.meris_iodd_version), 'dataset_names': ','.join(product.get_dataset_names()), 'num_datasets': str(product.get_num_datasets()), 'num_dsds': str(product.get_num_dsds()), } gdal_ds.SetMetadata(metadata) mph = product.get_mph() metadata = str(mph).replace(' = ', '=').split('\n') gdal_ds.SetMetadata(metadata, 'MPH') sph = product.get_sph() metadata = str(sph).replace(' = ', '=').split('\n') gdal_ds.SetMetadata(metadata, 'SPH') for band in product.bands(): epr2gdal_band(band, gdal_ds) # @TODO: set geographic info return gdal_ds if __name__ == '__main__': filename = 'MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1' vrtfilename = os.path.splitext(filename)[0] + '.vrt' gdal_ds = epr2gdal(filename, vrtfilename) with epr.open(filename) as product: band_index = product.get_band_names().index('water_vapour') band = product.get_band('water_vapour') eprdata = band.read_as_array() unit = band.unit lines_mirrored = band.lines_mirrored scaling_offset = band.scaling_offset scaling_factor = band.scaling_factor gdal_band = gdal_ds.GetRasterBand(band_index + 1) vrtdata = gdal_band.ReadAsArray() if lines_mirrored: vrtdata = vrtdata[:, ::-1] vrtdata = vrtdata * scaling_factor + scaling_offset print('Max absolute error:', abs(vrtdata - eprdata).max()) # plot from matplotlib import pyplot as plt plt.figure() plt.subplot(2, 1, 1) plt.imshow(eprdata) plt.grid(True) cb = plt.colorbar() cb.set_label(unit) plt.title('EPR data') plt.subplot(2, 1, 2) plt.imshow(vrtdata) plt.grid(True) cb = plt.colorbar() cb.set_label(unit) plt.title('VRT data') plt.show() pyepr-0.9.3/doc/examples/update_elements.py000077500000000000000000000045511252122757300210130ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import print_function import numpy as np from matplotlib import pyplot as plt import epr FILENAME = 'MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1' # load original data with epr.open(FILENAME) as product: band = product.get_band('water_vapour') wv_orig_histogram, orig_bins = np.histogram(band.read_as_array().flat, 50) # plot water vapour histogram plt.figure() plt.bar(orig_bins[:-1], wv_orig_histogram, 0.02, label='original') plt.grid(True) plt.title('Water Vapour Histogram') plt.savefig('water_vapour_histogram_01.png') # modily scaling facotrs with epr.open(FILENAME, 'rb+') as product: dataset = product.get_dataset('Scaling_Factor_GADS') record = dataset.read_record(0) field = record.get_field('sf_wvapour') scaling = field.get_elem() scaling *= 1.1 field.set_elem(scaling) # re-open the product and load modified data with epr.open(FILENAME) as product: band = product.get_band('water_vapour') unit = band.unit new_data = band.read_as_array() wv_new_histogram, new_bins = np.histogram(new_data.flat, 50) # plot histogram of modified data plt.figure() plt.bar(orig_bins[:-1], wv_orig_histogram, 0.02, label='original') plt.grid(True) plt.title('Water Vapour Histogram') plt.hold(True) plt.bar(new_bins[:-1], wv_new_histogram, 0.02, color='red', label='new') plt.legend() plt.savefig('water_vapour_histogram_02.png') # plot the water vapour map plt.figure(figsize=(8, 4)) plt.imshow(new_data) plt.grid(True) plt.title('Water Vapour') cb = plt.colorbar() cb.set_label('[{}]'.format(unit)) plt.savefig('modified_water_vapour.png') # modify the "Vapour_Content" dataset with epr.open(FILENAME, 'rb+') as product: dataset = product.get_dataset('Vapour_Content') for line in range(70, 100): record = dataset.read_record(line) field = record.get_field_at(2) elems = field.get_elems() elems[50:100] = 0 field.set_elems(elems) # re-open the product and load modified data with epr.open(FILENAME) as product: band = product.get_band('water_vapour') unit = band.unit data = band.read_as_array() # plot the water vapour map plt.figure(figsize=(8, 4)) plt.imshow(data) plt.grid(True) plt.title('Water Vapour with box') cb = plt.colorbar() cb.set_label('[{}]'.format(unit)) plt.savefig('modified_water_vapour_with_box.png') plt.show() pyepr-0.9.3/doc/examples/write_bands.py000077500000000000000000000061401252122757300201320ustar00rootroot00000000000000#!/usr/bin/env python3 # This program is a direct translation of the sample program # "write_bands.c" bundled with the EPR-API distribution. # # Source code of the C program is available at: # https://github.com/bcdev/epr-api/blob/master/src/examples/write_bands.c from __future__ import print_function import os import sys import epr def write_raw_image(output_dir, product, band_name): '''Generate the ENVI binary pattern image file for an actual DS. The first parameter is the output directory path. ''' # Build ENVI file path, DS name specifically image_file_path = os.path.join(output_dir, band_name + '.raw') band = product.get_band(band_name) source_w = product.get_scene_width() source_h = product.get_scene_height() source_step_x = 1 source_step_y = 1 raster = band.create_compatible_raster(source_w, source_h, source_step_x, source_step_y) print('Reading band "%s"...' % band_name) raster = band.read_raster(0, 0, raster) out_stream = open(image_file_path, 'wb') for line in raster.data: out_stream.write(line.tostring()) # or better: raster.data.tofile(out_stream) out_stream.close() print('Raw image data successfully written to "%s".' % image_file_path) print('C data type is "%s", element size %u byte(s), ' 'raster size is %u x %u pixels.' % ( epr.data_type_id_to_str(raster.data_type), raster.get_elem_size(), raster.get_width(), raster.get_height())) def main(*argv): '''A program for converting producing ENVI raster information from dataset. It generates as many raster as there are dataset entrance parameters. Call:: $ write_bands.py [ ... ] Example:: $ write_bands.py \ MER_RR__1PNPDK20020415_103725_000002702005_00094_00649_1059.N1 \ . latitude ''' if not argv: argv = sys.argv if len(argv) <= 3: print('Usage: write_bands.py ' '') print(' [ ... ]') print(' where envisat-product is the input filename') print(' and output-dir is the output directory') print(' and dataset-name-1 is the name of the first band to be ' 'extracted (mandatory)') print(' and dataset-name-2 ... dataset-name-N are the names of ' 'further bands to be extracted (optional)') print('Example:') print(' write_bands MER_RR__2P_TEST.N1 . latitude') print() sys.exit(1) product_file_path = argv[1] output_dir_path = argv[2] # Open the product; an argument is a path to product data file with epr.open(product_file_path) as product: for band_name in argv[3:]: write_raw_image(output_dir_path, product, band_name) if __name__ == '__main__': main() pyepr-0.9.3/doc/examples/write_bitmask.py000077500000000000000000000043031252122757300204740ustar00rootroot00000000000000#!/usr/bin/env python3 # This program is a direct translation of the sample program # "write_bitmask.c" bundled with the EPR-API distribution. # # Source code of the C program is available at: # https://github.com/bcdev/epr-api/blob/master/src/examples/write_bitmask.c '''Generates bit mask from ENVISAT flags information as "raw" image for (e.g.) Photoshop Call:: $ python write_bitmask.py Example to call the main function:: $ python write_bitmask.py MER_RR__2P_TEST.N1 \ 'l2_flags.LAND and !l2_flags.BRIGHT' my_flags.raw ''' from __future__ import print_function import sys import epr def main(*argv): if not argv: argv = sys.argv if len(argv) != 4: print('Usage: write_bitmask ' '') print(' where envisat-product is the input filename') print(' and bitmask-expression is a string containing the bitmask ' 'logic') print(' and output-file is the output filename.') print('Example:') print(" MER_RR__2P_TEST.N1 'l2_flags.LAND and not l2_flags.BRIGHT' " "my_flags.raw") print sys.exit(1) product_file_path = argv[1] bm_expr = argv[2] image_file_path = argv[3] # Open the product; an argument is a path to product data file with epr.open(product_file_path) as product: offset_x = 0 offset_y = 0 source_width = product.get_scene_width() source_height = product.get_scene_height() source_step_x = 1 source_step_y = 1 bm_raster = epr.create_bitmask_raster(source_width, source_height, source_step_x, source_step_y) product.read_bitmask_raster(bm_expr, offset_x, offset_y, bm_raster) with open(image_file_path, 'wb') as out_stream: bm_raster.data.tofile(out_stream) print('Raw image data successfully written to "%s".' % image_file_path) print('Data type is "byte", size is %d x %d pixels.' % (source_width, source_height)) if __name__ == '__main__': main() pyepr-0.9.3/doc/examples/write_ndvi.py000077500000000000000000000057071252122757300200130ustar00rootroot00000000000000#!/usr/bin/env python3 # This program is a direct translation of the sample program # "write_ndvi.c" bundled with the EPR-API distribution. # # Source code of the C program is available at: # https://github.com/bcdev/epr-api/blob/master/src/examples/write_ndvi.c '''Example for using the epr-api Demonstrates how to open a MERIS L1b product and calculate the NDVI. This example does not demonstrate how to write good and safe code. It is reduced to the essentials for working with the epr-api. Calling sequence:: $ python write_ndvi.py for example:: $ python write_ndvi.py MER_RR__1P_test.N1 my_ndvi.raw ''' from __future__ import pront_function import sys import struct import logging import epr def main(*argv): if not argv: argv = sys.argv if len(argv) != 3: print('Usage: write_ndvi ') print(' where envisat-product is the input filename') print(' and output-file is the output filename.') print('Example: MER_RR__1P_TEST.N1 my_ndvi.raw') print sys.exit(1) # Open the product with epr.open(argv[1]) as product: # The NDVI shall be calculated using bands 6 and 8. band1_name = 'radiance_6' band2_name = 'radiance_10' band1 = product.get_band(band1_name) band2 = product.get_band(band2_name) # Allocate memory for the rasters width = product.get_scene_width() height = product.get_scene_height() subsampling_x = 1 subsampling_y = 1 raster1 = band1.create_compatible_raster(width, height, subsampling_x, subsampling_y) raster2 = band2.create_compatible_raster(width, height, subsampling_x, subsampling_y) # Read the radiance into the raster. offset_x = 0 offset_y = 0 logging.info('read "%s" data' % band1_name) band1.read_raster(offset_x, offset_y, raster1) logging.info('read "%s" data' % band2_name) band2.read_raster(offset_x, offset_y, raster2) # Open the output file logging.info('write ndvi to "%s"' % argv[2]) with open(argv[2], 'wb') as out_stream: # Loop over all pixel and calculate the NDVI. # # @NOTE: looping over data matrices is not the best soluton. # It is done here just for demostrative purposes for j in range(height): for i in range(width): rad1 = raster1.get_pixel(i, j) rad2 = raster2.get_pixel(i, j) if (rad1 + rad2) != 0.0: ndvi = (rad2 - rad1) / (rad2 + rad1) else: ndvi = -1.0 out_stream.write(struct.pack('f', ndvi)) logging.info('ndvi was written success') if __name__ == '__main__': main() pyepr-0.9.3/doc/gdal_export_example.txt000066400000000000000000000145611252122757300202300ustar00rootroot00000000000000GDAL_ export example -------------------- .. index:: GDAL, export, VRT, ENVISAT, offset This tutorial explains how to use PyEPR_ to generate a file in GDAL Virtual Format (VRT) that can be used to access data with the powerful and popular GDAL_ library. GDAL_ already has support for ENVISAT_ products but this example is interesting for two reasons: * it exploits some low level feature (like e.g. offset management) that are rarely used but that can be very useful in some cases * the generated VRT file uses raw raster access and it can be opened in update mode to modify the ENVISAT_ product data. This feature is not supported by the native ENVISAT_ driver of the GDAL_ library export_gdalvrt module ~~~~~~~~~~~~~~~~~~~~~ The complete code of the example is available in the :download:`examples/export_gdalvrt.py` file. It is organized so that it can also be imported as a module in any program. The :mod:`export_gdalvrt` module provides two functions: .. module:: export_gdalvrt .. function:: epr2gdal_band(band, vrt) Takes in input an :class:`epr.Band` object and a VRT dataset and add a GDAL_ band to the VRT dataset .. function:: epr2gdal(product, vrt, overwrite_existing=False) Takes in input a PyEPR_ :class:`Product` (or a filename) and the file name of the output VRT file and generates the VRT file itself containing a band for each :class:`epr.Band` present in the original :class:`epr.Product` and also associated metadata. The :func:`epr2gdal` function first creates the VRT dataset .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 101-117 and then loops on all :class:`epr.Band`\ s of the PyEPR_ :class:`epr.Product` calling the :func:`epr2gdal_band` function on each of them: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 136-137 The :mod:`export_gdalvrt` module also provides a :data:`epr_to_gdal_type` mapping between EPR and GDAL data type identifiers. .. index:: VRTRawRasterBand Generating *VRTRawRasterBand*\ s ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The core of the example is the part of the code in the :func:`epr2gdal_band` function that generates the GDAL_ *VRTRawRasterBand*. It is a description of a raster file that the GDAL_ library uses for low level data access. Of course the entire machinery works because data in :class:`epr.Band`\s and :class:`epr.Dataset`\ s of ENVISAT_ products are stored as contiguous rasters. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 54-78 .. index:: offset The fundamental part is the computation of the: **ImageOffset**: the offset in bytes to the beginning of the first pixel of data with respect to the beginning of the file. In the example it is computed using * the :data:`epr.DSD.ds_offset` attribute, that represents the offset in bytes of the :class:`epr.Dataset` from the beginning of the file, and * the :meth:`epr.Field.get_offset` method that returns the offset in bytes of the :class:`epr.Field` containing :class:`epr.Band` data from the beginning of the :class:`epr.Record` :: offset = dataset.get_dsd().ds_offset + field.get_offset() **LineOffset**: the offset in bytes from the beginning of one *scanline* of data and the next scanline of data. In the example it is set to the :class:`epr.Record` size in bytes using the :attr:`epr.Record.tot_size` attribute:: line_offset = record.tot_size **PixelOffset**: the offset in bytes from the beginning of one pixel and the next on the same line. Usually it corresponds to the size in bytes of the elementary data type. It is set using the :meth:`epr.Field.get_type` method and the :func:`epr.get_data_type_size` function:: pixel_offset = epr.get_data_type_size(field.get_type()) The band size in lines and columns of the GDAL_ bands is fixed at GDAL_ dataset level when it is created: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 50-52 .. index:: dataset, band Please note that in case of :class:`epr.Dataset`\ s storing complex values, like in `MDS1` :class:`epr.Dataset` of ASAR IMS :class:`epr.Product`\ s, pixels of real and imaginary parts are interleaved, so to represent :class:`epr.Band`\ s of the two components the pixel offset have to be doubled and an additional offset (one pixel) must be added to the *ImageOffset* of the :class:`epr.Band` representing the imaginary part: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 59-63 .. note:: the PyEPR_ API does not supports complex :class:`Band`\ s. :class:`epr.Dataset`\s containing complex data, like the `MDS1` :class:`epr.Dataset` of ASAR IMS :class:`epr.Product`\ s, are associated to two distinct :class:`epr.Band`\s containing the real (I) and the imaginary (Q) component respectively. GDAL_, instead, supports complex data types, so it is possible to map a complex ENVISAT_ :class:`epr.Dataset` onto a single GDAL_ bands with complex data type. This case is not handled in the example. .. index:: metadata Metadata ~~~~~~~~ The :func:`epr2gdal_band` function also stores a small set of metadata for each :class:`epr.Band`: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 80-95 Metadata are also stored at GDAL_ dataset level by the :func:`epr2gdal` function: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 119-126 .. index:: MPH, SPH The :func:`epr2gdal` function also stores the contents of the *MPH* and the *SPH* records as GDAL_ dataset matadata in custom domains: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python :lines: 128-134 Complete listing ~~~~~~~~~~~~~~~~ .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/export_gdalvrt.py :language: python .. _GDAL: http://www.gdal.org .. _PyEPR: https://github.com/avalentino/pyepr .. _ENVISAT: https://envisat.esa.int .. raw:: latex \clearpage pyepr-0.9.3/doc/images/000077500000000000000000000000001252122757300147025ustar00rootroot00000000000000pyepr-0.9.3/doc/images/ASA_IMP_crop.png000066400000000000000000010574361252122757300175640ustar00rootroot00000000000000PNG  IHDR L1sBIT|d pHYsaa?i IDATxydU}Uݳ130"3E "E;ᫀ[0 DM4č$bD&TDDP "twuwuU{~5l0[6ٳ*,d޿)pBJ%=C[* 4[ id왂r}k'?isS\VzpٲeZl$ixxXwt饗k_nV-\pOYcccOna#f٢C [3K6!!aȈկN 7ܠ?~ laٲe u~ӊ+|`KWO.h~)+!!!!+T*i{t'kSVӲetmI~ oxΝjC=T9GyD^h֬YT*iWZ/!s}s\j5z[ߪիW?n#ԬYT>Wl6;80/| qSX5sL~+6x%\}ݷUV=n=7fcJ%qϏ#vmd[6X/\t~{uC={q s^{w](rIHHHTH)X [w^C[=+tQGnЉ' 3ЪU/Y{nv-^8@gl2qӯk}Է-|: ~[ߪ~i7Yַ'T__ߴvi˴^{ oxu- .}]wu*ӣSO=UЇ_R}s%Iv[_җ@K,zoK_RG?Ҿ:K+WJ~>yަO}Sq7YrYW_}~o6wC`ʕzы^^oxl6`IEq9sN8㎺te~h]v e=9CtgR讻?ys9u]qһn}{7M9sVX[oUW_}^W:%Io?_}s7Y+WWp iHիW;Nʕ+u׍7ި>[fS{4>>|+:Stu騣 ԇ?aqZ|z!}5\o}[:c ۿ[=ќ9stꩧj̙tR͚5k_/X;^Wjӝwީ5\ǚ1c,M{,,O}S>Y僃yWʳ,<+V'&&=c9~<˲|ɒ%Pl/~,w߽pϗ<˲ oxCjHc=Y_?/JGVK.-~0ϲ,?v>ye~7ߜgY/^8_fM|||<_tfk_\j/{,O?=|~_Ŵ~_罽\ݏ㼯/? vaye>i #8bΞ=;_pal6r/X w}k7pCeY~GFݕW^gYoi$$$$l9$$!a3 EMw]eY>s){ߘgY_qӮOڼlٲ}{_/?%KYxp}eyV׭[7ye 7YrJ~<˲E/zѓk7?c=1 O޻3k|޼yO9N2@64jZ^*GydZy-zJ?,~8|ye?i?cyOOϴ~8R]???WBBBFJJH8}6|IҾ;-%T*iܹZfʹ?CgLVR eY+Wj$~ʲѶ{Hh4\;찃>яvmKT=-7z_o~zы^),0<<_-N;4%ַvmʲLGqĴ9iTm馛tM7I*vuW-o?-쳏~믿>xGttO~I<]5>9:Sta̙3ާ_CZ >Fl7vH{sl}~7듟nXb>;,iyhѢiu-O<-eOZ+VК5k4{'hyBBBG2@09`iD.|O~Rs̙>Z,UW]皘׳Qu֬Y]ˏI%ϣ>l=OԜ9s^{n>s=ǻ'˲iTm袋t>k7D4mτkddDvZIܹsJU K^{K_Zc?x}{{N RNڰ!<36'˲iϿꪫt'^裏֞{~J%x㍺馛8ՀXj. OHBB3IHxj颋..;c;T׮]y\CCC!t~m65y.RВ%KGG^|!<ڵkCɯTmC0sLeY! <ܕ+W>ge;N\R?կ~UW^yn67 :5 .PisYvhn92C2!!!aK"Û,c=!-]tl6u7OtA\?wkttw_u]]ӿ67G駟j=нޫ=/;蠃g|wai<^;!!!ᙎd$$< 0gU*z뭅Wys=W+Vv_zI>[.|l6ws=\5M~]ߒh4tw>v~ktUWN{O?tu:mo+¬\R\p,+sꩧJZf7(uyu}ƦnYg^s9zᇧ}tt뭷: /7߬O|Ӯ*i{1jmX`icNƎW*? y;k9#Izӛ5499%$$$8R VB³jUgq.=y_reY~zGuO~:3w}WBA-X` Ж/_֧?i:`W~>O?s1Owko|Cp=X5 ]yZrw/|a/|>l}SҒ%KtI'WW_}n;-X`ڦ槃E_N;4-^Xk/m=~9s_l2{W?\믿^w}w= .FKjkwؘzu]z_^x/M7//{e/{vm7MLL{M7ݤ;L\sFmkBBBF9{+!aû| sGvn…ӎ8mZ_/\0,XSOzh>_xqחN[|y:=^ϟ??/̙3>8;ޑWzR2袋R}|YgCCC])dCx</SO=5mJj|믿~Vw-Z;;%-[Vpҗ4ar rH< 6$˧_O4F:>.\cxX{_r%ŋJϛ7/+^~oi˿K~j5?~׽.C=43gNG?_W;cۛ_~gz#!!!a!JHHCi…zի^/|KW'!aDutMKHHHZ$$l7drrR{$餓NUJHzijy?_F#ÄgR$!a{ۿ^kZzZ=z_zKW1!Y~>>Z;FFF_w}>`|ͪT*[  izB6%uWkժUVZh:,moKH&?_|z{{ .лd|$$$<" iHBBBBBBBBSȈw/}͛R/뵷v^hppPgI'x뵟ԧxbUpLu՚vc=SO=US^&< ###z߮vIZMx-]g1V\}sԉ'(I^)Is-[V+R_FK.ʕ+ ~v|ɺtgCַQGoQo}[~u1L{U3)k+K_Rg?h_WjKW/!!!!!!YUVi޼y袋t{+_nIw$?9y9}# e:S|&XWK쳏$ӟ:,r-z/Ij:400' Rd+5\}{g>7M:#:wSNgKW1!!!!!!Y []'tR0>$i]wՑG*||GZ|y˗+s} ]uUZxq0>$G}kӟT6V6㪫#[oB5KHHHHHHq}i||\o?ͦ$鮻 ;va͝;Ww}w쮻` &<3݊A8T*ڑ>;w+Ww]-\PZm5!!!!!ي1=?͝;w?{&+ppPys}V$͙3gwsQZf~{ZJ}}}]ٳCYwnLn3يj*^>~W}k7y5|_k^{W{&o~e$$tC2@1jyP^9eyjfʳPr+)k{zzez{{=f_VeYx.rV+(WZ B֭V+u#{trпs'LNN1:>>,TTB9- mxdxQoPoooX^=2q]\pk5ȁ;u={t1t499>5Mv ?6d@ȴ^ZjՅy̏ Ef|Fٮ Q tY6 e.9 ϧl3 2]N^g?7XC.ȚqM9={vm'i}"իefϞ(~Z(wCesHVۮk zvFFF"ק͚5@gΜ RuiΜ9ZrjZX,Jf̘AibbBJEVKccc**˪T* CR),~R)@HR699H D rb1p-?nt![rnSZo`Ť EnJI eMLLbE0# /9 GȓS*TB1Jhrli4E ?ȃz@\ cbbBRIj5 񞜜,f Gڄ2@S򄸣̃YqRW7TC91ͼw8+N`nZjrh7cbŊͦ5>>y̙%###aF7'gtA[ݰ{V;/~ =9 {:NQGժUdɒ~2%Mxщe IDATf"mBߊWմӮ,J5kzXXƤl`Z-MLLhxxXf*,a9HĄͦ,@Iƈ 'P!6Nc(gp/{Y i 2!νN] BCJ[IY0GGG, 7șhN5F SHB'7dhˀƻ4n@b܃J$I#_9d B O{<*s#ף NZ }>1){= wC##<9n6An@8+ e<^&''Gq|)#^g N'Gv2ne/TJ 8}}}ύa|{t I^WDFcAθn48]}W kG3ͦh4HrcuK?^FFF=nFs1V e\veʲL'pBO=ܣV/z ^vw¦Gl8}N_W2|~eiv*OT* '6 ph4GV GMR $BY :`58#%7 DDQȅ4E܀ 9AȽ,x"Wv@~cIGt-n@}ywp{Luw.8:x2[>w. ,4>>"#Mn`y$6xɍJ򘘘z ag|1\/=͟utXGj7 anpc#/y> GdnÛWXSn荏ydو3BZ[_%Ij.bzz^=Ӆ^ e͞=[W .@sG_7MZxqNӥ^??G>͛7Ou{&DD2@bs1:E֭Ӟ{u]/q=2FCJEN'I=A̙3>,(Ҕ7Љ6Rxtt4۔ezţ xzYĻ@<$qbdѣC1ys//x`I*ِT ș6C{ {rMXk57'#8/rH$B τ=ݼ1kyzԂn Q_tDDg8a:"X ƕ?6B]ܘ O'aӼ)(u Xmw:C; \nX0'w6sQɝ@}y|)I=pܖpg귿rW+Tez뮻jѢEO'r:JǦԥ^}cZ`/R}{ U*}׻.}j4:ujҥ Oo|C}{ujg}կ~F&|Bx|C1Ld<[B###a!# Y0M|R!M B7<>{d{|CqB~B6I#u!>!b9' 2v{EhKAAP>rsp/=3bqGۘscȍ@sȝݨu#.+ҮN Ɓ{||lXL@1̳p2~8 ejZp"vli:1=: ]Oڳ>[g}^7iZ [_'O|)\ccc{0.f;&zB{bi=,N'읠 :u5Z*OwwFOWqR bᆑ oDYrR勺/,hIll c{x FyuYrG(69~'x-ĽIbrH1 'jfhO?q2Q8^'݌7은{ʌ #lq{d5SH{d/J{xqR*Hp i,5dG20@H_Ӗ>WS}} 2<=66/̃^G A!zQ=pĺJoBB“G2@Qp@^ 3&R#CkFFF"Q266^5jvfX v"GWN8aR6D(w2^NI>NxbqkcBsæZԡգ}چw{`L_oOO7dSa(qӢo -d,I Ř:3dljN%hQ1zdˢ~[ ͸Tx@NET\q2ڐb/2HSZF'ԛv8 ^0Y*‘~sއvg Q?YA<81!WxWW dG) ʣ^8hGPCS>E)c7tG:߱\֮ͦ][XcvmٳgO[W8GKHHxHmz=O^J7@5kք(I;L$h4BTsY8C!ip) {K 21'NL{n1mҔqy*JNT*HO$=ӽ=ZfvS}2'Li*M&^`C$] 8ģӍ 'Ryɫ0}{nOE }L|C{f|8C3?8y>.Ƶ73G̜938Jp4lIhEւdlXn]xqTeaE֬YSxv~FFF`{4+X,H[u,F 4ػS,X}}}~8u{([ƽ9  "jaϢ?)נɘh{:7ybRu/芧yǕFҍÄ>b`L{ 7;OU(eV efys2t׍78ƝnvҟDDΝzR=2-Н3VK-߯-Iؚ m֗/МRĄz``fN= OoNuBŽq Őzc ;xX|o BSӣFQw}Z 7)EnXQ7C~dk'+ ON8{VGH:Bexj.wjBt^zўV<NMz㊾s #<nƕ׏~Ep»zOz {*֭[W s s#m'&|F[5BqTCD0 KD]1J=0`h#cGE GI=82 P__)FCg;/IZ d|?EH9YFyWXo,[?H*"J/o;d䗗@4N B*=C$y.Jz@R N>'}ƣ 1d1i.OsﰧOuTl4g>}w/GhKT A_,ߞrGٴapp@$/wNPKR!%7<0\G@<qTT*>Id/~#e^4i菏[Goa7EEc3GzܻGsh'eCDƈ&{$W:FCÍF`ɀ>og !"Q̙MG \Cu0}^1ID܏q* /˚1cFpD6T*ψ(HBւdl[l65cƌDΦsI={v 2!$XȈ$M;s z7?׉cZ $Ņk(`qu'l7\w6llʑz'r)8ɧ~,r>ς <DJC^g7S A$5::ZHqy`BI冞=zჃ +VI.H{=jG}z^Ðu'|0:"!I6qBGe'<,/|URDkE IDAT.#-Ϥo|w @)`R_D!'U ?[zc#k=6<F!X{SW:܃<}zc@Rxc҉>z։UʧQW^7 +O_z)~zG }"{9=Dѱ8/fx7xDŽ3Ut܅!c]󵁾xG F{!.<jH~>|0T1$36ȑ; 7Kc:} O8 @.~BNRkQߌ`OYĐ?=Jcĝ/z$xC>#j'à}HVK'эjZurϖϛ'!aS `mJ i}ࠆAӣ3f4.'þP2j3/"!%tZ\NJ|?s$ m^6׶Z"y|v) S ͱ2 s C>z';N`1}lsyyzs9HG$: \T3֩QNUI?9$P??OjʲpjϓnL!DTO;\###4׳,[v|}SHu}㙐 d$l 6 .IhX֭ͦ[ԭ]6DQ 'wB7}Qd;';уx cLh:?^>]0ƀ4Ex2TIS)tᅮ'bnl:1qc2uM8r#@:ݰC_mrNf1\8"$$#}_$wA7XAv !OW藓?qˍ#rw܋GɾGE.a{g)w9:6|Έ @f<7>PWOsA{\?=86zW=wi{[MQ774}nhՉ>3pg1߯Z\_" (Hm Yi```w =1]",\xF*zynE8N{s}o|7xĂP3{eAĢ)',nXF ȷ[82{ǝP/Ӎ%dQ,'.{'%">=NS]q/`ynBGkiC7]Fv[t̠+ >fYq_p F{ۜ8.z?7N=*}nzŘvr|6螧^|C3637~3wNliHքdl{ xl@x9Iq'vll,g11fd8ŇŖ4$7D# /n"HLXTi)l`iQN^L{س^ys ?'xY3ø1<яo| Q 4'7b96Xrƈg,KL]Ia>e0oq.wx$0Q OFy/_5|mjON'UxiM"JSy"$н!DZ<,i`AGz0t:=7y62|O|NtG_~tEO?d.q?C<~Xwz{{cwG\YȸP>@;!edYn9ieiO ȡZ<ˣbdž;+#)q7## 9Ӎq^[V~CH;<2QK_s1mm>|^RMH#&aj!H,Nvx9ZOV^#'~:U g<xArb),N@7jń vďMe'7:a?éC/rD7|Nn>'ޯD|^ʣAr&%Fo}Tdo]&:RT[!{KSot: {!Ґ ʂ2ѻ'bGDX`}/LNvqEEw\raauRĞ<*wZHG ߻hq~a___0 !$r~-}A]$y?I{rݐ% !T=ِR넘7u E&A=hX,'W7>TI7]眘_^6q$`4G3ߏ~ucs!Ys˹Vixxpr$1hL臧DQAhv2u2}nɲI#A###g*8 +7 FFcd|ѣW蘿H>i]qϡ13NFt jittp.FJ^WRȈJVX3f%g?>>{zUuOM= d!ẞ < gD!(݋ !x!NzcR7RxT|gm$]Z DSw$ 9q>('W8zBqDÍ(9rc :Kߛ>K7=799ZC?i ‰d>0c+/&L,zĂzo@7q{,SV+u F; ⨛? :+ឞ`aŇk $9_L<A/|7NI>u&ϳ,+3fݧ>nt0&H-sCz3%a~CBj>t9҇"&W$dsllLN"^x 6{KN |OQ`HсQ3dPC}[SctxZ=BJV+8G1XbAw+J!/)]hy^@L{|z7==/9\K ,fTb̸8[XK`K=F1>awV++qg,Cu_2lijm [F1>>@(UT¦r3/"u D:4xJMZbR-  0K񄸳q|0cLb*Jx*9ِl6Po6:iCfXc1Xܸ XHS"}$M#Z(a35qr"'ԙwr?N eRR6G[z{{Ñ5Ӟ<). 3uy͏7}O#t>e{~=Vj H3dHS%FQJ'͞s{om~+i Dv'&&400 V܃зȟgC7J\'\Nccc0Cw/Gb(đ8tZh g9GCwg``Q[ϔz$$<$dE o2oRV`aޭfQU*CBwYnW8@U@\kc cdd$/+s?҅q4izC[ӟtzB*J*J?#$dʽxZB}yy)ȸT*ill?2J B)1X~OIrO=<{hh s Gi'=.Ƽztb;H+QWt!q⑍~:N#GFFBʬh_?s # _1^oB"s7#n1cDcp3< ?Vk갎Q-\PV ijyԼyczK#E@&$d ,|x ]8a 9 Zثy߳B³Fu=Mɉ9D,HRMbODe֬Y!ꂗf Z$ɪ{ᖦ՟{ݘ熖G /7SOrqȏvݤ wZx_b8A]Ѕ :몧rlʼxcŊkʕ(ؐ,/|M8Q|,p6{ǓϽ1F pF>KTiWnY:t0oI$C(akA2@Q0Q=Ix,hdYm@<1BqA #YdYM"OAT0 X< / N9.O'N&ftSgxHUHe|f pϻ/Ki<'Nx4N Frw̉@/tHdC{J{oAew>uw=nDo73.L{}OƩuww>oO^o 7V} ^ {/rn0ohr.D]N->&hB=r@nĹ T£DnXG#\ƣ́||Gn{D O'~|7nbV+:[)5!p{V<f/MyN^8i1& IDATpYbj4dXy߉x;)RNrCuNbb21<"v9Soȉ?Ï)_ F∀P's./O@Fn ?'O\q=IvrCѳ8OT͐8d@E(KҴh2ob]IU9ʈufAivrmo<:⮓n{\&f=>{gcU5g@ XS*PT_H˃m"јBL KM&@L/$hb(0>BmJDK{{gu{ov2s~{Zk͘iC>ӀJ{Τ' q)vuBn:t`sTcPIۜ^kcþ;_m߲2:]˘(##vHdkDv@pc1kvԡ#ڕm'Ԥ.OE1u%G \%?z>?1 O ў"*&Y (ړX$mqw:j`=rԋLLcqG%;+8] y3d{2lv^=$z;;Yw~7Ä.u˓/u9&)cvDt2 +}\f '9D-K^꓏ {a3q$=&K{7x;D /0@%yΑ1(]]]_5Z[[.~4فH<mBf iOS lxXTOb׽u//kz衸0>O'?ַ~\}(nx[_[nw㢵.O}hQT#2Hވ@vLZbQ(</43* 𺭬,29*½Du|AgN/LD&x[ۭm~eKi t.FnUOۦ;z!l?i&rXT0ԗrqxMIٳg+eC@O-˩H`v6J']ꎌ#3el@_بd@ 4<9]ޝ]&c\э I_ٌJ*.&es/) &K_,YƕL09ȍz[ޱgxŵQOñ3}f ˓gL!?Sn:gc⾏`xi1isZ^ހ.s+9yNm!a_}E/wOOo=v{""╯|e\s5q]wG>򑈈8sLy=Έxk^qmŭ/yK.xrK9Ɏ7/u^|/&XzF8]N<.t:%BB6ZNN`OYт|g{%i{󳧒9^{uM4E50jpa9g8*S $ y$nw سJ3<#wc"sǭFų2 gEFȏzak& \|;]]{c4y'gД9WDgX3&.irᅶORSy1:3`}rd=nlle]%ҕQDE|(#[ˏrMf';\i" vN`yw6eZUGK?D$hٹc"e.@]1Lb<W3ջQY)Iׅ9?sD:N\|%~8{7nB>""ꪸⳟl|ӧ+:}t,{.r K902V&𕕕zOn9`nx]__̙3ei9#i )zEf9:侅̐Is}}aXD׫80 ,,zGmsT=RIx їrZ%G]ӀO)v:dZG &wQrҊwx ƷNWk_r

wys~| ZRKQjrBW&{npYDDߏ|ɤxϹL̐* Rx# il)>߂k?2t:#*nk3}#xȅsA,3C^D{YDxEz2ij10N{ptaYH !3|cM U??bE>)&mV+&Iٜe۬ `VT=:M.{DًjCLL!Dyq@u7$Ξ=?#TRco1FL8ޅ0S8{bX38r99o':26x42gnx.㍝EѨ-p&R`.QKagrz!b/}y y|_t*>۱X,<9|ﲵUUg~ -z&䃉@b` h۱*&VN'yT搁t:1N 4ۋdRZQ"#Ѩlk/O%Eo&ag.*Sɳݩ\9j s%|E0nä|쩶>Gk &? uuu9>z@1b9I|N.b4=I.h4ۖ蔼y6c3i]VUdg5V@FNK3=P"s!{&D4as^`C]8۠k4Uc}8kتӋpI aSDu~_(sӳh}si}޸c>դƳ_A\r!1o~o~s?n=}t'E/F4^~flooW0h~qԩ1 g}4`0_"2zqI^#&X@4p9Zr &dy DgN{K>Nc89XX2=`ckrJ2A@*!Zv4$ .!@iXw/ E~lhN"ƻ3jAU׫,v*jww7vV<\7LJJ 􍍍vnz:VRh;$ =B uxaik!@DWAȓwiB'H bsdvtyiٌpX+d2)Qkdh‹U ޑ ףG#dQA_=̘3N+QAt[t:NSld/k"[nꩳShW,iq;;;c\Α8S< [2Bq!`PHx<.r:=yO9s&xwgϞ^|+#"/|a7y\s5e`H3g^EuXвedh6#8# bjtK:ц`G'1h۱S@s%:{l 9&"Jf0T FTwqڊv9`LѨi3 @B]J|%Z, ԓ|fלqQ dЏSER& NU'e`:XzPCi#.6X^lvXjZ~xˑJ"ka w<9#dtL02B~@66 L&9nm~_ >x2].8i ؑɅ9QN7؈x\ z]6G,xvEjU@bCȅg1Sg r͜ȡ=߳Ip8,dqϡq:VR<><.XOsTY__?>Olmm-7g>?}//ǻn)ZV} ;VVV-oy˅n^].Q -F#cPtʄY~:u+vP6b6E)cXDžH!N1+핌 B!%I*qB7Llk Y $>,dxabKz0:88(t>{Y ڞza"u9LN)snlyWIa]wcLx>uu=ܠGd5w)@#mt:ް)G< 2 d2\ <EoN"v+R@fЅL\j\{uԅ4 ~F - O5`AEBF^YW^',U*C6 &o9gN+b. j)K<؁;w[ i2d2DvxDdb(4blc8<^=]w]#H/??\{wu]o|}{_L&+@loo׿wqGmokb7.OQ -4Ξ=[&ONe(ԩScUSQ7&] /%9l64ղ6L@tbDxdg#{ 0 DD1Ia4iX0{^#v@S-@~{/~q}{⮻*xu]=/~?.䄖@Mӊ4bgx<܌gϖt29OK/'xv(}ڊv?x%y|fC=pS/gPO{w=@)L;N˂P*@33˄8 G> ؋kpPuAw8A&Q{$EDF{;r6Aި = }yFo5%e8eU&ګ )QI1@ЏQ1ipD҄ΑA+@SW1!bY }WzDZ<Ŏz$śO?sZu\bk5!XGݩ+}T8e 4`ǖ !Լ)Nц%sRFQ=69H}ԩSe= nL4nN'a;w.NRk??_Ե|;|k ("\""677ckkL3(;Aa" ޳,a֏ɜgp2 21Qgmu-" axWDTA2sN8vy|&l7G4H3n"N)UL<#5dgu}>.R,Bu[ N^elFhpm]8Jb|9m2;N% D;mX\ȀvƩ*b{Mo@AwPVl[7wɧp:=G\ol8mƩqgn&; yqa`暵JuB'xAdζ{t%Ɂoێ N*g}JDtRRZ񵵵<[FFӉGy.39 h:![xi^Ggy]6pŽY(G412#b"7Y^@=|8#C#ly6sJN9y><<,[b [fMlMN2&mgvHc{9ζfB"Oy.2tti|<ooQ58x?u˅.59;]q70Gx\L&ǢuqdD,s;N%t^2<^KOyWƠ|-arsA^vD3E?lBf2LJM~t ڏ| e* r.ltb0j0u{c A\kl}:1Gq2P4\/n:A ,۫ewn?mÀ_m8 M70w bd"u{3,:mC^Yav| GW;-;\rv>}}NW^yer 'sN4iKuՊ2I{iUǗz -_ 3F gq8Z7DP)Lر@-_!CaLLCx&g^s;';Ј%A^bо$S ()S py/y&BS +Gd L Л {<{^ˊZ^^<Hn]> {R7l@ D2Q cF[S } Y~Ӹ9&BrC/&~:Ng[6N8L IߝS{-75ʞc.&؊u PWG ),>ׁ1d=!lyz8:$ c:ý8pCΎ@G[93Yyt:v+7G"l1t (=Y2ca ّlVR ԝMϩeJvPE=Nږ#-chmgy<$#*װ: fdžSvʻŘdXzG.#)ؘihۥ8G}n :x{4p8,{Okkknb!QT-겱Q1fߎa L"`\iĪl萁R {]4XW6\`5JɓA4UMZ˹VUD|ik)ؓG})뎴4l^oDx}'`dĞWG&<ОjXI>l۞w1qӇxm~~xxɤbEèӌ);89,O4TdЁsm~G$}Y1=`=>L"~%[&OD{Fo ^SNSt(Μv㡇2r s<& c҇F|G?h;mD'd"G r h5\)!gtAh=Wd1rtvEz/-@/)k6N]11wK"go)Kޤdʘ9%t:-kUx3h8V" ځ.gpVF,eNelHFJ {wbDkp$l:mdny{!Jc}36ե.uy& 'Ҝ[[dn4&,vU4N^7 7+++1 $h4b{{;.I^h`/b  .iT|@",ZyA8 0@OȮ? )E B,1byX`h4t:M&e5x b4L ~ӑ#3VGMO1i); )Nt54ٲtK;B~h7(n]g^Ol2}p;%BnL &/_ 5c2@r ; g+dhҘ*꾞ISSb{\[[+ ЎX=zccqAȍ*)u8lYGfP0c.o Ҏx\dh lq[%eNϩ8fn†nlv^ty N777+ЌفNi'<&]&rȋ(r$) ^MHb\DzGxGkkk% *cGDTtbQ!'ZR;UR{ -t$b h@=3q1!zcN$ =D.MooO5v M~ MxXS{}]O띮+04=>"r ="]c6h~T$6{o.Q1{q6#?8ۋ6 %BA#-m>Lh3PvPڻo>5G&r?){/IS\uq_LollIuO۬>c#RpN8A ze-V<c6J.| șh8}yv0ե.uyr& 'P"Ο4#SٌsΕ -#&C҈ -x '"{@G{>ylGyqqApn;:7.{Ge{+rdhA ёoѶ(irN (g#E;ڋ ӍɋS" ]ړ 2BTK6~4SLp`չ|=СF;;;?$afykCNqT5e]m`뤙![;# 1EӟL IH\rE!(.Hǀq:QV &N3K0ϵ^Sf&F!NMcI" H8Fļvxx1ls'D'M{ Ȅ405S-着U#L>)>Gh HJ;=L@ymBGA{}#bArtg0zl`W ]vLN3BG֟sBrd*Souc"K] FO3Gxσ#g@ !o^gYs>m2/!7;N&:]1(Bfn!1N Cڏ#ez#*Bs$ݰ;㷣ȏ:aO^\[VO&r#v'ԥ.O0 YhSO {c t v k;T!AL<\?HA'yp[y*G^xL؜fm2Gh`6/> ۲t eLm?X-75u]<Jg;pJK~6v~=0t#,^kXb &.ƜBc/X_ޝ:XD;S]7G)~vf#Skw45g EFF,I]ۅfbE8%"*/k[s[zLsʎ-:3^xl6n+yt)u.Ϥrcu$`jOC~/'=E)fr&0-pG;@1v]@c*&2|do;rr  mh'^wEb2{ ( }5džM˂:R7uɜa_8 .T{-7g&d tӓ Nљ!~em2J 7#~ l)y{VmkO FY+c7u_ݬ,O]Nˢn^ BS vS[n;͋]7 B<cH'4ix{НeLiR\Ȏރ~ݟ(NSϵul8 L/MX<핈!?,?n|`ɤ8(&LvPC\u~RXz'#c‰Xrg syV׫Y\rx 'LV<+{r#" ȋ4y̹Y4A0^F_s6@e IDATIY&=e2\CN$`&Gy 6k@؞畃ۨi"L:bbi3-o=$:GM,CMxy{NDE,2)EGʶ%dO}28sZYNclC93"^oշ l׹"'s%"d<77Oֻ d9[9C[h)@?xm鸈#XtcmPfs-6O-'~/3@^h#f9 ǎ:GLlg \/1Xd.؞uۥIm^>s>_qԏu ,cY۞x#8O~>Byȟ뜎uWf%A)$xЋ-gb;j8 EA3 ˀʑl{g[PkQ'dcay7yMd#:gYHl?G'Kls&&x{B}r$~bq M#G>=:h[D= W|Ghnt\Q܇k9DLI.X7 rQ!v)3@N]fYv:ַH\&?$.Zy%@cH=b2eG-`pX":qRĸV9a~/4!HY܇,X,)~q3y~Ʌa#FT70}yqIC' dƿg,mr4Yؖw$Q&\K4ĺvT 4I0i٫m6n;Ȁ,dл{&W<m}rsh #K . .bI 0c9bȟ& ׻&Q;x eN!2I38Wȋ=dBnD>쀅?X<qџ`te] sQ&QGX/U_/[gC  id1`BLϤlVRH0=t?_ubnA.^/-뉅}Pm>Qg@1{gg~}v0W-#7&995냰!3G&BfǬg7>˄l\T(u .^P7A!Ń GӔ Ep<G׫t{>##SO@=Τep u1)! ɳ/ɂ#$9-[SӇ=şV`=X&\m<(ˁgǥ8(r,+O1hm{7ʽ*A~7";2qmmɞyXڎqH@ /L\"/S9m[؋AI8cO*&gD5:b)Hř3gG5Y9*a\T ҶR2/O.xFzF;-'{!bLJ%p 3"^y"MHS@,u{9cPei"mRt <@"XQm4z<#V9uu:q/vEg5{ȱUmx&=ȖzWRЁ6 r|e766JMa04>ý26³xyVir:z3!M7u:Ne,"t2DDD/mv;ğ)K]7䎾vv Ʊ|g#2/GiWR3b@<^1ҧ7sL-ꪫG-uFv0a7Qj`: @&R1c_&(GOԩSv˵<}Ƥ0O?><c*P RRZ~N X"v=03IөX7h4b0`0D1A ÈXnJj SLLdL(? O 7uؘux~<⡋X$०>"`Pa gADrKth3fY # ᰤ0 :soR"vtg8vTDTӿr_Cw9{ϋwۍ%Cf&* C׀ڃN}càv#";mbMsJ 6 Y0Q#o=[eb"lۦ}*/:(uZ#N뼎 ˆSËq0`:"//G"i h4;M2'L&ꪫ+(B~b/N3sɹsN#v\RH( sS,rh;: (e d GOHy*ik6 4n{ )ˆ(g:}$n#3rgmy;dTP'G.Rz{-+tڂ# `np8pG6^{9r0eb{BHѳS(Mnj6%O qn#g_ilnnh4qpp;;;ǽ3NOl<1K]JM@Np$$h;6Ͳ7{6AdIXĩS*XQT v˻ie!x<.#a9S2g?b Yeo 0Hcmg6SG14er'b&|H13x7,Y&&tNɉ 8G~ee,Iq/o{q4 eh^ "u?͊0lg,DxSS#qc&%G;:bW9 p8u8sGgsٹv.DH4æЫUh~huc0vP@Н?d?1qcS5Npѧ WD=Df9˄۰t@f̼oӸzp}QB@ \:z ;M:n+UqcKgፍHk,EY~xmYGD9tN dYB +8 VD,=Ҏ0 4{|`ewR~" 1aBM[Lch2]Ȁ5G,ecRv9>r'Zb5Ndl:9ETRqw;#"5yMmzk%/y0;'uЏ ^=u>͞h#%c{'r8G} +`!=feszKGڠxeeB "B~ q{,[ԍzw {< ,_Ȇ#b($Ӳ>Ļ. m eݚ u }#/tb`Mz똝n 1ˤ{] {>d; vB@ݜɁ.|&ʈxի^?cXĩS.3KPțpxx{o|ͥFz 7g??ۋӧOWqX,q=z(u$G@ȠPSHKljYd|?=yس::=*m%9`&GC kQMa"6 0ȞqipͩOsku&682V<``vFf+248F6`])sG:"AۉL!Alj"+pG١@<4k6q3[g{LiuCqwrdekk+^UdBߥֈ>~Z& IDATؖljS\xŹY,ۖ %A{O_C2H3dH=, N9u;n؅&kFf!szH|CN1,cg/^׀ft ;Z(:΋IZ0^WM7Tn;1V "p}} vu#t,>bux#(:[7( $yE3Hdi6?Lr 1ջYy6;NJ[n1Lߍ~IpM]Nz}"xsgΜfYȀV%5 $ xA0 "gYycNlgL:N3piKLZxx ̖=AZh?42qgIN8irBY8AНۃ^ 4y ɜeA{M7H6Hd85S{ҭw/V&AN-ʠDv+xpv]RPظs&&NN=/}qш0!rި?ّsG&\o&Gǩ\c[྽<K](Zd}w_`pW_}u|K_7qUWo?.ϜRZi9̏޿$q٬N]tؙ0XI bQ@!" b؜AL&9:;=2 ߀ПE7Gn yr:u(j:^i ̓mτ,f;$jUR# T !N3ynzE>.sKװ5 Rto*{y.供x961N!xԁ/|!^ H 8:ggTTvG|gSw<X{WȾ,@(g:ATZ"?v۟ߵb Bowvi4I(7xcx㍕z}nWMހb @Bn&c g\b Cƀ`αKڋynqNw23AxcYo\ڊ~H$(a |,q2Ui!&N}0#[G/2җ+z$r==8;suk6G6H[S{;ur;PfY|J>~=NNvt`Bmn+ALЯAheBԍ~( ~9 i+)*.mXn(O#W/8lvsN$JK9hWwˑfRnz a^Vi+>KD}H)qʿ^G;\^k*5yxӛgb8Ͽŗ_M7V+3XYY-?]xxĜŘlI`aINL:ɤI0@I dn<&x$sN:ij9erc ň%bbMh {}}=677˄x.=&oHK@: V<tz0LNXy!+GZ 8Zm:B7x2 " Vq* -q M=vn~5AW>w{` jw5H-m `B}Nඛ :k' Od2)+o~s\yKiQSaӎ@V n}Y~FvvO3A؎!t]\AE=cN;v v땭ױOOUvBzro{^a]֖Px;;;1-t:-N>݅9\}o~4~??VWW/3)X|shTt=77˿n;뮻.7Do+]zWyVvm| oxC|_;#ŵ^#벷WR 'Kh|mm-&ID- QӉXYYNS챎Xx%de]9M`uu 8!k?:ND,#%aQ&d!o\k^#:s*`}<F(h`0a|xq9qX3QHi씌lV+z^p8`YLc&wf1~O>{3x`l `d6 + !JPE@IP f !.rADHD" (" a6={j8|pT*n _͛#";G{N]}{_o߈}sO|e[[?7\3)X4ڏ~T8~xD=R);b%>\.Ǜ뮻~}.|ɸ+]zW|$fr}ū_7L(I:/"| uotB[6&G9Ep<چ7w 677Sc o5dȓqr=EQwD8s!^&r)7৏!)qƔ5j4EIVp* 8&&pƱcR+tyo E_b;ް$,mOyVt èI68Ś<{X7qYٓib177< x{m\^/5`qq1+tߋ(d 5$+0:AhVĽv۞sUo9?p}׾g{~W^ye-oO|qB+\3)eB@YajZqСjƯ뱶~ fss2qJalR'cmM7-0DDCۺOm7(& =n5Q`6ĩX"{bsXݴ"d5[=ޮj~86Y$z 5݄Dk$z=33N'=?7׃,:I^ < 9e&ϴt!]/'`Bt#9JAn0,~FWNkz#g$>C+#R>tsr=M;v>96ʍ7^o}0ُ~By#m(JtRs)k!q+78$m'eRʄF`0eLb3`M !,,~9V4|HڃhEM7l9"4նGD:%{h lx9-9`?lyA-C"X ih4> p#Ơ'"#fk%ur9z^j 6M|@΀\,b [mE Au92{t|j XW#_sL~ NxϽf&ۂFz6'z5Ogy@Fx |3>D?^"ECƈa/mmTcmN LRd~^_r."4)(7)_&+^ $Mƞ b eSbspց7vEcy.Gg7 cCś2޼{Szr97# SlDf&bÖ &.<#OZf,wboԟCò @ ,L:,K{ #ch/Ǒa]l& m3s 0̤r-5G068 !뽟nic>HƍuӳxQZM28lmmHrͭ+B(&)~ +/y 3vNw0zF{ A:or^) =sg yn]tT cz7[/WIӳ3)l ٧`+߂ldlٻ`!Vwrcp盛 L.6ut0M`р( '䑓{z܃@ۜO0!Bۀ`屩3;m-Nt5!msH,ܲ'@ržqrǜ }p r۟՞<)$k#Cc6n7{ jwLcsnck=25" /4I䚸2's$mc)@9`$pʡ x~~&x:!b6hYnϫjD>AO* ?.!'~s]8/_fGq(`FlέG!֡bRu!!e@r+8hjNbg"oG2B~^N`M -(f_{] s93X>ỎA7:`BqO99)2)?[`BM ̖]^d21N>Z#!H6 w}(ol0L6P6x6i-{qŀ2|r\LڜCB)aE=9j0w&mP1~qX&y[soc0*N&Gኜg&>C۞^0Zr3)ldDOUr8,l޶rq@enρAs9lM39cevGCR( $,}r`)?‘k9`ֺq"D Ky(-Ͷ8oqBL D7s?pȊ)=k)%b|*s 4at¬J}LG'a36AzwX3a @[w{L"20@~6Iά)g6#hKO2dȝb/^ zK1c] O ^X^20Iw$}+u̺/Gg2m>=kAq|a rLʤleŋv-miKoe@ͅz[F1g;K|N,_<e~ٰGD:+L[vZoi%g RjgiҔ$#DJm@p8LVN.9YcQ`!wXV=^[}1 z))H@m6e1X.?bxLa'v.v7srATxZ|06 $MYMϜxJ_x;F#Ć~8o/@NOw$c5'q P>]1븽A<)$JctHN:D=<;c݇S\Iy˄ӂ8b:.x) VѦl-c/ ـ=a%r؛#K=;}@ n}'²j^4^>4rIu̞pX蟭\kKt!TXIBq1sgn+{{oor0>C'L{*k~:y`Ҕe|}X=1 ;;;Nsg8|&d zf3UzUUz^x$y Sῑ k{ /Ēq({2&x苽1Ax+Jk3Mnxbn0n<{<'eR&g/}Zc0Ƃ-6#B!)+C75ۊFeK ^185ᾜDm%,{78ڞprQckk+j{Y/[!5x69s²-&gSx B>.4?qᴁ#NKT*&É&}| ?ZV azw>cD8/w>-|LrqȮ5d Ƣmfnzz|Jv4K/Ƅzx`v0&&~8;;~[#ƞr\uU79ZV|_/{~ u]7Ib?X[وB푮j) \[TkjZcSTQ1Tn`0HdFpgg'LR&}\p'YlvT,0Q1Ga`AV3o+: ^ ! M^߀dzf˞}W8ɷns9)`,@ 2i?4AƱ6a4p͓TmP۫aJ/lƴk ? IH q8A4aT :Siρ7o7&Cy(^O^6/1?̹3SGQGcoBSIT$沽B-/M+>-Sвa:,ޏh6nS},,Ď`a򉇄 ݛ!1x.bzz:A+о~W |V yW *mIv(b{]B8 XNeSv?!W.8l茽ɽCd5<@dRnbaE}az^haYDD4܌;wh+j܈\#^*yuR;z xz'j1>3 @xz^w0yj>1ND2r '&{=8q"zt G3n^/יH'ED);B8^䘇"WKz~~Ndt8wMr4xꩧ #{vvvq̙t:* >OM] azF*y{.grR&^&d⓽A p5g=M{ X#/фp[e I($ < lXOm4 Ҡ2nnnt4|zd'&B RXb[6& Y 5z >-mO0.7lyhCZ 7ns(.cA8  e1G!<`#W{|36t]FvN5ͭ@v:Fɽs*!qqnZ-E/(D.\ړiA HS&,~#`n7?i-n[dy;`B&t!cVE߿ c\ 3 @e8'k@nĴArN4apNzha}}hО,=ȋyYVs8}e^Fh6i. [#;14vsaP 4/+aI|NJsJt:I~1 ѽnF#M 8Uo ?:ĺla=NN'mw`ii)n4ى[n%n8p@̤Zf3z^=Ęu 47sCx^jZ\b޲ 1`R&eR~2! 8q6[ɽ 9 ak= ;K4Y VZ Y?V,b:DZ6/P!_ 17sB|d*@sk6773 : W@3Tj 9\ˀd 4c:#'",Ĕ;tVhT.QDܸ8:5rC#:!A`ȃqM qI9ciNn9BLqؕs,/3,Ƙtnu^o:눋^h4J tRJDOЛX\\L991d~Að\E^%blpXĄ9h6I@gM#fcՊe177?Ӆ0r@h!@^ x '{8]{;Ngs2)HeB@iu`͚뜬kCU6[@|D7&'84 @ܗ/`zpذCa!6^ɛ<7)X+@$b>*oഅ$N|L؃A6{d&xJ }t?"!U<=0 !1Eqp.sɄk4mqq7W7Ź& a0F 'v ~WĞ{LZ * >G9\Ję1 |q61/fggBF>3ȁyX-Ws+79dq %ُ~ގwCC=o0}r2fSL}.zHHCl by9sE@&eR2! *Jn0` dXmMJ4݀1@1Cl>j4!@1:q @UՈa"l*c\Hz`k^ICk3,@0!B\ >@R i<À(Pfl G Y:7$IHgdݡ6" aYCr@HG賍 1&lgl{W|A,c3AYZZ4WGv SkHZ#a{LE677S ]AdžFd)C0/2?q0cm$k?17Nnqk=/2!5\ ٧%ـ(p.G٠3;آ[p+/V81;9ă~cp\Pm6lDL, ;b~2/Co) GFɠ k=*"ڑ԰CsRO! `$ Cpx<1851܁D9uW&NKs -㝑7},JqK 2<ϵ7OEeF#666 c,?iy9 :xX?dE(m #GЉ#}0OOF핱גz/b|D#^4 N^3zvss3N8ڌ˧DD>Va$6 qn Lʤ2! .c: 9@>pJ8[,c l,,l:l{̝+ak9#7)" 29d }?O@1!W`8 {=vvvaK:5ER16-q{/N䢟r urC66\r0rA*_a*ԛ[= t.h4Jm060s1)2:\1e(@7GBt[&zoH-A|nh:"= a_qoNX|p! |^/*Jɯsu>~a}IyBؗ4-Gt4?9 UwD>ól6 9Rx##"%#1휘񤭞K|A)/41>bR~Qʄb>!b|Ɔ 10s61NyX 1?Xr7mVh`p.6*8󙭨n \Y&8aVa@cJ~C-<:u@LC^~Q$8d6{w@ fG #P|*홚*uj nii)Ο?r9n_syأ6R[dcOγ9;&6<؊T0@9cEDa-3ץrU9e(vY|g}NZKˍ@&ncNH}*YnezL3<.g:J|r/_׼&&}&vB_%g1c@fs ' Qg̖htx`ŋbO F,܇.pLtv8ng;[m0A^7 qZˤ޺|nǕ^{I> ^RNH&}F nOu'NMMle+ף_(6}SƬOFt 355Is>?gz^uhR&^&A|SlAobrVB61a /*kK7Er5l˺e A]?|=l~56A ੩dQ$&-^ ˛!!C[}N>cBv<荭?@%:^cb[޴ V!|oAuw-ק9܈~KڒQ|!äomooj&?an+>u &s2˄պ˒{XX&.8|ުd^r RQtԐ@7 aϴ.si5DŽF( -{# GLR* D:?$$1c=YK|k 晍mV+5!7ʄLsU&}Z kD+Q7 eEO.lѿ&0lyh)oK'Y^9xn3m5x⚜ 9vXE Yp g`]%"RqX#,{ 'ncմ +cE}&_*Z֗,̈́H9À{BI\dr7 yS&@izeXs"@T0@b깒؆ ^H^ '{7cCl-ds[L~^uz^D>:n0b"w<&PAcr[y d~;ϭ 4cwwpF&ԍ~jb|L¤" ٧Pb 2b l5g-l*z΁|:VYh%bl @pPg/Slt8f[Q<Bv-6?9VEƋM|\'}ՠ9Ϻ۠zdlrw =ubRfbBXR/kiAd `}:nϥ]?a)q~I >+)Y9cP ,&n:lƉ'z8 KA _dc"bo㔇>D#\)/!K}M걡{Ǖukyシd93.+}=9N;6yc& G>lf`b '} !^?9,lLʤo λ1)P-Big6`Nn7k,m5X677 An^{Bvd:WʡaBhf4jOy@X_*wn> `z.hI^˂yh}anx2q1Hwn_Ċ,.X_07^hjls}-Jh4bx=|BsbB% "2)U`m,(l, 7#;#Ƥ%Rf뷷{#Ynm56̘x&R{S$ƚӚ|/}1Nt.An r}<>a\mi,GxcMD[IA$}2hvQ#ۡS ڐ%Eu42f͘YOV蔽CDlȏ5r81ϋ'1VHX爭j5%9MqAo9y.PQmeoJ9}>" @{/#Ù[uboG*'u% k/{U\r92{^*V5 1j9; j=2{I_2! x!,EDa 扉:F7b| M^=; `!\@Ŋ2qrrDb62(n{I32whNGQa4P4a=wgn/`o0ElEkNdlWDӈ ;<30G m1)vj%A?:Gyli %tp>fh$ݦruzss3jZg#"M=^+J{ȆI?!&Nh4{l6Ӿ.`zV4 }ɍ[(7ƩSL5뇍/h4#}zLR&doXK@87ۉycQt×QEct͑[O 8m(5f$v{ rˡ7B[b TZ˥1\} a &>}1F?tt,;Hz?o!v/$,;<k4A 333)(π8dEZMzEJmC6ɫjI={~rR ˯AjeyaeD۶t=%\ΦuXAhdPLȢn/ܠܱgoG<ȐGG%? {W3%HB _e%/ozIG\Ov dɞNH>ĉ밡cBN4Bc'|vi kUۍVwqGkn0$dαVB;zOiI+OVZD/f1MjE7JDr$"rCxΡ X#3Tbuvmll6~~`K:mA΀!m1~$2f vBF@럁=LZ- NGc:D~m2 #@8fA._VK=E큂LS'Da7xŽZ2w(5!_=DSDg e0{s!3-ll~,,,r9KR,..zT*`0(_p,^~ ۖSo"F91uC"ɝlj5YWx ؘ YoMH1z=C#ofXCp8 ء/R@'-HC$|My@T!ql PtT*ƚvڢ pAGcrJx%wڊnlnn&=ͽv2jmIG$0\/k>cN-@_1{Q!36!KƗyAXn mq8Hd-'{峇k2?2[&P d4=~_Hk?k1C?1 G7Y&~/yqO{H2|㩙گqر^WT.wx@&\3)Bٔ< bٲX-R{zo X VPÁ^ MrhK/ Iޜ`qO".`2 P`0NY"Vq[͂%=~AHc0$ !ΉyXh4l&.3sss)w^G89 Suٌf@-!1f: <@DK{<&;'\{sf)E`0>X=/! +<^9E71VwC45+z`C-)O9a >*:RLsq. k:ͅ cx#݀G`EDll@[l„,v99[`sX{ǘo=Lop{ ~`:R豤/\Dsh+';@6z<|L)`:&S[؂| fo%m:sy`½ʼnX}tG"c[ڙ92 ɳy&r)cs1acoV%Oϳ9=:u3W|zqWzBƉjA>l`=;z^r!И|8Ddk Bl0333vp•$VQ㪫fsssi^&7vy]wZl4[֨jqGD?˿v[|K_~wuWoqt>뮻`zs=|$yk&-O-6Ȳ du fpZbg XXnefCI\DV 0sy7d6CAHZ5Db24C L@ ܑAD$#c*[ 0`^Lí x:jZ0Х<<#< 獠OԪj+WɑI/< CB5ww7⊘P}UVF*J;w.fggcaa!%B`fqGn+^y{>pDG6J❯  [c{"Bx $ bvzdB{om3㽱,ɄtQ=H<@`] EȖ=ȴudwwp}r{bό P&C>sÅ/tCu5^l,--nEJ3n7yxG<R酝< _}\ϾohOwuWjWf3""^WǍ7w}w|ӟ .'?x'#";G>}{/}sI9 ϳYZ HcSu(L>P620— !w\e6Y7ąπ\V{4h  ${ vi"ealL:p?V+bh6k+ djJ;J&1ds>r&ajXXXH=Wߏn-ʸ}N'Ey6O<z=VVV +:a"<1Ga!߁ϝlX2ܲ﹀%rdnmm$tu^Dy.1TaD\~w3k&~W~%/'}-677]zWw]8L L>-ސi D`f1&㍽Do(P=6$͘8Gc0(~C Çli3&ğW>"_c_62w=Xg}(@?<~BbcLlYP xl9lB? [./ƍhh61baa!nzQd\dE]XXHm8w\:t(*J:u*vtݘO&3P,G՘Or̙|n8{lq_VVcee%Μ9~?؈"l1sf{n?q+V<۫fBǵ)ĒnId^8􆺘SK+9:'^'x+rp8LDjqq1666 ((4,7l[#lȊukBF9u9 aW&SSS#'Z-=r9VWWqɴFD:{[;mt9$ 7)暈xc0ĭzx+f>sÇǁ|;1)?2! 2&ћwn3pp Ŗn#_-X9\`ĆJBlxll9Ysaga3v#d\Mܡ<0D1A*X9$/<:xɂ38m4’n:r08v 8_}󞟟y6ūɓ<|رHrz4E/m=I̫rlG]H=yc4yr|d70X{Q.j</ 8'< w~?XګuF/a&7}m>xjj*^^f3VWWcww7ZV߉jOAԬG^k |[ߊ(|{~Z y?".UED,--]rRFX]]CŅ Rw,..&Q&d'ܱ,N4EXf3w" >HIcĘ@٢J{hmQ` l] `+3o?'ckjs L.9A֋||N D'CB dK& [Gg gvv6^GZXXXx+_ǎϧpO?tz$DGK"JǏQE/zQ\{sO\wuqdmZ155W^yeh4XYYI9{ n{P '?L=kbRe&N27sh}fl7x0xa#'ymB ^C3A,..|!{B^c9y ='h4>/:{[1r8q%" ]._nwwpB( Dd!d$d,iXNCvómuyxcu?M`K&Q6sX }Ͻ=MLI{bF#!x'ɍ=J^9j)N@HQKKK1??|+ũSb}}=r:u*zt:n D2?dlll6vH ΡL2Ov8w\j8|G?hªjZZ>}:\ѣnϝ;>`4bp܂뮻.Yz"\3dI$/{֬N'2bN]$o爍):ob{yNz"70isE#J0L!dUR rcFdAl!ewka>˒ z3^>R)rkk+N8_Wcss39ϟsf|_X^^N/}'c}}=N:kkkhJ&6jd?x{[-//GD%t-t _˩Z(-~IU83bbs^[LJruY["$,E:/vBnK{l9r$8`GuWt\{q8sLʼn'ig$ӖrO}N)фyl1 ZhsJ{ Ogo 2=x8p {Y/DFk\3W IDAT{30quPDCIV!}p O<.\H{Z\p!N>KKKqСXYYDZcG/Qf,vs0,φ'??7pCj/?ax㍉'rk^tӧ… ݇Iy ٧އM'bxpo{Y m(KK]c`eK9}aa.//ÇhIݎ'1rZ +ǹCgff0^F#Ձeff&VEՋ^8uTTոߞzꩈh6+jT*ř3gbcc#cnn.⦛nqܹxg%{bXWIq!7t1T*Na=q($$qgfuGN!>3Q^눏ySk!9u}G\ tSO=oӵj5/(J?MϥL>-p=-Z^ I_@D3 `6@$rpɐ@a 9K`rF85yںfOZ`bi~^`0 V{tzoy % \dU k ɰNB!OOO8| ~1!? Y\k+Nlll$UV{ Zq qݍ .q[YYI-//vzLOOR)dkww7|hZqɘ8}tFՊSN<8|jq7㪫|0;+++Iynk!Fى>$;C1=rO0J?nfcRq 3c}# ynPug\<0;EىzD@N<O=TF%Y^F񩅹ǗQm$F)B3}pu=FVKs… 7 dBgnxK^W]uU8q"a^YYӧO.|%VWW/~P>ۿ^DxWb|#~o~n|??oYפ\2! A:^ 6Hq1??_ ʖ.6[ ʉB|b7&Id`+u8]`Մ8Q3X^i I~a4)g?<=cLI׵ZD088Cpmy&^GV^Wx}N5?b!R)X^^NGFD9s&SjjX__Onnn|!\.Q2tNr42K}9 O!8lnnٳg̙3׽.;cww7'h4jbss3X,--ɓqԩT*nꫯN^x&ֽ 6?!1bǽ6`hZ}.+`|~3)Ϗ2! 8@އ pi7òX 4rρ=Q019ذAqX4/?55i?q.o@(&s-uA8lE=(LƲc† /93pV kw([];Gfx<`Eg^Nj²K;ɫDFQ4z={nv]HpI)ϕ< D}J@rለxїzk?~<^җ}ݗY4jZ\p!baa!JG/,,\h6\bckk+J8{l&vرhn[nWqM7}=XFttIsȽlo4o'BrKRT #*!(#{@N(:ٳg kPVVV??o{}c4s00?XcrCBd5h5\_}ɟIhꯍY{[Y}R8p@PGFex:$ Ar#"晙84B}t,:`Mh 6BJ t:h4 =a9XQԖhLJ~$pøNٰ* QJ%%wF8p@[^^[o5:GM%/2W_R)?@DB#<1SSSqСX]]M`XjŹs\.G׋t- ^Gݎ iZj5Nϻ曣Rc=Tq_򘟟'sW\󱺺~? buu5{J8q{{;x駣^?gΜknn?ɓchnF1ڌ% Xf$=n7f(zx^Wx D cZng#cO8ك@x ~{{;N>N'>OEՊ%Q^tkQL^^#FQx)1bzz:T*Sx)iDUW]?wR&eR&dߖܢcE%TTFN ~,úll:.=c"0;;z=m~NVemlFlJ3BƋp#,~6<! &lnyp(9!333 l#Gߨ"s{Jtd~<3ǚyB!32Fxk^+++SOǣZFT#"ԩSqWF׋'|ൈX]]Mpee%?8~x8q"^Wƣ>qWǡC̙3 \8___+›ޭ=31;;}:h61gKۼN=z4nRę3gҼezqܹ#GN?^?Lʤoʄ !?lXu˱lވ"!<)M1[ i/ Ǔ;Bb ]R޳@{xc*P/dY: #=> k7)4&I|8ޮH(d1~=lFR)<<2oxbjj*>?x/~^{m,,,~X[[GZv.\8tP:浯}mO?NJc;/N8~?GZPZf3cCڊ#G`0Hσ&E|j(Jqѣ裏l\wu)|g}}=ɋn8qD?>('Dݎh}~z="":NF>oxkv+++qWc=\sMzԎ~b_[[͸pB:t(Ë  nj0V&u{q뭷[F|%DV+!Q=:wy^:Aɯ7'psi CX7O!GTy A:NZX^^ztnοt:—Xw3NА3n[A;N 끼~?{CFQ?~qݴn#m6EE? ME@?%AEPHP A5:vT[QlYbyWڥvK.93@^J-,vy\}_\ mKR~FGGC@.foccc*J!X+xR$l]vp>Oi}}]!mB O%zK>cNpI뒽;]v~z`a,c!)JDϐ`w}jnnNa8b#nF#;ޟ셾J FpAC6=אz4o?|>@4]JL&QmllhzzZw^LyIT(~(H{ CZ8L9 *csP`Itoec N|x@)_H{xz 6bsB$~2^x@DBz=%2Ƞ,'qUUJ%iԮz=mmm0899x<l6{ Er0J0Gi\D-?zWty=t钾o{pG֗+Ld2xsRhH'%oto0d#6{&RTkBАΜ9˗/ZR,n!9'{1<S'Bxx*,<{syΚѐ1H} N677س +*Nx2ꍍ˺yZ߿i:uJ׽{B Oz8А99!^3y6+ခpD-yȤ?9Q ,(V6TXTZs^^,nM:dn'TZmllpa`R:u-1jJI(Vc@h %@l NC mU՞K7=,pwwWB[@`NRouVbƪ^B@\#LJzǵ]Ϊ\.Xɸ688TJ=;T*gff4<<bǺ^xA333שStppW^yE[[[Ns{[W~;I_٨Inlat2^!Άqt҉L7Z]]5<<)؏^xO}!;<<+%֞CiK"N>W IDATVd{?}3733z3u/߽{7e2hssSF#}6 axx85_/}? VރA, Y>^{b͆83>o!aT:Hf*!:&&&tTTT. {=,C\n[ #)d`p #vc&ݻb[ǝW2F.=hv> ^7H\x ܨ^.= /iXrppd0b1C%DBJ%*5??'xBTJ :K N8!'p Fֳ{!p6u%I&''u)ia3?s:c2W뒩h&Y|d7=Sw3VxJwxn?;>w]~][[[*=!\9ړH$O}JJyt5} _tE; 0f8::T n"o"Mp9}U511UmnnrU!u2LH/7ɻ^>3?oZC!fme*Nx(ie|bͼp.ýgG||G.0sW;{YJX%ƆF^\Th4Bd2&V}+… :}Cg72@$:v{!mbg7'xwXO{ Qa' ^jҗR$vIZ`h.E;]Zq|Cb'>g.T$\.|>0$KQQ:˅;wZuMjrrRt:x0ͅ%I|^A.}HWJ;v[+++QZ {QR,aO4gt{Qchh(xҤc/Ȉzb[x& eeExD"r9u]ʕ+)ݹsG=}IXL_WC,]VƆX~A>yHKTTw 83<(K'JO]=N[H3.ޏ^'l3JA<"n 29RPm}fggl6 /nCF+A~OK 0'D~ ,<+v7o|BjDKI=2*;>0 ElRDq@}x0n,`RB4662%IUU @pyC>j4;;g}Vvڛ q R,'|>rh\cMXs'oW9ޱ;c>F{>w$F7g25 {P{N8ƣ=RVd2MxNLLL.,,hcc#xR25==T*o!۵kO͛mܢ1L*//pPzS1v;X5]Ra ^(avtm.}@BA P3$Dtx[y/Q=4ap R>+WljwwWVKSSS!rL1.NGpb#Zyimdd$[FT(T.HuYF. cv3$<7\.\@S݀. k))b&GHF%}>ݣhlr0yus\.R>W"PX ]#NKQTƆvvvty=j4w^!YoN;؁wԳT !ϼ ?}XDbč4CNx7C>gϪhiiIӺvZX8˩hd*Jt:!^a ݹsGCCCzAHh4N$p\Ȟ^w= ^*}hkuR_nE, 1`Oepe|2 $׵<o A]p":<< Ȏz9 (-p@HgҞB~ZFCׯ_PKёffffRprbc0WzH[[;œl鴒ɤ&&&z^a N,GFg8CS1.d^y4c(5s JhDI2c(y[{(zA#xy|}>QI= | رT']X裏k_Z/r)=C!58G K5}qcPP18i˄DDŽ>G,hpDz/@]6 2<ЩSjtyutt .1VJ&:{ժRnݺpKV677C=J"IѩS![ʊOVp {I\V7E@_ގ' q f˥8Vܴ2V-q8={,uk tS}'9|ĻK?$۰]#ϐ3qiU}<CCC!Љ``@k55pZ v[kkkJ&JRa=e1#XRyt vuvppf'JEgϞZybbBr9;/^`J 1K|߽i\$}C1p / q 'ѵ^o'_8uugscFX\PI}B>CH*^TPP&Qޞժ422FT(ZDhO ^ammmi=Szz0O:zԹ~b]c ub]̽D ȭh3K=! qżcC3!yx/ZFGGUTJ10cz>NLS.ٳg599ݻwOn7qmooҥKb|}ڵk!ݻ* * 133x<%MNNu022 boww7xE J c||\B M<WT $5Q,}R/oG`Ap׿K@ꇸqsߤ90|G˶ v~^V{tn$8m4A&p)߅aNR羏Z,x4e2moo+hzzZú~cQДhddD*!&144_H'M0,q!h`d2J$ }M;;; ޽{=cs+-I䠌:)I'ϻ<ɽ=Ǐ"Yc@*9L2t~e9ϝ N$抹ѽ)|`xx8\FL^dį*k||<رHCCCa0?A2ŢTוH$6o}+zȺJ\z%{X.Od{6ݻo\2fChQ\ǚV/NdoY_CkL&U| 1882!u ()LZ[[Ӈ>!% =!9`llLBA7ns.NkuuUx\G}ƍ!+㾱aMLLr 재\.39677($X/' ibA,MQL.[ <>"u@]M=pyq! r)@OL[\'=6zH{xF ϰ`3FFFT,ijffFbjZ@(* qL&Ð>d2.L,_kM\Nu]`}Ro&!_& Rx80䉠QI=g4y1T9*ժJRЋenGU.{=ȬU{d|I'@ct댺zUIa.|+!&P<88|>G|c! itϜ9 j59sFlVZ^^ID {慰@ęa2d0OmtyzzZΝ A+++z饗422{,x"/AWTW=* Z[[S>WTȈfffTt=-..TՂ1`hhHrYJ%=偩TJbM6![`= v>y <<[ ,N]N K=8L;Pt "cSx6c8;n\xkx s|F%;ӭ F*HhjjJ<^yp}@?aGf(d27!Hh9l!_Z-XMoVRknnNHaflW1` P\P.'VLosٍ{@`GGG%gc *cNfgPgq/.0ߧ?:sc677U.y.6Vi?yV꣣#{1͛zAX>| 3uR' <}9j{"Rx7P0}K F2 q={831+g`ppPŢn޼`|ӧ /^kqqQ~ĉ0{GK|O@a3[9X.bwKG@3>@PDГsCsdnـm׼D `w!' r5'/p=ӑ9t\B ˗o~T*VTv~Xs\qd;a5v \Or͞$XaDHyb~:PóR#حޗ{H? CjU,0=\1dNy@Rq ?bDBh| OE$ 1BNńXj;88l6.|V:9x0YhLjh|)x4M-낹+|ylK.+޽vFK ayLq5%$8g]rnmfZXXٳgUT(ׯkgg'nT*GFF׵y]tI[[[jArf^J\vۨT*)Joݺ9$Iu\6JC*\C6-@F ;D"scL;NZX mHJ$ZXXЙ3gt ---rdL蝱) ` u/r1R9Ǝ6{JF1HnpK2N^ w`o'%rʊ> ~b_pGFчǫW=4Q)y^b5MqPvkAQ=c'!3;icNIZk:7d9?NPP,&`>@ǜ*d2a]e2jjjJnw󝞸[g[Ϙ{k_<~[{_/cL= ǟ9}BAW\ѥK@sy{GT͛7ug?5j@4˩^ T^1r{R83~n.$&.ᡡߑȃS(6vu/v!-nCZRX;@KbDr=uEwn`9v'` :Эn.Y/=+x3\@G톻> `etKԣoooOTJFCRIL&hL.aXI)wkooOsssrZZZ I$!4x1d8h[/N"cLX ϟsyB9p(ʼn3\$Ɨu 闷 C\88IrbAbA[4e)pԫa<Ȃh= wE{x[:=F&ڟԛ !H$_͛7%J< )܊B7[]EvC&,!K!e-RzG_.wp`'>2+jskzt^:{g@kyI9 K3yUc LFdR<_+!w]ݼy3ommܚmQ?>n! yooO֘{r}.GםGV1 !sL^BSmX%'He^f___ӧU411oH$TA;8.S2T6 7 fGG'!-R8UT<?,zK|?O@288*nNnscۭoAWZwu"J]GjQC+~~(nD-XB٬fggumꫪ!KhpB, css3\J788T*'|<)pvz`Xݓpwފ(rRI}0W;!^.p%]utシ }λoW\.g}VN^}Umll?ee2u:Q. $6LLLZXX͛7jڳ}^Qw~đy1.~p# u` ܙGdt>z߻͉\⹌sppPbQJ%ԙ5F ܨ03N'}H䁱XL< ɪVrvXT~xxT*pH絺|T 2"V5I?$Nk{{[;;;{E@_ގ' iq ;j ӢDRynF-Nn<[)Jbx[7R8)zz $^E#eppM2ڵk=,KW Y !7X7] N"  |vBAv;0>\If8tB\vc֒]g%[lɐ{_\'*q .Ab=u}_ʊ]% }Ԕ>χq'@ (*J9 @t4==iZ-ݸq#e\0ώ{ uy7Ih49$N% JmNۍ hۢs>{},y5:w/ }D_8wJ_hay{nwch;ܹZohdd$\*>>pQ_, j6/^Թs紶WX,*u ˒؏xE / c@1sSDɯ3Cawu}- &u9%S_Kgq{<~ѣ>T*Ja<mffFAZl65::&^Rx7?uu޽{s411MU*iJ%ۥRpUD:TMrpcA7VN :|߁[y6R x٥a.B^A[).~q0Vf/\R)٨w.^tx,*}Rbt r˿U,h4g0ܿ?߽4HS uώ-KҘ{nz]|,e(ԑ:{ sԁ=;ÃHXs|-95\.e{#F\?0?!__Gbx'uݼyS7nNtf=? }ߴ%Jf~+ 9>ea `K:ad2%n=\H ~B@_ |:aEbLFtϻ9AV#Ƅt:o#jg<x}(-wpH'^h&Ƌ9eUҎ[R\u{q6/=A^zTT444bbFGGH$/kiiC bg… jZzWx?w<:A滾2|{9an:91uOW_}U###NSRQV Am...jljgg'ė+LjrrRہİ!uFFF4;;9yH @[h|D=!nv[\wMD?J\J'yK_:_x<ncatAS:!?.ps0LQEPw9懇~7ѽ{B>Β<U!nH=wb-.͠o]k,bM,9q4H -4{Cs$y6"랓(T2uK82= I)4}?X3?y'e s{p?س= % H|MF Q b* ąqaeE̲hh``@N"\.PVXs\hlw^ȢE'kjjJnW|>^S2ԩSž&˩ZΝ;x';[s_~ХO@ق8j. ҭu I`0E}ZnWřfgg,}/2?/OO׹s4<u%% ;wN_p}R gr*K+0 [G9`j5LEu# %48zv^LR"mErZ.L&577Zo}[f`JǚG[@]:V6 t_RK_ Bf3%DR:I$ʎ˜od2laf|i9/ׄT$N,_|ׁ< qGn M>>~v'Q.,7 wпLr3oАD[RHk%\:aFi +͆U qFH pNu9U*kiiI+++*Z^^WUχHze1ăȝBt: ɩT*amWB\.\.۷o֭[!cGRE}yظvsw{ k9*211L&˗/ʕ+j:{lbI 0NBS,5==ׯcCv|>@ɤt޽l*ƄX@bggojllLúp>|6)}666?Wl6?znܸ}c:::~o#HB Owїe/>~闾:qPPyh4۟!ͥG5\|+ p)`M;qva988|>!NW8}S,x6fffd$GP8m3HP(>tHL U,m ~4>>nCVb㋺hc1~Lj'4aK*?g xvRT*ƔK'Q>hH x\7n=ĊqDL&{8˘P/Yp i$u睐-" miZ!xrrRӚՕ+W455>=t:GyD.\^]moo[>+AUQo'DBT*!}=2움W6gοrK-Hh}}=ܩBm @]788$ggg533VY}ӟֿwNЇe]z5Nܿ_###[o\.T*)χ7s{y2Ӂ]|Y * ֣>w_ixŗx?z$t?j?}cSP/E=~=(w{ĺ}^455V .ҥK?IW_t8޽/o$X,jaaA Mگ]{-+詧 8]H_ФvݓeLlX_saрL ӕ7]'AjMD܌G9x!X.S[s3kڋ*16cx 4s/bNBBx<+ZrvPRXi-,,ŋbzG /huuUDBsss*?Ӛ]w }A<>Db,87x$ߍA1 jjj*x{R2Yܨ|f {p'A琞__?P_oUZU2T\H0P(ꩧRzx={VT*ѽ=i}>}_T ժZ2]aVKOܸB; NG[[[a~_c}{400-9dt ݼySzǔ577I}'> u]kjjJ|^dR:88ؘ.\FWT :sX,^/C!5V?=/:ئ_;O@ޥRTo[O<$׏؏W\ѭ[ڵs/333k=ߏCnppPz]Z-z*r28] 'K:}8oS!xf!5f38-|^x;;V7oxh1Lї)oE߁ŞHg<>61I!3ƻ! sY@ 6D~v9z2/y.I8(b3;x\`2/c84:: 57otmMMM$.A27J >兞 9 Аv{ϙs{fHhvnW-MNN3yMOOSy(\xi=Sy]x1d@8ƆCۿt:yieeE/n޼;whiiIˁCj?_}{_cgBw{`K~l6'cccoؘݮJU,~;Z eϳDK x/wrJ=xtg.ȍ$K (g?d?$[i mt`h4B YہR k3a =,&8y0\093W .qIqWT(~9>SpJ%Jп&ƊvC}g.9)wn؃7ljg {[&rF-* Hx-?^עfȚ6::$ylVJEׯ_7M]tI.]Z' y ,IٟY}_?'K5::pR^?/*Nہ+ɼ[X }FGGtꫯ.(kkkj4y}>_]vM?;mS](뿮??ߕ:D3i!p(!dD,DO.ޤ!⣣:E|̤j>x88<$`ePCFx }yKplNLL39EԻnyFOG,V Yl6U(zcEI[}!5 wz'9&ANwƞx3 [!u>E5MŐ=תaOv3[@&&''U.kGQҥKB5`\V-mll7>>J%菂o}K/RHb-q//K>*3O4~hw|oos~14oSǃ7wwwOz 򘏨DJ:;H:e?,9JB~q+t AZ:te8*m=](y`ft:Tqe5 Ep@ġ@ 9#/ŭN $qKd\D.ArYP`B:{5|mPg}x3k\JRq" |`vD4ЃԝlڧK`vR+s] \txxJ!Fd2kii)\ɥtt:Xv8֤V);MOOEmllƍ=@߽nN>ٷ?kOrI-2Ooe_cŜU2TRt̏wDI{W\BĊ;aܻbzc)SR& T*g‚nܸ/}KZ[[ JӧO+LҥKٟgGGG!cssSu떖?ӊzG/|A~իaUU u:)hffFJE b7HλY|M-O?0ݻw}M׭[_CINǀ峟l++=;ɤ{W^ŋ$-e*|[: xP5]2 -`y!sj@w|Ի;h,ψJ`f>~AMᗽ83悅PR)x%vh՟ Z z8oZ|S?:u[vXCI灾r (j;Ng9L?`l>Nx>uK6~ԋfgM.'o卤(t`MNb~{D3O>cgQB<˽p3gC_=%]<PPy^zUoF|`CCC+՛UT4??eaܣ߄,!Kc8ȁ*Ax{'dǜ'ȍ{SRƆy;_o'W>#stWΩV|\_]=}Y_GGj6jZZYYQVS\ܜo}{|կvww577gyFַkmmM'7ojnnNgq[nikkK7o^/wLI7~>OIOO$I5006i@7я~T}$yH{1/?j6?) B 2cccG>_~Y_ӧ>9(6 "7;$W' <%!HQat:E-ü+o['D|'GQQ%]~Y\1^%7Qjp{1%b^ "h8pOxs ?H(>܇XܛjuUܛ`OB΃C tkr\n^cs ;nx.eľđ@0n9щАjZs+*d B3HR(s Ce}1I{]/k)@crK=C?#ݻ!djܹs?o~هXF+wg FjFv_=$E-//v};QX=ܣ\.^{Mv[LFJ%xfffJӚ#<|>U-,,hhhHjZ^^uĉ:qnܸW_}U?~~AvC>9}Hb~{> gyFXL>}ٞ98uꔾo곟>O+H~~/~~KlV_{{?g,T?ݢ 0P&uԁ[H`3 BiI?Q.B4Tr0 ˳\w71?200sTn޼$i!'`J;|s;c`:P9 qp#Ws;Ip[sF&@2BGĢ_>>^w/8n'N ~Tɷ@^rC4iΨ' Zh I:CP(gj Z!ܹsT* I^9VJ՝d<}2sUS|X;1D }h͛z#fϺ%3EsO>] P.]vwwCQzfl6@*JA0HhmmMVWWCN_ښjfffkffFz/jaaA$ʯx ٟ{Μ9)}ֿZn;"oվ/ _Z^^$wO@{~Ϟ={6܌oo2i&s$×L[ynD>w-֐I=9%Xۼ@%wyV,fOƂk{4iKbQ;<d``@j[_AoXuoD"%\$r! CnO`xT}V:tr땁7|A-N2nv49!u?ϥOiB`.CP7bq% *SH'ԣG57J 瞽()t#C!̭kFGGCeF>ПQG6FPD{O!>4< tIRD /ilssSsss:t2L(l(d9ry~d@t 1[[[fj4d2A6\܈ljݧH$T.500J%^ִRI}ٳgu:tHO{OhqqQVKo֑#GwuE =:rVWW500Q$I~w/|O ۻ ]<Pa ^CܺG5-Ǟ Nb BM*vK-Xw-P/U hx^rK$U*+mކnU*>3,v[|^zGx$ܮ w/[֙gq" síyN%$ҍ$)8I'ZⴽQݺuKSSSښFFF422x<)j5D{922 =o pȼ\t{|_ICWJPJR:|^3xJt <2bCCC!_y) ! PB?q }k_ӹs¾JBr! *I O~W֯ʯhiiI?~\333۷uM]~]z]>_) H' wil{OD~ Q\ -~prp0K )q8z]]8LqoPpPN,qzzN8#F-iM XtBaX iVtDog,Ro'N bΎvvvByH-xX,B08b.ɸd=pA=Av!Iˁ*!e^ mԒU0a.WP"wIl07 s4cOkCsY`nż9o'wP:rK=D+r#r}/1Vh44::Vq/]dMzX %!VL&H!;'ٝtfqp 9_iB nd!ymllx33Pv=9t3I*U,S\di{{[e]vM:p@ɓZ__MePzȑ#z٬~WUjU_5<<~ZwvÇϝ;zGVW6ܜ|M-..I+U鷷 ]ܼr3@.[;;;ePj99 )qB @un{@~8H'1. pqhts;pq`hEjЊbfFh <N\< ^ F+PͷoNT3nFOZ.x<};Nȇ|rz|.gϜts`5>bvej\d2l}F2:F=N[v;ȕ9=Y@hNY۬yN8FNٹu!pO16Â`v]e2w>ڵkZXX1oo? ]8OA '@Jگ䇰[9L,Jr,#>eHj4@!C`>N8YC2j^qr,d2-ڀU߹MjZO4tp6'рJrq{ބ=_{AkD_*SER<O q7}.s𤊑{J; <!ONFc=靝MMMpJFj)mllT*x70@ Cưxwww-;;;=g,r"JRUWk6aͣU:uJO>X7o)BΜ tZlVj5S1|&Q\ݮn߾@>7/--ijjJCCC+B777u%I}{.]%Z@*GGG裏jyyYVK/RE~:OFCki}}='NٳgNMO@a7IF-< X齲[f9J<ӽf';Ѱ0ޅ?xq ( 'x:P(3<LGx&xRhww7xA 1TJccc!n\.Kڳ p*0$'\"߫z$ $LR\!,/7CwA2RT%:B#wscA%ݕm^9˫1AQ;Id]֢y<,. DÀ?knnNG+aMLLh{{[RIӪT*~| 9|PhOKup++,{_[KR%!QiԵ{Q$t#3߻"j||\A\ܙP/4`&)>xRE?أTc#w*W$@* r#$Vp onn p,!(wГL@3N>~H;Añb $d x-@W"T+9r^%t`V@=Ƚ3& {. *D~ƚ♬";vC=DI7nd>x46@Ŀ3ȰNbժN>d4::HT CiﮌCiyy9Xc&| ﰃ~7`8igcS< r`P6 dޅ7nS̽nO9#dRZ-{[`܍x|Fki?C]>L&:a>vo >Bak${a4<5Q CCC#yS`zY_/s{{ ރ}-bxGt @zƴzkkKbJK/'Oy#oGMa@A'3<<ܓC{ Gk hx|srH(kV+=ܣY@Y{x[F#t%* H2w/%{M"WBw\.A!V y"\PX(t!nݺʖ:uJΝw]hrrR>}Qcg?YСC:xkrrRuQ>}ZӚT"еkzΰ' v>K'Vz KS2ٹ=+"X$ -ioO `e!2`>FLnGHzk2- i?l`ϱ!{T*=d@° />j4*I~4 | o'B !:~0ap{dJ#[|@{uPmb񐡳Qqf;0w=s500l6BMD"'^<]9~իW2;88qMNNWw{bIh@n֖{*u:-//Y/jۚ 塡!]rEsssj k~:J3l\rEbQ\NgΜ… }% MNN__w]~뷷OcxK2ŁwnŗqxI9HTlx"anED 1FvE[49q˦y) !ëR)L&588l6a%IR) D"+Jwċl dx[3=.>]Qrz'< x艴_w jbbBӚ̌RTM7aCֵ5oa.xg1x!d:<IdwRւA_{Y'H^'[NaHv[Z[[ʊ;֖Ţ{.֭[*:rH'J_׽nk g.*O2RQoj+|BvBXB Wνd1`p/+9H~ @׃X,{L*}ݧ#GP[rгT*{GteJ$* :rHȓzz״SN}Ţ ~~[r7'zCi{8!Hnw sVn? h' \@s/u~9s`T{}v;v/}766B>N\pcó5>>L&L&1MLLhyyY~H'~UtDuɥ?y1xL32uqcz;vLGѵkz<|ndd'(BFn[Hb{, "gU^ٳgOW_ {r-ѣz֣>rx ͩ*jZ\\ܜ>OirrRsssT*W= ni}r6׎>Xy}!p P`r9t=(~2V2|<ghIoI@{~yܾ}[Ŵz=ܿQ,{Lr;Ȁ0V',7ĭf/|FCg< :@O C5"T KQvC$HmC!H^T*Q% =cvZYYы/h=Agi'dD3kp {5C.]zHsssr!OF YT*zާzHA.`ȟPȽGN|L3<<rN~ÜCZ3gΨʕ+=e C>L+̻l6Cnƅ/ v[[fT* .= ggg5==.,,ʕ+_%% =sra=!OԩS|z(my:q?.m89 q[  =">"䇃`wCDwNVIvR%v [=Aހ]MNN_P/3âdrT|' vqqQdRof :݈$7}lz=袏=1c%P%>0|^OK/}bnۓ(>*< HN4bNj*JU< Iz'$T*ibb"sT,{cq;m 9=P_&|<Nuuk``'7C}O D)J&_4\I+K؍?y^zCv.Q*EsjuuUXLbQ|(3u 0by+ƽe|@X ~I ;\.l6 ~E'紹zggGJE333T**9CxPF+Uh4A^__dh||\_~YRI|^nW/511C=H3\xQg>rM]pAsss-BAw= nj}r6:a*fh<vppKy S>.~Q2`=Rp ?D/zzwΜ9X,??a 9AdRj5T' COdXdUBȈ{XAps9G"HVt@Tukgg'~p :z AɧqpŔtaMNNJfffnWSSS.BZ IDATWN֧߯X< :vA#`Z{4p*뵏hiiIV2 rd2Clu\^^{^---:cxq!:q04;V=_w<5W0}i4˗5;;\.jŸvw8(_n qrT*zI<NJ2T&Q"Ɔnݺ`^{{Z ^bJ펌hssS>l؋oܸ\.\sss?kiiI//~HիWcno~{;Zܥ+uk")^V99`yN,{c[y~PD+V$zN8|pS]P<`w' |9TU>W^W2Tv.\ Ip30WQKA!ֽ,BZ0Fw*T=ђ舏ji~~^Vzh<ߞ|;έAg|<&R`rrRlVgΜQ"ЛoWȑ#d2ZYYŋ#S {8ym'>@vpoGT'Ks$6E=D>uݺu+\7;;17Qq90; cs|'Qo:{{=EO9Nw<9pf/x  }-r&LOOO^.Pȱ{D;;;Nǃ!P)ORvr!J%%I9r$g_TUЉ'v?J&W_|QZ-\`XVu…;?Ojh}r6Pܺ{'9Ppp+?j;1ae-~s03248pp 9pt+ghL{E:}[ Áǥ'`w$ir:m!En=w!8X=~X91zG1t>z1D3'x<*Pq }EI|Ay8p@D"x }v[W\{Bopw}*9Bܺ-w?'v%x.3;ڵk:}~-..*kddDkkkJ$J w~b1UUU*MLL(رc&eop^@ + wCB[[vvvB6 5??*r qRZ6U& DU*!JZ*+J*Jx[[[YP@zJFFFQTh4d+(ϫhٳU}<2ȑ'ǁW'1eYV.^>orHt{%v=-9qO0$h!%@T###=`ʘ4nwg',8pqr_p@XhH=z+\ߐV'N;{sOw!_`褃糞^@ @ﺥ?j}ͽ###:~UTT599B :p@(}ʕrGV~[oy^%fSlO>2{B'O ZEe7?f3\_(ZP6Gz/f7t:{'|'&&絼@\}C֖488gyFFCwt Sɡ!|+7S-J~q>K':Xf?t8`y)C._OQ|sxci ,8 C祂ȸA{Epf 롇RXȈd\; d #Opcw-]qB0V`>$hUʏW ;츷="XrIy' c`=8C?Nkq<fvZ[[ R!54t1:srϿ)lE( 넮26{>^uU./qSbeeEf3xZj#сBΫz ȁ=Ud2- %;^:t 'u!Cx =sayr= Mx<z 4 rcGFF4991NkjjJ7n|ggGdRd2U‚~?55IJ6_vMKKKj4&&&B ^,ӡCꫯߢO@燔j{%vZ~?Ap pk4/5xCt$xBps{;nx>@҃e:8lO\(;22zfNnhAf]~=TboݕfYq/K>7;,H.3vxaqo3@+'K wcXkJRd2:w[e2!3QVU(l6XX\\5==F@;Y+x( |}ԇ?a ׿E:xy7R$I?!bx~~Md_uUUh}oE[Z 0H9ȆBWi$*rLȃF|0e݌jhhHcccCnJ&aJR!r``@tZjU[[[O?Cݾ}; Nl jB(aƴBĕ+W466<0f3HRT*!dbb"jZrѣGq^ZZґ#G4==pQLFBAt :u*$K?XNGC?.j}RooG鷟XHx'ԍV,ryG4~[R <\9|8('l=b{hH:'!ϹwhhHL&?Iˊ.믿]vMs l%C` I-J;XSbL1y9RHЫX,Wib[g9 }\=z{ޣ|3r=ijM",@2\.JU288nW todc̐#ϟh@R'й\. oi-2 &BNȸhJaMm`!wa%H'tL&sagR666T( ͛7ÚC766$)x766tɠs`anwd2@_ys ɣn7nX,yx.Dw>쁐0?:!|52.r@wX'NPٜl6o"$"rZ__!/xNd25͐AH+3ghss37?cB(R^‚zAhCeX ux i]vMl6jRj_?C5n ԇ>!mnn֭[*Jj6Z]]U~GC='N77uAkim}ooG(F_\ -um\<=$|{[\UFTp.)\“_G= @i?2jCw}7z8ypA DOU`+$ A{WT2XL@%sҖNU.CUB42.twY?ɄY3hr]?ؘ|&{̓0t"J4\|^[2aX.֎>4y6k"STzcbmaz;rY###t:}v ###Z[[ {Ji-,,C&XB-e̞~1^` Bbz}e=q .E!A&  kRh||\gϞUX 5će||\dRZ__W6 1==( bz:55r6:bekjjJ!܎ܑP T*)hii)T?~\ǎ۷uy]pA7oʊ~~NZYY;~{7~]<ɆoCP Xf `xL&B2:xlcaY^=kxx8^x[9X!UBȉ{Y0H S{hcy)cI™Jti-..j{{[ޓ|dPOR%,>z<0'& @HD bYH8Qp.@&!zBjX,o$(S^˲HȈ4;;[<7?j,z=~4s||͂ؑސPm9+!蕗e.3 ^5 <nes(B=d * :||HfjᝩTJê!d)2N^C>{'{B=_ Ya/< yu쏄DX[xbajh֋/rӧO=BdRL Ɔ&&&ziu:{*f!_N A?b?? /7xC|^XLѧ>)}O<,--;S5Oۏ= wi-ib|F, V_B2}-p)'|~:=M:wM觴_&I@䰬*JpQu]---1'5a^׫06Ct+qiyXcyu0.)?na' $1KBck9v!~@>w$[TTV%y#x'`zd2z-p2z bXaw0ws^ynF d\ܶQ=b 4~n566K.9&I57qƴ\. }p`}`.Y367aGvK{C?GB?D/߻a"wQcVzN:T. 2tR\*4>>Z-y-,,1d^6cyfgg1iZ-MMMKrZ:7nlرcRP쬒ɤ6664;;7>I{1]pA_`$~ ]`5L@C1lQr(rϿJ؃!  pOL:}~[,JAe#2(k{`B<K0`qĂr|Nyyqf8qe+8s!mQkLWJP(2S.rwT󊞶mc=BVTc1 ~؟%:244b!Q)0χ{m4=aY8x&,gy#k N'{$=G% d aܹs!gxTXؘfggy7Hc仁2~F&sz`xۘ;B پ{INmoo\%I@8VVPАժnܸ{L400Qmll422@wvvԳ.j2rnY9F#xE!\șd ezB.W>2/]>O֙3g(P(ɓ'U.umV= v>K PyB8؜w" I~ ;,Q/"s/?6;\;w`aȡDeqR)]zU׮] 㧆R "ZQ0yȁR8`|xt'c "u-.l,/?;;Ue2pw$+CE?`ܚcW>/CqI=!5fSb1&QvמW>sN79^C}|: oH4}K =Kg;kC<izzZNG?O4X__[  IDATۏ.mXSxؒC= ݲߕz;wKBH=Qyqy$|g~ Ͽ qsyPMNNT*izz:ҏ<Ɓ^)>0+z_<ljښB)%d`v`=v'=tbrCnpXl,p\pg Fr0hq`wB.}<2dIQli'TvL>ct/-IA^1 /<33n aVkl\UX+d -}vB'4|t{\zN+L&=8bQRINT!A.|Q|=>A/|ovZ]]n߾l6d2ܧœZ-}իW>:<  )`2 y[xa'@߁C<&ruo9xo'o:xǵIOMڷF!1SsE;$*W.(yh8v+:v.ΎtMȃ* !g()byH~ccǃxƐ#` o $O}g>W|8a`N!}%U.x)!)`/x"ys!_(!H EUjfCՖ7xRO8D.Y;d-K 2DER\sgg'Tð{gggPƍr:q^y啞8)f3^<"7![B}³!d#4T*\G 0Bewww-7s\oo?ZWKiP|ϱŋT*_P*͆ժ*$)ϫV\. udscʏOۏ= wi`r\89!Ŀs p7_5 +_hE-H }YP @n]wag?`+k \rEA6~g @R1rK{[spạEs^9~nmm`ct?\icc-=iOA-]ԣC\9ˉokk+ \gq")(Y3Ro_c袓 jZ+[9bndnK$}jRgWpGRR;X:DnǏ+L8΋P=C${H۔Jff/X,Ad ĉJR:Ϸik}ooG[>$$Z9]ZIǫE,x.AֹwDڿ`GQ0UO҇'V+T4JӪV*=I;l[;Vc9)  95.#+C?@E[Kh [RQ6ƴr GFFJѭuqK)>X !HR*I1l  8Enwϓdν +#{5z=!g͉-zkҌ=N@迯A:s=VȏrObOcccj4͛7H5nZXX)w܍+ sfI0û{'+'Jpa$ iGbF\.CCCZ[[S&ĄFH`4::Yݿ aRfWjww7!!]xԝHĥU{ww@X~i=!=| `t@>|>;ފ~{7~]B8=Y=!+Qk[ޟCxC*Z 6ju7HL/X=n-TI,pJ%ĉ{Øyn4Ɓ x 8g4n:EDI{Fr\ 3QʽUFCor6 T-rI?P> G;PT3uTVBE%g ԁqiA::Z"!3f{ 1z~S9a'cs%0%#o|dp4s} =eQtO77nYkkkz7C?)E^dDZƌ zB U5teclmxB(H$TVCr!RIBA{o&Ii}~ 666B~{x[n߾1r}{fcڋEQ$-S)i%nknM))":Jlimh TFIhYl,WII.ry-%wwΝcf{FnkY9@p~sߟyB^RrpfauasooU*$I0FFF%VKrYbQϟWT s?凑{,O?tw>;Cr_>}u/}I{Аjo;@l -,֗6nRd2A;:S788X΃% B8AObPt; Z%!'mp1ד6},S"P>W_X,#Iׅ I =}Bq9K|yă==AAlVWaMjj4WXT& 22X,*NmH$T*4<<#H-r@G@&D_ 43,!g@ iffFZXXhj1-307{AQe yk*B67;㢯o~J͊{q@&UeMOOk߾}ti]{z{ޣhǎcZ^^b 8PocccccHfL&~,--馛nRRE={Vj5x`blYv'$"f7{ގ+o]ߨh賟n=sA~,I~Vs.=? .?{^yK\ٽ nt8}j:0uxOƲ XX`]]tP\31p< M]y@NEZMVK\s&''v@' Pv[[6sA}Z[[ 5\R<҆widd$H4 ]w}YI  Mꪫ466W_}5ĀlڴI.7q0#~x>HmH;sۭ}3_ҹ:{ץBn=gnP u@OR'$pM3/s >c)a_k/>{Z\\쬶m"7dsx 8oדO>5?^G b}Gv#D6TV k*maÇ{:88dgńbg3j2 ٬VVVJh4+ZXXĄ~W~E[nwE"=׾`\r@&bo߮z'O6 !imZ^ɓ'u!k aoDp RtjY" k޽[|;gx7jݺ{/|AT(}Nw}>InVӟ~w~G1=U٥G@@r/2HuBzT*x5{ unApYGZzC Գa"A.B@Zn%sMyd\N/^ # t~~^\NH$qh4fU(BC\8`a` CfnHrfgg˜D" m3і00ue 3qR gCrTw_vBw(m^66U5f@'^?ӫҏcecԅPT422x<ӧOkll,3gX^ZZɓ'u׫\.aiB5duqqQ}}}I~7&@zߘW催yt. e@p~d1AH^WP2 nǓꫯj֭7?~\<;˒ƫjppPvRR zbbBd2*:sւjxx8=ʊŢ>яj``@wUV}v?ZoY7 =C! X,Ӯ]/*/ՕX=^ͦx}8qz{߫끀ڝŸcGQ /8ؘHI&0.]*:A1x^R%"H(Jd -ԇǚ=<>߯O' IAr5gu8`ឞvD"ёH 2Y;wN/_Ν;C D"j|+ڽ{BW a|ؐD"zjadq#&(x2+5pV~xqq1x yB~7ĥtk6ӝwީr|P0.#$WOFRN>X,zJfI-//kbb"-ca k݊* ZYYmݦr[q]>1-..꥗^ \p!if7C[[_e%I|g>JN<%]裏ǎ {ٴiFGG/+?# WhAZt  Alb Q@bwljDԄuKl?:td]z#$sk* ; y !W4Յ eT(Ev2p%P$\ǁt˲N`z%bnC9Z[[⢖dPCwnvq8qBz׻BV"\.ɓ ӧOkyyYh+xGAFJqO\NSSS 1~|iiI Tإ7@|fYEQe뚟a`uu5؇|g.kbm-..2c\\wb}ւ%Խ\Cx6Va߯M6)ͪ\.krrR*kN,SXΝ;f;S{=9Fu~,c^'ԙz!U*08itt4ܓ#JR0 -//kddDT*ʊݫBZB\.m۶)ܹs:rGv=ںu>듟~wW'Nг>X,Y[[S:V6UT )|O}Jvto鮻 |>yLLLP(݋kF+W6-I6<6J~,^0fZp?q턆x,{n\2q%ⴍgbs^WV… !PVqBoʵZ#>{I ŀ+'r?+TV;~ @!#@J|-^iaa!ribU@.&''599J۶mӱctEm޼Y+++:|ПsY,?l62j H6 bڱc"N8Ɵ"^ "<24`ӦfIJRaL2^ '7 f-3G1üf.1?X/r'R"H"aLAT {Zy+JrjaLiRD~]JRڲe^~ux1R-xnCv;ː>Ǚ<;} !c \g ;`eYj`A'nxeeEGѶm466V}Cҗcǎ55'y˖-d2*ZYY cxggg]?4>>[*Nŋz[ArYwy:d2|?'xB?jΝھ}ſzgw^߿_/z!r9:tHofqĀ=z4x"(.r 7nw~}S]wT^.=r^exH)_jΆv]6cpod7Wt[]gh?5p" p`.k||\x[nEڼyz)ɟI0R)=c;3g߿5ُٟ{,_ )I*nXT$Q.$ӝָX,x## Wh8bp@T$X8 =6c>w}7 EYܴϋyϳj5)YSX[[? K+66Tr0A_Py:A$pyѿX@m<ׁJ@)`i\,SR j/cǎs-=d2\.ŋNzaa!CpOPP]0mۦL=㕏Hj@3L?po/9lQ>'t8isgͽlݝ;A}z F]>`1Of]tITJzW;2vHӾiǎS:~Ml ږv{vy4N6=;ĭx`x~ޅ2iiưjaxܘWsСn{}XcX(u<+rYx<KŢnfo/qP(;vC`٬| wNͦ{1?~\[lѻnٳG<Ν;az3ˏîS"vڥx<^x5=zD^xᅎZ.]RP[z; -n23As g-,] 96!:v5q tNןzl|>11\.2ik||<ؽO_'pNx>ǽnm`PhwyȔS6hUȤ;maGX7k7 <#j4 Kzr?N,:soG +zi]"EJ E0}qwrD;b5 N6<<9~N\u۳B1Wz$=fi>uތ)O̷X,dBJǵ}vm۶M3%;Hi"PVl8bcq#Ƃˬ8RI, X=x}}}Z$o3DF yXG=}GO aÚЁo߾u6 apamՙKkQ60z-,,{*.r-*zWu%5M|D7xR*:5E}k_ HcGGGǵm6 ?gZzTuw?!w}׼D|YzOy@&q0 ,+ ;wɦu^:Om^ceozdAX ^,n07n=B?{by^BF3ܓF:as@IDA7VD"-[ҥK 6M-,,x<ЩS455ܻljxx8e% ]|9{߹mȃ{l3@6g޸49%܏y餓P{KN}2!̌mO|yR,Eh4$=47Qv'0RSGfL3>xߓ0ZvBI&߽:gs+埞Tؘy}Ξ=omY[;x=Kc~uuU\Nz]ZMgϞ?ڽ{>ȑ#:u4==O~a=zTG>gѱc455;wn쬎97QV>Oꫯvi:uJ{fff{GwGA'?OZguwg7~7wޟس_z -XR|r`OK?te͑~!Ha`Svˠqx-<w6sn$~m+rv{GOkd&uA:j'fXioG's.F'џY `]Gq A@G >~(dJ$!OR^G$&ڿ/顇O<zHFC Bdpn6xb?OMXYYpB@z`xe;Z >vxɼ}o' <*ڧ[t2wyFdF ZZFGGe|sD+4.GD6%c^ܳ='xF3+<1'i_ #<>]HRi@`$0ȑ#zdBMBD b1x՘닋ZXXFM6R/pt5ׄ/| :~VWWggj׿k?#MRP<L ϟW֎;sg0Rg_XFC###[_Ս7{Lԧ}L}}}z߯{7ĈP@򗿬{W7o=ܣ??|CW~G@ |έ:6&P,l4X[5Iͳx pۃ ƫ[6x=/뎉q0}mt $tuu5dv{Ɠѭub& KёH$reU*`t:r^&А BhHCP*1?={詧 nB|V+&2j.u&\ `,So'VVux!b{Ľb>@اO(PWd=qJƴ}vU*♉$OhZU2\I;0?ˢ;14+3n>H0f{ $׽E'O| &I]tIJ$9w+`;aaaAdRR)̫L&x<)=󚛛߯ÇwHGuw!s&ѣ>ZM6ꫯVɓ'u%j5]{]ofI{@s=?w8G}Goo_+?G@r'8.aT[޾e"[^/VL^6}ipIC___8`ӦMڶm{@s.󞿦]x>~v~۟ĩۓpxåѢOܚVxRub쎏Chc@ hn+w8b!K :o6:t<}W}}>G#W q 62h85]C"'e),Tɸd~p=.v'u6m] {aƁv ! ϑL&Cj'%@jȨ{Ӿ>ٽGbV ȁ;'I_'%v cQR<m+oWT ޾>MNNԵ^3S0*pN 뗏phP&*|Ie+T*gϞFGGui=G?H$˿ N>]vo͛CBSzE@~W# Wh<K]7ı!Ο9kM-Rgf6(3c?"-EHL7pϵ'|6޳gjaa!,l$B'$ ~K@uN ,͝\Υ`oI @{w,S.FGG5??D"QI >t~jp 9=N8Fp5 ѥ=חe8qB__Ғclcuh#2X-#EyϻvϜG40)ܛ27Ym1}s $!ԏ!#xUO<_tuC4ʍ*χqM{$ =sDŽÌ3uܯ[Ez9gqof-!. C<{\cH}]uO&[NNNWWc^о}t7{^HA͛7+JihhHR)x2zF\.X,jh4|>gϪZLyJ%x.|E/_ĄoꡇR$]w:pFe i}{WzG/=r3>u[P\Ha# bcM][%ĹSȔ=AbAEL rP4sssA` ]?:yu&{ Z|=]ݲIr_!CH5͆ϗfdTTB6jH{ ʊ B K %p7DG1PT/R(JG=P:N# 0.8ǫ˧;8\{l-CDC\gs0$>tᤀq-ޯh7ηp{6|=@k {uc``@l6=22idesBmy޽na=۽Ѭ=܃vurx:I2 ܏q*mȹ|}x@;VhNQ^}:00 ]듟uERh~~^vҎ;488'|2dr.) v2ثU}TV5995tMJR׷-MOO… _?_J[+?-G@v3#ٜ\mM`Es.INR!BL0=nsIXA Vҡ!߿_FC.\Wxe@8o -: !ǃx{hIg[n T*\.hk@ }Q5 YhϞ=J&gp0,)SX!!MNNCc?~\wkffFgΜH {9҆ jx#\E?1{JnnTq!]1ϭE m+N''O>!NKk8jZ!K1 W]u *Ji s y 㒓}}aHN-ύ<=5kǑK՘n ?pI$alV0!։DB\.V*ootEEQΪnkrrR:znVĄŢͦn&Z-=3a,|E\|X{566vmzu9]pAdRccc*J:~mo{^~pZŋ5==={;zWzG/=rRkx@ 69S\L\@L |@Ƚ\ˉHM[0\=,=X98d_N\&mM~U.o0Gѐkee%5X0~҆MZׂݻַUz].\ܜ,OtZ|>x8u4u6t‘  T*irr2L&#iZ_ו催b||<}qJ%=zTSSSjZ!5l-\ ÌQ=huvXKDcIU.;,% ^iq_{!k p/(3?Z}}}"CY6?aA?VqʊdGbfεXYYQ. EZ-=Du R7>jbhh(d}c`Jv5{܃y& BVPc`~bx饗o>}ݺ|?[oU^{VVVdtw'ӧ}HhT/^TPP萱1N1Hvf=߶m~~K/_ֱcǴ\.~U*__;ב#GsNԉ'f7+.=r~6K8w{8%7gڱaحklXJnԙ% ^|Fv[y'rDT({Bi@ k:HP?I{n E"% -..v.,x,=1Np>.\ဧlҥK!ؘVWWUմÇT*+h'eb,,,{ AR2N`y_efLu̙39/bSRo. d#y!PrHxPˑ3GXѩr5?#uD +&#}%\Nx+ O {(6mҎ;wj5g,hT!Y˗C̒soC.BG֞f;Ϗ7뉱9õ\׀nbڐ1*}qF>9>~teɓD"|>}C[CԔ BG; ̜DTV/Y.\е^Ǐ4x†UV5==)b1s=J&˿KJʏ^z -tvPGRشqs{҆ϐ-}3H=P|.tll~{.%0lH#nݪH$3ghiiIZC|rG~~ҋK\@,X쵧6}9,K!~'MrZTN500d2)I?cf|TU% E w~H$ѯ迩RIxs9-6Cw\Bzl@~a XzI*Jھ}{HKZC9Hڃ<[]]3>#ɳ|ll,hbzN!J!u۶mAbXr/dK4E!x8Ċ9b;D6!"x|G2r/ixHgqPn hx}ćqאwxH|ݠ^2.]!رCtUWVlKcccVu7СC* ap!H0Zch?[ں[.2<m%I---8$8D"nSfmT*z;ߩL&3ghddD/Μ9w~~Nwq^xp:Fߏ tҺ!N=L IDATw :00K.o2/*j˖-~WCCC==ú[t]wuzꩧ~ HG@`uKol.ͤjǵ<0 % =pC]܃ucu'md,&U4<<͛7)@ˠ{K m#4v&dqi뺽Mݚ\@#mW./},hcqj>sd`:r h*JeeِI[ 6P7_!:ƙ} ֔J@xvpMl Gѽ(.!\萐InP2^7wE{@,ˍzB0gӦM* Z\\TT R*Ji`` O)ΐ9H{̕ڐN8|a`>x@h<>!;kǎT*J&X``4<+DժvڥihgQR>߃OOw[ZXXP*Ҷm۔L&uY HV;%}G???PV̌$'>!iPT,Ӄ>}f7+FKwKܺw @aC6*.-ul fl6j|:4lH}ZG 8LvlC$[$]jK<-v'25s5\Rkkk!-mR #Fi4٨xDݒ:44BGb CT*˗/kyyYw.f_@aQvMbGpt ]v{]Ҟ:Qg>B=8= Y1;??M'dG~>m(mdn,.s D;r؄@h 9cnCb/H5#m/SdK;np`ι2̺<88D"H$RzD"]tICu 9Z-r9=:t岢Ѩy}^|E%I}ʊo}ݧJbARIkkkJڶm¡l65==jvvVBASSS!^JR =ΝS6U>G>MLL'Uz^i*=rfE6R]@P6\l^FAJR#B `t: G"a$;H2LR%@}ZC$?f>N`"F}6@I\ U``؉f Y -R8cRHP`y.OCCCAB-[hyyYA722`ՀfLFJR!'mL]ؗJ{K( `?<O 〾,>1ǜSnBmiG|q9Nciz.a̠^|2#f'3G|qu bqqQ/&''CqRQ>А¼]H߳V⭊D"J')Q_@u)"mmb,I_6 =b)3L,P(P(WUmڴI>VVV411*J> _^~@1$ óI{@ͦ^~e%ItM ܤW_[l?A׫G@z姥ZCl[y7jdT@g5bCt糖f9{ @/Z@@]Cgj``@J%5iWKx.l dz3A﹌+}:tx`>Xah3LT-UT,fll,GF|1q ewwyV{^8 >@xolS>#N]rn_+$<{:.lvx/HF~e2E=VFġ Ij5x0Ĝuqp'ĂWrLH>p$cVw{5E!Yd2YO& hll5[H$$`lWU S C=K'SO dLf4<ph ٦lz֝!Òhژva7@T:@Ue@-$%MA0۽g.sOa1vϑե%xA4Vemm=Qƥ[~(! 3a[ZZꐡ hݒXl0 ;vLbQZMovy#^g+Hu/h 3py95%u]囥.9r8@Q$kߥMN\4kkk*!6`E-//w'N:)'([5niZBpLv[8J ^Cp2*HhbbBzp]tĉ4*IKdKd*JAM;~399RB&*JxxrmL'`ȦYqz2P۫H$q{x VfPI 8RNSOϏϙnCE.({Hp9bs=:D圬Y%hTdRtZlV LGH$B 7MMOOk͛w^m=zC^*[R ^'̙3ꪫtm)H`;#~855ӧO~U^+^4bFyu/D7h9(섅t߇Գh;Ȁ[H%)Ν;C Ąժ"HAe8i#խ$o3A)k:qk%6d 8P0VLa:+)ꫯօ h4t9e2 *2=Vk,H"bH{˸+288bh4\.yjZGVg˜+xs'x+8X, q$Mx{9?O닷cdc3ǩ.s=6z ԇuֽK\3w}gyCoB!).am} x\ub $"ىGFFBvEKd">e27rKΝ;M6}{ZsBye2`pX]]RCAr>_ElV=ַJ}U.++?z+&4FgEasrAJt G| z6ílN\ظ,Xf͆cZ-={6Z |sw{l\³ Vǁ}9y˫j@έ hzA200YE!uuuUaZ%<*4 pjht=S[^:`qΝ:w\G@Z/}D>Xq-pEѐ B`@u0gTF"F,9kP?R!{o;uc) +ܯ\666,dcӁhh||\CCC gook؝oܫ^7oӍcېh4BOlJx)} 7d2sBĄU.n+Nv#m6---Sfgg)I>ijjJ3d cwjtacgՑ#GIZ\\Lz3y@z(=rƭRll|'>'m,^ln.ªAFp.@$;` 4yD4eΝ~Kcm|w{ n! <>ċ-<cWx]ݒ2+<1~u ߯s)LI.5r$EdR|>\"Jd2-[(̙3$&qIWWWC\{g`NZ{$)"H\ύqƜ4j>Dy=xFusYbva`,0<.a6u:-XA|z/uf^kƗs 2saڼdzA|dF``` #/uk%m!ZV, A;v5>>\'o˗:N:{j޽ڱcN>0>CΜ9Ǐ+Hkі-[533bt:VzWވ# Whq׸K<2gMKX8p77uk,y0:<'vu@y!̌Znݪcǎ)[?9pp[}z 'φ턑{eIu!Ͻ}_GQ?\A夵z=HY|LZP pi@`\G\Oc1~J,~wXT\pj{,g;`Mӟ`3Ma<5s܃@[38eqq1x|$|ubM[P\%req=5sI͛C OX 0s .J$ҒRT +++ "IL&dJRz[ޢw]RPP4~࠶l٢iEQٳGwq>VCɄH3~cKKKr};566BoC,oWz(=rnIڸ?=^k^zfziKb8=/ԍg6:2 @H$SNuP6n+=f- s4d8yy,~SG @PnEf:TJv[B!ܜh?:ʩ#@\VT FӸu`lGzt:AL&U*B=}ӽUx0zzRK"~ŚvJrDHSZVC{tK $= .˒6`XN~]O JIR{8qc-փILIf:sEudI۹ښRTG{{ xVWWOҥK?ڼyvءGyDx\tg^# Whqb%uO,kkgq8x0>-a#=%>|sw{ aD'!kHA20;;BD*RTR&Q2TZ hоV}wK9JM.\B!kB*@jى$ j6j49sk1}xQ|Z)6!SXixgb9㡮KKK!3/utpfSW϶ CQ@ٰ|VyLIg{2:N,z]kk:wU 58af|{038VFtqV:2&..]O[C:qhT[lXy@a8qׅ:=7=x񼲾2'ht#@__*JJ.--)JlPчxgHfd2bXHK.NsyFFFzȸZYY kBRщ't}cwܡq zݱ.=HBb66/l4,TRnk* d;Z@Kbu/ IDATI{J%m޼9fM6ԩSZ -JLmq")ڿ (mw2@6YZZR.upLf0?J:$&v;%? }֭[uyTf3u> H$|A?KŨt TN.8Ek2dq2* _ؘɤvܩ鎸Ld&h,O{nVU=` I^* qff&x YZZ mbQ'JELFv&''裏\.+ill,Vhx^ Bc|Y nI ~H u 3?h8+%H@rD{k̗t:SAVLxRb֝?^+++P>W6 iI˗sN9rD{X7=a![6%zu/$ 0SX xCFX#X<>̍P<{3boUP6j ubQzG,S\V& DMŋj4ڷo_^B֓l6L&i~~^z2 k*{ jTT$Iccc*z+… 7J4B l``ڈ,4wdCR =d l:?qi$ ݸ&ꖙu`x]fij5m۶M6m=:uJx<PϰSS{Zāunel!!pPIx31 n]n7Vd2@=fS ZOP q\.fqzƲ}ZZZRPmqq1M6V٬fff455u]|P.\6oެs΅yX}lF=\.vF3HU---wiI Ȼ* S暓B~-<ccVVVW K\^gK:ۼ5#}Ou&ѶmtYZ-ڵKO?&&&T.9\.o֩S:,ɲd2k q󱰰bqhg)^9J7ġ\.kppPOHrZ;k4͜sLev{=:N\.+L41^ӋVZHoxaP`YL9 8"aB I*Tfc"N1vYLcm<ͳi$vu7u%u~ӯ }0Lߪ.ssN/Jr䜐ŵٵn:LLL^d̖D7oƅ^Mo<^-/d+Jv뙟d윥gR`bbp. ī~S H[iDk+ hTyԼI Q=%]ګe+ &@FʣVHZ˱RO (f@*$>!}AJR-|*R8nv;[[9G/c^8*}B ŊȋQʿ >j 8 3蝗*ZOXE p<ڢ`֭>k#GX n$8RnRب|@k(ZLRt>4n1s.ybG&KtZVѠfffLYBr6ryQJWR'_EySfʓzN>#GXnP(L3)TVi&eVeZNQ,188t:m R8NAJlٲO63772 [Uh9VT tz<Oޥ 3 J{R{^UG38ߩ+@KsCSo$-Q bii23g055eJcb`<ɓ'1) 8B@il̳gaahm4qNX@ ~|R),..ׁ3F-?C8;ߏ.Y8%T*lذ&sssC6E4E,C4^XZZgw5U^G2}Ylٲ>&'' sɓؽڭiRk+ h%FX(Zyw؁Çف;gƝRd4nl۶ u^)YRΚLZD*4\Kʇ<%8U9`|L%T>**hpRyk /ypMBT)xAyJ;@%Efo[ZZºu /`||ܼD-Zbڵزe >`߾} `_)Ѡ8Uи->j" Ni*@2o{Z"[f>[׃rx<9j4&JCCC6mڄI\<֭[gI*Q*z}ve;?|޽wo^3THcb&''_vkssZE<ɠG~4*!>)ya;A5R68 Twj)5gD.//c``VC5T2lx e|/ A-jTeG:g2NI Tgg'rI乧K=(+|?r̃}V,dlZ l^x!oFB``155t:m#{zzxV DNMMxb>|[dG򵸸H$$I$I̠REϫv{-r6Z`'AsVʕ^ļx;zŝ(C/C^~ڬs׃QԣLL`%䤁*,|/ǩT׆b*:ߣTpl6[ ֢#HRh6HeM9KplGᜑZى1ґRBYszn%XQ~$~?v܉rǏ#N<h {lk1^=餶&5=y¸*ºibXf ;fƆRԢJ ŹZXVMvynK)rrr 9& +HObv޲(8r-МZ^}}}q/Ncǎh68r䈥zu0;;k]TpJ`0hg3KvkvZ[9GzaET-^faS>CiKLzw'Ei,*-G-T4 "9W%>~I^8BYxLZ3h @ŚH`6Z_UfTbT!"¹`0h5zzzP(,S:_ "5D-PPD:QWW&(>v2j*˵MI[έΕRhf=}}}&566fY|p'kzø5kK}RG@Z Ϝ?MӣI.S>KYTF֬Y\.f3g`bb֗2B8TNF9"xܲ<yv3[Rguv裏É'9B!ttt`jj1qqPϦטi&27[jT*efz?]@ `ԸJbrlJp89 #!4 ܹǏǾ}pWWcR^Uf4ONRnV@ѦVq^sJkg3>,=7f¤Eatuu!Ͷ?2űs DŴ@j O E?RZ"P(d%ST JƓfсh4iKv Z7j9WU׉"xBT DbOS*zCeRD Ř@yll(UJ-M\aKi* :,"MqԽh4~kk 8ҩ,fT`_8s-1U|_<dž pLMMG_gg'RjgУ\'͌q}}}p\((Nj*q*É ~ƵZ JR<]]](4 3QA"kƍ( B$"ySpoaÆ 𰭍n~}j75Ե[r6^djaT@H-tx"u;S4 5ۍ6UDy>)RgbV?eRW*aoOieʑohTAP*F:~\q ( rC~FFDKR"8&,--Y\۽ZrffX恲h4VR׍216|'YT!'2AFؘmfJc-8JYS"׎`vzT)W|۷oG65 088Reݏahh@Jɓ'QT4ǽuF^Zz+cEŁg ׀=*{MRoiߔx<1S>OC=N臺gt*JEEE6F{IGшG c(vuuY}^S.fW#''^ fr{i XM|Z@L<`4<*s\.c||d+chhCCCD駟ϚGdϞ=Wjg.TVMjF "\.[ *j%`xyij솎ABALI duS ,D BW~=_u~nR,+ՁȨ*N[-6rn~ }>%eʔI4RU)߁@zcccVFB(2 a?Ε*|3r^ZYjF&>h%-J}}}22ed24ׁGR8f\U=׈g*KӃX,D"dTZ5Eg1==9>fH7~HNVrxgg'"B|sss}χd2[bVP(ȑ#8uJ"͕[nE*YH$D"bQFa|> ̌:4 Yww

ozTxr=1KKK8s持_V<E,.."HX:Xe ø Iar9u &JSo*Q|abDEp~ʶzem#XFIަ499i6U!еb5אϥ{Fo: ߏxzjV4׋P(dh4l\.<h4jKm^QzyvZ1|>~aaa{Ŗ-[d0== ߏu֡x^Ӗ "LPG5LSfSo8Q~*mbb7t^MvZ[9KMoz7k0n6S@/ك-[[o??X r[r k>S;_'^luX@GJSS<|f4Rz;$ 0 `V2)L&Ip[լH!ߧ@KPqzs5* ׇvRT RqhsUA ss,T tnSՌFK&йE)W BEJ;vr˺,ܑvn6W=^|E*ף2DSΩ3r_}9zXQ-FBP0ZuUBם{Q 9vg|ek5lB5&-E3kjPVc@ uT66i3ےڕ465y}{Yϫh4^YlҒQX^}=c֮]~Sւ ֬Yc1r fֿ9T*tww#Զ0b~yO8arx={g?*mrr-V65O p ͙!kHx4pR*/5 ; H&bJP$:H0)ł9Թg zB(]NSPsZQZϒ6ZGTB MT`YS\.c~~(T׊".p4 .^(, UX,bff\Ωi"bΝzؼy3\pݍ^z FWWk.!#" bǎ&k>3[\'dffԧx&iNkSr>n e 0*D<X7ˡsssfyqX,fE"/K^j~^`j保'/2gJVi lK"U9HP'-Mal^8سh#yyyR D%sCl`>]걢YS7gpT$ZcTt8wTTz&p;2 hw^0 oF\ų\%[+p7T&/{@&P$Rr/a>K-<`j +1>[911!Aсi,..b˖-FKJFfBF+TsZ4R=<7JQgbVaUh;^~^s2PjH&rrjn3)n  N7~}R>靨jSgX g>|m@ `i^׬YJCy 9C`zs oUВȠhdž%E^) 5xĩX?Ydhߩ$Q"tzMSAA 8z(,.R_TY<(J-F_*l?F;*Jr:4&PN4NՅT*eټ8 9Z VgAzzzpLƊq1" ݋p8rl2/aXmy> o7ۍ}?J >O"'E]G}^wfggMQzWx<ESvڅ'OЇ099O}S-E)r} BGN:ᖟJ.~ 鮵Xk+ g1'9o-܂|#H$`{ڲ٬JZjB#gK WLW\TZRpu.yVK.W) a"|>KX mݺ[n,C3qOGRWD`}V!IZXX0vMad_(KtzETzVTT,}->2O`\X199B~ H$( kДr@>Ke{ FAV"Ԥn+|>o2hoU)##gRᡑn$"JC@45XS i&>|-p\.:t000_|P]]]r򗿌{Z|GG>a߾}طoRشi&&&&z-]޽{-@]x7>>Ufz0^Z bm6m¦MZ~dZ4J'ۋ|>v. x޳m ȫكF3g`Ϟ=_Ca˖-v1KyA\r%iuZBZ;::^GA-/,*g*UM+@M_H eȕKE,R%vZ r\.ѨQ5|"r-Y<}`K"'Os|rPUn1ѓXyEPϨW,ǵlAhgT*r{gg5טqT҂ $fggQV n[CM+ ! Ͽ2Y tuuO~۶mF!Rn,~\f T*jA`MSlNJwRjz86 X)RaԣA"e5z艢Fy%=*iWz󖖖LAsz3UN`0w8tU~?~}8F:JR|ڵk8qℝ![uQf56g;_)ՔƽO815ב ׬RY@3[aV"֯_fT*e/zփ mf%<* Bzzzi&?~X__كx衇pwmhhzQVk+xgLMM᪫Be4MLOO̙3uD"j]w݅~R)s=v&*ƵR,E=%k)5L[q뭷X 7pmAr{Gxyfx^\Rd9s ~a뮃Ýw;. 7p/}fcQ ** TВJz>۞d``fX ҢO>6 `ṳ.\ &cLA:1,f5??xzJMM%`ZU=T_2AgʟG ƅ__G??0ַG-sְMvZr?qDQ\ve㘛=|__K_݋wk+֓JX,q\s5xg/} g۷bSjպN`\ZDi#ZYiܩ0-,,ՓϢEʙ !J,3JIVkUX,Z@ 1S|&M,JDwwwKLz þ+&X;CiZ,VGʹZps]w,--dRfib'N`͚5G.X*Jk6&3p=\]``p h q NZ=<HQ<{5øIr98p7oF<޽{ }Ę=c]o|Kî.WMF`0RdNŚ%28/ZH$b=D{A:6ф ?;O9*4Yy饗vQ,qy:eڸq#:Ṭl$)~foox<+gg ۴kك;_׌rEᮻ~ܶm#3 -oy nVSR>" +_ nVo Z"wv8~ .$рb>CJoCK$O /h~cOx^7/v >%BD"bf%T*G. ÑHO;:CR8hTx;nN*qT>:?2 GrLT9.FplqHӦSy 7)$\΂iRvd;xLa9NŹ(ƍ(GFHRh6F/8ufgg흔1L=_iU zlz6J'$p{*s^>RD/R w}QoБ#G022al۶rlpF\ݻQV5-` ye_a, gMq9"9E٦JQ+ȳsjbMrd|Kqqlݺׯc=fEXz혘<4NYNEǎ3E{^TU8q~Pk׮_cii 6l3gP*FACPC=-[`֭6+CӃb1=z6l@2D<?A oߎ#G`jj+v<g=vMv vg f}#P`Z4(?˦- @(!o?R=J `d*eb1޽rsss( CT2+7tpp\Ǐoj0,ǫV_.DOL=Pԃ%IASfB#߫컮&cM=J7#8kD8ny@b$I,,,XAHoT*Y+& Pl{^>X̨z8.DƆD7 1 nB!bӦMسgbjj3ǵTPum{ *<)D}S,)sl*\'UX2Utq )O7 UPT0887 xꩧ#Ϸx)\ cUNp_8? ԣHS04EgA$AGGr\Kr5N< IDATtuuYFc%)Lk.LOOIEɓuݘ7: bzzdؼy]vַ~^b1lݺSSSp\d2x;߉k֬[JuoDZ=܃뮻~7lOܹG֮]mž={f\vex'qØi1ґz?7H```;wıcp=K.Fq `,Mʾߺ`] 3A ט{kAx} (_ZKQi1Xs4*ˤձG1z͚5Dww7099B}kH_M5pI$I3.*8Nʄfs;HgkS=g\.9l޼ٲ8p1tvvי TQy0'˟kIO|Jr^ /.."H YY٨8R@ L+*" t>Ti*]]CsMVO~k_AP& = sMA"`~~<s\K,3k+חOLу wӒJyzF h6BXXX0RjHRJ$A<7FXD6ŶmLF8* 6oތ>⩧~軨r09':;48{N *TT(2˸MTݗd:C ID"D M RsjVΏAoZqj^9=cùE3lqܑHdԹs<GXD>GTB8F 쬝-˔p8L&R|>kעP(rY b13TyjX#3>aۓ\qDsssBزe :zzz0<VEGGz~yPUz˨0sLYƾ84@H 'TCg=(A<b>Sg2uI `%r'O""@Ha~~ނ׫*BdF`!h4j{{{Oo~-};ށ/B,3x9sT ~pxwF4ER,J.b{81矏??Ν;X(0;;D"׿?SR)nÍIo+ Y@X-FCARLh X%X@Ciz&A~FIkw&YXZbX,-o 5c%hg㸕¢^$9TEM"U;$zc\~8.c1AUdGJ*TTTl t1+\C~SLQ' X{p===4`p>Yfu\wEa-zn~" X333H$nsm6$IA~f@)[>?sT4ׁ<=_DkEK9UQ/i60Tpt {/( ^5A`/< ׽+]H ~Ȁfs$X,f®J۽SVBvWc׮]f鱺#F 6`||. SSSFd ) gK2_zDoI.Xqk`1 Oo &`0p8l6`0+Ç199@ p8l ?|\.8p#\wuO~b|ptABrAl{;Iܜ^\J R!Zk];5p}jXj= фz4)j``1Fa^=OTTIzF6lMp?!<exrVLˢL4A&&&|y¹|6JK|d'q0_,h欮.SPH[೩T(UE)TT~yBH$P(LiP )qù|¦M022b1jM%Z^/ŢZK^G86y(1Hن*Z(7 byy^z)N8b͚5Vh\.رcm\.cjj JfVE[PM^$[1D Jcr- `|tS Ħ@ijpP{7\RtQh0Y\gzzzJpQd2OX-сDQwy8sL;4x*QiM X@)WTԹ(Rn琦^^^){1<*I4 ,G3&z^?p*^=FHsIT*VѼl"͚ҽDiKؽ{}m2Ѩ).*3mm:6e:XQz{{z-vZbnn\>[n<7! btt·a}.Jjju Xɛq ՂOh4;CjVe@_vEw#srŨVnTV~ޠ@2׆J=<Pj@lP8|>SsF3uww[1E['zL@ ~twwHH$7ݻ-N^333FT*5k HR,ׇf+*k5SN!X)===H$BVe1(,h833csS*<9r4&''{nE38pKb> *Sc&|>H$2 ('?I\uUرc*q?͛kFvkv[r6* JMZ=y9ӲKAk5AFpCf3Zi* Vд "T"Zc׬4J`ybwuu!`vvzXXX 022bNpKTk(J?ǹc97j)8L:'T94(9ʏlINkT[=F41׿zCB``Ԙbh맲} v9t\FŘV媳xhuBxrŢͬWDP, 4Hγ:zY%4l-$,`;6T(L"qeHA!".~@ ~*&!,la)KьfGיӏ~1jY IDATTf^}9:1KMţwVV (L$8{bnv\r|X 333]TF/? P{羥LR 4JRt9 TeN#4-kH)F&I2컎]cﯮv_^ GϢ y\; -4pr:' *Td2!HǎN$Zg槅ӋFիW111ؽ{7:dߏyRԕv_FBTB<G4E\Zfggׇ`0({: T*aii X O(TtN'J*j$fffggQ؞L&o} \. . Jd2h4XZZjCnݻL&qQ|>Lztv ˅FPE\?W{(X=J\Ǎ4/Feȋ] <4 XK3M t &ki:>`k˼i (QJH~_G,x J 7V~tVO贺\?z4`;gKzʚ1FB1Q Eovs]_&ן?3Y*$wʬ aC+%sd)8Awv]2qV'+=̐FD+j4RIJo2F٩>11!皲=Eg3=ˉrGhT:n7"0<<)) . !NS|>$Inr9H:X,l۶ j ]__8 U>\N䗞BF>#LO< < ੧ݻׇ|+Q H ϑd2 ˅ }O֭[aZEvDQL&8pv>Ozw\MJbŭ| :A4. ~t -D[CkњNz=/v3> cbOB vK1,^Z1X:i#iY%h<&J玔76Rt@iS $#*T]h&X=Պj*[U/+fr8XYY=UZNh5״^zR erH$hnZsRX,H 6ݱc6o, l ݇Zvp\.rd2ajj ###k׮ {A.2vڅ%y H$'OG h}}]Mˊ¥ZitLm@BrJjm1A+jU~WpMt }ӊ)_0ShrcVݰlCP:;:*"mՊ13Z-x^B!,//cii ^W|@RůVcii B|X[[b~\VA9WU QTkB%r R{d^F^Dxgw^lٲ7|3~򓟈wM704Ο?/t:iZtN^>;v@*SL&xrH$F, d3[kN\H -Xy(ѺŦ7-|'IPh#A`-WN,ǁ(ij  I@taܹ+^n˅ONN4mixIA^&s o@H4='Y3P7-לC t ~00udddRdYT*s 9!:YN\.)Y(?ڃ6[V'DNJF‚r~?mמ-|>/1z+%Mn޽{qeapA XJEғRi3%/ٳ>sI5㔴2ʞrrur#4.heߣ}F9WˬSqTjӱ`Kq8ߔ*ʚvכ3X;HAv;c8SRi(ѩǩd|>oFC2 QבL&Q.ndE1 pXYYA"&}jff~& rLsaz8SKCMC=A]X3gHl> uV|OSԻ6 ؿ?\.㥗^kڵ JT zǎORTd2t:ƫt:h4fd2H$T*%I leɓC n:͛7o{^|_ťKd.Uʹqy&p?iÁVP5ŊsHyʺ+X,&6G!lH~:FEpY >2}9<_M&|>Á/bqqBVUdJ!^W<cE~P( JId(Z nZ PH$ z&&&zaX6FXZ-Q+$VċB/׀g=y::`xx>OvpofEBR$g;Ѭ\.@ur9pNv. X HARb{o&Yp\BRu,_N"駟ơCP*`2"gqZ nc߾}Xabbz/_oooij>+g4Vv vQ. f7kFr7#%a0k/t^ldkσ t Kq Jg 4TZ, +Jv\ZZˈAպq @MoW{E'' ҠAfMa?t-=ӤB77Zu0|fRX[X," P(ty(ƌMM S-///K A~G恞 +:u0-3| `Ϟ=1VkZSO=x<.ښ&6mڄvSDt*JfR*@'h\.K{A$k3J'@Z<׊JΞ=[ɓ]{I8F(S Agqt6 Pf\N,YN!JIqBIf3|>T*!ϣT*ɜcByvR)a :ښ$!`tzxLCfW.&^|Eazz###p\XYY ~a,//̙3ؼy3]&XZe,~8qg?VVV`2 ~%^_S@Ӧ n "%{Mїjzk/Ep@ !ǪFk:b} =`0(6'''$f ʵAK(= i?QLk^siʛVTc<G.d#G?My2X]]E /品ETծx2s-JsgR(.\nɄ=|^p8Jt: ۍL&H$"sOep,pͨ4(p8(nR)"cΝBXD<G(*vڅW dTLg<?-ҠU{D"A +Lb֭zjT*ph4bBUM0f"8^! 4Xє'=@Z>hK,MB-|6OO[54;11O@ajkFygd2I].^,pLd/1Q};ĉ2 N'غu+|ITUE,,,[oٲE\>G8F$^y:iVu9ahz֨ko#j𮽯>͝`/ҥK6u{ҐʛR Wg*-ZcbyhJ@6Nh;v@RA>I]KϠV|6?\5ɦ |.K7oQðHl:ɅnN.J|xv:N{ػw/&&&r0<< ͆%8 wzۑ%0ii hR >VXLG4yMl=ވS@ FY[; ;AZӊ9/IѠB[5&x^}p:}֖S#___x@hEB±οEL0yZX9~ݖX+FOi!&d_4SShpdhߥe2D7MX Xڒxe[kZeSǚf%J$phWVgL~N_P@PŋEawjb8y'JkdիWq5v "I5kvë13~5*s}㐴jP˹QQd|EQ{9ek Ir1lUZAc4|ҞKwxUUq,..d2E٫n1dXd2P(N}/Ld,dfn[maSKoSY3A @\5ǁ]J?=2zTz]j5\.D"m aqq< N(f{zqw LQ! ^ciiI [16K\=T.7iezWrg @ab^G:HXZeJFkz"1wu22AFSEtz;~\.w0` mrŹ5z n9#5zXyXN[IS2=m'T(/0P@Uʄ6)b:rIK* b6q뭷8x(X鿏='NOV{n#!n˹wT~\7Q HO7-v^ڂZu9W൞Z>Mo Tnzi2&XҖpZt*fff000 l‚'t 5UD+T;A3אwbZpFf6%(\+ V \>PKHeI%ѩ9ךY܎;H]9qRRg :+ 1 '-Z '0eV.>O{h5f6-Sf Nmےravv.] 0}>.]vo?я`2uiŢS4 7R&9ZaqS ʽl6%siy:#12>0.\ }~{^Eχr~_+:~E)b>Wot\pRftt-iZq7oÇEa zR)_7v'E<ɤ(h<$=q4Z-Qb%Ɔp~M#X4R>z)7T18{saa###rxߎH$^zIVtUTU;wN[n,n7 Zp53z?~T/O0xѺ *w#A+~8|>#GHisaJ IDATA8Fr)B|D"4/p>S/We1bVT @,)3OJq-^1`0kK.3in{^%s%"5x6<t!N1ׅ!p(<xQ~ؐ"qhJ/A*-BE>G<G__*̅lƉ'`6wVaXVF} ___Hz6+"ǖNQT$F &( d$ [ٔ~&yVVhzzh4).`-Ͼr nV R`ll fbQj\.oE"3f\.# b||=vލF!chhCCCؿ?۶m8HXy3C~z~ѭNrzxAӊKY_:@QgxVFU^?4~ W3|kkEj! !`qqQχQn\|Yrk>:HE`̋DZεDŽV_~X5cVD#u]! Dy5*I3zp,N ;Lvi%/JJ{Hz1X>&D{`4Ŕ"K\k*7RVu_bn7$ZvIho39`0d2@ tQ"rFFF]z;K.Vaǎ-܂cǎayy?ۍR>GMЮhp`buɸF<7{D֩,Q n~>RC}&zh)DVhkM%dIl߾]5Gb2_LߏzXXX@(B4E*T*_im$ @ϭedhV EI_*)TIT*G /&S'n2ssGye^χjt: ,Np8,ASSS2j;wbeeǏ 2qM7aii gΜKp5zNt=2@}AT4eGam)ש t 3/pP]G* fSRQ,j/rlX<pE'Oٳ3/Z>5λTfpZyp:5H:YM+D.K@-@w%u~^lF@V1 ZjJ ȭל]f#1FC싎uW˞1[jPC(Z/RIAZ"2!NBFP($2DFH$0;;B@ ={  a۶mhےrnch4x饗d?쳸t̝\RY9Ÿs͘$Fңpl,hH/K6[BC$8qmahh@@ҹ2[8z|jUK l唁 >ϳ)J%*n~H;v쐠k׮a}}]1fggqQ$2 H Lw(˘XV%6rıFQdYٷq&xg@R"2Аup_QnV+"dj|z̘?ӧql6l߾R ΝIIl6}p8XYYs$V:}s8x "N>uI#H$^嶞~N/---XhqԞu`N{C / h!36^.j bwM!Zg0:ȚMZAgڵ 'N@ѐ۶mCPla=D!λ  ;΁1Ҵ* A2 ].:~l6# dꤻm6R9=o|f T2DRzL&Q(XVwq(|YLNN"͊2Z n*|j`\W-_+0RN9r\|XYY;P133h4* +g2ڵ wlɓ'qI4MI1M\+T9sC{8* GD{C^/HӱcjS*N<)9r9ަFARԁ4ŇΈT \X,' ` A6?)@wM ~bdI&|zhZBEjiwrXF/G.iҡgtMҴ+~n ҆2(mFB`)L9JhR)G@EYPt zkdLTJђp8xSrn) @Q[,`#S+.fd2$Ix^8N?)d{K/?88Ʉ%;b۱w^\rE(-OP3z%"hC)Z(34Lp_=\'6zMiܠ\paeB18F#&m'elZP:Ν;|Їh4կ~Ә޽{ĉV7L8wFGG~8qÑ#Gpw#g>;NE}7,qnn7(g5 @~r9)JDtD)a}P@݆ONPZ;>NW\V'Ŕ{^+tG29yfl6~uKx.\}8Nann###dpw H*c!YKlo7< Yvmu5z /AOi;{:e ]`A[JNM2f6k- F0:lwۇ) T*x4w=:k*~z=@9^hu6 i* A{g3Ap>p\x<( +v*}:[nGZ1j%0)(^WRҋAry7oF<w:Nee:ͫqq4-bMH+FVK8FR V BaZ[[A__FFF$yvvHrY*ӒM ӳR IC>FeV1h?\Mok?GI{hAlQIԞY]<h,k<(Lx<ø馛r裏JeJ2B I,7nömp7K'捚KKKr$(h!xo4fp݈D"rp8EmXa1˅X,۷# NGd"DXZZn BD/'S:\|E6<^|E;w|^B+DE9wZw)T~xP14ݍ̴T}mr066|>{lj'8)J$ g ?N,..ٳR_|>,--ljpYx<AQfRժд(S.--: /^I,..\.^v̾1=H3Z*z%^ʕ+"GsPiWy<3X__G<zH%?1$N'nL&?~r)_S@F ld⠩l/z+z4K[ \ŷX,x- 컶P'}XͦX]u܊v,NOO;wŋ(b՗:N:FŃW{$H,ho泀΅xV@6[c48OYc$+=A/0 <2iNp>شZ-x<ueIIH12B*kP>9sNKK{tWsƽfz tgn#jaaaT FC^R'x###(b˗v%*:PوE$NJʙxPxgcuB M{T֙u2KzRjd2ɓl,.."NKP($iz)+n4 \z`v\fX,H&8r䈜7f@dZ LR l6[HgPHͦ(fSI-JZ )il6Es *I, P^VD‚x荙GXD<G\2Jn7h4066~G ٳgDP& O~zJM\v4l6K%=@w} MѱJ |O'uCWe4el^F(LIK^ϐĊ/@K4-NF*k1u/ycҖy󽬬"|t3DoݝT+.|KF+KtrPw3ׁϦ|wc|H1俩HͤQ8׺V>#?B1fI_-+y@'˺  1΂;n1#Zٳ8s 6oތ5|SB$E?~\l<=HPGE^D"}8wț*E.zJ¹Xśrni&\t Nݻp8$xɢ(2 t0 xI&lx'F%(Kru\lqYwdnƧׇ[baaG{ " du|>"KD&AhTZ-ay&aCh4$&XłR`0( Z8tZ1>>ETUA9#B݋L&h4ӧOcvvx6Z^ h+̄@,kFKuV)MQ~j?4Za lJ5D`4yŬ+&S'(b*buw0W^~`MSJ9'A#7yZd6Fj eglPQ8? du8p:~?^oH+: Z+.K62E }ca%rN% dėhKˎjTI"PsCd6%Zbyykkk8o.&+N4aٰ Ʉgx/>x EX'3G HP|c=\+?ԥ:deetZi]ڃsڵ'H:FC_Ț>p4H@WAKcjb6fscbH/'&̙3Bў9KKX}]p!p 8ps#^&4z:X|i3|9О}>tՔ fjc'a*f`PK(Z&!R^FZQP]QBBP3J4=T }JndǺp4gۑ$ 3z ֐f [i<nP/EyaV8i׋t:-1\Z xvCCCh]n9i2D`Dfi {)wxƄB!l޼B[lAŋ1;;-[;$\.nFLNNldzߎ SNa~~B###&I[>ڝwމ;S뽋e IDATgs^o|j< 򗿌#GH< K:cݖs}L h %ҫſa2LRw$ۚ7RI%XjJ۩)Z9/KKKX_djK$X,VuvP@\,RL&#AQ"U)Bi=M/ =J%Lz^ t:8t?ן l|"L7x#^~e>}F3gСCx"VWWGGؽ{7 .^^xKKKXXXBW^ 2L!zo`Pl6OFFFp]w{X]]G?Ѯ}E_Zy@Ӧ!/\]I@Ug'ﴅSӂ.4Vtؠl:UQ16kkV4clyڒ?zLFy2۱L&#ʀ6fOdl⢌;NcӦM!SS`DZŕ+WV.ߏ`0(u(w$/3q\ 4gz6R JFT> =t:-{ |>T@[__zXfy6 ט d> 033)}ɓ'a6sNܹ׮]w܁Z˗/|P8~{zx ??uٳ5۽{79"r=11!?׭HΝ~Zr6G` BQt*^uY|еWa$ύ1hm9m&^e>6mYJbR@ i9L@2iФm T5c08/\ ` 멭g-z޵E^+\]߄fM]=5Mx^X,KzQ.M&"<,^:k`@ZZvr1Z@&V!6;;+tB t+:j>iVcǻޛzr_j 8e=O2}@p kC%ʴVwPn: ,5dOFBիVؾ}VnF\1b1T*xEij^2fJ;veuu˒򙙣 hmR颺RrH&[yaRZ  B^ Sܑ1OgrxLd҅;wŋHR?}Cx;v}000 .`ffVUhtUt={@xvB!|c=??a nwRJTލ- ʳz7< qqLH:kO6^NS AV^=dh ߓN?zh&-F MVC.C\i0z# '%)xg0R}0zmgI{41(zw:tI[*DfI>T|>)vQ>?e>u1^G#=k2˔.%fn"@6jEPZ TFt,,ψbUte {YXXbόt8 z)$ ɾj?ʯ:c9/d1k0niZ&idהki@VV2KX]tEV%#ə%ER'VL&t:-VeU#Տsqʹgv@5I`eeEYĘҕu8“]9b(% R4齤)ߩi7#z=>Rh8G׊]hZr|v;8iR):u /_C6ؘj'µl6B!W_EZR(Vld!4188WD g?Yx<T*QN' (Tw`XJMzfC6E ZRѵh42B/nߏ|>J)y$(|A9sh[lq59p)n׿uɓعs'b;&͛7cǎ넼M~V(ӟa .fp8 baRfk3~-oy5^j= uH)1u_[ Ieb^ |sM pgUd {~vU8-bKWICh v Lwڅ!b1~)B~CCCRŗW0;ښ^dq=N7_{C6f^$=Zԗ(׼VX,J܁Q97fLcA6c/nheN&f] 4)/$ZtRt!S瘘USǼ^/JpAyċGK`4Z<FGG_$EtNc~^Jεq]k9&ߡ.>sNO+ ~_23% <l8wJN'$ * ?.^'*LqlXDu| =:k2$=Pi'o}}G8z(9"J5rv-NB:"LX%R:, 6m$ZMZx?쬬(ʕJEj0(K0Μ9@ W駟F__>aaagΜATիWowy'z1<wYl۶Mh1~%ww~iNiEV"x:lhp)'-Ji;~ђ-j˦4# l6j~D? _BpkkkrA(x<Er9޽ 0LRJ+8Z)T.ҧ\Es~Z{8ڳ@`PBL5  >hJ]\_tRl4PRF*.WV‚[+|rp^c&L8 Xϯ@R3LTqe|>Z-$ LMM fii H+P Ȯ|yf+SבL&L0aMe 1דMz ާ|jl6eR`Q>jY6%&!'b/APN'"(X ϟG"@Ղd( j*.\ uS6+jbrrG &&&P.Ox#a׮]8~8h4Dۺu(׿֫ /b`֭Zx{ރ'|_%ivvGŧ?i;N&n7ML& ~%ZON%cVtOCӡd,ѢltS:x]*?G CiV> R`\.X+n]>A#lC,t: ÁS^9;Ny`߾}8qBPW0hy`*1ڋJ{6hwe9RCE~s,JKcLvJH+Lp8\.'e5Xz,Pa"8Jכ)Gky7R8Zx<CӋ@9rx$ٳRc15EFCTprך23iq|6 @tZdV5;~]#K,QlP&9A3\.WW-d2# tbqqfYb$c-2n&Hh4*%⢜3T1BAh\ٽ{7J4v;0 $HpݨT*2|k_~3'<2]ا@ g,8٬@x!)cVnݺ\& 8uHevU"!Jl}݇Ns=F! .L&FFF0??O}SH&B2g|D aߏjIR)|Ʊcǻl M#}o}+B2 |I|[g>__5__J!X,֥A<#>P({ac+.ؾcg<[Ɠ@@@EZMRUU*D[n BQմ K!$a d,_o׾.?ܗi~_4H}ss/??'> ,,,m^6$ i# UbUAI-! ^ZyDh}ޏ`Aft@$Ri (s1kBf@XR䅯e>~בJD62X-bu^J5N{hEY89-҄F{;5١ISU5Qg"Ӟ6!N3^{󷶶&D=z`4 X,$=qq?sμ^@L@<5ǭLz򴊛L&̈Gr*~J_dpGJ-Ώfñcr.ǃWT*!JI6lbccCŸ$Á /`eeFnWŢ *cccc +cv111-w1<3?|Zx;ށ~BR~_~g>#E mڴׁ`T%9@/$cJL ܩg|մiKK1(W뽵̆EBg-1_hur꺯^ LMMl"N5dLA###Hu,-Ȝ @,z)SZ `ժI#cAMpGpI+*c]iܳ?Ԗ{Z1~j zz D=rziE' &)Մ%X&3q cSG IEsZxdSB#: bZ7pe|PPT n#O=wyJlsO½E.=TqߑlhecǠ0=͹$dңf)T=e?y{ͣ'=˅@ 0Bn+f\.Kp5EJ%3@O^,C^k77$ZǽĽJo&$_$ZFp+Y&#  "HX,"\.t""`llL@4ETg- 2~?n޼)ww^O5-dpۍϋvP( Ns={)$ feyv4 $I!r!^rajjJInrrVKD籸($f^v3ZDq]w=yP0;;'x˸t8} gi|ŋ`0Ʉ^x'?7n`bb;7_k= 6l/mH@nkdh ɀqkhJ:C6o:4aXRI@@2,hDtCy 3y5X,(J0LmdyǪI |T jIyZ͹~:ƀ9~'x-n֎s1xS=%Aip idDa삎|ir>q޸7A.hP FuIOĿ5q{ FFFD}  `Q.f%51:-:{]|;NT*`MYMz8/NPrTJk\N<-ttrv; m xꩧG6vvvDH$LİѣGfevyaX066XZZ"bt]ڵk8|_3S.EIt:%[v)8Hoj> ^4u|>DQ$ ,//jMoz$bH* rxIC666`ZZ7oބnܸ@ |3뮻P*;FLx;`۱o7ÇV+{6&''.]BGӉv؆mnن6mtu@gP|4jYd38J VJ_>4k_Bb-ut= j6ZҵG{| {>7hVɸ &&&$f$]^ ދn|JNo 伎%qIͦȱ8:`!z :22"+n>.7[Ekiז{:G/־9:,h$狟3H:i_(`qw^K VUvQ. wPWzӉgҥKI8NyhG1=@:%_@`EM.sl2qItLOFŲ#1(4t0.+j\. (A b܏ɴXZ/&I$M|"N:='2N]G'z\.1It:%٥*'nG4ū{vh4o|=^[[C ˗Qհ/RW_EP3<9=zЇL&cnCoŰQڐܦMKRzc"W$`Qh4k 0IY. TЪKPіnEa爖~T*ƍttTJ{3d2cccc@\pLrY%$A{y`?=YZS$[Z=;XksH 2U{0g8VdW:h]d `O{`H[ʜh1E>EϘ y=v^yv;:666dYZIp8 gYx^GBOV"$ HRu ~_2Po^8z xBo˅q+z lbxP^Jߑ@Nxr,7χr,FAYY$D@``]. Mѐ5krrZMX?+YLH2`uu~hv.K֞D\oJۍD"m0:N81^z:uQC"^gΜAP FFFPTc=-AUidE7b~~fY2kcZHp8$X@x$5KVqTkg; pq޸ R҉p("btP;p IlFϵGPI-q$be2q 譢4^ ^Eh0OKoXFIRm2pQl6WDK,εфUqY&zp͆h4)\.,//YC8Kz.ϸn$IT*DQ~y~$It:>}ZRI˘1TFR8|>/dscc;;;5͘g(Img!\N֐'qϸOIzjv#cj6 bQ0"+o&?Ǩj.\ix^l6b1 q gJz9LOO… z*2 Ν;'Pw?um~mH@nFMI&|YЪI`AuAK :(K_PJ|/= p (5@'AʿXʘGi+xbblb\NlT/_F\(GIh7y`|c6]m[K}gMM{8o#N@=I@Z0s?A& g6M{}_D36I~l[%Z_׋H$`0l6R$$$+\΋cQ*rpQh6B JR{@{^Zn>loڻ -:rRn-H$|l6Mx<C͆qJ;v W\rrsq? Oz4Pf, a{{fS2=<rn)㼔eEL&! |n(U.R6::ϰΈEύC$jիWl6QՐJh4paDQ<8{,9J")s?_F6E"@ՒXVnzbqą pIu?omH@h?'fFA _vze|gZk@ghZe6_Rr<lZF3 im7ǣe#Zrvp\r}eJf΢b`vv'N@˗H$DГnQ(,VKR\}Ly\"@V%Ziyg4˭>uh c8&:j Hzs@?&z~q9!aYgj^hb}ܔz̦~=|^g(hmnn"JIj^>FQ8q Z?8.4A-5ưx8f!~"Y}2zqq~ &͚-T zf2on[29Nx^x^b1Y#ɇzbBFGGEh4%d2P(rI\Lp8PVe^R`~~tZ$b9NIEze1CS[,ACE l6KA\̑#Gp8$bxߍ9E,,,j̙3pw#)4" t$c?񏱴D"^Ϝ9ߏ\.'$l3lZ!M: p/s-/u>&QK"X@K&dV5^/.}Ѥlpzzt1>Z i B@$&''1==~+W`ccNGOIsLZ#eu~p>5q8Oz]dD1^_轡y7Z)i` c1N{AzyqGܟz=8vcl /~h4iXv~/A̷m}V~/Vsx9sa=z^ObȴRMV:sF%ϟ~.nNGb<0xzb1<xC㔦v]j*An"r9?\x'OR) H`ttcccxg/#s  aqqKǃvd2Ulnnȑ#X]]ŋQc?mHBhC m4dӲ `#!˝ez}Qqנ/*'m% (=08*uEEb1a! {~^W{{{8tFFFP(piς=F{< &?3Qб>FLҡc ➣}^w?5)0Y <|s=0Axu9^{I_[K8o6cY̵Pǁh/>oaƫ~/N#{J!I(C&(֙xۨT*@\pPd]iF?_ B||}(OȈX{vwwyb.K0X,J-V&!dHZI3f(fO{^uj¢c>J^<11%$Aw0$q4 rq|D3ȟ9NŠm-Y^L75 圱io,il6p8$Z8H @pv/^D\ :;SH 9^RvZM,CR J/vvvJEĒ. X__ؘxRaP(tJeufr@Gp$zLt:rycvvVO?4&''ҳ>9>}6 .]B/%ܼySRrP} p. zx׻%@ 4z/x@mHmH@nӦF|Yk` Z5%e'M&P 06uZM /c(IWf_u౶<Խho A0?z~+ vww199)&FvсԜO¹'@QvGF8A_GHr]&V+ N8Iq|_Ф*h( AiBKԌCψڳiw?Ʌȥqp$e>}?>@_-c5^__1$vpy~PfC1#KC,U]'Qjy6C=C Ӹ6MTU >& e:kqq$ ΢P( ^fֵ:(#cZ&YPcoo333S6Ȕܧ|^pL&_JEB<G<8&''c}}Z ?񏱸Gyo{p|_E*J4`ggNΝCXDӑfY Clr.n7fgg1;;}{X]] wz+҆6nFk&ڲ-Dʡ(E#hP7pc#X24ncF"_N[[["C9|l6%h/t-#I^#ٌ;C2i5 q"HӅtzUc =A\g]ل咽E2H$l6%slsq8wCJ-"t: Ht;P{ ';^n%c=}=p COll >h«_0z)aZiY$=p zn6h|d2y%Țfx^DQ[lzh%S#Χ.EyD(B4r011MG&ۍl6f)u:hZ(2~zH&"Stp JIL&#D  \)i1e2|>R)9ZMȿwɄt: `rq0k<#RcT \||(JX\\͛7E>V$iN_0 dcaF ՊL&bee2'O+_gEyJǑJm6%o(<6666 {EXQV133#5`za m"+ _t5om; ի5`ٸp ]Yi V2 9e#T*bu@Qn\.cmmM,j9?яddFI~i477p8 /ՅwI7];X, `R:- ג 䓅4Ւ*J5)Ж\-NK8"$Q\[b׋rJX,_.SOȑ#j~~F>& o~qUloocwwN>|nWH60BBDrr033j˗/V!\.X,bccǏ//" }{oVauuUjܰ(GR 4i}dիv0=NP(… x;lnCr6 [r,D"`J$/—ޞhA\lX$Ke,Ɨ-,ު&"-DIkbw IDATIJK/t&\ߺ =܃|wD&mFFObHaf! :8C@}Q-Pj2Xp-{} H2>-ƴi˹ԤP(|\ѓ~N:Uql<iB6~SFD'%u#xmJ̴!nԏdroo"qb1339n%+I*[Á-`er]A^hH%z;>!Na۾ mdP˗hEˢπp8hpZD-AHV@Y,9 k EPO-`4M W|8__F2qZҦ&9|.\,7c8W>bWUq쒀;@"ι1Nh,Rnt$ }Zq]wZ .=J1tzh'Z\/ߝN@kp^s_%bsνD  mhM ^/7#HH-cMb1rg27Yq4$7אsIOͽ̌VnLNN" bddDbH*\||;|>B!iA;v tE*yx099MD"qQft:%x 蟜8'|H^B:ykZT}xP,cST;C煘"3ѣp89vc}}Nn[zOgԔvxkXI>K_N8iB!]xdnW2sq ʕ+D"A*cRD>ǩS033e;v D˗L&ŐL&O}J2gՆa{#!MhЁA t2A _L] Ҟm-}W 4"aBT E`krN42L^ׅh&w$a3JGMP511χBW^yLVu@I7I{-ET*R^# \.yckc'!Yjy~0kX 1NcP,5x 1ܧ5<h:`CLs^Y#=2.Hrp5EIIiӑ$FX__21A|>E9w=ƚ4:F@(1T*1E{z\.łB ,OFZ֖H,t:煘fT*G-O:66X,R@ ~j]qt:L=>>頸+T, xʷH|(V*|t]~XViL&y&IA ʯjBrŰJ"V|>ߏi<3FD"hZ_͛7q~?L&<_rdNLLH,̿css ȰmH@nFE`Vx~Du'8%^mmKhEڲMЯH|j@tpA0G3J}r呁4ca58&٤n޼)@.^KfOyP*dI " FGGem[' ʾUtLY*j1Jϐ43Q9N!ʼnDR#hիXYYɓ'(JիWquuA飣Hz|xcjj l("ZT*jFy&8.\ ^Zy@m^6$ q#stN@g (6ʝxEЁ(A%J3O_bl?C %_0i0f0Lb9 jc b묖py<8qxa6 8Mo Kği S"c+qv'\=N}MV}ho$Z8.3/_a&^WjýE &xާ }FkQBkh ^ -} I ÁY" ɣL}-A!ekI=G %r<3$:\X,#dvK }ɄP($ϤT*n|>l6+R)CZGӑ ׋RF!23ZooOMOOP(76#Ȟ$ĄrM#ҵx<.Vٌl6+2̀t-nU8l~,GX,J$$/3xF8͛7vsg>{<6q[<[ݮdzkHe6 ^.\D<ǹsJdf3F ˿ַ6$5Z!@˩`N @]7_BkB@=(X,IA# lm;a`" q3`fL[c$R-2Nੵ###B0D>ͦ;96Ӊj*M HZT }ֲ&%/$Ap)/b^)J65l\3/?kܓPjʱqh ) qO\k}HT'EMOĹ%ygbO«&oz9ְT*% Yz~z3  B\HtDJ'> z<\.,,,V Aw:BIY k=ʽ {@E$K$Zz-^`08vr$XEP2нZ ah4d2V0h){B!R ȳ/HG_,0 $⼱x&1/2I^D"'O" =y]&1vHSSS(H&8s _+++B:⒬ ZzX"~u@(n$CfEVJp }7R۴iK*`9p`F:&|_H h 4}x^d258XCKR4u $s X?άĹ#Hvp:bݜDVõkvvQ.f $Eݼ^d5 @h&`<@+eYz gG{NG91rN[ fEH0 3:^Ux&jAB::::(<Iѣ 3?ApAܯ^4^Cm4̐h'FE2D&Ar!ˡX,4dD\)}|0K Wu(&3(1a INf`pKX3e-3xL&e>{L=s:ΝNSnpRod#&Lү~/ʎ $a$It:GA21[Gɴ_ı^zkQ$_?1??bX,\.u}sp!EDQ|ċ/SNImd2)=il#Y<^̩{jh`<a mڴ$FKZJZeh"a_jM@_zZk(c&/}BLFb333O2U*Ұ2ЬazL 8i0 \?LN1Zl$A0W~ 9vxʿޚT}=[z^ĎA?I{N4I1K9s33iqyN6LZ- ,//cjjJ9$co$?V+|>nȱ&I2T9Na7DplXFCX0ՊH$׋@ FU9e25ZGGGQ([G9g*I&nWR w:V:$@{>9<<;|tH \Fz-Fjv-i~HɩSzaXv8$I~H׿5|ӟO?l6+)tV+V =p$y|G\---rM{!tbll RDl Ϻ:mа7#XÝZ[tJm%m]&8$$MKӠ8xj/A-:3_F~A oEe% []UCL#Ii2ĒrPmzNn7B><(¬Ƀ@}}ʾ^/Zj½h$~ Ϛc9\kυqpz= "y=Z-dM'6d\Dt###đ#G`2077'GA"@ZX+9ZiLRHsV)xӛބSNazzz|۰ ܆۴QJ G=#:HVk@ 4pwj&F+*ǭSq Ƙ ̵%0JG8&$ VNh4 ͆l6+|j=^0ʂuoD"!`6Hv'zb);::*;Sla_GGGD011?ȉ٬܋sTxČ5ZE'틀ޡX,&]ؐFʿ(T*TW(x0xHHFGGQ*v%gnaVx-?*2;}Azqa3χ~}ϣ\.g[V/Ѓ>gV @@^~bd2)) z&0 @m0,Ic$($$ljRaӱ1DR,OLLn#L˽=$:bHDVZMD) =FRI<%Lo._̰Ev%TR$$B|=Q:Nt:qy'@\ƵkīW~_\\DE"/2<666p]wn_,^xᅁ|p|.K I- pcyyY-Z-LOOZ_D>LJ>!<җaaڐMj5/" hd8i`P~Hc'-" a`i ßŭ%[i0,OG:JxzmX,LOOcaaA1iI\8'VK<&Ԗ;NNt)@СIǪS:ރ ]{9RZTji^O=T* ɪ$6~_"p{{{T8>M48E{JUFyqΌ#+&F3e#-k&a{eJ2@J!gEA}|P]ރ=ے`T* bg1ַ^/3^V V GE(*ժLTPQ(xO$Y2=,@=;|>0.j󉇢ۯjd} &1rr~W=gI}>4}z$jڀH=w78ωC\pՋs! YH_' `FedY=zǎ9d2LOOcii @8~8bJ N# #Ht( BY\'+J͸f~x[ߊX]]dѣGemm؆oCr6 Ͽu,-`mٵĈ6)`@uڂ3 j 9X5Yb5A^jG{^-au2yĉXr6M3u*% _LivLZ\7?' O@@59ᵍS7-DN{Icszy~PM^15iDp\~7'gf ̿h^K˗t{vLp-JAdYוJEo_i*ɂZJxaXp%Cj3Bks0557n +HҙJ9cuuu YZg T*햘.ÁL&#kB>\Aq\u:]c2Mb _zT(9qxݎH$"II"R*;(bIM™v *n7oZJh4UcjsΡC_u|S˽ދsڵkr$l6K^{Ν7Sk޵$VcKh4?!@?M fr^9`İHj@%y>i"{E޻$쟎wV~+GfҊ~ЫFb͟ 5:@"q2΄5ʧuGGGG /СCB~:"ol68Nx^r9T* ?!x;;;BB(˲Gm6d=ppYH,)O't:D"X]]GT%3ؚFFF055B c\Gx<&z.,psnZMl LslZ%×N+[iap|&COOE+&3ZZ@0hae?ă8V ^ d´ޛa L&mgϠ^'[.j=!6Mx<ttp AF/^7~|8_:ڻBkZ*ƀ[4qeZMMW0z\h\rqĝw ߏ^'@pdd^HDդ(#uKdnZ Z {T%cߗy&)Ϙf!HHr3xF3n^ 0l?AD"O_O`K$Jj^Qzz  Q,Qx|> ͊ş@)'#guJx&٬N{ N$/JΘ`0N\^z1:: ۍp8 ߏx<.ħP(3䅒&p8 -7-DH,$͛`wwO=b}}bQe~t_22VVVH$`2p8cqqQ^P&I4~łk׮assS6p]݌υşaע = i#L( he--X,"A2Dd3cS=N IH0dþF j?pRicp D[u d 8_&V ~ZǏKɤX~z^ 7Ui!W$杀P(HIt. 2ănzL1=70(XĬwAօ;fLLLj hD"dq>_|T NW\O;9?;s\F.ERER.R)P@c m;.EqĎViM ZVcVUvLKdQ.$Kr;;;^g,~gF6[ݙ{=99V.mmg=ng5Ro!t*OIO0<\-7^9>@c3'r1ad;<kx֖\T*k``@R VV^xi###LOΜ9c`ffFo߶D΀V1!>sHq<"֝ *6R!9Ćhy.Sg4 MQ:6'?I(k~~,Tʖd` d~{'?k9F&9a_}<_I7=&^ ϛdbOami{{[Ɔɤ~[ vU04^:VRQZt֖0D{$uȸժJ!R)uZ-)YCH$T*d2iYժY1XkVKhvccCLFjV KYKJRҹsk}}]CCCZ[[S<WVwSl$)jrrRKKK/n+Jxs0LɂݼyS333{UTY}ݺtVVVя~{>O_];wNW\e;55%yQzk_~Y߷;<wi-x1xB~x& t:5Mv>x[Sg5 w&^\[9#h9dr/!"NNN*LۚRղdA$U%ڮ^P(IVju +t_bX޸?c X^B844'NGQ6s=5N GbN<z饗\NO}Sg?iMNN%I/^xHXOH(z)!@@>W>~g~s^B="bv)"g)788hd2)I0p8l do =AjUZMdRDBbQ`P>\fi0XP(X,ĥDQNE"5 U*m9rD+++vU*0zXoxecccyɤ677MuMkkkvNDQA )HX}7nHe+ eN9ψe VGo$#ߑHDzݾe`OX_|Q7oԩS4??\l*HhhhHO<~gVT*eb>EQݼySkkkJ$Z[[=1СCFL_yݸqCr* R{U Ѕ 4::󚙙;" 栽 ] @bd`ﳢx pqX}+@@@"g?v/ߡϐ <")'G~<2/HAQ@ of4޺l6Og6>h^[qmllصbudlZ%'N̙36.>Sp˸~3/HpjKBft!MOO۳Տȏ_ޫŽ D, kpF?}/ _'H_тt]))Ro;=F  nw?}=7?) :ue"<"3[2Olxt%گoZ[[ӿZGzxG'Ҥ4 9 r:99i {C&_6 À DQ}ST^scrra6U>5I׵dRp8H$ZVbhRCࠑ-sA^G-$ /p P"Ą&'' UTTT/)j<HqjdUыŢ:|>ojccÊ2x6b8*JFH`a)LZFlookyyYPHfxXZZR$OOk_.]e}[֖I666T,5;;t:=j6 :zǏ̙3zg}MBA>hjjJ{~~I??aON7o~yʊ?'7|A=_]??=}+_Q׮]<`i_Cq}ׯ[:voomo:wb666Q~]pAԧ~T*zf_~VW_ճ>|3;~tEIx^ M2[QyvcݯEɂϦ9 ާ2c ϐIq/O c<2,y2 >1ʛ3b15 E"}v*Fш'ưl%)LQTcYh4,keh_akԔF9~WUsss'>a˯P(d}c>xZ\\`,}`k,;wNbQ@@6#>Hzb#?.cu:􏳗f~OB2'd j4U訊Ţ$)Z=QJ%Sic}:Bׯk``@Ν۷ڡPH{vvvo}2!YYYÇuMmmmYlD r|@?ժyCNI⢝w$4vwlݮjT.mh4by$&&&%ݾ}XE$ٙ:V/:tHU,vꫯP(##B5{ժN>[n_? .jϞ=kgk6zWlb m8qBO7 Iӊ7b1IܜN</IO<55ӿ֮/~QgtMCDF/;c\z;jPɾ)9Hmt ?Ѩ{9us=g$AhH]nR7j5ۜ:uJOVW)d% ٙN^Z?{-,,H?}QaI:||A}_5\ci{{[z}L˿}k䇨H{{?co65g~gԍJnk_/,􁗤K>D- Oj?/\XzCHo;bk1t_G290_$Rl6-Y^^x=\gH$t:mĞ ;<A2b1+@Bg#48A O OI$f6)}<#mX9 &cj۷oOPOƔMի]hbbBgΜʊUa%o6(9HXJOhssS˿,i_!Ɋcdv-q^8]p >Oٳ_7Q Ÿy%.ɬ^#xE`z%)y?I2Ŀc c4>=B<IcUޒϵx1߼ySV4ay,x'b~GQj5#DVߺ̓uB#^K.1Nߒ~Ȝu, $cF19tV >.PɨxCx׃9d1V~}n!k[ oO:Cɟ#.J*g# $\,xa+8 E>FmC qN1rc !Zx \\2BIwd{l M2k4`PvV^IK BVy+GJYdGHO1/3$28Qlmw!=}]>|Xnbr666 W۳3o~~^PHh7Z-E"E" [[[[[7d(ґ#G'NҥKcg5yehii e27A[^}s__?q訤}bH>NrYw3?퀀 h?PPX k|gOoعvVeL?-`\o/$_o~:~G'_&~u콵k{7 % R)MNNQ-..b`` 0.Ifd\Z` P5.$kdge f\.\./ߧ &ړnk񦧧ptGVX3>P[Y[x]zx=$hDŽyBȸu=!KIV"<1|˃~b8ĄY눹'p!9/388hIU5Ɔ~Q+H 2AJ(K2|uF(b3g&%tqOOloSh^ Pj5b1[x[0u]M|3˓ν=|ۊJ$:qΝ;7nXp8ܓ rZ[.t]w !eYuݹ\.)mnn_VT_ t:zgҒ555eXͫ}|SK/{/NEedbmg8:uJ=fgguIhnnNR8 ͙|.胃^jjD@;վkk>|3_7> ~__4O>?~Q OOt޶v@@ކ6>>'|NJv/}IO={1eY }d{-~KK~q/D x{nyO! ~G Ie`gО3v̋ŢjΜ91u{*+AD=Uҝ TQ,STe}`a%6v:wlssRr_dsGY:55rH$#GZjccC.]2 2 8v-ٳJ$Z__} T{ ֯Eȋ76xL'{tR$ﱔd;skkk:uƴlj A/4N7{Zg`evwwWz0!Ě$}GlD~ƅބ`0hnWD€o04%jqWoD"V}}}][[[&_"tY5 [^"FuСIBp>^L FQ.S$э7l)PHp<$xH^Wȇd5M#Z*{3_8{ƆffftIuu:V戥Qk?SЋ/h簗QR)ҥKfPy[E@k??AOoG~G${H>}Co{pM:6"d|@=>ݿӧOߟs퀀 mxxX/>۵Q=`R! 8^Z>뭻y'&ҝ>`灳{[RۧD&P_^rƸ!8xૢ]ƒxO(xP.SPPPfFargjUL"/j@"'LJ'h٬  CV%@ZZ׿ue@0ڐ;wNBAҾRXSt󌃗0y/kݯm=2+@tP_^=K%[7|7<NRJ *cbb¼^H(k=<"S^7ښ+E5;Bm_xgg"Ź cl6DR)eѣGu}nڵku}U75lnۚ8tۍ 2E#x<ݻV&kJ$65Ɛ`0KV`.E/f:taݺuH{V!1aok* T6/wS{Gc$=~  =&=/~7S?Oڲjv@@o޾!* xۯA*^dX@LbdY07K2t"|\ǧV^{zcB4܏xK?@xFL t7/v^zPijUŋ-kWT]Tl|.Nce4ր' @1^&Ă'$3 ΁8߾9e#H | 6@ /mqb1---YjnkcyeRi% w]wiffF[^{5?{kppPKKKj:w\O;nȓO>=_… V>Oꓟ|;ؾ/_~vlT,X5y5c@B@z^>.D,{QtFFF~, -Nz@Zj:.;@OKҝ43n}|?#s=OgL=e>ummmmh:L*ݱ"CL=4RFd[gfz@D8W󚜜gi H$466SNҥK2f2MMM駟ޞn-K-\#+צSœ>Oͬ c%O`LrqV>^8V&Q,fh4j2<֋$KJ`J$K?^b 00`k4=1 kqqoQ>WV #DÇdhF2el_$O Qk4Vc$LZ]3ԭЙ3g˗/P(h{{[fREU.Sղ8 wvv MLLyu)> BrdAxV^uϻ_YYŻr\.!-,,X,1grYXL\N[[[u=? d2J&۷UV U*SOiwwE%v~y6@vvݞ* UBɉV{0 kUs=+` +Xk\<=tzrwɤx %?{-,Oǚx."0by'덗0!,>`ޓ,(H(СC*zWtD 8u:{YB!6(a^Y^k$n >22bT65 VZf%<2|^}{'xjlmm)5~(҅ 3Ϙ ^/wt6Q/1"{r=`Kj~?3n$@$;JEKKKYSxfwRQ"P:V0,O`b=/fSJň^>XljhS⽭T*V&Q<Բ@H$,+kBGzZ~G\ښjRٳ17::jj$bH&&&TTzO4Ԕy O$ vrO`遟x+~s8u{ky&2_MMMivvVL"s ---illLJE뮻4>>r|;d2Z[[S Ą٬2nݺ9EMzn_ Ɓ@@6];ZY__70477g"ʩTJXLիWkzzZgΜQЕ+WtQ _Ɔݟ}P*zx04"HWQf0~x }Zg {_ZiYț<1cOA rbJU^444 0J%[BA(͛7%,//i찎ȼu u떒ɤa#OVX@u<%B$nQ1Y‚ᰑJ'OZ]]URT$ҒN:{W7o/X,fsktmr9^j||\?c?˗/+ 㚞?c}‚ #2yAhX/DB>ymnnZA#3dBFg5$K/ihhHtZN‚Ҿ}di;LNtjZu">ihhHj4ŋ7 K^qu7A;hv@@ޥiȇ~`%>I>xGꄇ;_ #_Zx)j IDATZb^dj˱/ |8vmx@_D~,z{iXѩP시\ḽV^^CfssSn‚]I3Reӳc걈#"@ nu^bNz2 ta֭[=%qVxbUyE"J%r?~\XL`Pvb?-8qBN׿uGh=%>8nYSȓgzCП dQ>ĄE^L<[f,H60OH$'O95VXT>?n^6pFe2+닙5dݱnYoxuhhHccc=dRۊb6*]enDriّB@ [nijj 5l ^?.\𧼁ߚ{ĉ=2WkZq;l퇷x@ޥ [ҝlXޣ3TIz0eQ(p#IBC |@[uGJR{xx>ÊMH-J(`gzHK6COwNg˘@\H%8$ t`W$ѵk4::j`:==S)RtdXMS,"{\*,OsLV-kWfggGOV8ʊ^}U=zTNG JӚ]b岎9h4~U+b{̯~'{4YH+|T,3ҸMNN*Hŭ.}CCCrjZ\\ t07Nf3I6&X`e[PȊH9r<%>FjppPjU}{533X,fdĭP(h~~$O͜PcuuU*Q: Ny8~lb XP*,n$V6 Z֯[nY訢Ѩys@SY*nܸR);vRn };Çfuu#bs`NpssS|ޞJOˣGVR(ɘw蓨{ͦ%X]]U0_KI###Wi?^cccַe2#G^kqqQЇT(t%3yV.mF_^=L de{)ِ~@5t=(z$\K3V8 ^|~y qqȒ DA&exdXxb&ϭ[L5 $YҧJ%~zzZ ɐ/pFMW_. `P\NT[ k׮Y\ ;_O{Oٯ y̌U\&{1 -ռ :w/]diQY b1"/֝tNjx''+R8H-7:ț ^y啞R EQ% -..9 m:b1X L MNNƍvT(2 GGGm{{{VbKؓŹ'o`kllLKKKicc h4d2JbkxH$bٞժZ${z2uϫV)+kllLy; aӱt:|fggav9x/P,SQ*22uJ%a;;;z*g1dxJvvvYΪT*)Z^^6D Z[[SVؘeam栽퀀qҝ'VDMl+dxB^Z%4]I|VKA VcyxK$X0p\@K_y@|!/Kxo1r΋:og^Rs``@=I#Ѩej6ކ-RӱdHb )dr9hNq >b7o 'ޢSNn͛zm=R,S\ MO@"ܿN(@ _ Ɔ5 Çͣu:*H嗕m?wɤXIyB`u2A|ȈѨeL zOx&'' R)+h E請r9KeuWwr**7zn;ނnkOzmUUKF-榎9H$bUȉg BJnܸMJ%[SSS* j4z>ƣRT  IQQƑBxvvv@0!8wG͔ݸqCۊFZ]]ΎJŎmoo u -..~< wixP"zKtd?2KG@5 lLP4>_ gdd,>xzB=8=` |p!C4松uø8da&<7)Xݮ]B97";dk"HhiiI\捀۱1uS~lȈ٬jeYZZR,S4LBpت9[ZZ2 xկ*L*J)kppR`>Ē ) /u A[Bj jN&ܜw*SC 8' )ƆA2ƭA` $n[@*R2)Y[YYBxPԃw Q2ill7cikpggGX+dYl ifѰ> ëAp}}݈%=R;d40R6JieeEnWlVXC(ٳgh4ǕJeMNNAlY|^~?QT8Bf$OxxNX Y쬝j&Yq{O2{,xIOJ& @ M|׏+g=!OϰP{xՏ9239gZTR4U*rL&D8Y4ScM"3<R!.aŕ&4^5|x@<?!+Ucy{dK|d{$$9 `HLd¦B?~L3Ȋk![g522b g"k:ޞRg!S):BNDzE"r9a-//ۘj5X0| YJn޼ix +ש`@9|1nWbQFFx8P%LRo1C%akl9^k ǿ gؕ~ }yA3'7x*Xae1^d¸k\p|L$$|ƁZ~!C_Ej޶"~271x=|^*˶f=|~>>`{2˺qya U*z"B!IoˀY̕'G"f ŠGu j4޶x1X? 1iGx,Sdjt:mKi hb$ѣGÇkqqQ F2;ׯZFGGJT.-5p2T4Չ'l6-O`o9350ccc6x8? -]+2988h9D"^Ib( ؘeCh4l!Go߶vv̻y@hV>'' !|X/Xdy ' @R*{ssuyAOr{|*Bx JxkzX=^' !D{ٰuǂw,Vĩ@ d,--)X-d%R#ĨviDB`b45L>*OYH=H{\=TRO:bl}[XCJLl6aúyjmmMfSj6=Z=v"s G"KkKV3iZ%PT`{/t'ΉI q O qJJu-//['~ ]2z={ _ Ú L[3eA#лF:xn쬭ZRcǎY?MddlȈuw˺{h4d@irrRׯ_:v[kkkF95O&D(SSS֖l6H$kqpe%Inoo۪j={huw:Sޞ*TMֆhX68rbQځ.m{0wz@>/.y^ 2?~/ {by}%y zQ?1= `hչG=7^fZ~ #InO<$w=1+?Vt'!ydR Jb@˛{a]? |F}Ϗw}=xx)q=q/5!Mbx8FFFZfk{^BJbW7+^H^j iuuղj5 رcOym !&Iv$2)Qkkk#655ez %vwwM oCM dH*fͳ/nkhhHjٳg `KD6LjvvV|^Od2iWng}* bye+=4Ԃa}@HDZ^^V@soOp.rcDqC`p?3EXL'NPZ#(j443i9Oe%Ց#G"|6-<]#d-^^CF?9ndJpaa"1e7LX11[kŢI:^7zFfd|5X@ ڧŰLJtόe8Ň6FZrժUO$:|^}Us= 5 drvv:iLjP~%%HɓZ]]TC˹jlOHL&ŢRwn]x{:8,c{^ 0tzG7Ep^22`po@"-%ųx0˽<q2{8a!: =`\g*FGG5==bhE 9ƤŃq"$nj)Ne9Ɔy:|3RR_ |bbjm ƵmiIs3VsxxXH*ficIwĚqm-,,C$cfOGIz1 , Z^^?PjmCHFQ\6aZU*<'xB/Vfוd knnNfS Q<::l{r###P&*FFFK IDATtQE[;Sf +(H\.xB8 BvۂӋŢ%$ .}v߼yH2A{+y6/zٓn:L'-,J}2>aMA@Nl&ݩe9~l. >tR ~xaq *@?n3N|5}lۏAָ^O{E$Fp]>l6F%I|^[[[F XSH<<g,xI2}:e p-/w@>bL !TĤOV5>kuuG#U"ayyb?JrrIfmR)'a\8bPHq{{j ‚:dҥRg^뒒kmmȈ%#kweyoJ%5Mq#W(y/ILƂYTJJKCCCVk>nBÑdߣ>#ipXZM###Z]]Ţſm7&ZXXr$YPTlO Pv:jj$ xt䂛v횭v{((y|v, ÌHa:tH`P7n8mnn\.+عK;dq濓V.mĽÃ2/ V2 v4|^Y@>s>+E`,ϬO@.>x Y'~>V\VRgN*2"WUT uOOO+^k{{{$c B$q/,,oիWub17 _2k0@P ^133cE<٬5?? ֬~tm#o߿o^ʊ;.H P(y{RI#/C9#s 7Kb^OGֱ*2-ɤUTUU4ɓ':99Z/W^5XRRQP7H"I3]}RmX{x/ +_X !ʍ ^D ̂D4*|H)%߿$\A?u;Yá!u$ /rq-^qtsM?X ^F?kBz4z/s!A:Q*F]D^6dZr .F*˶/l333ؖ|zjwww$31Unb߮풄\]g!~z5n ܒ샍Db9`-\cNOO&ua޽g7 1$" or'9Ab LLLX\{|?35 Uyݬ?>=Tr_[>٧e\zY/2}ޞZ鴊Ţ TBrYW\V*R> K)|=I`7^`>z=J%k~~^O<bNMMi~~^~b1H{'-.ϜR3c_#o4tzz9ٞ;>>eF= 0jZZ[[STʊņ/Ţ$Y8y`<"5//&bP$r& ע/y'FC%]VkD[~Nu`pn~5z#b̬D;s@_^GH,KW[[2BiaaAjUZZZ҃455ey!;^O\NHD#vl6&eO$0["P&72/bj6z7)^1q]vپvI@HjO^ 34Rt< )4{k?p>S>eH>p l^=iL`!EXxX4*s≈t!b|^M_8p/]xz/!3˃r?Z6"SRаC$׀|2/CU{>ȓ?߳yX C<'e,1\ǓPHX̪VM*\S,SZ5bGmI (w||lٞɤbYChv+kk~~^JbG+A%X,4ʕ+#)K2וL&jjsF-yS5I&Ls$ /36٘yj,SZ%|74337nÇ=J$6tb!&&&L;hzzZoVV3rπ8sO /^c6xζ푤dϊbW,/XH$3---fggx-HA t:FVr(v{=}{%\]gy +~y x}|yZxPeW޺*i$^VP@<.bc$x>B&]d^,z=m [>y^nAbb I)a/Kn =6z ՙX2x!_o|?N> ɋZ"Xɩ+c#ZHCO}U*#`bT*5"$\9+u3!*_Tf}6U86FVR?o#/mH$c$$% &XL|^LFGGGt:Z]]"txcֈ":(zPTzԴ3͛7}գG,ٙ3srr2| a lOđQq0Xjbț'Dz?= HD"lmmM|-B!KLɢw" *JYw{ *"& p88p8]vپvI@A`{+-EAIX,'ds1/Ó P"q@\@7.ԉy`y @U=:OK|_A=,~@ؼ|x Y|)O>o^IWO4{}_Ⱦ >{Ϝ'^~xcrrҤ;@lVޖ$-,,XzfGg->C4Ţ B`]΃hCa@>yt TX㽧T*V؏{RXP(Zx`7իv!N9'@e>`p؞XF-60IK/dAd@9c  DTDAP7Fl6Jb5JF ٱƅ':<^Z({|iiICߗd?+3%jnnNyM.[s&ds٬JgggH$ JrGU(lJLMMٺ0fF֩;451c LLjU{{{ S-AqM !!QF\3!DBdR'''weFT*eϭ~D"aDާ1l풀< Ase{/^oJ`># +C^I'H=&}/Ku"Vr^^S:>#?8bn(|0q{9⃾bZ\2~d6ehEC_ ~x` -DRo5\s *A!xmO^y7TxHLEԽ{T.mH .J)HVzWtUK+ǦPejjJLF05+qJ%HoHV"cvj)`0h)JŒ0L,,}!A"pxCC*j cP{{{fH$b{晁G3"$3~6Fl.%Xh4|A{'P K2;Xpmk? ]Ĩa̕t^ y<9ùmDԓK -Ҽ,X]Yw: }Z#} 2߃v*/?ϰ A+D6~O_җlONNZZK.D}vvfIw}3!q"[YYѓ'OT*,s$lx%p8T*e3r<"ΐjQ/,KPHz]<xYr~^~K307t`y`'C~xyyO}/,70^ ?==vmsCVe<f]qOבpVU Xfseѓduf<_˽jNd,襗^vwwuvvGG IDATzRA r9iqqQgggJ$,, ZXXcԔɣ E0`OD2K8`PW\Bx}HQo4#uxxh!p8T:V(lZt+Vl򙺼Wɯ7|zP oD| .RB49{C<`E2/E&'>x/'l=|{}qL~>%0cZLXz҅ݺuK@`DVBC055ehP(dS5vMf-EVVVvϫ\.+HhwwWN,ۀJ@$1Y_,~_{{{VDdzL&dd,tͫ>fzs}<.zQhs7JtUqK{Kl ӧO-.l mկ*փh4ojfWSVݻw%I#{=mcO鑘A=fpL"^\.K^j tAZ-N,jivvv^M&F#3h43U*u<3AvPc8]]gy) i4^ǭ^:9=.R^rN>E {$y2=/@?xB Aq S^J:z/%Z^*{3^jqnOngl333ׯ+ˍ1b+rTLƈҾF2B̟T*YZZ``ޓi5 J%EKzmKwh4 H(V)ٙ٬:٬ `BlVe Rͻ[&?ѨEh~~^F6`Pzݼ;@vҴb K*JU*df UB*U45/ԝ;wuM+2N4??L&jj _'*LڼJ%{4f)-o Ϙy#</| 4RGi-//}YմVeDY0qI<Ӊ»W'(LڞByC7elE{}}k/^c(p2,/Z~p @1OJa>1 ZEcdA^ABۏbE&#]?I#kJ> KtgKO;.b?5da^} s>3oXcǾ!F>#0FOn{{;;*J+bT*(Hh{{ۼ ȲZA{pP`z 632y}áU'=3q8b_iu,UΑA.}L$---)W(2!IGGGx!e׍7P7ֿvwwWᒄ\]gA<|J]`V'//p_abbbD-]xk<{itdtz'Ixݼl}S{J̺HA0 {&|f0$Rg mmr|'0:˾v/b|B&|w7N13OEl$ &xXjղ9% qttdYEQjzzRO+G QO 6wxpx^ۢX,jggG<sssid2vXL'''F‡áyKR>| )+Nk84jooςOOOU(L&c C1;xub1q>99it]x@NNU(hccC|^=ҕ+Wt-UU D"Wd2``yݮŢ?GT*o7xC?gaZUQ"yvnooY91oȩEƬB`~ooϪsS(Ν;Bfi\.!XB!#7j՞m$aKRl6Zf)dMI p`Њaj{{{$el/ngz={kαՖl2wI_r/gd+Mx9jaŘ\{(> XȆ܃qO?<-n P}V32 SIٻ~  c5.P/xt<?^+w'''M*{<<,>΀X9,dbeJ2C_x?4==c @oVj+c X``!|h(N[8D@t;Ln̘Zn߾ɉ*^dZ/ f)J'*kyyYx\*˪Vj6>ĸ|QW^5y`:V\ jt:w^ iݮJWx* Z__ښִb7BxooOGGG}VWW577+W(hggGz|M>|hX,T*饗^ /T*Zfx CDtAjexHvvvrl{͛7u-K_įP>55vd2iVOZ*̼AdZYYښşxё=HC b`.jUi6i??Oo~~@x\tZ??w=~siffFkkk7߼kB.= h xi nesJX^/]X[B/o>+3G~ ' $- /Ê|AXچËϜ{BH~YȳF@@&߇xOd A cr.@-xW3yt:{sk Pejx T05{yia'>sd%DZrlrp8l@O{{/Yl֎w~gV??/~?}_ﲽ풀< )Z'h؂? Ae8$h؀`نx5BG{0/볝l=' 4fH kyx[/+D@`Dyd7^ s/Wߏڐ!2@?O }L8aԸ__@i86O2Db=1`tdL \5`Pܧ:j*2jh4rUB _iau ҝ;wcmnnDTdڹ9J%eiqmmms j?I-//ksssmݼyӼCXUTdzOzzWuUeݻwOLƂ㷶TV-8wtrr^{MgláyR*49y^"D?Oḍ3#bT*9eNz==yDgggZYYQё*~PD"* *˺yZ byS1&Ql7o_֮]J"I*J?}P8Z?AmllS~s?s~$I߫ng]]Jцet8Z2`˱gx b`^`!VH*K9yYXFb02vƋN~ ؼˤX}r"nkOO3^_2=e S|NB˿3d*C.#OX2yϵDxiP!8 {l'~3Ҥp8ׯڵk&{ r杹v666T*Fb5zBCiu]T*iggG^ςӤB!KQJZf@ /ɤ?,FC|^?C%I]~ݬ`PW.S:!\N7oԵkהNMRD02mqqQLFnܣѨ`}؟W677͛ fl6khDD52H$b ӧj44/ܜv۷o횧CDTOlVvV =n+NZ*knnμpVK@ZMZRľݵg.q9h2}_,yMNNjssSz]\NDBlݸ?xGU1@ݼy4(D\T*&+JS^W"rJOf4=%YO(,z񈗼jٻY3<a#g?Ytzz9Ǐj8t/풀<[TT|xRf%{ǹ|q>!tK~Hx >`mۀv/xJLq,!,>,# MNxB臏)[s$`ۓA''~Aw>O/AXG_>:߾]z92ɓ':99K/5{cOz-ܷeY~\ov)zF[5+j+HEC /g `s&x0%c^XYCD\Frp d$^b511a{K4w43ɉ9kq-0~=xPkG62{zzjB!^DŽe=9}=?z>FwvvX,f{P&Q>A5ܜZIp#"ifggU* q%I}KF1 LI,RΝ;zwG,2:=55^hDMjUPH#AؓժZ:X,ǏkqqQPHjռ0F#y4٫|;whwwWZd{gzzZZMW^ܜ^z%mmmi0apOq< Nu-mj5[^fx<{jtM5 sss z>ڵkQ2T*p8?@db1~~K{{{Ї>jBW_}UVK+++_{.,,q;<džbs3x@ XPRp84oIJGPȤ@nWVHxɼ%zvvVP=@CJ877g߃8-,,R -NָT*innN\Ϊ3FHpxDO>pRd'm_@ۚS"@2A|@|*7({_>99Q"PV))()Xy GGGjbbj4M-//_V,[o3mmm]X]__T>W,Ɔy`/X1}_?~7|So>)HQz葚ͦ٬nܸH$bq)ӽ{H$13@@x\Ir=KL^ɾ4۳i8|h+!Ϸh$Q2ɤILDQ˜zt:Ui=|ЊZ-)~zfZXXPPI3TEQ5 577g$X"/3eR45C`0PP0399ׯ[)Hl?W'> ѣG&+;==ёVWWnfD"}#я7Ύ(4 jarN|^1kȏ%>Mh2*y Y|d<{fr%I#=\Y#>{t|_<':%]JHކqIdzO'S~!+H?'d~їI zFn s?33Zf@ M<: d*b Cmnnv>Rz/ժs [o)]~]'''V#EE!NFMH$J ){x~@ JL[*X,<| II!T2Ą>y֖|>iI tmmmYƫ=}L L}R<߾}[r_k>% }_V4ҒկZѣGV̌*eP*2bCH$ZIU*K쓔@ I,Ν;+HL/--r9K\(#311ygZ`0jRfӀ.)K666l/' UU CF?>ѿjsbbB]_ kggG'c~H333_ {qپ%yF/e, %Wၘ/G<VeHHtg'/H=+X ǂ/i|{+?i ccN m\ɸ}|%O;_X!¹N\_ke2/}I'O?s=7풀< 4.y<`Do:{. t2= 0X}@HŸn`A ,{IѸ†5Ǧ?X y!{B-^" \2Oڌָ'~=gOR4=W7>XybK $V; 6*=/Ofe~~~k0\.jjY+x KRFGV󔠩TJ~Rrmrbfggm3_0+N=k_W(2};;;S2K P6U^W߷w(@ԓJ/wnL׳ /,,XjTPfddjUj۶^^FF_!FTHc*ReF񱖖trrblrrR'''j4y}׫=x@pX{{{򗿬۷oRhiiIz74 TTp+ _ ~<xH>}jHDGGGvZYYQZIA:Ї>rim}wzzj'7HDKKǨ,"\;REۋd#x3 Z3qmooK:3g>vVWWu-}^Ą_ԧ,F/T<//S?W_}uvI@"K? 褋>4W@ w">Bq[9qs^7񈌃x̓v1xCDşz 0.PO|ƛ'P|KE0}8y5G8qI4{s7sA $r8Z[29ML;cI&V&*ߏkpzz: APu p1Pb1 vT\e=zl׳w=BU!'|uuU|^O>U6i`&6+Ib:99Q\6CF&jH sc }yl >rh4FSSS&/bH˼gYX驦JY5MKG T*vV믿D"9}+_ᰎkP(7qHFV3A;CE(Lg( |jSqj5mmmzh4VԼlV+++&=u-F; 8}<&/_U<ڀPoaEC_M.\ۃR"C`m`Kt{9?[g/fk づxr)h9g~w#CKƽ22qu]=1R' l/a~mPd$o~$x)wAI2rIvmzI@; b|j`nfff,P\V,3 WWQg?k-8vo\.f+WX Zf$x7nP,3/b{l6JcKsZjC Z*EMhvM^FG0Cܧ9{I<@y9;d- IJ@{Qdlނxu7蓗2I'0|yи5d)a>{c'v\ 륁 Kz a6SSSXx܊=6 +}R``$!YUmx---Y^5^ 0}#vl6ABBIsV%HhT !d G&h4l6MZ- TFh4t5+T*o  V̌y0RxW<ϛ|y'{r 9qEZ[[ӛoEQmmmի4p~ɤ=z=MMMY0T60TJ7nܰO<ꪊŢ8'~_bQn Yv:5M}kZ Z[[9`GQ "b*VׅԽ^OVKTJLF_clT* J&Vɼh(N[77 C0b2yػcO 穮Ip.e%yFAz+o!h_L2>NpP^XOldx^ >'Rq`Gw` FzA:^vysE_L<@x9x⏟2 }NLyb)nkVi 2@1^ y'6\O/pXiinsX!x2 FF:<<T:@=AH$c]~]fP2`skͦD7 Z)֌x_T*#ߣĄP(d1# BV?ZutHS*񸶶u]hvvVnW[[[zZZZR.ёn޼`CUU4O, i}}]B,NǬ@`$fazzƁezH!(k{{RVg6X:VeE)ָ-}w~x C fouݿ_s}7~#Lf$vvv]ZZR0x^vmmM*& 6ڞkJ$ɬP(h}}]O>59-R1J%KHX;{Wz`bAyw:⹂Tov魸l*풀<- YVq$G3Л.gt"#ebns^;҅I'3=c*`K6r 7pxzR:x 223 |8c+^ϲG411 nWj4dLO<"Xd2iN }rYmѢL,Hl65M z~~ޮjl->|Xh4E[Ǐ۾!Psh4&Y[[3O0;CvYr9+Fuh4)% ۏBABAnWZ^^>|b],-*e=~XGfZ*Ni'qvb7Xl6kDɉHaCdH~K2y]zU#~~H75 Gb moo_WK/}sښ>5 L&N-5Miaaa$> i~~^y۷o[* innΤa V_ X2C `O!D r\1#㳠!zO8X{b$$~1n[ c(~UU6nܸ}摡:t^TjuF`0JR2\j53zТC>'''U(F5;;r`-C^0JZZvi uE"  )wZ^ytt@ k׮^+HhssӼSOB"ёju55MuAGdƍ:9{Yc3r9ܩuWUwW n0ˆ bdLE2, l$, Ԁd*U9gsdd_~'ODT{F}~<<906\jɟIimmM,k^\\TPgbooOZMH*4A666T׭e{oё5fL&R0ԝ;wB`FP'''?K2 >һZM??W~Q_kTV3}K_oGi}}zg`mAI/lU~~tABڇJPPTR0TR1"o:>>⢭Z+JFV{ѭ=a}}]v[RLU*t~~n\}3:~٘RFCfS@wiiɚJM#4ۃ f-2H|??as^Ӈ~hr,䞑HD_nFjj^Bͦ~wW~_oU2k6z7D~b1B6/KKK裏Tuqqa '~F+ޞ?~۷oݻ:??7 ;CƻVLGQqAmllX3KjlOM(L񫫫FrWWWh4 ]7*j47׫u^z 2m>~t<"?d$^D|U@A4@l6´' IDATo=i 2%8>Պ~/"B !4f|'%H!H7 > ,醘h!t9__{7+geWL&3&(#x822,md0{dVWWdLtJb rFMA>cz=={L{{{T*V E^D x_]]߷s;??7 Hx,ښ S*AB:j5 0 z\V(t]w뇇Z]]h4ښ񸮮TT2 ɿ7~7T?oom߾[n)O>w]뿮'O(N׃,'X`aa8R!m8R(٬h4mtB!} _PSXԟ3|f2P(hccCO<'%J׳AP},$g=ؿ"J߿oH$bF#W3 ysfȪ$Vᩬ /@ti k ^ļT,1;guda~84^ʄyB#iP#ܫX,fflQkA} 71yqi H$4-2+]Kl6#qr9d\0tJOӪjVIjeeŢ\GT2D@ຟc{FR΃ɤu0~"mAnϖL&n-0@T*g;SW*A2x<Ko썍 iuuBéVV BV g=,:;;n*xR9P )DɓC_`}Tz: .x2FJ&{=-,, ++oWc/^\YV5zLc&G\ᓩVcX^*$~zn^$"n$2>OcN@^X$ 4H馫8ʶ6|(/ Wc^ 1 =p@ZS]g>yH7G!\}+x߉7s ^,Tے4%"σn@5R8-R>8:OpNB牜x8K\'pE87Y扢4ݏBف;Ǎ qxxL&T*rlB2U ;6^$?4c*r:eX=23u2=rټ 4a#H`PX̤AFu\V4x<6KiYNG$F홣@qmUK:??7L<7@αv2,EfDLR>Np8D"Rh4v|>oP(h4j2 ~ZZ%@]\\׾5[ &DuqqT*eKJΒSYY3졭VKgggEj+_l9??W:V|mq={LuI\ x\+++*zM>֝;w̼{lEGR"U,1D-[u* 2L  V~"@ Ǐ_]]5N&s...ocN@^eI1/×pt9PRٟy(&n˸~;X/zt.x7x@@Y|qm3\' YsYy^$endH3,1"Ƶṙ~iƸDBHD'''V1`9|E>aӢH7#< %\Po( 0iz>#^jdĀENOvx 1|(@czB0K"#3>S!n{2<}.QȚ@F@K8/4'T9\? g <\,|_וѨId|&$U\a 3\YOA4r>]("" )B'^^^ԋsd =/X#\x<6=C<=uD)id/xR*ax}gL<;;;ct:zdR"RѨ?~ooݺeAzNc|2Ǭ]la#b1*HXGu2&PL0ԣGX;ZYYQ8>(H8*z0+phh Iw‚2U6Xd ~εw:R)5 z=*b1oPPaqqѺ#ǃ`wcr9---X,[Ï9yE 'llT9䁗Ŭ]Cn<@{|FwQYIЧ#皽@LU.#Kf̉z9xp99""Y#Rcm#|7Hȧ}u{{DPgZ q0;Ql6kRuFHD:99>ⅅ~t&+Oj6v^OMU׭쩗!W!+ qasipzh4jٛX,fRNcGA!XL L&%dDF3KLr<)@> )X $=f<k{{" issS/^!O_ZU^ݻwmt:F̗e(i*+S.9F.tTVH$t||X,fP(֔d,4-3M3 #sɯhd=[YYe'ͦ,x...T,ǕT׍x"L։Ve}eFfF")?G~d2Nc F;/q?cN@s%MXl/ @tY+TmxP3!%^{ f@>ɘ Ro_'>C?=`ވb{7ǜ~JoO4<@45| '+ B~{񽔌^m*2)W<7=Y[[3pyNVX"[|6PUAi D`!R(L\.lWCb@Jb~7S vyyi{P1ߟ۷ö>l6rPYYa^!@@TJKKK4pqqQB \EQyZ--,,?֗e}+_5]L&U=$ITJXLb2I  䫫J&V` {.},8@6 x8Z7=>1 Cnk}`7qA!HB=gu7w΀JӶϚohsnfmd-i6b1Sg,Ɯ0dž? @G}f&YjcfdI^jĿ+/__0.iBdžr^%+~-,..Z2D"X,gϞYɚ$ E'd?cNjs"?"d\<ɌPez}lhlB1{c9U@LFgo\y ,xجg8wÃm8G挌i 4f(4xJ}8~=c.A`@^ h?H>x|]!hhaaAt?VKTHٴh9Y $O|, )gA`;1͏ZcoNg崲s ,..*n[=R*2șY7= sc7@u<&@'sH$bUf=@NI0\]]JC}&< afd2D"F}`08?ǏǍ깿@VKHjۺurݮjɠBn߾m=NT3 zf&_|s[A%+EQ1=d.[BqccBIX<7TZfYT*eY\.g+D5MSN1πi +:x(2yYPw~ZU N72#ISh3 |?9賑 Fkof̝?7?efL?H sFz p\τxُ'pD?f. &c^4kJeb-VL&ShVG# Ywfd)- ǟ39k  YtxT%L{9!WZQbD +k:&K1lBXwH <[D3:U9r///uyy#;/9^`0h$9CFq/!q_җ$IfSHDgggR;InWB<6,,,XG$aϞ=`0P.SRNV *D " *JR-++joodᰎRլG4QԅB!E"uiT:(>wlE#XJӶX{gH&Fc/1'5Y9yE{8f#DyLy]>j.gz?/0^8D)W 2)o.>[#yia>|õs>Jz]<P}ZdlJ$2YƬ P/Ez|K}<}TʢhTfSkᰊŢ}l s9O#|ރkkk[zA=?zYRdwp^] LdBYfH Q*D}f̹%e79 sh}{ooGl>h1쇤搵)O6 @n>lDץ >4\7j>yJtZu̾\.\.'Ij4z뭷tvvE=yDv.׆) D } 0#yr{| {ْliJkggGS^W39 =$B+6?kd( i%LFh^[ĝC6///kaa$KdM0oX,fiss;kVZسgϴnI@ ~  WWW׾h4>@FCZMeg81]Wb"fiiIǒd(?U*[H7t?/ V |&F_}))bLixa ̋{fP˕8Gư (%P(t:Ǐk<Vi4Yf #,@dtrrO>DJR02#+5(cizwMitڮX,fr%2!sl*h2677-s#TwWVVt]]]]|.hyV-A$Kd H&9z`g*HXY5˩lZmZU,6w-+TDˆsJc>3J ggv2 m߶>%VKt"ٙ/LFn`s`9T2P%8rum{F#OLy<w ضm_X_tpp0ehl6`Sp~~_/z`EvqzzjDȽ^YYї%:::Z;7>:MĒ,`0D˓H,ο^g= x@ yRIϵ}G"Iy:k" \3sMZd˓NϑE|=p8#ւ'f 2ht< 666s+LT*eҽ`0Z- Cq,R%6)JY P6UޞnymllXRZ P"PWVSѳgϔJ,UTki8HX(FH[@zx򻹹i,M&ee2{ @5{nTm3,uu:z=5 Z-kVY'ΕL"~<k}}g8up]\\|wm, yg}oáb.//UV-Cu/Z_]@F!fx<|1{ŅŢy ֌l#I:99ʊnݺeRx<KKKjViEknU.Rv3yU2j427M{&@<Ǐ:}n%O|>bjh4j|>l6ϟs U*, bvϒɤ3jUggg&C%L*H~ʗ5Zf*Ǻ{^xaفl6fi$*͚1{0(ϫl"Bb9FFvfD`Iݶ_;L: b =2˩P(hqqQFZe(M b:99$֭[*-zj ?F#ݾ}۲_Nm H$R\.L&3ըX,7jiwwҒ>cmoo[~"@{Z_1Z l֞'\Ex b:88?WcN@o}[z'1r\˿lWB(|%/Y"b 5}rF}i) 1vR ݀u<=|=i:xZ_'?G:!l\$!hR0++^Gؽ,@|DD}M{y$4փEI2"{O A",j cfJU*2@'2odFmnn*BV&iaaAet:e' E V$LmB!OE If _ZZRPP ɫ qOʊZ dGfie2n@fBÛFqpMIҒ?#:Ȇ4Mv[KKKv=d2%Rg?H'(ջwT*i<階{I*7K3׽s~6?cyE/~oR$"H7gclgA; y҂ x20ʀ rK}+(pdD+^2'\oAbwFh,x@ˣ|g~O#S[d2˂qMY(̼‚Uwa _X,f2⢺ݮttt qX"dĆH(~ |\6;U&;0puuUGGGSw A$̷mUUeYk6Y0RhqqQvۤO<[@6|Ph2X5Qt:m HϺݮܹcz{h4jR!%PDg{* |D$GYB!UU={̤D}@1%``Y62跑NG۷ok2XVʲ]E!;>>-Y!i7U(~š~FD"P(d9g2([RkD"JdEh9 @ `ðiVdTT4h4+:,Ɯ $~=L&ɖ4\Tsψ MK xtK5O^8O^:'D֑+o%x#ìi/ixD=<٘ dc}>+׉^@,`J5w{{(XS[o?}7eZi9"ܫrl*kݺuKpXgggE"mooXWWWFT*z2aθOv[fSDBd0`\^^j4@# h4*)tTdGգH}jUj~B=@"0"_^r;s5|׬[H&~4Y%*L%`:N[I`g^`0PT24K\.gDt`0Н;wl"ا[c[~GY^^V۵迏C![^ObQ[[[:::9J$v\ ~Y٬][&9.:H 9͚*U ȶHפ"ٍU{Υk Y)JF"#"KbTHFuvvT*L&c8l6F2.gxc>9yE-ȗ>j@TĚ?!n:^r$=WZà OxJ]KJ&H8'$Z]]U.qbѪQl_ 0{k/0I?C!{SƠO{+}k511' `Yy  rpt@x3t"e& >It+,戟}֜3gZ|ės%[[±]/g3^|jW|:^:0|$D"ʮYXXaoR~@3Ph4߈$&hOiT0l M!0D-눍)ڢѨLb-x({0ǪVD"v|@!c<H$B~>A2ti#y0y{c/L-jggG:;;ܾ}ۢ͋A)xzh4BFRKKKe=b j49(2eqj5.Lz=%Ib1Z-j5 ˆPŊ<%]CR2at*H A2PȲN?ŢFBjUO<rtvvfAZ쯬H$kq4U""K$z왎lߥjh429{E'l6kn Q׳L9.^BsAVL$uX,f Y8V<7>e !ϛ\=uT ɖ>=#>d.Ǘ`#@?˓ gO>mM %\~ipf {/|}&g3"4C3,B XZZғ'OL(C3 trr-3f\.gCHrF#=p΀XjEs,--Y3[ՕvrfBu0,^ v]+ KRdvuueM1(IDATGQmooRXq˦HJP͈ Ă5& Ld-U*Hb%q~PcHt,3J2 ZMVy > h4 ƋdѨܹc@ HDCE|,<|*˖ "񾸸Tpɐ=ҷLX 0/LՕɤ~7xCb"+ay}#0\Xz-ߐ%IIR* Bz!s+++ٙur9eiM1 Í9yEq^Y_lx`Ч=eŚYfygþ,&K=\"]Mݳ?7ԭ^ ?̹p^KѸ>Ox2#Ȁw>En2NUZ^^HёEׇáR6y{ $Z;D;;;b9;::`0 :WWW:::2C.IĘ_0J`0P\֗ekvH F1%c-@d@(  -Κ ᚮ۔3fqáUr5ys7W@*ƞrDIo§,&eDx\T*:991 𽸸0_h4RPBVLx@4U&Q.S<t F} fLzjJo!]XlH/B!eYW} {j @9O[}mmmY${$AeY8!♆BroD Y_]1K*)908kr!>Muig5(=dAQ_/s3 ys I7(h6c@SX@E ^J%TZY~ȗnAI7%=#*^ as'Ǔd ɼ@`~{B|g{}߼?u92.IfP&5ѣG{S NÇzXdr xіd)Z*LKJ...NM> LGFL&ѣ/IE$X:VXT Э[TTTVm]qg* =$3+"immHc^7l6-; ^!9=={)I}D82&6<`E!#pP|%.8^"峥y@8]W~ b/..;R! x&| G"ۃ `T:{뭷tzzjի*666TVs[8RLe4M5Ƥ%Y4 0@=r+(%I G9OI&!B7# eKgw "O '|Ĉ'|ϔ.>ϼB,8 ,t޼=[]]U:2M3y*G!OE!WWW:??(<4܉DBx\,j6S.jkkKV w٬!GܯF*C{X,zK=R8;Cj4Vn*Jpa-,\E?~l\~3hj6vOgL&qP4%xXmD}i奥%I $dǤD:y2Q{$Q.S3υ@EpXD8._g+Nq6IJ0//{ C?oͿko9cgr>1/_ @N^D?3Q9LSyi2{o_/ ~X_D,`&IQM߉?wߓ "^̽7Z`(voÇԇ~TV-ޛ ,ʏ=|tFCT(5nmtc `^\\T:h4R>驑nkMx4?Cv]qmuN|`>kS2xܪZy@ I\XXPZ5JD"t:m=@| kY+ M5CɤIxR 5$I$VI rNT> )sذdRo6L~|`ӧS)r+Y.˒t}Z-U*H2TheežfuGĠyy6IV {JZ@zy;,A@y/Ŝ [}.3?K1,ښTլ %xٟ}4LTGGGJ&~j~;;wc?sO mUx;o/2$j,&}#Ή49/4|z%Rӛ=Ɖ򶼜glg pn^剀h R7=?8\;F[tJ&^ξz7JҔ߉Fm2+ݻbOZա?>#---iggGBP( B!=Q6$ B. rֈRHD_W'( *JYqHZ3%IZ->-RDy#|ufFax4EzssSu'Qe‚0Q !K&^r2y1xfSKKKRXԋ/li$ /UذH隗q---)؜ܾ}[FC*Sd\NTJ7IY@RQ&Q4ih4:忪^16g2[^Ϛ>777f7VYT*Ck$軏{BKϞNcUO>5F}dZyMV:6)hٴ&$bd~د"3+c 7xV4x<ِjwsI%jmɺݮ헃[F`/{Ig@/b??grNsċL\6"^H'$NDK^%JTY@LT X> |'/D"agɃt#ϛ}Dc~ -cy pߙ+^{a#՞ن=Sjrl)VWW[oz J%uimll裏>R<W::==5sq^WղJ7= x5J<jր, 3GԚCC-..T~_jU^O;;;Sx*Yq߸GfSl:$YD >hdfL&Fd2+E'bѢDE˚0jZ&Cryy5h3D67Cr9cy#>51Oiַf#PpH$l }h\.k<X,* Ac@岒ɤU#ãatttd{$ U}NJbJVg` z왢Ѩ,:ӳ/[T8 DNULy93zD"V}{/͚<;;`0WU} }+ɨ)J9MŅƂժo0 (^u}h4yTp8B=@&lyƇ~ o)Izќu +6xQ:$Dc;>3^}$ۃ}G "uuu5eGV:^o1L,g|fœ|c'猙-59&4#‚={6Ef˘|&D?V.ݚ-//w~wXO>5D'@*;J%3B:T :;;SXT<W׋/J#}°|uuT*%k}. C I ʊݗ@ `%v={f>@P P2d ,ʓ@ `f 5Mk C~fd>/B@ ]XX.l6Ͱ k]ZZRѰD$ꪶU׭= U*\YГ'OOR(\B !$T*:>>ֽ{t~~n}@wkN&*JvhTJe A/WWW*J_}I$:<fG`'Z㥏oӘwΟ1y߿ot=oϵ__?3?|y?S?__ޞic>c>c>ԋ/S?SkI>|hR?Ŧȇ$e2Orп(|&2?c?7|;\ww}%|ih1111Guu:~++ol>>c.?iO?jZ{oSMoc>f\51111v/ſЯگV~?e|)s +2:~~A[[[{?Os9:?'7\.`0է~w_KIXLTJoMxS?ݿÇ;w[HTmfBw,0RTEEE*aE"`BSvQ]dA*@TPt>\t2K0ş6;Ylf3]nkΜ9:o<-((9mx8q$$$H~~~xaazJ_E?ůPuuuӧeo>\ĉعsׯRXXtBQU9ydx6eB\.KssIСCr%ٻwoȿ9d Q@ѣ-"2n8yA$G{tvvCƍ'O|*׋SKK[Nv!Æ '{ш{NCt)*wUk].W'{<rJ勔ӊ+tJbbիWʕ+e̘1RTT<F *ʑ#GdϞ=2qHoWRR"˖-vM6ɧOd֭رcrisN 8jkkѻ~d#㟵eY3_JggSL 瓲2)//b{'%%ERRRDD$;;[~Y|9AGGZJ֬Y#>؏SKll,J<?^=z$AHl6rH߿ܽ{7d޽{2j(q:""\ky?|>ڸqc3i$/_{ʛ7odΝh_555'xOl"!nŋ%&&F= *쫱QfΜ)n[~!{'BaAٳgV|8d-_AAAId X8I z c( 0 c( 0 c( 0 c( 0 c( 0 c( 0 c( 0 c( 0 c( 0 c( 0o=mIENDB`pyepr-0.9.3/doc/images/modified_water_vapour.png000066400000000000000000001253771252122757300220050ustar00rootroot00000000000000PNG  IHDR V%4sBIT|d pHYsaa?i IDATxy|LϙD$BDl*XjR.Z%.^ڪVCm]EUZZkIb 17ad̙$5ss|3ds<*B!B:g B!(;B!B8 B!BD!B0R!B!F !B!H"B!p)@B!#B!aB!B8 B!BD!B0R!B!F !B!H"B!p)@B!#B!aB!B8 B!BD!B0R!B!F !B!H"B!p)@B!#B!aB!B8 B!BD!B0R!B!F !B!H"B!p)@B!#B!aB!B8 B!BD!B0R!B!F !B!B!ݩSsP~};F$DB!pQNA>ɓ'#B!ʺ1a##H"BlO> gB!3n6'N!CށQR!B8"\"B!p)|B!\<%\Bhft:ڷo(t:: .~]d2۶mC1u"kO:uBӱ~|Jݺut=zA !JGo3H"Lhh(~~~:|tt4( [lɱ}ΝFt^otؼ=K|۲e qqqtЁM:"4!D)u/"A !fE[nFoߞc-[ѣ Ym=z(Vk2d*Tয়~իy*PFЄD ! grYzE.] U1t:HHH << _~e9####~7n`͚52d"##8p u___y\ѭ[7t:L8<==WӦM#3336mD޽ӓ 0anݺoڵ 8SL5L!D%BSYEFtt4aaaqN:͛8pZl իY`jg_QF,\mr|IX>O2%UV~#Fg!..3f4i}h4渦7nбcGyW 7Yw5.\ *ϟ[n7!Cyw<נAXx1_(L2;|z͞={8p o|tЁH-9B-_e򓜜رc ˋVZ|ټy3z"88OOOѣGpwߧsC-IOO/R QBcAAA^W]6l0bŊlV<*޾zjUQum/^T322r^WGmվuVUQuԩƵxs[m2e(:w\vEQTEQÇ&IPU]v(;rlkڴԃf藞nnn uUUEmذݞvAUEꫯT777bŊS5zhUQuԨQVjRCBBrɓ'۷oj/N΄ۿ @]ek\߫W/Om۶FREQ|Z|:n8oUwءYFݻ(_g;|W{u֭ԩSU///gϞv͕(9BhSEQ춠 WUUUf`Ul3&GQf͚uԱj+y衇T֭[9F5 @m۶U(U1UX .TEQyݻw8W^V\*.YĪ={m۶aaam(:iҤU۪85gBe s@ל| ~IUE]lU> Lzj.]޽;{lUQu׮]E:p 2Hݻ_e ǹ|2aaa呞]m۶}lق(9DFFr!(tL))):tʕ+'>8q"G{ڵ (<ӌ;UV |̙3󤦦ZmOHH<]vі58sZbΝ?~<18[ٚ3!DhȚ5kaРAV 6{ҡCB`0p/Roo\m )څDgͫWT\ǏSF իgܸq|GTV}f? xb ͛7vӦM˳_nVZۛgy tR^}Uܹ÷~K6lXvX~aҥ }^O\\_~eG+ @@@ׯ_ndpm~Lͅ زe Vߪg#TR~ի|4oޜݻwS\9s,]H1e} e߾}Eڷ8늼K,X ꫯ7ߐ‹/huMHLL$22QQQy *W\zVFׯSB춬\tƍ8֥KeFXX(eܸqK, ү_?6nXYt)w??ЬY!\̂%p={*7of۶m9jҤ +Wf˖-<9 .Y涊ziڴ)GɾZu3gXcҤIܹs'OۓŋGFF{n!f!m^9fJrˑ գuߟ˗ӳgOܹsNtt4+V,5#XB*@> >9,,+Vp7nl5n 00~e˖CѫW/nݺŦM桇` ШQ#WβepssF(?O͚5 g̟?uһwojԨAbb"qqqܹ#F0~~nذa`Ν@λ`Yd a^ۯa0w6:>ǣNL唹Aׁ'j1D/؟: v:8\:t|>^1Ճ1ӎ=e _3ٿCpȽexn, f)wijGƜ)68Vak~]wgΟ̱PMב-=2׾k^bܜ Щ7<|vQ"cXTJcXK~YJudcضҌLt ,D~B8̂_~(Ɓ,[aÆ{nڷooOL m}vhb8 [ kXtaOl0.eLm_Iz۪ˇ ~/l z73/R(97fl\v)ށrFcd~Ќ 3_97t /k~2Mr-ɯ\9{_6\>sLRY补 sPRIf%5Oʕ+### ]vmp.@E!72qliHMN<(ExX[_mI~(,Ӈ^z+pC7nҥK *aqT Oq\rΧ߄=WvvX[_mI~5׃f@&& z]]_ ,Bq0C> gʕ+ܹs;wn$a1TjmJ?ɱ$ڒj`-]*@ I~,ZtvC.nij}y#kK-ɯ& zp| K3T=ܢ5E9'Dr-ɯ$ByRH=MCFkّoO`*7<ꂹ:lj˝PY"9֖W[_mmQ`RzLrΎB:A-kDM#Cuwv(B!l]P '`8ّ؟n^ЅE&n{ !p>C1^B8\(u:;;! "@Hۜ@>rÝA'9֖W[_!<)@J`gP=t rzCGPY"9֖W[_md B/K> ']Tj}0D[t0jAӞX[_mI~VQ_̜%D1!D7!RU/2p.B['ABACDNZ[CBa: ,pyەH@@z+mS^I  Uuz]peN5s5.HQ;rN`eX[_mI~()9l* FՀ*J}Z^XлY^:;|;Imsfv t؇鶙'~j;ljʌ.9jK "\J$Xn>/(qP*ձPsh1/j96eM;x{Yh)22FK\%j͟j-~˻>;ˣn\W) `|H~%Ն.D ar{o5c/n£q?3{r\<>g#%+#X 6 ky6:Tm5:;J0WㄹF5?Zzhz3\0W.f2V ͩB8Rrr2'NdŊ$&&ҨQ#&L!CoժU,_+WHN2e ճꛖܹsꫯ8w˗'44I&ѡC-/O8 %үgQjfMrv%oW?\mB;U2qcE 6-c>0W*qK;XA-ɯ642`̙3iРK.eСf{QYfQJ"""WL>PC&M1owyݻs f̘A׮]_h۶+X IR"wv Cf h .ٰfpA͡_DZ8^c;bu`ںT8aj >> 4Ȫ=<<ݛAAAs…6֋;@74 P)J.7T--$$ypRqB!HG9BƍshѣG4F#66x `7`޼yٓ0ywX"F* {9hQϹp8nb@Zk'oRVR#1P楏!lʌBƍ9go/,Ȉ#aܸqVۦOXƛeٚ5kM:e2B*hN (WիW+1119гgO|||c,I\d;qh'\_g\ƥ1bیm]h"vcmI~%F ^ l63rHv͒%KoG1m4mOÆ ի>@pr,Xk7nM6ڵk}τp uFhh(+V 55:w o1=@c\OYCFGD y~|ՙY?o 'l'9֖W[_mdGTu[yTRw9DUUFҥKYd ?Ç3w\f͚śo޷o_4io-[ }WӧO.@"""bڵ/oժuԯ_ٳg3cƌ|V `Ύq=8eO9q'}wUVI%ՖW2uDhs[hATTfjÇh,UU_$22E1lذ}= ?ln*z IDAT0hѢ;vKG\r4n8{Vڵk8p`vXfMgqix䥤^0Vv=v^FjesUǝTԞX[_mI~#XO>$ɬ\Ҫ=22`ڵkYw>"##>|xjԨ={Ӊ.JbnphѢE͛7gӦMdddP]F6/xDRGԩ49;$!LӧzW^ԭ[(6nҥKQ#Gdbcc _E1b5kfU`xxxЪU+:vHs;…^{T}]ެYDUUy&ӕ^2>¦^$9(Bb HcռDDDHƍYlc613X]EQXh-:fڵ@Q~gfϞ͊+={6˗iӦ[޽{pQGiҤI|7̙3'/v:뺭Vg*z왟pA g:;Or-ɯ$hr1w\HKKVŋ1LԬy񺸸8L&Svqr+ԩS9z(\|h)>J1R SN>`YBdqDE/GU'%r)ΎkK-ɯ64XPM:55amuˋ?#~~8 DY}1oro//\/E0=붔TK]YG} 0d4|mv13b]Ջ^GP->@gxXeݖ|B_-};'0{u[jo.uQ01'44ԡ!e}v?BsNYW8%vQv#XEɓ'ӧOcڸ;$m۶L0!{!*UX-#JZB!\FЅЂ E5Æ ذEQ0L4lؐm۶oSOa0ѣg.ʡiE/'ZzPnp8 (J7ɱ$ڒjCƀ"c@h֭yU|d eӦM$''ĪU )#T42S!8 Rn8;F/A!sQF8;Or-ɯ$ڐ1 …(d8-ܹwA7vJ)y<wz+\zMJ )^J?ɱ$ڒjC.D ɱ.m} $A8Fȸkc~~Nܡ~jn[L>-ĺ~\4v\4 #?-r2 )7pT{[$)G綎ؒٝ~n먭;˺~6Md$ڒjK+D' P³_fؚgS&d@^5,T*^2}+0"+@2O׃F+/u!^.@w*9gva 򇩅BǓ1 …H" 8 &Y'ѥ1m`򧹡B)@ A%qgCf*$kW?Z n^ΎFf09z~3>̏sT_$ڒjK .\ %8Xh8\ّj ՜6Z29m*?f>q9ɱ$ڒjC_%K   @qp,]Wn Or_i9C$9O9Gr-ɯ$ByR2'6X'[4a柞jbΔ) B!Oƀ"( p8̦drnOTt v5^Oݰ~n먮`w^X^l1vGEqRB!F B tv$ΣC3FR>'h?6c7LG!\,D(\H Umpvڐqg6Y \?mߘ쥆<{~ TPr옩 ˪'`?&9֖W[_m,X…e1*$:;"۽4nY(13 ZݠvmcU*V 7U?njpΚkk1W#9֖W[_!<)@J7 [ ZVFwU!jRH<[tzhe*YL|Kfw"3^jVZYvzTc ɱ$ڒjCƀ"(o˷7Δej- G,~UAV\>SRU }XQ !Мۊ .\ B"58umt!(9tز&uoj3h9~՛-|W^ĚpԌ#f*Wx=F ";ǝkK-ɯ6dp!RHg@ =߁l?-՗8uΛ;&& ͇oѰq']sUcmI~%̓o%ROgP,zwp/WcWx(Es}n*qu붞xa7~M, 3n\{$ڒjK .\ 2S!5 ܼn1)wym#^J*m ,H f綎~nًؔ? I3yrWͽR r]o9/RTo/v2])@ݝw@ζcV+uƳoGʺ&c3uƳ.Fΰgܗ⥤y6c !(,'''3qDVXAbb"5b„ 2$VZ߹r tԉ)SP^s٧ҲeKN>ͬYx7m(QI"qH5Z^dsuΚk},u ]伹K3ɵO]v_j&1,wC.s[P8R.pr7Bhրطo3gΤA,]Cb6:th͚5*UAzg鄆g4i~&M"%2(6\prDWp/ycjz2a3s1{#.Ir ^z%ʕ+ǝ;w[ow^ ݻwgلpTǎwOh?Dn*քF}ˢ &`pV=&ZFվoGOU>6m4xrJ)\ 0zh¨X"+Wk׮|nݚ3{lf̘ጐsW*\7`,-myySB:B_0dKIWԊl5qX>'Z Jpv7].rMC:R^Il'ක00(GhocRS(wč'F&[aěkBz BqFhdĈ0nd֬YOxyɸϲ@ }ܹcǎ;Xm3]^x!Y&aaaYD WڿZ<ト:XNq ~5(1H xi a 2.uN fys#i )եBixf3#GdݬZ{FyӵA'mpƎˌ3VZgΜ!---r~(k޼9O&###3\cA~sv\̮|73ez6/w̌ 47gbjK2*U]QUQFtR"##yǭϝ;gARRIIIُ`wᚤkF&M=zt۳fݢ?r|pa<|8r>ws1^X@[3X&T].p`|Q@E1I~%F|^Q[ ֯q}-ZpÇh֬Y!ʋ/Hdd$ .dذa9=z[nQ~}硇,SsHGhʕ]C,#(f-.3s2Tw>esU%9_mI~Q1 CY^9ͽO>ʕ+<³Ӯ])g>c0aS3_tC+0d֭ #H$''3f^uIJJ~֭[ [Y((s&ISZsmt E=_mI~,X}W^+ܾ}uƍYtiB#Gdɒ%RF ^u-ZĈ#h֬{ܛtÃVZаaC6lhu޳gPn]tbEN*ׯsUfϞ}ߟe˖q]xW^^^9qaׯ{>gZ DY}1o2,ixN"u[&)u{w/7d4|mvw[sU9oau\ZP- ^k(:o{º}'0{U)5G1E8p̆:Xq瑒 ӹ4ժ_+|>+ks_!uF? 5gEuuuuD  $g 5> ^{βeˬVA7͘fTUn[v-h":t@ǎ_u [DٳgOv[3f`lذ4iO?Ͷm8}tLXԯ_7|sC֭ >N5sRKwîO-_-jYԊ,x2c=Mɩ$sm}} z^[Ps;UYZ36enFcdMŸ9,HX\(wsԒƎ,xFKQB썃Wp А>;_MlSbe)vJ.]_]v%00^O.]hԩп6l5kxǨR ofgU)|[ wy|B yoۅ=- 3{vL!R9jKB BeԽB v(]<Ӹm6xꩧAر֕/`{駠⥤⭤dc .( 8bjƬwT2Q29 ՖW.^(D1/^ŋsiSkyygGP:THS=7<7y\E#8g#)73R9 ՖW%x!B!$o;!M6{2>u:혭A`oys& 'v!(,BBrΎdpԄDs7mܢ+3t םls!¹HJ彟? _Z0D[t4കEpԊi;f fF45,x粤9 ՖWrD)@J=w6{P2 brE5tyİ "vc7~|mnęC0㔹>MIR+)R†"jK .\J$!#(ޝW]5wv*_lȱ(ɯ$Pup7CHR"4Zix5fW"/8Yf4ڒjK L6|3#X 6QPtR`W!dBQasv$•U˱*s ?e>Ysmg#.lN w@J@Ng(HnΎ[8]Sbc|1U!_mI~5a+"Lz,OHFgP5p)ΎKMqvX[_mI~5a1Io>wE CV^I$-߾w.Í88 lyU kUQ76$S`߿fqOQT/8gqSc.?fj%/%[/UIhcQL_mI~5a1^ RGDPMk-?|í nïm5 Nn `_ݕfsjK:7xhc,3{0'} jE7L0D #"/B&Tc;[`$cW{m; YNo BdX⾕pM5~UA]ɱ1dUu ǐ&BBaaYsQ^"TIwf;l\,c'}N$اJn|7ܼɱ$ڒj„ /[!K i(p8TiHr7⍢p|?W-&pN@eX[_mI~5aF C_f)@#X%RgP,#9;Myg)B!!vaz;97:᱔9cmI~%Մ`5F)j͝A)$~cmI~%̓; •jBQma; vwf;ܾaΎF!(6>evy7o~5wUUQ˗Ӹqc #GJ=-՗]GklG]< vN A@'~9}NLkK-ɯ&l=~9rTg^ɱS&ik7`TbnW8|CiLSQEp6 c lkK-ɯ&ժӮ]B5oH)@J=[\1r\/ntgGP:TPnGca ,kK-ɯ'x/ny'XQH"D!) PLU"Ng~+REQ?~9ɮ]ׯx{{ӠA}>1118p qqqNXWH'x4BVB̞!ByYGdƎKpp0^^^jՊ˗ϪUӧsyf:t@r\2\vͦ\O "o֭~~~|W_~۪ω'֭F+Vh"Nj֬IXXk֬)ę!c›Tte`A16v.__ o%o%,iqvX[_mI~]ƚ5kaРAV$$$w<} "88 .d]x}sY3:tA~nw9r 6oL?~Z ()@`ǎTTcǎCF`` wXf:s iiih"͛7ӅXCZ]>j_ѮyP/q^_0W瓴8=[j 8:=J?ɱ$ڒj¶?u7n.GEK||͖-[8w b̘1xD8hdĈ0n8sS9G}Dr믿&$$wyL&MT8cHRf4L[o@.]pwwgرlٲOOO'G) #)9zEZK !p2HA`كlfȑ޽UV\}%o޼^ɓ'c4ׯ_ڑGR%`޽{[@bbbQ?s~b6Zwee0߃ V-n;c{YM w!˩$2P3}>ɺ-?Bw>6Xmn9߃^{.{`E>™VMW/z w;xݶXd!G,up)7uO`x&Sjo<Ʈ߬quxΙ#Il=:+ub˺}]LeϛC,罟~t\Ȋկ#KIX\:XX3bJ*z"3NʨQXt)}GQϖT$&tB(B@D) X^ *_A.E4 7 \/"hT"D ZDPBXl&;8933윲Ç[q5S^v={6g歷"66XǑ1"&M|/B9sVZdSzu~iPC ܹs:u@N:̀'-ׅC[S;gL)w `|Ȼ|cZ=|K{}bkū9p٦µgcc_ axypOFk&BiVæNDı>Cg!..o@^]ڂ :?GP"U;@ ,IW tѡ!>;,?܁Y?6rm[׍F?s5q ׯ租~+>EQڵkYz5O?U/ҰaC.\XԻPVhҤ ;w:O?MZZ~mez-fϞ$Ib,!ހG/ͯuVe|dgg)5jTjd+lr7߄f%<;DТJɆ j =Q,S1$ּc&6 bZb~2[],܉ȱD~%ix/",iQ5GIvv66mؿvZBCC֭[Xv-+V(7]vO?Et9s禧zڵksJF_|M/^L>}ʼ8b  İaØ;w.,ӭ[7:ܹs>|8={ 66.]0l0f̘Ann.111ԩSS:.e*A|e_ryH_|77oqd#ԃ&=>|ׂHȽ'ai{i bR]QpAl:&AR&%.xMFI&j2dL47nмysٵk֭+3LÆ<_yV^ĉi۶-/:-"221c0i$\Œ3h׮QQeϝ?p@X}N:%Vy}gƲb bcc eʔ)̞=L˖-IHH`=NYdIKH[k 'N&# mB`#gG{_p[7&{ku[CLӍ E j^yey7!33֭[~z}ѿ!Ȳlƶmې$իWzjk6i҄ۗ۷È#c,^^o= O4@l X`A:v_/} ݯj}(`ȅlE725ҤQ[ V%@iu.^R> Lh00 ?qqqŕZf͚5Yb_JJMq 4Ah4Ӊ[W'ƀgJ !m>TQJF/cuKWPIBBB %uK؂Z ]IwECWȱD~% SٳCR~}g"TxG1ڏq\<q>UB)^|V  ._U5|W9I&1~xg#Tx"=Bl, Pp,7~ܾ};?0=K.uv8B7 pµgbaAp+WdÆ |7osv( D,Ty;:k%tw_Ύ-^<ȱD~% 5VBwF>cر2ouvD&q-_*_"B{ ^k䀩e1b;X(ȯD~Uq7ςCyfFM3FLeIWpI7g}~AΎHu{SV@4@A W o@ :-[pQg"Th-@~6ThIwuۨ[O}2Q^kNn*V|<| @N2:9"A 28; \BugGSuȿe^XM&(~(~(HV2YpF砱+YJ*uH7nKTCApSNC9r\sES.r^PON]1n9Uȯ*L۾yC-[ƍCqIG{Ύý#|"Uȯ* ǀغy9s0o<ӝP1%9v%oe~'RVeS;@6BA01طZ?T JS;w`=^͘Ƕ8I @#u'{oCday)6z$^%}ӘKW]"WB1b#,,:dU/pBdBq"P[^ܲW~tl忽V3lymKGg,0IJ.ՃOul5AH].ot'L A;Zz5-Bѐ̅ ,K{OIDD*H6m1/m  u@`޼y 6>@uffsСC>Q5j԰_F Z-^^^FmƄ 5lٲ .DñSb*7vv( PիtԉYEA$=ZGfDEEQvm&MDJJ 7od۶mX^z ___Ξ=K^^Vk׎$ h>jK2ZT^P-4*zhrv( mjMtt4raÆrpۗ5kh裏J--bbbLJZj1`*`ڶmK>} #((h &88%ހؠA$$$0b7o^;%=(deeQn2jʳk܂P0u50kz^c/n* ў ch] *:T%r._u„ ]FšCXh[q!˲⒒ýСC/ulOzz:3f ""k׮o>rr*>f(!!e >}AѼys~mj׮7o7odʕv)hjk M_u^kC$וnޜT%r._upPye+ٽ{7;}ʴi;v,M }r>L|||̚5ӧOsQ4iR6ߏD3gDe+|}}{EZ8q"O=TћL333$uwn_E6?\C2];OZ]n^)S+1(7\/)pI2LEr?.)!(~*!ɱPE"U+oٲƌc?**н{Ͻm(֑ʕ+;vE2V^MZZs̱:6g5kSO=U:c@lp ڴiS(Թsaaa8رch//rj R߆]~/>}Ky従YŦ~o6W>\c+"u|w5/-nGV,_ 0;ƾ[wZ^ {7bRZS}[ǟa[co]=àd/ch9O ~y0ג˽.9u’iLyL${Z QZfcUiI_6˼6Y;w^Wa4C͹是aVupSžQjѦg}Q0x{>KccǏuVo9ڵk{ƤI ۛ.]}vRflٲ*+؇,,DFFrQ#? >{fJKKEL:xD:u<Է<^n^+Jݷ=)c9ya}bIsT*6x#\{8h(L9.UpB B+V4ו7u"/v,̛ l>oj| n;hJtn%&t؁ >LǎLQgApGg'HLe{{xx8aaaV ˗/ʂ >}zu:t]vZ7ׯzDDD0}t$Ibҥ$$$c\{ `֭ h=>qϞ=97nTZD, >H^}Uٿ? .{ 66.]0l0f̘Ann.111ԩSS+G,}=s pr%W5wB,͎;>߿?-Z`޼yn\^ׯ_h = > ԨQh'| /Ngnϵlْz=G&**p N7F.!l(׆H59X :=t?I{}g<=ċV{@nKW]"(֭YfS8W{Lk[x=zX.O>$&&VZm۶-u닺 'ހبwQzB;v믿d-W/rH4*Q(y>[%gJ9Pu 5.!%~5'[VI.h~uOuDuW9VȯD~Ua^ףǺ?q>~>ҧ >>Y-Ɓ;v 0?WUIݩYJ2ydx z)^z%4h׿ŦMcD%vv.CABQ$5><a. jx u,  2ņZ^yݘ;/ԛ4i⽌0M[ (WN*Uȯ4׃zXJLNoX~ȑ|lڴG}hڵk [nU~у{rM̿srrHHHSN1|>ӢZ7|'x ! `YJ ~7G[ .RtopMjxp"17@iyk=դ[4ܜkJ3 2d"##4i7nܠydzk.֭[W43<Lrr2 6,:ӦM$'g|3O>zr/Eڗ.]Jff&oUlr̝;(kOj׮Oƶ# `]jRX?3{=Ɓ|4GMe>/xձ_ g]x{*L\?sx bbb̤u֬_⍈,Ȳlǝe$Ib,_I0Z {ٳYf1~} w}e#V54 a zit^~ ZsC*1,> eæv^20T%?b.5&_紩Wr ww] ]M]vk׮,Z#Gyf>sq"Me tv$UP:>>ŹCd*A65@ lR>^6=F`Rˤɍ[rCrЂ p%ʜw7СCD%bl qvlVp1#w!c62LJUI<㵊 Z빍rr,TȯD~UQK;SMRFf͚tܹܩ}K: (T ~o {SMze>\)0)XI F#|e% rr,TȯD~V\߾}Gj-+ rv6 lݟS vvaa rXcy2;B R1:WN*Uȯ* ǀغy$X`Ν#''dϟOXXO?%11Yf9;ܻx"T"1PlT0'hy!K}WB Bȕ˓ν+h_&M1cvٳgYjU+ :i` 8 9ΎFpAݼ=t?9;A^e~nᆪGwޝ{p}q%G&# Bָ>=uՔLLu醳CۃApYlyrM4xH_ZiN9;A܀?-ku,Ts޺DbtnntG?^&~7?Ew@}g^]WCy^^FcM*KrE`Ԑ:rvMX]"r~m丩-Vg?-Ɩ]%{8K#pqrrl[Z7: +AxwnOʡ4L%7nzN=2v&EnXT9h"rH^2 ~3ؗ*4@dt.{ȑ#-$I(bduvܹs9r ̞=ٳg[MLL^t: %KhAwe;w&L̙3J ;ޙP9;x#Av"G7l@=ȯ$rb! Uu SO=e9$B4BEmW^?C9+W͖pSNѯ_?:vƍ%&&޽{sjժUT111 <2k,.^|PF4ei p;gGb;?_Y.`vpSq?{~;Yub0~R,5A\ MW]n<Ň<|.:vτM% ڵkP M4!++ V\Yb|}}ٶmժUSNhт%Kp¢k̛7{yЧO f"::֭[X;hr6P|y6VFPYg p7e-&o@*q T{7{URZ@ȶmxG5lٲhΝ;'*rר(Ea֭ gv÷K!\;?Cu}_'g"  ={<"""k׎$ k׮EzQV-N8QFMZ_ ' Syd#+ߴ[/4դPs=S2k-Ŧ]29Hil!H=ȯD~Ua2i06o&xOtAFyYPAAA(BVVu%##ooo|}]d8#!4z'Fέ<,iA UV'*h#Oz}BCyj6"9X"Uɨu&%8hv hOYwŘt"\9 l!˷n0wźv\N:P=jIň{0^?ϘnZ'iv8lPe4(^fԞT<ܐ!twrUISn(խ(^n?ˋrr,TȯD~U!PƄl DSeffZD$瓗U.]ph.r$Y۝T r2V~?;b&)>A{J]Yӿa0nw8uyz rCwƾvGmiԶn55gRZxۧd cQtΘ­yoA14(jx4[u6h޼9=zرchѢEB8DMOO'##mۖQ: rK}wYq1Z4='k]Ԧ}}.>CdggMhh({lذ.\@tt4}f͚h4>#r7od޼yݛu@DDo6:%!\z:u0gbbb,=c$$$T4VZZ-Z`ԩ̟?,BCC0a~ .7ĉjډt x(_=.L쿨Z{*uߖ[#,@,tk?)x827% מ!7;JVavv΁Ki0$({/j">>kZ-xq0`իWg…ٓ.KD;ر[nqMN8M:t(ҥK ƌ3"SSN-V`` f7$((H~gbccygcKM0]`ū^_˥5xR2ufN nT)d+'BKWE1UⱮu@oݻgX'}%55iӦ1vX4;ۗ+W Ç_bf͚f1iO~gڴi\h_$55Iظq#7nD$RRRhԨ-[$!!ӧ3zht:dɒ%EcD ͜9/_Β%K_>:oF98v:zkSSf".ʡ|LvaKUԖ$XRS%VuioW) i= )/"TsKr[2_rr,TȯD~aTj:{oٲƌc?**н{ϕ&Y)_.pB K4@Rr;v+ɓ'3y3~8E9/qNnBjK:~Gs4 4xg ɬɏp2Qݴ^C'aW}n? }gI|ne 1D~%J΂E`?~֭[[(\ĉ6@o{QsYJxE+ []A{ݯܠ:ɦf=[V )6UUɦfVK դl;GM4h9!cgxGAAXX^Ӭ=oͨQʙGpWb,< Diʞњi꽔^nI*^,!]'\{`:ɱPE"U 07ao .-GzhǾVUjj*ˋ={P}H'TgHyJSbk9+h-Q3twtuܲTuoPG*>PaX"_uRǙ;u*xDDȲl1رcvJ~$o*d,4%9v?|Z eeݠ PSA9~FjH [NM>AR&D/ǖy)S+VO$Ѥ\t hOVScD~%~bȑdgg-IPhڵҭ[7F~Po 庂o@zΟHDWAjH׫t&=O}b3%_&_FqxAwZ.{hRӦ1S;N-&eK*a~7tG&$YnFy lȐ!DFF2i$nܸA͉g׮][hgy?dCa%99hѣGpOzz:V"==k4lؐP'O_)؝h.-Enʆ6 rjW&=hP[j]uykꫵW#'?& cIZ Sty7!33֭[~z}Ѣ2,#˲zw$˗|r$Id2yIRRR$'xª9sS\hK(%:HJ縢)XxqN]C$0@:A=$wMriɱPE"UG9˘֭m n"r._u ]O;c_,z66otJSldnyоo *chׅ cܼOF{"ݡlz,%Cf4mm=EcM*?`AJJ肠*%<j6>[9,Qz_gGV5:h7>|jOu$ 2QT "TJ5uoܝ"Cp+ܥ̞o>稥bⰩyJ)AܙKp#b K<ڈ 3-ӿ!+a!yG8; 5DGp؇KWbFth/G~LMgVa!yq/;; :Ԕ D#n;ƂKWz:;6otf(xM BimW^?`00rH$ɲFNNs̡iӦ,[;vϲb zI^^EDGG3zhvŋ/y饗ʉ&Ş3%~BziO{Oʱ51ڳx>cuKW"1&M@FF+W*GZZECF3f 7ofEט7o=+*k05kѴnݺh=z&i@R梅PS}.x\J(xph.F{0MC©)]nyZCUU99HW]"po@J(%Oh,t ܹ|,\BQn-A`რAΎv< e;B rH85D{GHSuX"_up%t[7.!o{kǏЮ];գVZ8r%APcwv$Uݡpq IDATt0MARChIV LUD*x"3f̠K. 6hFFZz $##Ña emo~}dAA&܈hTAff&> $axsr0`a09RG 8 R~T >܈UIYYYDFFrek4ibq<88|p .꫁x-mH,g]WY;h.ur{a"]ia8Ula[w8XGYVz:/XB;s}Ž4?ɗ)>6Ǖ%},ia%,mXo/,ۏuC*>fS׽ KY21=߷z̊*>ғb^﫜\s _)z=[[&ܿ=ޜ⦎!YQU9w׼¿}r(cGp},u JRJq}z*uaΜ9XbРAgڷ<ٿ?]v-ڟNHHgƌ$&&ҩS'?uaX[󋦱&B峯Cb!D1{x5:t^ qၹY }Shs(횝󋦧1sr_^}kxxp\4դA{'?esDiv^n/{Y_ɱPE"rH; J>|;:4gPu'šNN]{7 6*l|;w]v2d>>>]bڵk$.k  X2Nr,Rl1䤩CVqԎrCjIW.pHrׇ7w!._uY7"ƀaǎܺu7{8qM60tP~9B\\߿:uЬY3<|֬Y?Lll,>,Zr )UnL\~4M9/!NHAJ@4@/j~$I7ndƍHDJJ ,s!$I&L`E9s&,_%KP~}^ux ݓ-K;QCLt}:.+fd:V{OiaS'X"AO lf͚ƍ̤UV̘1cǖ{+Wk_C7o (G\\| TV;oҽ{JܔD))*˲Mל}:qD>3fΜɀ`…ۗhgsK(eQ]y}Z{0.Wʥ'-h=$ ncD~%ހl߾ݻw_ƣo߾2m4ƎFSpUVq Gnׯ۷^+ždž ?~R>R9Og  JCFQyR79D~ord_r79z0R͆Gˏ&I3X󌊎\|1T QWH0X"_uCA[l! 1c,ҥK8ps[jUj<>Z"ހ$~YB4x3}EYf '?}%,mR=ڎf*_ ~JFձ-pW4Zg4qVwԛyv"5t-ж5 i ޥ_9y O1 da [Lݘ1^yPz+*[grd~uԜVCcD~%?~֭[[h׮`1Ǐo߾V =y$!!!t:L{ǠA߿?̜95kVW' RܲW_otjgVP8.k3oiuC BFl}XP{A\J':›^T䈩'|iSKNZ\{VZ0/ CUC+##0AAAEKYTsϟǛ.OרQ#S4YDD*H6/Iu^Kl8HZ9FݫjhA>SZM2>sһwo_{Gdd$vCQ3qIQ-I(FOYbio7mǎV@+ ,lHs5ۼCݎ|7"*wX2 ouCBA\&9h"UGEހx}K-\[̢e[Xs;F\\/fԩExڴiÔ)SoJ'-KܖML^KJe:8(g: gnOw0 ԭׯ/ לa~L\xrD~%Ae g{:X<""xdYr1ڶm[jUڵѣV{ vR_M Ĥ9; qFg>2?lu=gB-h5|կs_-*a$K7:\ MW]"k -[/GIvv66mؿvZBCC-f*SNq}FO?z 6lhZB$&&c:. x#^9C !22I&q 7oN||fϞͭ[ƀ2o޼JܔDDPZFB)Έ >CF:;ޝGQ{vg'=B0wf#,X+X`[f^n+'bDXwX]Q[p E%&"{vAO>e*\ʯ_s0 /@xx8 .d…y7x7&33""" ##޶2#b&>^+?B>oGj4wֹ,k0M3?N96k.&̄.b=I?vMg@ dNZmro?uqNڦyc2|Wq`P{?G=~pta{@>M5ksOT${g !0p, vv4 5) 7 v\e%SP9nє_s)0,3SX}WnA{RoGHV HNG 0Љ::S9aH T\l^'-0 Y '}ѳ7y◼S1F[/:bw-F';ݐAL})\X6HC!Ҁ>M5kuB?' >ɳ? 9:ц+Gi%[isv#>AP.ggK=W:_ڴa#R~Ғfi֑%?tdG3>M5kuB?']ѳ]b=!9p-=7dֽcstBt_C_{Rq' t ??Gy&uK9T5cs)R~͡NG e)ArCG{;ePo%ۂ icÑxO ~ #tz"@ֳ:Lg/[EGQ ,]-h3NGT2oТBb}UGTzyqc!һ~C}0x5@[4\ʯ9uBwg?+Xg3c ر#VS5 aÆaZ>}zm^|EOhh(={dܹk{! r{;.noG*ͤ>rhʯ_sE |,YBbb"))),]K=C.^={b_K:[29x<B/*ʯ_s guޝ" Xti?ΟgRRRm/(('׿5O>$Æ f1k,҈}4}i L|: .C\"j/@z G ?#µAx8ӥ07&^Spi'ayȥ!1Q_T_s)"{ 4ejܾvZIMMuY̙3YregX AO}?-FCbRbsz\-" (ZY@+7RSg'D A|hy?<3|`e~G78Rfz%&#*@ܰtRl®]jm}@BB:t@^^1zCt_v$M׺# J<$C Byvi\:Dpi#QWBqѩI!9i~3ze ]@H#:t,XNjªmb,g0Y2h,Bkj|wpU'jmѕSFk0zC\mʾ*>cqk.TD]Ӆ9}!-$AEN*@ Tgԩ|bb VXba޽tڵQ{lj`,XX{1fΜYϞkpt_ 썸] ÂjB|`S냫^5W 86* ĎB;b'ϻReX(Z6\ʯ_?<{uk?Qӧ3}Fq[qD={CYa#A7y>ntY~5LMfE(e$|ɄDZ܊&K|lImgLz|ʱ_s)"= >1֣~ZVаіնjxm"l ;r:|C'1,tD8nD83w^*&SB;]Z>\ʯ_ö˘S:K緥 ѓ~åW[+ {f&""\IKK#..0xǙ}7dҤI8&M}VrFɩSxfѢE3׻ stJJJg\iTT {^3?5A+ hڰF6~Jȶzۖ8ftr':f~cs)R~Mb|@V~5[z5ׯ'++'0|p~;&NZ4yyy檫j1.3fiӦjl޼Eo2a7E^Iۼ@N9PRpticoQG ;+];f~cs)R~{GDDDb 55Ç׹oN6ȏ#G\WTTʃ>_޼">GO_\YyPt |JYZ6|ʱ_s)&7aۉ#!!pΗV׾ }ΝKYYO<[:%nyz8҆ ޽el߾}Vo߯g}UVrKS8U gL;Ey0B-uzDoNpLx -Gi !M?XEy)Sͥ˟{Obn/~ FXsTaNNk=܀+ 4l"D[Ƀg5(J* 4C8_H͔_)R~/v.>DEEj{]kW׾ .d߾}dggssSN!yO8A6mj.IO:v4{37A 凳)NhcXRk>\ʯZb~@wv9ߩZ[4,K k7!! ɓӧZtꫯjO"b8r&Mh_EDDNVVF?''!C7 ֭[0qD BxxKVVVDDDķzp}@ƌѣy8uz"++uoVs˖-cϞ=tロŋ3ay:vK/w}ѯ_?r}ЫWZ',x2 eIDATsLfϞMaa!՞8lذ3f0}tJJJHJJb͚5 :ԍ8%+X""""~WK8_1Q#IKK#..0xjqټyKۜFEDD?{i̟3c ر#V6&/"'44={2w\v&q ɓk/O:_ 6p]wѷo_ܹ3cǎ%''Z[ݿs`5vQ~TqƱl2̙ڵk+4iYYY/=lڴe0Mvň#X_oC/)+??%K`HIIq򩧞"--ou1uT͛Ǵiu]FDDDرc111ƨQuOC|[n52 ƒi֭[})r{ \֧ra>s/E濌:V|ᇌ?֭[Wڵ+_=w$MR[>˵kR^^Njj1RSS1 +W~mvP~/]m]xx8&00[nw}v`ƍ7΋r̘1rL, cǎT~/;;R暪uouO<^mߦ/+"s̙ .v{>cɒ%DEEqI~iVX/\mTݯ~+KN8c㏹{8tӟѣi套^bƍĐ˔)S`ٲejW֬YömߧcǎX,vA= jp.°X,}4gX"uax;8h^ """""1*@DDDDDcTǨQ""""""DDDDDDyyy= GGGxzzb֬Yo6m۶aȑprrOl_FPPlmmラSԩST*śou R'N;^""—B Ux`֭z*wӧ6oތ/cǎŨQ`ccs᫯֭[)S HvZ!..N}={>w\ 003f̀+N8{Ǟ={ iSZZ#G3f̀ ֭[y?>᫯‚ hhhy`ooxѷo_ؾ};{9\|tk8uOkkkXd N:[jlrJpvvƌ3࣏>֭[qqicH%}s֫W/kύD"1j(u9rd&''Obu\[nO~6''FBQQO#//6m۱i&L<|rXbFff&^{5aݺumTM ُ"P\\^6gMP(™3gD"V߼y H3f Z߹s`ee%<(H$aҥ:Zf,Y"H$aŊ% H{Nhjjj{A6l H#GhRpxZuƺXA"z#FDssskkkMzƱ^|EA"X޳gO!88X-^XH$Ç57D".^^6m4Wׯ0zh2A* ƫz׮]u .\X7gA"7nl5%H۷kOD"$$$h,OJJ$Gi,?v J<;;[pqq|޽{BTT Ʉ)N"SKOO>Bzz/6" P(8xz1zhH$OITW=A???X[kOY5~xDFFbϞ=z駟_~ַ.<==~zlmm|rHS\g'Nŋ4hiMlll+@.lƷXzz7|\ 8|'''_^gS4}3U;SApA$$$ 11'ODmm-IvzTsn ՚N Ϳnݺ}!((HY# dIXYa]t EEEl"H$Z7|  kkkuL9={?%Kh}ZK4`֬Yprr?ѿ/XeϜ9^viPrwwQ[[K.y ßgϢ HHH@cc, >t'RT݄LޓѣG,TTfΜGAJJ ƌŋ#9p,$,|,Uh?x͛7ƥKУGox |Ä /fN-Q@/Kн{6Ax_`xQUUNNN3gzۜ :3f ƏWWWXYY!77k׮E}}xudBIIzjz|}}] Y@@pa$$$ 2 Ǹq~bȐ!U>!.ދޓSO=~ꫯra""#cBdAz聐dgg֭[8p<<<4w!::Ǐ9t5j2&&F4{_૯/o555x5'ʐ7HMMmv)Apm7*r%%%pqqQ/S堰Z*,,P~k/u64gcǎŪU#,, =z :{Eaa!\t߾}["sSa())yxOOO$"U,C#256"0áCG=?rrr }Q֭[:jB]V;99!** .\л9M{ 8 ™3gZ?D%;;O=1tͩGcǎAP`e111n_^^3g^8qww۷u!rmؽ{7;HHHsamB,rԄcǎA"{r1QEs=|gOQPP{N+!2.6"K¨p|'(//wTu>z( zyuu5ϟfMosMhhhܹsu6iQ llχ x7k7 Zwӈ:OVg5>3\vMۨ >Xf)))8~џ@C">CuuV"-[?Z\ؾ}Ʋ?999W?GRRrssb O>>۷o9rH888`Ŋ(--U7{4?)S`ʔ) ř3gk.xzzj}G g 47oĦM ɰfS_~ӟ/B}UV_~gPOək_"2\T*\eA"RTxW& «* qqqT*_~WWWA* RTkm۶ &M|||W6l{ W\V"ȂD"8:: :9~0vX]pvvF-:s~XA* »+ BhhE* {}Q]Å~ŸbccU4ipyaɒ%:ޜ曂D" nܸqT*f͚sߠ ڵK1bݸqC)DHNNnS)))T*֮]+l߾]1b( ӦMӚE%??_x饗={ 6660eԩSm˅N\NRmTTBBf A0o yUWWwŦMPVV޽{c…9sC#ѣ&@Dd 4u< ==]O2òepE}[o &ùsI&ᣏ>WEhԩXn,Y]vaȐ!={6RSSu"t@WZbLYYb ݻ_|cixaL0غu+>S۷ F:>Ɏ;0i$j|K0n8dff"//Oٟ4pNDD EӠ er91 СCQ[['N#<+W_4ʨ#HlgggL*(((ɓ'F"ty0tc^cd2'"|:u >F13bDDD`˖-z, \pZ DGG2338R+`d"5zw{c^TVp vU/"aC ^P}~ 2~躎hli{^ ޞ]V:~E(^u dFlD^fߏx{^ N@|?HPhסʯ%^/ pu]mLP7lH[&"A-loȽ|2i$L8c>|,G~;\ 0g?~ÇG5<>݊ fX\pmӏ;oxq{vLD~{Vᱟ!v/>]ym; ?W(Z c$о_\!яF`ϰ8toAwe̱_qYr~bI(MsEAjPsiIup oÂ0I(X˴fxQTTG?Qc޽{#((v¨Cc'tL25Æ k~gg  #?~}i[Ccq1b~-Get;cСo4>OKKCVVN MD2~x$%%ᥗ^Bee%BCC={`-i?u6PT8F==!tẕ_q1ⰲd|l̠m7o֭[lO`߿?<==QPP9r+Wxl2$%%ax饗p,\HNNc"͛7wEPVVHlذ3fh}.Qf`\ h}c"".N&[ԣZYߖ{BB[5 =>sTVV C 1asbǎXh&O @gۚ-  SttA^T;8i,I3Eu0b~D d R ! mu9r rpBe4Sd]s,.W\/Κx& $i>`|4DHYɌ3WmC=]KgRS>=׸u5If'>Jk1$+EKlY*15+U}_Bf&XQMzJgnsz dAXA"޾ʿ;Xm@@t]p/FsCDq^ ,@ ^ݔFjtCyU:: _<| 2wT:AzuNDDDHG>H IDATQ q|H$}qds|آ޺w-8ďǶ;&B#q!mPcgrL1b~E' dAXtDNA467sQ 4 cn|PpڡhAD=/i2VI7W\̯H ,?vQY掠M뀳' Qrxâ1v%"p18 FZ+vB' nȘll:Ѵ_^6pb٫*q@V`9SOT98LD_@σ<Y 6" ; 2-9Qύ|^Z4ISUkF4Xׂ۠;Ңᮋƾ!M/u=|CDD'|Zfdd 11pwwSO=ORކm`x*\p"z0vLΑ3\ٙ;z٩ȁo.˥cq1b~E™ɂ_|<عs'>SܹsÇ]|qqqشiV^,=%%%-$SkfsG Z[{dlxΆG!G}pgzY_ une,x@cs,.W\̯HjAuu5^uƍ[ ~3 gAvvֶqqqJZ'ɢ >sh4~x>@|E`oom۶I;{РA6Qo?[FB:QbH}@NSNaٲe1{l( ̞=>c`ѢE C^^> -- }>44ׯXfȴX[nr9mۆ-[\8=nK#k#v>jQ vF;vsvFΨtt69];s,.W\̯HD(@v؁}!553gƍX`fΜ Twc~ Ǝ |'/5cС\Y"62 ddd ** p5ա_~ZFGG#;; Ӣ5cq\+Qe8;;c˓QPP'O6_A釩#bb jkk;JK<7oO?'⣏>dbvXt)>/^ (++ڧ XΊ_L5l&ƚFs1b~ODP(0o<?~?5ֿ??AAAx뭷uV<I&' ZtpBuǹs;Ӯ(Ө:AR\o2u |J|y8y|v hgXӈ7!MR35yVlg+?fi,k*vNq@}?bt#vNq*:T븼v?W>ǀuIZ1\wihၔΧ/XU_D̟?ׯGJJ 6]3<-3!%lKo>/^{K.չͬYp!dggGCxx8|M|Zddd`РA _:nExtv_3*z&n$g(c?Cu6 P[9ft6)#p{x'!""ݻ&N7AHOOGLLEwHhwAAgr~ 6l9spq  GJJ V^{qݾ}Xp{&l|Xx1Ə'"--M㥲tR`ҤIصklق{ >>>x7xw/ (pDDdiD>eTWWX 6}UO>RRRj*XV`ĈzG}@m6H$ڵ vX'HԤlӫW/:to6ML,_Gu!KL%fX .3)Ԙcq1b~E"B#)) /*++TٳׯD"̛7֭CNNzxװzj̝;}V=pѣGӦMC`` jkksN|HHHhs-,,@t6o{CK3V.X:n;뀭/k/ \W^o4OscoHcH7oތwy-BYY"##a̘1CBBJիzjc!''Tŋxbc #h: Hrd$˳/y@TӀv4fhs1b~T-GGGX+Vhv5k`͚5rsstPlۦ=un,@:"HΩ"~3v -zP(:!v2 4,&˾:du.(/BϷFI.IymH)+Fk(Ȼ \ Ek C@oLwQOؘ̱_q1DT{f"F45)Zevr5\ /@l' kЫQ :ib:y#pnWG.@K1/8}Tt=n[̠B0 &'Y@ѐ>R\=PDDdX0zWg:p-ilPJGg nrpTÓ1b~ |!3`\6wZ<Qcu8tHp`\nseЁ9+.W$"B$6F-4wZ9 nmoPt٣^e,hPL8QK1b~ rj'gsG#}hkA88xhaBmR^ \\ג ӏ{U'@hK،ū 7+/.]h@q\< `鏴Y &ewŅ|R` M*-cpZu zZDD6! ##f5l?B9w]@ICZ]+`PsbwO̐^'o X\̯_p, |E4wZd6PS| %TkOAP>L?T8CTڡ^cѫʝ]EaD9.KD,@:#_὘ѣQ~ (+n~R)'CtDƲj{G;vGCD;6)1b~}@Ȃ6qp"3K,Y~Lo~ CGUoc|o_4Z)ƕafD'a!D*]p`D|ADDO!ގ(#h=nQf{vƽQס4d޸ {x@ ctD?;N.F R1:v@)2S/\]s,.W\/[G#h+`c۾c66,X)pG*v^"x; u{nU7\;ets,.W\̯H , 2LֶO7sa-Q ̹k=Ȼk`yB.Af8&X!iaF ODD&NdAX]= /g£p%0 m-Alq\ tLp^IR ÅH1C%"b't ؑK M)*]PFCN.r 8Wlw}R  Pz6|5FkkT8OT &XdA#:]c=^9~Qg0F=\O0 xU(GښMGV?V].fJ[sLa~Zn?x航*u'OB&aرX|9[>hp#Vgc= k{X:.n^@@M处r2+@fz mlQgkԸu}ھ=VQC1+ʻ{ M U dAXC~~>z-R[˗/#..111شijkkh"=gΜW waE\SNF B ƞ:b#ݥ]q$};YEF~ uR!}@$U$1EsD:FȚZ.@rpd@J>զgFVVCح\=uu5[p`( :5dW\̯H,vx$h"c۶m?~B þ 7 ;xv?pGbJ/GR͂}}!>b͹9j&gK(ku%ܪ*'7 :C&!lCj=7?3f 88317|GŋN.c۶mo '''@c˖-C˞AiPY^ye.w4| Ì~N#$zΑ(i~8fR4OxFS ԩSq),[ X~=fϞ Bٳg7 ,Zaaa|O> &_Wܾ}o6p)1 Y }6^u|Z5ա_~Z뢣w^4444 Ux1vc(;vz4>v^ApA^iZbH/\RWciZ1+.rرCjj*fΜ ō7`̜9fiOX6vXO>_~^`xxGzj"]! `W^A>}(-U}ApOp4=掠s;QZK1iB*E_OdB*sM5 1CXڇ+U}_-<5ٲe 1}t(((ɓ'|}}[OSNg(fFlٲ&DO=mۦQ];6a掠sIϩff`p1-sL_q1"ȅ #::W999CT.\ͶQ΅MP]]W_}ur d2xz*ǟ-++:FYY$ ݛ?GuMVV(q@/ہMIEe`%Yڃw]1ib~DQJKKfYC-r9Ν gggxGs=Ν;X|9<<<ԯ 6޽{pwwdz>0ܹsZ8<[P~"dWG]ۓ7i8YeE8yjJ4A>/;, \y ~\ \ f~ xFsٞ=`pX(hTJhu+W?k.qQW7?¿|v м9dD8}C__I:nTk,~4S>bz'lq,eb~~44bM; r!5uXu\^+?zc$LN ]ߗ  ͛ǏcݺuosUKͪGZZ/ qaڵ ^^^ӧf͚C!;;[=V^^o>:~FF  ݧ[ADY>UW-ʿgw:; 냬='(c?Cu6 PI⌂U;F&0&BL<EZe2_"y+0罰mlhv"Ou H\ 4FDDv=7wMBn+ 2U"wH1Ѯz#P(zdff"::VϷܮYRRRj*}:^z%ܹs .Dtt4 (XA\X{혾%1# ĥ{<+p6/{gQQprʼn!ѽg 1+.W x!h2 :6+.@W[Y5v|ڎ+&+ɀ&6"3`KH/9C IDATA"""R  XN;d ָ WPoE 0 O@ #:bZ,qnsGй_vʭdHT98UOI̯_q4YI jSh2=>u掠UYsF?5RBT5cGdd 7~@D-` ~Ѥilg]t++ʁ;@v&prnr:KWYW  &2E+I$fX;r'W{Ycy1-W뾁njD- Sj->̯_q(d KM5e#4:[~r38(n pb?`my`Hb^5@r';W/ho/F= D 6mUN@GA~Mnb3TJ6"""RXdr9<*VU궵ͮIPl`GDwPyWz?iv3Eu0b~C+4AK̀M:3w35Q4o$NCHypxHc6C4] s,.W\̯8 o! HGi:@sGйt7ws,.W\/ ]up;_9\=GX%"" >!3`BFw"PY £ Q0  3 D7"2TM:?tu6=|(nd6 W?; o\C7꬙":cq1b~!ԠQ]V033h… 5wFEJ.tUN8w*\~ Na>$ QF0sGaBn1ήEEfcQ߯=+WbذamV.F䈺 Ѩ&=] g߄FL$m:ۆzHMmlX>_xN9+.mhVVVuFp1L8ppp@DD׿jlD8;;O=rss1Wp/i@`#8axoP<< x+HO[_5v܉~[c˗/#..r6mիѣGL7Jj8ރTp|*gw7ƽU2ߎ5o_k2aC =^/Cll,͛w}WݢE`oom۶a2e oߎb,_z]īr23sLznsҼws6Nd:=X\̯_q(RtHJJBdd$MӧOkm+h=x\.ǶmSOI<00زeK'ipL"(`-owQU3;P t&HSPD\~XORQX|]F."%B I !Lǐd37Sr^3zn9{r=sUWkS7BqAUWcs{Km,-Ѿk^JJJXp!3h nq7od…7777r9[lw#>SN5a< È#co(;s1H 8qOOO._SN,XbRRR(//',,LPۦH)oP3slڵncҷ\; -9p{Qacy4Cx%WZ}ѺM={6_}+V :yq|1m4F "66VaÆ7D3; 2d9Bjj*sᥗ^ߗNAȂnݢ{z#Gr/_NBB?w𠺺|:u7H"ŝgWr>й 885oSsն9딗 #ǭ AÓ" ־}!**s3ƍGjj*/fܹ?n88w\{{{ [#..8::_My뭷nX:*iFCyy9+V^`رذpB9}-h`? Pq~w84| Ʋ`ڵ ggg̙SO<ӧ9Qh0ꔟ~UVaeeQT tQxyyw|^^2 w&V2[7h R)̷ŃW,܆2GS~| VgK&]e9iyډIY(#^ u"9S}A,r_J}'מ)Ȯ:,Ѽ[{PX:>E,bn03S~8b)Tx~N֭P?{Jrr9-8sWv_muT):si?'D%'r^l;:e0븲E\ϣ&vsv5u$m9Luf|5Y/&E#44Dş'tR˥I`˗/gZ+WJRj1,X'|0`@mիWݻ7ׯ+...dnܸARRR珏gc2<ͧSSPG;L>sRV~r%T 1-sGM▷o,c/1Log! n l\fD 0aF.ޝ7j2 s{df4R)(аqǜsw3{Òߑ6h_is޸ӹ]guF~zMYY7Fѣi%--G6ϪUX|92 Zm1Ey>ۧ>|8 3fsN֭[W F_hQMj PIwv#,9< qp6n: Ipm}VW.3PuOnw wk8M5n\ -o1 s"XZ}%W5ix[sZJ#"" W_eϞ=!;;zyqpp`^qBۉH L4ӧ;h>|8qqq;̘1ѣGrJy7(++cٲe4lK*zxv~J  @g7aξoG 2%\v@r􀾃4]%A}E.yVfcG3&5 kQYSVZj`o:f^34] W_L8'6ߢELH }\M6rJyWX|y>zرcSoc=V[yf>|xbmE ʲeH* ++Hܞ8Stl#heQU Cb$e KDJK`,X>Gsͺuj'?3X[[ll߾#GΞ=[[V㧟~bԩ|? /d&NȌӀcǎI03E#Ekl%t|jbǔ6h_iЛsN,Y²eˣO>|w:oD4 FoԽd26nƍu2K!Y|9999d2BBBXj/>̙3ٴiD(PyCfea`ZyZ} !6-m,4MDJ9s@:1lذѷ_~%_~19(((<ȬYX`ӟڽN d蕚΂ B,X-tG}C;IdxYΞ+݂ ٨Y 5u4gfԨQ݅ #h?/%sz; m"XZ}%W06Z- E| 1EG\a޾й ;[m,۩u GDJK4jVBoVBoo[nȑ#6v(e&5ƈ#X \|'6vODJK4zCu4ggܹzjc)2Uj(ծaf i|NwtEn-7}(ykh_iHR=CرG}0gcԡjpćźݺAqа"M+߀h:ӦMc׮]\xءt8"-8; )@zKKt"~҃ syuԎ**l++W7K^0oeyr@N2 2[fۺet5'%AWDDpDn_;&BƎ=_+G8rFMjȪJ¦ \N#L\Je2;e޿@49ؘJt@LSgcG`Ѻz_eڕлIW H9\o8+܏lw/\Z0'XjW5YZs 71E7v [,*+kr\=wvC#7eTVRw~ }y|6X4qJCdj,vAZZeeez?C#DeDD Gz\J{qsB irp) ZA$'`NLL 3gΤ}DDWh5UdBZ2+M۩T jTdTZYSee3I͓F&#̓>~IRmU%]nJ:AiѢE 8 .P^^F': ັ#h4G04_RVɉ2nH[ CFyONRA,exvrWX+X4qJCv"zK>}Vrr2˖-#,,LLFoG3U0vRb܉Sk;//WBaܝ BAhd+x#=BIYSY)rį̄X됱h$:^zQTTd0:{n,?IEҗRj,Q3>dŊ^GMoV\ZBW N}eC\үAШnBe9]Pa }FM\Jm.tdr=Jל\Yѵk)Loqhx-Wf 6J+ z̜9SNOg#DfDD\IkItHmr[pv.[;1Hmg##2 /k?u)`g`(/[N3݋lw/ "A/Xv-rk׮qM2y'1U"ͤx()A#A-H^ӧeݍNѱV㡇GGGêUgҤI8;;#:uj|.)/SY)aɗQhԤvxO~>9TԓgYK!%RS}"_i`ٳcڵ70o<4׺o4hӦM#**Ǐdƌ^۷o3qDⰱcǎIvna H \rI&ˎ;8q֭/*:pbbb׿ų>˸qشi'OfhYPvܸqdggsA^yFc_x1{fL8'xV`x뭷h4xy'klL&k:ې{9vK/ --+Vm[b=z'JiD[1dȐ;aҥKٳW3u>y릑}^gklM/egu69JOpC}`ǐ{ޏ?u˪( M_հK)쾠[}Y[_]/F2Ju-]Uuw4:.w29 1+-i "{SCQuu6rru~G#:eJϼɺ1Dz޶yi}?΍^dSNS177zyzzJZG%n[*vO1uufg6﹟`i[;x1801^ٟ}'Qfkh'JO% WZ22D́㚷Cml͕n=]9d2d ] D¯\d/#M?Dz'F(eD/TO|/&Q}[=ݸӹ]ggF>7>}߭7ٷoNyff&{Mǰaؼyۄo˰at/a˖-K>3wfĉz>̬Y(*jB-#''ɓ'm69š5kXh`ʕ(JO΁صkӦMLJE*c+tr!9 7DAhRe2&Raaa*J;"iSr1\]]Yp!3f??<'NzرcX[[裏IHH'Nh^:_(vpRPRtG0̔Èڛ]e]oNV*6/'atM?F04R;J0$qJ& VK?eyyHQ[sJDE7Rup3$3f orp:ԺJr9]%l8>0߂ħ3 rVq-)bDBΥdyPimnޠK-m,xf#|i,}c]zIңtT֝􇰰0h4:@.]h۪/^$""BgۥK RGc^~eϟϓO>ɋ/H@@_b|SgZ(1EaLF5 u>66]9:s{=> jdN#{psۗC'wn^ qx[[5B[M3vMܿe(ӝ#߮wYf駟}v{͛7?Æ 믿W_r&SO27qFd2js ǍǾ}Xl3g3f k )oذGyDԽ{w{9%Ot@5*˱@fYs]=-gs+)׍up)-ƥ2[;]wq X4I91lذ 64ϗ_~ɗ_~WJuM4I&5{Xf CGG!((]D,St`#h-מ 9(!aTeIuUߢ`-t[ԟG0 qJCswzK?W >|!CazYj/_6vx耘~s۵2[;sݯ[<| l5O'?} 4^3 AE}au{v&̓|7r6IYon6e kz+B`„ lܸ[nO?1a>3O>}Xt)ϟ7v~,AZ]n|.A0ѻ}Yh8 eJ_^0j]`t S !XsT m42ThqJ9+7t\GrL帹1dFΑY61E)a"cGl67. 3/RΊ!9P슲μĨˍ>\(L6`SUIȪKiJ@t@#_\Y7~''q#hw/9  -m[5vZ@ GL N7z\@vSN]]hXhGEh5qJCӪ ?$99`{=nܸRڵkY`bcc믉gҥb7 BUk@U*zBK5U*_h YYB`FZ  iJ]??eTUUl28@JJ y 2 ױ: sT \< UƎFh/5v [R.s%9~8Fwȑ#1bEthxA?U^\ :]K KNǭ(4-{##` j`ѳ`Y[[_xl-W8:Z@Oѱ:Sul#hs@†k'-Ǝuz1B,)Pڽf6Xk,S2k,/_ΦM(((߬\Yf@po(jS`^d\Y X\Jp|C(5n@cJw+FV1f!;Ru(/xqR9;" o|\r+++T*YG>ߟ+V1R": (8XyC&8H7̾7(%9v)v ^?mSUKi1NTXX˘m1;fJmeUQim~֪*eDN#VBo777N88~8xzz2~x"""j7o#,"ag6 5kغdg\ %{/c# u~׮;+\t#`?r9SNeԩw Caݠ{oph!\N6T˸53V\HUkPA06*ʕX玹-+nBPnkFnNVNBWuG0Z&,,~M¨:epcGa~N8̵ gq-)䗰;/ N; &Xji@WcaLMmwo \f3iȥJKHH@T2"xD?c(,ֺhv@UUxQTB6^ 9.9-[޾;5)B[_[HCUvqYa&o^U&k:YjS6F&QAyգwy .pyrssY|9˗/7>>^{ӧOcee]?G}ƍq~~~JJJb񄇇m6Xlcƌ… xy1sweٲeL23gΰtRnݺ'|p0VR\Au *\6v$-`JHҝ߻Ni@ZYڡL2ZU}EŮ b mlDJ\_ U6hdF͛B': w@nn.}Y-[ {{{݋vٳ'ׯ߯=իyXz5cǎKpBW& P\++@̫nTz$sCi>~6d# t$o@Zq U6CCcT*{G|tڕ &k׮ڲPQQAdd9"##fN^{!&Bax*M05 n@QܾiPAA0#)))-44d*>פw ]ЪsxyypE D 3 PQ3 /4cGִ;[J{Q%E͡97AW/]'m#FHZkI1EXAJc`+ ZJhGBw] j<<frQ0<=nb_Q)p|UVퟧbN@Nmx0\J9>s!HUwTUj\BcRBlvƶm'3@~8RemG;/6X$Ճ MJa1 4TؑX.;wPT`}CTTs`ܸqxbΝ?s9uÇo: kFll 6 I j xQҥKٳvQy"u"777\7Sۙ:uH>[J5ܦwr_u2:sݕ?/?RZFCZtQXwRwGS ڲϱ,x3F~ /E_W׋Q ג"Ȫ5^ɵt2;op*! H?ru/JRJ%?<_}QV֙Iw8ԕJϼɺ1D-:mHL.G4Qse uǽ{/C_q+.ɕ<w19:lG[~U*F: Qܫw;:e=7PXo5Քeغ\]NܦWG)Os)s$)J)u6A=KvpE,Z ѯ|op2S{o7z1X]v̜9st###ӍۻwB`9s bZÊ+Xlζcǎ\ +--={h"֬Y@~~>K,!11޽{;>>cۡ#:|:e1 b휛ԓ1tHmrs? ㅞ7ze&۳0i3]wgHKCFZbPXA:%̾Q%I''c%}f$zx\z7hЕ}:`׮]L6 -ZT{.www.]'|ҥK9~8ׯgʕȌ; Ki1#^O|f+3]Oa%;4xٷr(Bs{/T4Bۈh_2 d-GM2_==M 0|^}UN>-: Ht@4r9eQ-7om++霛M-ynhȶaN w6߫ey.n;!J± t/.Ľ[>ۉA0 {` &OXXQQQh4lW.]h4NhhhI{:ĤtKgOsB93<~m2 o{(.`3h1Y:::B2 oGJVǏHnA:0 TM|>SΚ5o߮Syfu2\wlRRgΜ-ST|׌1Ν_tymÑ#G63#: vZ« |5+Jsp/*0ܔ rVRp+)ĥTVVQcYC꦳ˮ<ˤCh+-ѾQV|g?""ɓ'`>3=s=Gtt4֭Cvw3<5T~ׯs!**{1~w֮][O?ԩS/~^`ɒ%L83fS!`"i4JZZ̈Kqn^76g96LNz۫ ` v`5Bۈh_t@Zs\#vɒ%KXlyyyӇ{vFF6eccÇyxQ* 43f̘,_d2!!!ZJ'`YD y]+wB~|:vm:_` ̅A4)r l*2mB^]FrRǫq+6z|'z 8$Z{F*U +-IT m#WZ}%RF55ё 6aCCKKr6oػWI@#PfgO@vf; `TV-RXVX]8 y+XtdRnkiJ9VUJ yߜ۔ڑJ 幸ԇF F bdgrYG *^MIY¥仸IZ Q֌nk[Ah#1 ;Qbo!\Ja9%-?Mpri{*n::6J;yQfk[R3p))2< ytλm'XSm,h_i|~/`ǒ[i,_W z IDATB'ixèkߍ#'dnshR$N[imͅ JA+,1UM6}%W"d!Xh_E[1Bq!~5iozg7w+bL={щ"G'zJ*ZrF '76FDJDI y1v-qw))b8] 7ҥdSUu,%ǠL-h_iA .#MۂHrVZvv@0-۹ t >~m?W-7}d1&\ew'Sz8)Kq-)J%`$ZD~%'w@܋ } Nedxu6{^0y ΰхCH @cL9}" f=Z3%+۾ {0~`vC͗#i1R28nZؼT m#WZ}%"& f2]NRVg_QN[7LǺ$6 %ooJjR{m \|O88C +km][{TW\}EY?TU,F{dg)CDJKDDD0#b{]+rtt=@oMGGom~] bsv㗰aڳ"8O ôtӡT KvzKݺGRgA7>`:+-Ѿ 9 6}e9S~kt_w/ ?*p} þ-Ps?[޾;]NڎTq\:-˵n!gotHEigϥ>ɫ גBR}`QA+ fDt@V 7O8yJK`$m+k3}ae}#Rއ# &fe'Lt@Vqq׾-wUjj i;U~] C02o^C^ᖷ2 HJd̈b,4vm oSUic<^&֙یBF]:4v8ajmliDJKD$tMQƎ4m + frri,ʐ;rrl++:ؽL-h_iAt@LQƎM6+kF_<&hJFkdrRQhZ&C-WR(06FDJD ̈%BG K}N ʅP*ʷ,`Z3#Lt@*))^cʔ)x{{#Yr>>I&燣#}7ߤ~G;;;z;#JeŐ /caR^\Ki1r >ypW(^ۥR{pͿUV⥯ J̈ܕç~JUUf@VgylRɊ+޽;~!geӦM=rݧw} 裏 /5kxQ^^?@Vx0 =kUk} peUc`DKKDJDt@3"+00|rssqpp -- wwڲcҵkW̙Î;ӟT{իWsϱz}Xt) .O>rF/2LH=ޮ.z 7|@JGbez=vb|s(ox*r&*lw/ E+XhѾ+xRrGCpڲPQQAddξTWW{~+n_!0a;#l>.3r(7K+ ג"ȫ5R BS#Lt@֭[ :ӧזbkk1gB#aܯ]A49 6cԩd2njp,R+p)UV֒ԑ?W$9  HOHII .{{{ glz)qttdԨQFꊉaȑ8::Mdd$wiV=Vgdffr!u{zzRQQ OOφOhv'o4ң}3ŃW,܆2GS~| N=S3?,~t-pN6E~*^OOʹҟ]Q)ZtU^Lju#;~: kSF u[/-Ii003Gq,Rws9}|j5_<븑ZFSWJm kuNRkGK~U*FϺ Qܫw;TȽm=7뀶}%u_G S:F3 ={6_}+V :yqL8Gg:uDDD'Nj?lfmNN.dzuRP_.mzT m#WZܾ7qtdwݐs95gsйug×Ǿo>ONTTsέ-HLL$-- ?c^z%N:P 0'''bcck6leee믵;uG?ou &MiǍ77o)߼y32n$ Hp#꺷!^Sn\ދ;^O =)ttơLmeE+%s}x3}%W"dڵk̙3G<22 N>豽{|( ϟϙ3gsiȑ# a׮]n|u@~JKK).. 11۷0m4@p6lRC=DKoɓ9{,+Wgw|u18r2t!8 fj䘄&gbb"#Glq{omаz=yd.C0/r^xT@ mضm2ׯhC&׿Uz/z-ٸq#ׯח7|%K5;r%בk4dxuE,\Sv&dxR1Ed> *\Kq û3*kO T3 55 77`r ˫ݯcklh_52qz i4-[G__nY }ǐ*F_<;n }*Box3l,%N4xL@vIY.12Y\JuHSm,h_iZB&;Ab)4?B d&_ºhF4r9iHыbЦSb=J¡L=BD^{U"M6}%W"I[왩9 &&KԩSzԩSHJJҫ5v1FɽA gqfuKvPp\x,r 9043 g).'P}(JI8ZZ@%R` 4|m[s3 pzv66HxDxcSprny@bgMs!8p9NK F^ƁQ0eM+}Ѹ⯉WUO#S0b|E" dlڴ ٘9s<33:3\:v…8zv9J[b 㡇֭[xbmsa"kԈ#o_Nyq_  (:)kU5۽WMRptM2(NtGљ(-|@s _puUs|4vJ@ /4{zC z 8!Г0i$,XuuuGVV8m۶ig̛7[lAII ׌;w.6n܈|s!''G:֭äI ˖-Cll,RSSY;& D]VO7kNj._㣠VKRC!qAw⏷_w S{a"صkV\իW ؾ}Z Z׶vtttR,Z Ǿ}0n8k$$$`޽Xz5N L2ׯFL@ -nM@_+¥`%q 3MRV[뉒 VwoI J7(^pp_J,?*5:% 87*b܋1b|E"puuņ aÆvl޼7o+[='NĉTlkh, `hIõ4!SZK#PYV`v=Gw  5ĸ7c|+;8 5n `EQ(Cu7\n"82dFѯ9rу_3.M 𮭆SK" kqowwgw2B"zA"AaX*t[x(,$J2#B*4DDRZGdfL@HA1yJEk>ĀR Cbܛ1b|E6ݘ̌ 5r t z ČKn=> $8Q ILD[q:& ֨-J׭I$_0 m2[qt"W{|0y\|-bXC{3W\Hsq۔J%.]DA*"==cA!Jh"u{=DEEku|]ꙹ[,݂틹_Y ( v-kqoI nN' rlڴ Æ Crr2222 t<@wƍ())u|M^˗/Gbb"=W^yW^Ň~C] 2 _cKD(;\lN dc[ z8$.B/_8y؁$ѯ|*u5|9=f(H;Yӧsà2{;10b|EAdCjͫO ֿx"VX?HNNۯP(oo?~\8 Eh Hpg /0l4*5S#FX"W\/11@:_mHLLSO=ep܌TT\{i?13A"AR)NeJl  eowD^}2D;X&c'm&1F "Ѝ*Q4  0~8; w-u@{l#" FQTTn@llNy`` |}}QXX(j-/0Q@ ,έbUptjT}+L׈}r+Q_[ 4;:B}q8A({nvpҥ( ӣk:& t& tU,^_n=B'''d2}^^^P(_hw)K|0o[!k{ ΍J[h 486E{v׺ASO|t贍!q޽>j8_¸e, H7 s='E?Y+ɩpplа RNw77]࡬S-/]~F_~q_q1"16`BлGjXvv6k[555 Q[[bhjj;OUU||:XOlUL't]8PSwůq⣓:ecԝh7_tʪEu!`ge-ӆc@'] nفӚ, cQ^bQ%yeW_A~5e&1R`?__ul.R=:~'ÿ1NNwB|^vڃ{茼px} ;H߱#gɷ:e- -1u'~ԝ頳~MЏ'6>+X㉍~~QA|&[&~T*`d2cǎ]>ś\]]oի7aHR'4ewH+#3rXf V^-OOOtm={`ԩO3x衇_}bڵXlαyyy1b|+XבW `}OOemusv?~[7 xb8c@~c'I6C|j0 yNhM_R80 {:Wv\ IDATS'ڵD`P9<Nӓ Vp|7Qy+yG 77ÇjNԈkfӶ=11ǏǺum۶!##۶mٳ;<> o>+`ں&LիWm6sxzz"22$}!qn3gy2A# 99/ x'L$33ӦM3k6kl4vNɩM#""kqc@L1޽{,̚5 K.aɒ%5kLtG F@h<Xt)>S_&E ]ۇzܼyPXXlɓ1` 0:Y^yZ ޘ4i;t̟?QQQwsuDžPdҾP[yp'@+X-Y_LDDt1vݻw))):婩o#G`̘g2ܽ{7g+P^^} ylǀe…9s&͛D;wb̙5knܸX6l@vv6qlܸ˗/ƍ;>p=}ja86-ZCV;)Tv:[рUrm_=NDŽwoVV$JJAg}? M+" mYΖ!(((@\\^yO)p|||+ߒuuZnwߢEhѢp,y߬ؓGqv@ã\p,:uʯ?z`{\ 믿}RRBCCxb|xꩧ:& DC#UZgN`ӗ>86$U4;:SLTT222T7$DVUUU5gy/Ƒ#GX1& d 4_T*7D2aQJ+Y ^kSBa#ىhVʺ1wn]!..YYYP:@111SNwxJ L@Q!u@Md,LBGKsgc.H(~m&.cK]+4wdn'2P8شi1sLmyff&ufj ѣuU*n݊ѣG#0ח?DzpG3mq,ktVF\)@sp2 ԭnY]JWtq0ɴR1b|nIII4i,X :t?<8wyGg`y˗/kΝC"%%YYY̙3q9[N[~O>?999/pB\=Lb~Sܟ!Y;gp7C_relb&jcq1b|Ţsiڵ +WիQUUhl߾]ETV6舃bҥXh}aܸqz}T*ū \DH_b>i1! 5PT\ՌiDDDSW^j8ruuņ aÆm޼7o+Gfffdž/I3ɂZ&DFm'nYi]Yǯ8 |vU@Y&UmLkq1c˖(Zmo¸d1ʛ}p(9=Tk$8tP㽁M?LAOdep`d a|-ǷJyB& ֨()jpD Qw/XZfdOj? +>X\l:5{Ϙ%D  BBB{#,ݜލ+>X\za|d & V$((YYYv횥BDDDdc@v02l`BDDDdl%GDDDDD+T*`d2cǎn J nGթ'^^^xQZZj[RK"11~~~JHOO7X;|ggg 8TwΜ9!C+wzeJR &N-kƗrss)2BnnB>w;RRRtSSSQ^^#GXeKvT*|x- #<ݻw6xv'Gss3RSSuΑ Agqo:_`|Wh\rߞJ|5A8̏ (((@tt4RGll,Ͳi/"ၤ$O}.\@SS􎋍q-s6fu'חy{{{-BuuNƷsӾ״o޿Dt/Nk """ʽk<== &Ν1a|WHLLƳ-w F@@osKB'''d2^^^1l0#&&f8x ;WWW`|_Dcc#V\ _yqL@W6l ~رHNNFll,^~e$&&ZuDݗc=x̘1x饗,2۲j*|xo:ŗ9q| tJɓ'ɓhnnƳ-wD"iK477`]] WWWaoobڵXpi % ̙3P:}tM='HLSNǠAhٞIJ={^~ y7kXzzv[l>޿=Q|W,N HNNRDvvNyff&1j( w_|x8::SL]T*p!L>݂-݉eRRsLH$L6\ͶyhllĘ1ce_XjVZot%"5k֬t#w矱i&ov܉_zP~ĉAEE̛7W^'|0V>:tagg-[=ɓ'?~~~H$8}4Xd2H$_---Cvv6^}U̝;s̱lg-cʔ)hnnFUUΟ?>˗/GTT{oXb0|\rEgׯ[J|/] ]& ? ((%!}JRx饗 I6lcK7B||) O?-?~\nnn0qDUO.X+44TH$D"RΟ/]םX,899 BzzR%Y|ӧ aaa$ |iGNKDDDdB'"""""aBDDDdg^R4C&!>>;vұW\AZZ TO>9993f \]]TܸqT]!0!"""yֳӱeYȑ#1{ldeeuz駟'OYːO<wENN{1ܺuˤ}"""""ĘLg޽AVVf͚HHHKd̚5 Ri'$$aҲdDEE!;;[{ΰ0;1^xLO@lu<ٽ{7ݑSr9r~DW^^'3f "##{n#{@LzO9U.,,4u ..No_llv?Y'& DDDD6:+ x{{땷) ]{-S]RK۩SD]w7p٨ s.%$$cCUUv)^T!q0!"""QEEEEWsv|xJXUVUU^+:k*+66u[nԀ|4inf%%%)66VM4ш#~m&I}L={ꫯV\\n͙3GOgϞ_Ljժz+ 66VC ӧ+W\\5k֨VZ4h$iŊԬY3ǫVZ?AѣGy<>}ZӦMSӦM뮻Ng֙3goӦM;Tvmƪy4iM4#Qf dv*oѣ>-ZFUz :TtkԩS7oڴiժUKݺuK/&c.\nAqqqjذƎ.h+V?7xCݺuUW]qkjj޼jԨ5jCZp222xtA-^XZR||4i'xpرjԨziر:ue<UU /.l0$I={TLL֭[}믗$}Tn]tMW_}UK.mݦ犉G}e˖iݺuW $I8zգG\s +Vq8pjժ;wjƍUFեK]uU8p1Ç+''G˖-nӧO'I5jZn=z(%%EǎoCPs X О={4`UVMk׮լYg[o%K4fլYSTRRl٢zJ֭ӻᆱ>p~ҥ<>}%c<_] Rf.i>}Z}ю;ԪU+3F?zj=O2zhU _ZժUӺu{n+&&&9^Z6l/~ 5J6ydUREiiijР?͛7Gݻ /=c=m۶)==]wq^{5͘1CNR:u4m4_=zЦMgjɒ%T6\bTb;{W_m{qo[q 87_ӧOJ*桇ߺuqdeek(++m֬Yq~qСCٳg/=cLN8UV?7yeeegϞZjСC~u8cZha??uIKK3?jժ۷X=q >ok1M6 z~3g4۷_nf0㘫̜9q zLkf1|ot݆} ;\OA?SmٲE9995k{͚57=Xnܿ曀ݮΞ=b=^E=s:xf͚3fݶsN-\ /[m޼Y۶m 7ہ)߿_'8tPЏx6;طרQCZ޽{}BvԡC}*((~Y_|!qt}#wyرCڵkk߾}??Pqqq~Ibb9 n3F6mtnK[Z;5kԵ^C/}֭'_?ccǎv 8}Y#` yxu_Z}v7߀xw~>|xpޏTѬG}TOְaÂ~w+e#<|tM4mTƘz뭋~?nĩS4ydI^ZjZhc:qoWΝu-_o+;|:rHXΰadcQQq9aÆƽb͙3#mO֔)S.|>:oK ~sϞ=zj8qB-[{z//Zjڶmo]}6mڤxmkIJMMUÆ j*UVM58/ƍ+33SZdZqjԨJJJt;6lO>{1;}5p@JII޽{[oi~ٝ$jJZ߯U^uwky\s?CGV5p@խ[W۷o׮]ԲeKegg˗kȑSÆ j׮]D)6nܨǫK.땜C^S*Ubkk馛nҝwީR^ZEEE0at;^n4bwSVtZj*11Q?~ԅҹfzGm6]wuڷox w}ZjOh.uY>s]wƍ8Svmw; Zlo6m͙3G3fɓէO޽[ӦMСCtR߼2|'|RZxx m޼YݺuwׯF#GhĉJLLԞ={*V[Gzw.zg߾}jѢxJ׀T$--M__J:׀"X 6TFF}Yy4m4ݻw%K4fܹӷٳguM7FڵkrN{7⽯w8NXBoQyyyZp?^zZ`^}UTkȑ~'ܻ ,ȑ#}mVڵS֭%駟V^^}wzoL>_bb}^RRڵkyzhPz8nͷDJۀL:U#FѣGn:=:u&N(I7n^z]vsvlI@UQFjԨ$o߾}9ְaÔ>WBB{=X:uTVVSN}єt;-)) 8wT?+=Ç%@IIIQJJeTZPǎV1}+?i|G[||󍊋}]Gi޹??#_styÇnbgڳgMed֭R +:y|c}UllVX׀XB{_Fݻ}s˵rJu 4-ܢ+W턵k.}zGwa?~\+Wm}ݧ?On>2|#c׮>LÇi@.Sk@FZjcǎWzj˚0aԩ/Rփ>f͚۷k…jݺ~_iӦi]nvÕ;l0hz'%Kh߾}ڼy_ٺ5`9RGդIԦM-/ԲeKo >111kGv}dl"UK.Z|y?~\5jP۶mrJ=Zj)99Yב#GtY5iD?LՔ)STfMhJIIɓ}ߪ2|h@uf͚v |#c>2|h@uؚ.򵏌"_.E24 BQgٲen>2|#cQ"Gv}dl"9v8@:tP~~>!^]924 B@Ѐ ꤧ]BD#_.򵏌"_;D1cƸ]BD#_.򵏌"_;v #xvx@ЀDk׺]BD#_.򵏌"_;D\KhkE]pEa׮!C dh@u233.!}dlGv/ N>}.!}dlGv/`vUo^r dh@ Ύ;.!}dlGv/ SO=v |#c>2|=pQSh*>>2"Gv}dl+; :G.򵏌"_.E24 BQgn>2|#cQqn>2|#c]*7^]924 BQ"Gv}dl"р L0"Gv}dl"р ,^"Gv}dl"р =]kE]pG dh@LU B-;;['NtJm߾}:qDVXRfM]!{p|#cQ*}y?8gѢE!?<ְ]kEw1Ƹ])((Pۻ]wJ+%t$ w2^r2xEBQ"|#cQgذan6ְ]kEw4 :fr7"k.򵏌"_;Dv|mc E]pG d*]ǫN:ҥ^xᅀݻj֬Dw}:p@.ZHUf4{l;z222uE[l z͛7+--M JJJRff;veXk@;5nX?AM4ѐ!C4g߼BCZz~?W׮]v3gƍ_7nԨQ4w\=o^YYzꥭ[gѺuT^=Wo۷;TJJ֭[ jիN>m/ \Բe.!‘ma>2|Lܹiܸ `͉'|c_~1'NXC IDAToܹO?7c1v򍕗VZN:ݿcǎuٳw}8c}٠琟o$y1Fv wJF2A~FU0n':gXv}dl+WH:uT/v///믿O5jiܸz5k6lؠ2eff/33S]7fSN*Uhڽ{>,I믵g 2DD͛=>B/''"|#c6 رcZdz-=c/:unƀiF_|cP{~[>޽{+<$\1+6U.r9R${b9r$XTvծ][}WUzuMLLKJJJ*<{?@4 ԩS5b=zT֭ӣ>SNiĉnvQ]JF}۷,Y_ך>}TNIޱPIIQbbs׎ԩSAz[1gEs?&B/=="|#c6 ر˵~]{G}0?_Isƅsu־6mTxLI?+{1뮻wA$mܸ1dF_AA 9sƾ+o|ѢE?~Xiiӵc܀ %iРAGF"< t8ƌ1f̘8)<y^5mTm۶ոq]~2C 1UV5EEEc dիtɓ'JJJL\\9r͛g<|c>qǼ{3gΘVZ4wɴioޝ;wqҥKۺ26wZ^r#FVZرի"^Z/&LxSVV:v쨻[&Mɓ'5c %'';^bbMӧvں+++KÇWjjoaÔ'TRR,Y}i~ufggo׀4rH=zT&MR6mv@4t H.]|r=:~jԨmjʕz}Zhm۶iĉUjUK ,cʔ)Yrrr`hɚ:u߼i„ ;vJKKծ];_^]v۽{w曚1c~iV0cEuAj߾Dk{qJ˻N|IZIʷ@Rΰ"_.kW.b.B.Unn%D85lGv/ K/v |mc E]pG dh@ A{Xl#_Xv}dl"р "|#cQpG"_.E24 BQgǎn6ְ]kEw4 :O=%D85lGv/ ΪU.!‘ma>2|h@u.!‘ma>2|h@ A?~%D85lGv/ Nƍ.!‘ma>2|h@uƎv |mc E]pG dh@ Naa%D85lGv/ ΄ .!‘ma>2|h@u/^v |mc E]pGma>2|h@ AvG"_.EA)--uG"_.Es1"pNAA:t|orTʗ:- txvx@ЀD"Kpkk.򵏌"_;DaÆ]B#_Xv}dl"р ̚5", xa>2|h@uر65lGv/ 24 :˖-sG"_.EA)((pG"_.EAqG"_.E24 B@Ѐ ꤧ]B#_Xv}dl"р 3"|#cQO>n6ְ]kEw4 B@Ѐ ]"|#cQ'77"|#cQ祗^rG"_.E24 B@Ѐ dff]B#_Xv}dl"U$//OCU͕ {QAA߼ y<n!q-ZTƪYf={=zTJJJRBBt-[=͛%%%)33SǎpEX6ְ]kEwU.Zt;GyDZұcF;w[o={i֭~ 89s4c Mn8yzRvt?R}q!Z"|#cR9shܹ5jE_ z|cuQYYN:0Du[RRth 殻RzzOZZ(qFZlXAAUTT7>sLegg}WJOOWaaE4~xRݛtA~˖-C*.4SRs IX?^1\I&E!!]Õ<<\kMm۶ɍ`*Yfqٳ/>gϞ5q{=|q̛77֧OӲeˀΛ78c>l1СCq0E;Z_~~d/PS)H~g+W)yǕӧk|W^yE'OTZZoo߾Պ+XB{_ڽ{o\+WTΝU~}IR t-hʕ}swڥ?\{=e "T7fΜ}ꮻҮ]~$/T׮]ꭷ҆ 4ydedduկ~;^bbMKjڴiھ},X, >\Æ SV4`j8pvavv 5`m޼Y/6m} s۵ma>2|*E诿ц aٳgUV-%''k:rΞ=&M֔)Sv2ej֬-X@)))2|c.Сվ}{S)_@3ڕtPyрD O|mc E]pG3aKpkk.򵏌"_;Dŋ]B#_Xv}dl"р =mkk.򵏌"_;!C dh@u.!‘ma>2|h@uJKK.!‘ma>2|cqSPP:(??_۷w (:%N $;^]924 BQ"|#cQgذan6ְ]kEw4 :fr7"k.򵏌"_;Dv|mc E]pG dh@ βe.!‘ma>2|h@u .!‘ma>2|h@urrr.!‘ma>2|h@ 24 :n6ְ]kEw4 :cƌqG"_.EAӧ%D85lGv/ 24 :k׮uG"_.EAuG"_.EAy饗.!‘ma>2|h@ 24 :n6ְ]kEw4 :|Cmkk.򵏌"_;Dx"|#c@ЀD;v]B#_Xv}dl"р |X_kϞ=2d<8--M͛7{|^vv%D85lGv/]k@TPPVZIԩSiF_|cP{~[>޽{+<$\1+(--uG"_.Ed:yN*I*..$ծ];`nڵeѷ~[zuMLLKJJJ*<{?,Kpkk.򵏌"_npO_|Q/Vv.8v +*; YYY3gΝQFԩ#;*))8JLL-++өS;c{ DJۀdee~&Mw۵^8}Gu+&&F\qoFjݺoM6Soϊ`.]/I7nTzzzGe˖(===` 3g\W_)==]~-JKK;v\/I r<͛χT )]Q$i/Fs /_$ic\I1iҤz>"<|>[+ϣ("C {[e?/7#77ZiӦj۶ƍpHnou9fϞm13f̨pΠALzn;ydXII3#Gy1}og5{7vӪU+wN:6mmûsN8YtiК-4v ŷ6"_.kW5 ,0;ڵܹǫԬYtݬ_޼ꫦu֦aÆs1L:l۶̟?ƚ_~L֭Mƍ͋/h6mdobbbo7w۶mZj{56m2/iԨӧ :4\ DLo}dl+W=zc x<~sM޽MBBU{g1-Z0իW7M41YYY<`ޑ#GСCM:uL\\ҥ zM64gԩc222̱c*<74*xv*.X[n۷צM.iرc5v؋KNN֊+.阽{V޽/i. *E*D"_Xv}dl"р ]B#_Xv}dl"р ]B#_Xv}dl"р!C dh@u.!‘ma>2|h@uƌv |mc E]pGӧOKpkk.򵏌"_;!C dh@u֮]v |mc E]pGv |mc E]pGK/]B#_Xv}dl"р!C dh@u233.!‘ma>2|h@uX6ְ]kEw4 :<%D85lGv/ 24 :;vpG"_.EAyꩧ.!‘ma>2|h@uVZv |mc E]pGv |mc E]pG dh@ .!‘ma>2|h@u7nv |mc E]pG3vXKpkk.򵏌"_;!C dh@u .!‘ma>2|h@u&Lv |mc E]pGxbKpkk.򵏌"_;D'|mc E]pG dh@ Nvv%D85lGv/ Nii%D85lGv/c1ns ԡC}n]R$i$~g+; B@Ѐ ]B#_Xv}dl"р 6"|#cQg֬Ynf]@c E]pGÎma>2|h@ AYl%D85lGv/ NAA%D85lGv/ NNN%D85lGv/ 2 &O>JJJQVVV y<n!q-ZTƪYf={=zTJJzPv IDATJRBBt-[=͛%%%)33SǎJ5 EEEO9sF$9tn\\vK/̛3gƍ_7nԨQ4w\=o^YYzꥭ[gѺuT^=Wo۷;TJJ֭[ jիN>ˑv |mc E]pW~&Mo$瞫pn*Ut-񊋋OhĈz'$Iݺuә3g4m47N-[tn[O>D;wTN$I=zM7ݤ &h׮]?^zW6m[oUC]~"cƌqG"_.EtsEK҆ TVVLLcvZؚ5kk>sM{n>|X_kϞ=2d45o\k֬}qG"_.E Ŝ}-[w޼yqsac12쀹-Z0wqG$IN|#׮\T+ɓJKKWZb+Vqs=Pwkʕܹׯ/IjРn\Ro]{t@x_^?t IvzW$I/tQ 5k&co߮ uկ~;VbbMӧvں+++KÇWjjoaÔ'TRR,Y}i~5fggo׀4rH=zT&MR6mDJـ5J_~st^ZW8:pjժd͟?_GٳgդI=Ú2eJWSLQ͚5 (%%E'OԩS(//O&LرcUZZviڵݻ7Ԍ3xOWjJـ8ۻsώ $FxPP rb 8@DT#bU4R%-?%V"0hBP5BIH*h+F")f@ yZdԕ~Nޓdx+rT,..Vqq;ӓ~\5#Gȑ#j+,,BIk"_{dl|t@b푯5:l|푱-Eб rƏ!G-GƶA3, <ihk"_{dl|t, ٳg=Bȑ5:l|푱-Eб r-Z!G-GƶAҥ#Zö"_ ϰ "gԩ~rk"_{dl|t, =z=Bȑ5:l|푱-Eб r!E"X@xgX@9~rk"_{dl|t, iӦ=Bȑ5:l|푱-Eб r͛!G-GƶA 5:l|푱-Eб ϰ rf͚!G-GƶAȩ{#_kt#c[䋠9C;5554h5p@ZSZ=ğwzϰ "gϞ=~rk"_{dl|t, "G9Fm=2E:DΌ3!f=@a[km/+VX#_kt#c[䋠caA䔕=Bȑ5:l|푱-Eб rjjj!E"X@9{#_kt#c[䋠caDNAA#Zö"_ "gʔ)~rk"_{dl|t, |G9Fm=2E:aDΒ%K!E"X@9~rk"_{dl|t, G9Fm=2E:aaA=Bȑ5:l|푱-Eб rxXkk"_{dl|t, =Bȑ5:l|푱-Eб ϰ r֭[!G-GƶAw- ӴiӔlskjj4rH+##C^{>ϝ;wrrrԹsg}կ~Ƥvڥ'*;;[]vՐ!CjժYUUnݪCjϞ= 7QII:-_\w}~j 544hĈZzq-]TgqFk&o+PnݴtR=c҈#vp*8-{#_kt#c[䋠K{ճgO}{gizHiiizt'K ޽{kΜ9zGxGtwG$]z:pOGTVV͛7k$NЄ okΝ>Ln!Iyyy:zi͇~_~I|1M6ŏ3i͛7ǏmڴkJtm<5\@+IL\ffs ?uI')---܌$kGzC M:B|a[km/. HеKN=!E"BdeeI;))###~nCCMj:kGzC =Bȑ5:l|푱-EЅr9s{/szN:Ii4?_ݫ . ~,77kJ=-3f >~!^/_O]O>O7|SsQii&MEEE۷zJ7pmۖϚ5KUUU nPnnn8F-Gƶt,zbb.%%%?ynȑk׮SNq\s裏Z??|wI'={RؘtnVܐ!Cʕ+[+\^^KKKsYYYnĉnݭ7Ǝǵ#d,1Fm=2Ezbεn~\MM j 8qBkBH;4TRO˻Wª?3t#c[k,=KZö"_ ϰ "+5:l|푱-Eб r!E"xUp<8`y:*XZaDΞ={!E"X@9EEE~rk"_{dl|t, 3f=B{Уö"_ "WF-GƶA3, <)++{#_kt#c[䋠cA=Bȑ5:l|푱-Eб rϟ!G-GƶA3, <3, G9Fm=2E:DΔ)S!E"X@9~rk"_{dl|t~߶mۦ1$Iݻc@m۶Mwc$غu+KbA,YDW_}cIkw}EI}$tOl4!0ߍBm/S^^_̦b[#iYypöA/#r***!F-Eб ϰ "B|akk"_ "wF5Fm/3~xG9F5:l|t, <3, u=Bȑ5:l|a[䋠cA̞=B|akk"_ "gѢE~rk[#_kt"X@9]t{#_ktZöA3, <ș:u#ZE:DN=!F-Eб r!F-Eб ϰ rjkk!F-Eб rM!G5Fm/3o}JJJԧOIRYY6oެkÇ /ԴiӴa[ٳGvcIk[-EЅ; ιV?ب^{M^{m|{.Lc˖-SCC QXX(眖,Y?VYY!I'p&L~[;w[1***{#_ktZöAd:u)h_܇~_~I|oViӦCy:ӴyM6zMI {3f{G{УöASOUIIz)YF=jZ|~J233)眾'tҒȈ_KZ  8B|akk"_]h߿kܸq}ݧ|)i)+RР,I}Ǣ:b1eddHРxnӵmM?1cƨ #///L$i*((HzɓUVVpFI'cUPPڄsԩS׫@֭K8^^^2t7r!FR݄;QIofJXkv\R}}},[,iDҒfǖn&݉Q߃>tz$:8J.bN\CC;pҥ뮻λ X,6nܘpΝ;],s3gΌw}I̙3],s;wlqj'UWW yG85TvkV[|D Xɝ4Ez"/B N:)55Ucǎ՟'۷/~۵zj]s5cGVΝs%\S,W_?6n8&acc^|E]r%:3nGTSS!G5Fm/.r7W^8p233m6wݻ /+--E]J=C:u_R5jyjҤIɉ[TTף>l׶mTUUiH6|G9F5:l|t]@맊 ͟?_Sff hРA?|YFw:jĈ3gNjPzzϟ9s[nz&שS'\RӦMSqq5`:t'QhOw_8pVXѦsU\\|N?.R_, ^"|akk"_ "gʔ)~rk[#_kt"X@9~rk[#_kt"X@xgX@9K,{#_ktZöA)//{#_ktZöAȩ{#_ktZöA3, <3, BG9F5:l|t, !ZE:D!F-Eб ϰ r֭[!G5Fm/3{lG9F5:l|t, E=Bȑ5:l|a[䋠cAtB|akk"_ ϰ "gԩ~rk[#_kt"X@9=z{#_ktZöA)..{#_ktZöA3, <ȩ{#_ktZöAș6m#ZE:Dμy!F-Eб rxyBkk[#_kt"X@xgX@9f{#_ktZöAȩ{#_ktZöA)--{#_ktZöA3, <ٳg#ZET@-[=$)==]{{WTTK=FI"_KtFm/$&Lq[n 2c G~zt =:l|t, kIc|a ꫯ| 8B|akk"_ H a/ "B|akk"_?ȩm!V#|-akt>6۶mkf/рLg +e, =Bȑ5:l|᣷m6wym>ņ|' o>~0R&p#^);~ gX@9~rk[#_kt"X@9SL< rIDAT{#_ktZ{#_ktZ3, ۧu]iii0`*** kF/f̘e˖颋.U^^hdG9F5:l|l, F^uUUU'ФI4l0=S5jN=bd͚5B|akk[#_ JO8^XX;vhƍ>MlG9F5:l|l, F6mڤ>}(%%1\I͛  {*333xӱ{z=;I{juuuϷm|/$}ن-T3g_%I]tQ׮]xќkqMIZrݓk޽{]ҖWI ozz<ҹ+W~L;ݼÇjk%NUVcO-pKl9H1{0Or} h"tMz뭷t%$|RRR>ƌѷHǚ~ 6LzRRRϷz>˗[ƯnK]tQ^4a}-KdkUUU5jwΝ;3Ј#oz>8>"5j(z)f7i$…  .-\UUUڝx7L8w͚5.55Ս7UUU :溆6̝{nƍ ?,o-05իWln&?t6_:|l<؍;=nڵ_t?]zzۼys¹Qsdk{q/[vt_~b_L8g Hb[hQ|׽{wΟ?b1aÆF׷o_7xs/"w$\ﭷrX=t7UÆ s;q=Ln善78y睧̷?6C{2>h d;wMݻwק~?Kd$=nO-IMM)ϸI;, hӦMӧOfI͕$m޼ׯ_ݴi$zn2߿ ?CeeeOԹ瞫ӧkkOG>TGۄwL}o߮}&<Kd$=|;sؗö9nͥgy;]-B;&tÆ s }$ n 233O?VZT}7[b7nԩ[vmsYƝxkq+Vp ,p[]]q̭X-]u].55Ս9{˱fs/v/;eʔCa|go>u͝tIIĉ]JJO[ouYYY.-- 2ĭ\Ybsiii.++M8޽쾂:>]yr;wviii /t3gΌ_ε/X,HIIIMt3ٳgB~+ya3z5ٳg/effTwi+½->8>s0oK<3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <3, <\!9mIENDB`pyepr-0.9.3/doc/images/water_vapour_histogram_02.png000066400000000000000000000767421252122757300225240ustar00rootroot00000000000000PNG  IHDR XvpsBIT|d pHYsaa?i IDATx{\Tu?(h(X*.i(mQA[k^ʒKòMkytLJĆͬ 8xgyy7!4`л"""""zp!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"""""Ͱ!"JAA .tΝ;[}===Ox~<<}[n5j@=t>>>pvvF&M0zhfBϞ=)))t kѣ֭ <裘={6]fQ`@=/ ::~~~pttĪU#FkתW\\C ahڴ)\]]'|}m`0ڵkHLLD`` #`̙~m߾O=իggg4o r&MT{JTrr|e2S]rYkhѢ鉠 9ǏPSVVsupss'uO>jMB,Z>(\\\ШQ#ʕ+VsMKK`U_[nSf6lѼys۷ǢEp `0PPP%K $$hҤ f͚ej>SthРPVV& уAj ѣqF#((p%8phӦ ?ѳgO,ƍ'N^2e \|8q"ۇ5kX믿lDFF⩧_|ӧ Gbb" pl߾K.Eee%-[&  "ґppp.\0 6Lԭ[WܸqC^KNNf㊢EQȑ#EeeeCBtI("kBBB`4?~b^yyѣU8}kݻw-Z˗/DXXPE|G'NZju|m3F("Fe6@7c (عsٸlf'Nu3fT(0 Vm/(bĉ~())1ۭٳg EQDdd1;wN:u Gff&~g\z_~~wn1fMcV^^^h׮vڅ#GN%3UÁO'@۶mk ())Ѹqc<#׬1_+'|b~N.o:ub,X_}?RϜ9c}۷knMDd "U эn~h?G7qD,Z=~iXr%N:U.]pfΜY(bcc}v7oikGk͚5ӧq1w]ݻw[߻w܎q2k^cBD3~oZsNom@x׮]f7QFYgQ706 SLb >jŋlIII())11ܹ3_+WOKK7|s|aO?sYpvv6n@I&Ǣ"P111qXg6;ڵk:uLT]#')ED36 ?#>֭CII Zliv{ ?k׮E۶mѧO\r۷o+ڶmkv5QFXv-jժƍCQK@tt4rrrl24k O=7n/ĉصkbbb _v`p +W /֭[1xjv!!! /GGG|8~8{9 >4??BBBϣVZ?xyyᡇRu!$&&bȑX|i^yy9:t_̓/,Y/֭Y{F~0~x;w'O_SmpڵKzO~~>ZhCꄅ_~ɓ'l@>"5BTT}]ܹsC.\lbccgӽ+++ѦMc޽?z*{W{O?cDDw\^~}ӽȍj[mٲf_7/Bd6{𓃃}nx߿#F0$,, ͛7륎NoKo6DEEEؼy3惈6 BTTT… Xln݊_lիWGGG4nqqqB,\cǎ5޶m[kZp^o6233wߙP\\ڵk|+///ŋQ^=y1\?{6Pz8t78""{g ȴi0zh?7nk2L<0azBv /`ŊxW(_"""""lҸqc4nзo_70`ߚׯr=h Gǎ^xb1énuk Mג~~~zalcǎxpĉj!Si]?7={ŦSo5><8~:mު>SCalB$Mc888YfpU ggg5 iiiP7 0ƍþ}Ls+**zjtdfSǣ~8y$aÆiӦB`ΝXhZj/yyy!11IIIW)))5jMscbbtR 4̓-[|ddd9| رcqy$$$ue˖ 9'''f3cfrzLs#\ҥK\V˗mb6lX`Ν;J4i*NjqǫSK.… )SnL#..h׮6oތ]޽;+L>puuE~`ԪUކjѢ%f&07FZ$** QQQ8nݺJC\\m"--Fݻ7z"""""{f"$"""""s=w 6an139M=f&؀]شi%f&07FZcBv!99Yl3cfrzLs#ED139M=f&|]ջ.!"""͛]ٸG ؀]0~zjlRjpo=6 dRSS/]MafrzLsSoÆ -[6/B'sWcfrzLsS///O؀]Xt%f&07.j i i iw"7]MafrzLsSo„ s?~{^rr2fΜ;v{FϞ=1c ̘1.Vx{QQQQPPMMֱ!w 6an139M!C`׮]վn+*(nmOz헬cBv!""Bl3cfrzaaaX ~|P׃bcc1tP4nөS'.UBul@TXGx;...6iٙO>]v'\]]Ѻuk̝;f4i@xW )))n^b0_[c͚5 +4h^z a0Ά`0m8sAPPx\vb6lѼys۷ǢEpƍ;4† .039M=f&cK/L<CE~~>F8!0m4DDD릹=z_~~ po&FSN!** 1118|0|I\rk.:t(,Yݻcܸqpqq… 1zhSLW^y/JJJ0qD1BEREv!==׻ 07֭[.Aw`4i3]s1gOW_} `ԩn^qYj wmqqL6 >>>ͅ?`ܹ6l֮]'Nয়~BݺufB6mzj̛7 64ꫯ,$!^z%Y :uj-~BvO>ѻ07޼y.Aw+W$&&]z fQ .Q1*++gj>͛gqUM曦\]]/ƍ5kEQLa޾}؀ى@Q͛_MxT|IT1KQtbQFK.#!!=a0`0бcG3gTퟴSĕ+W씥[ƕ+WPN>4h` ԩSa1xcjeeiر# ЩS'DEE^zpttĥKh" Nxzz ѴiS (i!}ܹshy*ΝS=5VX= IDAT$''cfٳ-g`]ֻ07^rr%.44Bdgg[v1>}&Bv>uɓ知};ǎ 8ⵝ;w޳3cfrz;wֻy"xee%^u!/> GGG,^O6 !0eʔ{,YYYfܹs~bBvaСz`s3۷%.,, (((@Vxm7nD׮]1iҤ;GӦM1sLM67nСCݻmڴQuZs_z%ԫW'N1yd<ܹ3}Y!"""zѻjܝ͛vaɒ%qu<#={6.n(V$$$QFx뭷rJԩSO=ϟ>}]c"^î]ݻwc֭hٲ%}] k׮U҇"ԴtO}1_IDDD5sGѼys*Sѣ һ )+4hPֻUj9!{n#139M=f&g|>EupQhTz6|j2UTTo˭^$Nd›oɿUbfrzLsSoժUc mg}ӧO>hԨ.^h߾=bcc.cl@.X;3cfrzseӦΝ;#<<| Ο?0yd899\!؀]puuջ07榞%<0ڶmO?T2F6DDDDD6 DDDDD6 dJ"f&07] z`s3kذ%Q !w 6an139M?z@D544B^^%f&07N8w DTl@.]afrzLsSoѢEz@D5']Xd%f&07&O]v|hTz һ { ޮR=f&07|4o\j=zM56 DDD@0~@K}K} (-] مczq_|紴4DEEݳ:u >scfrziiiz4m#GČ3 o 3Я_?ǁP^^@⋘4iLz!8886{? 1sL$&&7oތg}ӧOGrr=;n-l@.]}v;/^XjuΡZSanꕕ]}ɓԩ5k#G| ۷gϞ111HKKC@@ OOOٳIIIĶmx#c^f=zK^^ f Hff}DFl@.]}|h}38^C͵33f >˸/dgg#%%IIIaÆo߾XpIKKCZZ >ێ`ҥxW={Ě5kij@MF>}sN\z...\]]qm`B@ggMD@o" ""7Mc->dYԀرcMXl߾wF>}P\\oP6 DDDDvm۶Pbqo<ᅦz-qrrB^^ 007n ;;޽;>}`ǎ`vDQQ.`fjq07.]w OOO㎎qu̜9mUmdz>7pe uAsAKALךzLsSͨclRBCCqƍj*++gF#33Ӣٳ'8K.!33uEh(O%sl@.~2.&q07׿]MqwwGHH:ۣ=z@Qdee!++ -ZC=#7n~cǎ!<<``cBvuE33k~|^õk+WX~%8pl!!!صkvmvUXX1w\cBDDDB=::ƍ_|f͚_DBBF>}ꭍ{~ f 3x ?4*k@9r/wXd~6mBII 1a =zX}aÆV6&;;[3FYFdggM6C EQĬYL9"<<|8NB̙3ؿ?͛gj>f5oׯgr-]Tl33KHHup?1B.\e˰uV?(++c=f֭[رcvСC[5l8|iСCniq5NDDDDo@Ǝo!.\cyZԫW}Ճ.]B P\\ڵkbi[pjy~o[IDDDD dڴi=z4Ο?7^CYY&OwiуfOjܸ1BCCѷo_,[ _"ԯ_o,x"E׎\㶌sۦ[Y[IwGdd% f&kM=f&gvDt;ǏGf~bޏ? 899uU={hժiuniͽu<34 Æ m۶_PǏGjjXnn."##QTTd6>c ̟?lԩSŋ1i$RDFFbf!CqԩS.^>-G,!6TxUr[Te=w}f{,0XsE:,~{'s]_{dDǎѠAaݺuOo:)%%;vs=\zӧO/D$%%^zӧ;`ԨQ6͍ҥK1h ̛7>>>Xl󑑑aVѧO 4cǎ瑐֭[[/;DDDDDk@t邕+WbժU|2Ѷm[^Æ 3kѢ1yd pttD^pBk0N ,] .Lbz2338]vؼy3vj6{ꫯ0}tDFF PV{}(DEEhnhh(o^qqq<___h{F޽k4̆ п˰103cfrz;v9rDJqh"kj`ךzLsS/330|p+![w v مO>Dl33ꫯK!ၠ ˰{l@n#nBDDDDD?6 DDDDD6 dlLךzLsSan56 d""".13\k139M=f&؀]:t% f&kM=f&07""""" """""  wֻdp07FZcBv7Իdp07FZcBvaڵz` 53Hkl@.] bf2cfrzLs#!"""""Ͱ!"""""Ͱ!0i$KALךzLsSan56 d.13\k139M=f&؀]ӻdp07FZcBDDDDDaBDDDDDaBv!//Ol33cfri مxKALךzLsSan56 d,Yw 6ZSan139̍o!(ZSan139̍44.13\k139M=f&؀](--ջdp07FZSB"\o999 ջ5rMh^gv i i م"KALךzLsSan56 dbbb.13\k139M=f&؀]HNNֻw6kM=f&07 f&kM=f&07""""" """""  z` 53Hkl@.] bf2cfrzLs#!tRKALךzLsSan56 DDDDD6 DDDDD6 DDDDD6 d"##.13\k139M=f&؀]ջdp07FZcBv!""Bl33cfri i i م 6] bf2cfrzLs#!w 6ZSan139̍'|w 6ZSan139̍444Btt% f&kM=f&07Қ5 9r$7o7774jGnnټ( G}v/^`8;;iӦ9s&***,?QQQ񁛛t邬,@XXh\pC | f&kM=f&07Қ|r\p'NDHH.\ܹ3n݊=z溸`ǎfwqqٳ1}tL2طoq,_4z¯wyXd 틌 t4wΝxѯ_?̚5 ΝɓѫW/߿NNN СC.13\k139M=f&lYd |}}Gys1k@1k,=ft ׯ_Gbb"&L-[RSSqaٳ:uM6޽{M۝4ig`ES`` x Ę1c< """""cs`Um> -[ӧƅޖ-[P^^nqctt4fO]~=Mp>|8ۇB3g~1|@XX7o`5 \r 1z*ƍ#...]2s!@֭6looo>|lc=f{s۬nu{vޭw 6ZSan139̍f qUL64ֶm[իuVDEEaʕx'ovV Bqq_xճg35n7|Sl33cfri*)) 1,Yvڙ'L`6W^h׮^xX֥(۾ڵk.13\k139M=f&l̞=sqn;pss÷~k_>QVVf1ŋ_܋/Zg|V7mZ3 22',,zضm"##-?~x"22EEEf3fN:H噍/^&M2+--Edd׷V+>dȐ{rϷW` , ` UƷ<`<*c-2ŻO>3b>QKŞ={Jqq>\]]8~vqFZ]pbh۶& F%'' EQ̙3kJ*j㏅(o5[XX(Es5EDD-[ZlwܹBQQXX(BQ1|-ZO=rrrSc"#5@hZvBO9""{M~o %%IIIHJJ>3\zaaa}iiifsҠ( o0`o>XEEV^Ν;aÆ0h ddd?Ѻuk_ҝ,33cfri.BߴiE-[eEAee%<== ܹsD&Mꫯbԩw:u*<<}:"##~aUֽ w 6ZSan139̍Q&rssѾ}{ 44Tr@5 {ߞ?GDDtOڝSv!"""""Ͱ!PTLךzLsSan56 d.13\k139M=f&؀]Xd% f&kM=f&07 f&kM=f&07""""" """""  ׻dp07FZcBvTl33cfriMBrssѾ}{ 44Tr@5 {ߞ?GDDtOڝ7 DDDDD6 DDDDD6 d.13\k139M=f&؀]ѻdp07FZcBv!99YlP$53Hkl@..2 53Hkl@H3l@H3l@.] bf2cfrzLs#!w 6ZSan139̍ҥK.13\k139M=f&؀f؀f؀f؀]Իdp07FZcBv!66Vl33cfri مKALךzLsSan56 DDDDD6 DDDDD6 d6lؠw 6ZSan139̍Bzz% f&kM=f&07 |% f&kM=f&07""""" """"" """""  z` 53Hkl@.𑩎7 IDAT)2 53Hkl@. :Tl33cfri i i مݻw] bf2cfrzLs#!o] bf2cfrzLs#!vZKALךzLsSan56 d\]].13\k139M=f&؀f؀f؀]4i% f&kM=f&07 z` 53Hkl@.] bf2cfrzLs#!"""""Ͱ!"""""Ͱ!w 6ZSan139̍B||% f&kM=f&07 K,ѻdp07FZ$33#GDF͵޽{^^^8p N8au/Fpp0ѴiS̜9Ο?( ]tAVVmfdd ,, nnnAtt4.\pgU f&kM=f&07Қ5 ˗/ǩS0qDl޼-ѹsgر4///ᨨuGE׮]QTTdٳgc„ xm67s敗W^رcylܸ 4@߾}_ݹs'~iaƍXh222ЫW/\vDDDDDt?6ܹscEÆ E޽Mc 4vI$&Ol+**b̘1fۜ3g0 ⧟~2-]T("k!!!SNfرhժ4}7BQZ=@.1) GBßײ;}+vl___1777lOTTT`ӦM8p MУG_4e#::lB`Æ #88:u2988`طo gΜ1b 8,, ͛77?׻dp07FZĚ+W 77!!!?(++c=f1u8v4CoհaCx{{æCUMmV7:=z` 53Hkvр?W^ŴizY̭Wtinڵb1˴-xbۼuۤ#%%El33cfriQTRR>c,YڵӻQEta߀`٘3gƍg_>XTuE(///rYkܖqnuۼu$""""zlbIHH0{Yfpqq?`AAAprr4={,ѪU+X֭&\?{6ygifvA<l۶ ?~kSO=e6md^xӶm[,f_uuٳҥYdY~5jiݺٲeKM6VZѣGכŋGW^ Gf}i ByGfv;2Cn 2x`d"H#)))foii:tIMM5:t0G6|IKꫯ6mڴ1]v5EEE6nW_}e>aRRR̀̆ s&''Ǥ 3~xsСFShZ|vw76zo߾}~F-((PAA9uQ-j1C6j/MPh 2AyGfv;2CnpPVVw Df65yGfv 1 ϟw Df65yGfv 1p3 aA(]B z;2Cnޑrk ɓ']B z;2Cnޑrk \K 2AyGfv;2Cnp3 aA(Zlkޑr\cA(]B z;2Cnޑrk e˖]B z;2Cnޑrk a g@ ~@df^쐛wdfBOqAf65yGfv 1 Ǝw Df65yGfv 1p3 []B z;2Cnޑrk 9s]B z;2Cnޑrk K]B z;2Cnޑrk v]B z;2Cnޑrk a Baʔ)~@df^쐛wdfBK.~@df^쐛wdfBlkޑr\c g@ ~@df^쐛wdfBaԩ~@df^쐛wdfB_"3wdfܼ#3;@ B٠׼#3;!70 fϞw Df65yGfv 1 .!!71]N*++S~TZZ}]BRI.{LR?g=}f,~ g@8P"3wdfܼ#3;@ &L"3wdfܼ#3;@ 3fwDyGfv;2Cnp](l z;2Cnޑrk a Ba…~@df^쐛wdfBlkޑr\cA(̟?lkޑr\c g7=zTSNUnn233}ǏWRRR'?IΛ7OYYYj۶.r=3wA?^JMMՀ{%\[lٻyf|ԩV^_|Q%%%2d?DiN^^%٠׼#3;!7w^uU_$z{E;>3g̙3%I7xjjj4m4{Nަnڶm/IC{CrrrtUWiʕ>4^nn%٠׼#3;!7\;uIя~'uv%IΎYKuk1{{w;fC{4ݻ飞={J6mڤ_ڰa>J:#XmڴQJJJ1tWVV*===n_Z޺_{1$GՐ!C4dWқooą4{glkޑr\ ȨQ;vD222T]]cǎTFFFʄ~ =󘉌1Byyy1 ֭[&M… cʔ~ZgώYۿT^^>ot ˗/D駟Cر^x}WյkW=z'xO}?ΝN:'SO=uڰaNUUUO>ZfwРAzw5}t]v9r^xjժy.P@>sy=@ױcǸjСC5tPOuN~~2&_yEyGfv;2CnpEŧ 3wdfܼ#3;@ cǎ"3wdfܼ#3;@8B'!75œ9s.!!75ҥK.!!75Bv.!!750p0eK 2AyGfv;2CnpХKK 2AyGfv;2CnpPPPw Df65yGfv 1p3 rK 2AyGfv;2Cnp0uTK 2AyGfv;2Cnp/]B z;2Cnޑrk n!hlkޑr\c g@ g"3wdfܼ#3;@ UUU~@df^쐛wdfZc.'_~*--U߾}.!PSR$=U&>sh|vx3 aA(TTT]B z;2Cnޑrk  &]B z;2Cnޑrk 3f]B@׼#3;!7P.6!750ppBK 2AyGfv;2CnpPVVw Df65yGfv 1 ϟw Df65yGfv 1p3 aA(]B z;2Cnޑrk ɓ']B z;2Cnޑrk \K 2AyGfv;2Cnp3 aA(Zlkޑr\cA(]B z;2Cnޑrk e˖]B z;2Cnޑrk a g@ ~@df^쐛wdfBOqAf65yGfv 1 Ǝw Df65yGfv 1p3 []B z;2Cnޑrk@=S*77WJJJRQQQ½eee:tڷo4~O7oԶm[]~zgT[[?~233Kx̒(55UסC/ 3g%٠׼#3;!7Bjjj4j(IR$W^^V˗/o{jϪPwq֭[GyD=&MZC ƍK/iպK4|pmٲ%f͛u7SNZz^|EhȐ!:~xK]B z;2Cnޑrk~U׮]_K>_=ӧ+%%E~H+Rs?=̙3j̙oQ5556m ս{wI… {nm۶M$ ,IJOOZzz11?զMMKKK*++<=<&Ɣ)S.!!7P Doҥ%٠׼#3;!7$##CW,ꫬT$QZZZtouu;poݱ6t3{yL4K 2AyGfv;2Cnp-ȏcs+T֭%~FÇgϞѵ))׆yDFGNNN%iݺuˋ{Ip˜2݂駟ٳc߯<Ǭϛ7/eڪ*}Qqqq%鮻:|i:I!MZ٩-O,>O? Vujo(Vb۶mGX:#Q\\^[nݻ L:tD"STT\r%o}gu'UVV<֬Y&))ٳ'`D̎;k555G&'''7ᅬm۶D"+$R#ɔ62zJ*5q8nOj̩s\~5khŊ ݧVX+Vモ$Jz֮]+W[nQǎ_"z4M6MM͛7kܹ***ĉ;aCcƌQqqJJJtwj߾}qٳU^^1cƨDK,ѝwީ8O+ 3wdfܼ#3;ltD"DLRRR?g}fС&55t=|' K/ڴitڸ}_}LFFIII1 06lHxכb222͡C.&j{#G _+ 5yGfv_;c4?8WVV~T}@ٿ?wHRI{jNXe%]uUְw^nªUtm]FDf^kv$uQO}qVb=jNV55Bqq1yzV,5/K+i_\KU7z;2Cnp7#-[w Df65df^\c g@8P"3 2AyGfv 1 Wdf^Af655رc.!flkޑrk a Ba֭~@df^Af655œ9s.!flkޑrk K]B z٠׼#3;@ ڵ"3 2AyGfv 1p3 )S]B z٠׼#3;@ ]t"3 2AyGfv 1  .!flkޑrk a Blk6!7P:u%٠l z;2Cnp/]B z٠׼#3;@ B٠l z;2Cnp3 aA(̞=lk6!7P"3 2AyGfv 1 .!flkޑrk@6mڤ;w-++СCվ}{oק~SVVڶm/\|l Q4+R]w]_>}RRR;?$_~+5w\=Çk̙z5sLIҍ7ިM6M޽$i…ڽ{mۦKkFSNۛ[ ]|~0+zf^\ + ƘV[[wyG~{tN~ OS\2vZUWW+???2hժUѵ+W*+++:|HE]qiΝ:p@S\0aK 2A 3wdfZI&UVС?ѯ}:vzl}G:~$i׮]3]z饺⋵{ڮ]VL}$:[nZNbQ_ee"$Iֱc;VކY1bb99913u)///&M… cʔwg~:P߯<Ǭϛ7OSLYR^^n^\\~I뮻CZ+):$NRuH$-VvjoE{S;ϓ4ZթXm۶]a髆[kzZ񟪞.]zj?oO_q\Q\\^[nݻ L CH$bMMMi׮ytMꫯ%KL$1;vwD̬Yk{qǜ5kD" k+--5LiiX%\zJ*5x=ZS=NrGԬsz?wҷwdfܼע^?A}Q֭#G=ݷ~mܸQG >\m۶բEbh"E"vmѵQF<kkk[oץ^|B]B z٠׼#3;B{GݺuS߾}}__u!}EEEku뭷w}ӧcǎ/~ݗiӦ闿5l0}***ĉ;a͟?_cƌ?L7Ѿ}TRR4b~@df^Af65 zҲe4|=zT8p/^~E]}ڴi{1qJNN֐!C4wܸjojjo^W^yׄ zjL$W z!7r7n%XٻwoCȌ3|9o@l@׼#3;@.H4sH$uoqfoq|۹lk6!7rA&`w~&x:g@ ."3 2AyGfv #X2~0e+zf^o߾sԧO&?_wzl57D}wKBa~@df^Af6ۧQ{/_,5ӣkn r7pA;*zt}ǺԻ["@@@{=+FK8P"3 2A 3@ 'O"3 2A 3@ ~@df^Af65dfg@ѣGUXXΝ;+%%E}Ѳe. H3=z|M͘1Ck׮յ^cǪBgժU~@df^Af65dfH3ywURR hĉ4h^}U 6LSLщ'.1TfϞw Df65df^Af6H 14+W}3fLz~~Kرç)33lk6fl\ci&vRݕqvv$i~>|Xqukv];$]@Xedd$|2GZґzk_Jޕ%:B"ߜz͡S;V'[ujժUު*(99=555:~"Zju=DM]wYi&Z_C{Zn]?$bOMMM϶ȑ婒e(}'Xk:\j$]R(y{W'+:nժ?GǪS[[sFƘsoDԮ]TSS֭[]vVu7f߷~{";껺>ksƿj$?uu_(}ѿך3luо]vTl5~?6kIҬYw D\7С]?JZ|ƿz={D./b'V~XGyҥKuw__.թS'K $fvZ1BK.՝w]>|vޭ+=:pRAN>?LaÆO?U\\ui @ H3oSOw*++ս{w=1- g /g@ѣGUXXΝ;+%%E}Ѳe܃jTjj {/ޒ(55UϏ޲/h\d6x`%%%=Fԗmn 5h ?TRR~6^vo;SݺuSvԭ[77N}QJJJ4l0uYm۶%\!Ch͚5 o%3zaӦMSRR~=L֤ ԰aLZZyWͦMĉM$1K,9;fzitb,YbJJJmfZje6owӦM&99ٌ5ʔŋ.dgg漼f"A+ر#9/YqFirsswm"op/voFix e[o'}f1{鵓dFl2w3[l1+W47tD"歷ފK%3z-?Om۶K/5q_[5%&&KƬΝ;Ο?D"}Zmmѣ߿k3xD"fMt5nlРA rĉ4v<_֭[x f^;KfԘ.x1Zʌ^WSSczm ^kjVZrڷo1cĬ/Ԏ;ܬ,?vEiܸqڹsgA }ᇺ{c>0''GW]uV\Wռ3hBt[@-zfeOPOn>5SNܹ>vZc3C5,99Y:txzeV^ȑ#9sflkM ڵKݻwi4Iџ ܽ{Y۫Wݵk$5AќY㏕VZ+дitر_On^!k6赆}'ڿzs^kHkN8Z}z駵w^?9D\աN_g}V ,PjjjkM"lBW\zC*++ܺ_{s\\d&IرcN̙[jƍ ~0|rr3Y<-׼V[[ &}1k k(3^KdĈZn$]vZxn֘syiv$zL&Lo]Ç?9c;ukgGPFFF‰2=nٞ[kC{v 2n8I39uwzk͙3\wu&==$''/|wMxzk="4&\֔" oB g@80p3 a g@80p3 a g@80p3 a g@80p3 a g@80p3 aj{|IENDB`pyepr-0.9.3/doc/index.txt000066400000000000000000000055431252122757300153140ustar00rootroot00000000000000================================= ENVISAT Product Reader Python API ================================= .. raw:: latex \listoffigures % \listoftables \clearpage :HomePage: http://avalentino.github.io/pyepr :Author: Antonio Valentino :Contact: antonio.valentino@tiscali.it :Copyright: 2011-2015, Antonio Valentino :Version: |release| .. only:: not latex Overview ======== PyEPR_ provides Python_ bindings for the ENVISAT Product Reader C API (`EPR API`_) for reading satellite data from ENVISAT_ ESA_ (European Space Agency) mission. PyEPR_, as well as the `EPR API`_ for C, supports ENVISAT_ MERIS, AATSR Level 1B and Level 2 and also ASAR data products. It provides access to the data either on a geophysical (decoded, ready-to-use pixel samples) or on a raw data layer. The raw data access makes it possible to read any data field contained in a product file. .. _PyEPR: https://github.com/avalentino/pyepr .. _Python: https://www.python.org .. _`EPR API`: https://github.com/bcdev/epr-api .. _ENVISAT: https://envisat.esa.int .. _ESA: https://earth.esa.int .. index:: ENVISAT, MERIS, AASTR, ASAR Documentation ============= .. toctree:: :maxdepth: 2 usermanual tutorials reference NEWS .. only:: not latex Other versions ============== Online documentation for other PyEpr_ versions: * `latest `_ development * `0.9.3 `_ (latest stable) * `0.9.2 `_ * `0.9.1 `_ * `0.9 `_ * `0.8.2 `_ * `0.8.1 `_ * `0.8 `_ * `0.7.1 `_ * `0.7 `_ * `0.6.1 `_ * `0.6 `_ License ======= .. index:: license Copyright (C) 2011-2015 Antonio Valentino PyEPR is free software: you can redistribute it and/or modify it under the terms of the `GNU General Public License`_ as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PyEPR is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with PyEPR. If not, see . .. _`GNU General Public License`: http://www.gnu.org/licenses/gpl-3.0.html pyepr-0.9.3/doc/interactive_use.txt000066400000000000000000000271321252122757300173740ustar00rootroot00000000000000Interactive use of PyEPR_ ------------------------- .. highlight:: ipython .. index:: ipython, interactive, ENVISAT, ASAR, ESA, pylab, matplotlib pair: interactive; shell pair: sample; dataset In this tutorial it is showed an example of how to use PyEPR_ interactively to open, browse and display data of an ENVISAT_ ASAR_ product. For the interactive session it is used the IPython_ interactive shell an started with the :option:`ipython -pylab` option to enable interactive plotting provided by the matplotlib_ package. The ASAR_ product used in this example is a `free sample`_ available at the ESA_ web site. .. _PyEPR: https://github.com/avalentino/pyepr .. _ENVISAT: https://envisat.esa.int .. _ASAR: https://earth.esa.int/handbooks/asar/CNTR.html .. _IPython: http://ipython.org .. _matplotlib: http://matplotlib.org .. _`free sample`: https://earth.esa.int/services/sample_products/asar/IMP/ASA_IMP_1PNUPA20060202_062233_000000152044_00435_20529_3110.N1.gz .. _ESA: https://earth.esa.int .. index:: module pair: epr; module :mod:`epr` module and classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ After starting the ipython shell with the following command: .. code-block:: sh $ ipython -pylab one can import the :mod:`epr` module and start start taking confidence with available classes and functions:: Python 2.6.6 (r266:84292, Sep 15 2010, 16:22:56) Type "copyright", "credits" or "license" for more information. IPython 0.10 -- An enhanced Interactive Python. ? -> Introduction and overview of IPython's features. %quickref -> Quick reference. help -> Python's own help system. object? -> Details about 'object'. ?object also works, ?? prints more. Welcome to pylab, a matplotlib-based Python environment. For more information, type 'help(pylab)'. In [1]: import epr In [2]: epr? Base Class: String Form: Namespace: Interactive File: /home/antonio/projects/pyepr/epr.so Docstring: Python bindings for ENVISAT Product Reader C API PyEPR_ provides Python_ bindings for the ENVISAT Product Reader C API (`EPR API`_) for reading satellite data from ENVISAT_ ESA_ (European Space Agency) mission. PyEPR_ is fully object oriented and, as well as the `EPR API`_ for C, supports ENVISAT_ MERIS, AATSR Level 1B and Level 2 and also ASAR data products. It provides access to the data either on a geophysical (decoded, ready-to-use pixel samples) or on a raw data layer. The raw data access makes it possible to read any data field contained in a product file. .. _PyEPR: http://avalentino.github.io/pyepr .. _Python: https://www.python.org .. _`EPR API`: https://github.com/bcdev/epr-api .. _ENVISAT: https://envisat.esa.int .. _ESA: https://earth.esa.int In [3]: epr.__version__, epr.EPR_C_API_VERSION Out[3]: ('0.9.1', '2.3dev') .. index:: __version__ Docstrings are available for almost all classes, methods and functions in the :mod:`epr` and they can be displayed using the :func:`help` python_ command or the ``?`` IPython_ shortcut as showed above. .. _python: https://www.python.org Also IPython_ provides a handy tab completion mechanism to automatically complete commands or to display available functions and classes:: In [4]: product = epr. [TAB] epr.Band epr.E_TID_STRING epr.DSD epr.E_TID_TIME epr.Dataset epr.E_TID_UCHAR epr.EPRError epr.E_TID_UINT epr.EPRTime epr.E_TID_UNKNOWN epr.EPRValueError epr.E_TID_USHORT epr.EPR_C_API_VERSION epr.EprObject epr.E_SMID_LIN epr.Field epr.E_SMID_LOG epr.Product epr.E_SMID_NON epr.Raster epr.E_SMOD_1OF1 epr.Record epr.E_SMOD_1OF2 epr.create_bitmask_raster epr.E_SMOD_2OF2 epr.create_raster epr.E_SMOD_2TOF epr.data_type_id_to_str epr.E_SMOD_3TOI epr.get_data_type_size epr.E_TID_CHAR epr.get_sample_model_name epr.E_TID_DOUBLE epr.get_scaling_method_name epr.E_TID_FLOAT epr.np epr.E_TID_INT epr.open epr.E_TID_SHORT epr.so epr.E_TID_SPARE epr.sys .. index:: product :class:`epr.Product` navigation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The first thing to do is to use the :func:`epr.open` function to get an instance of the desired ENVISAT_ :class:`epr.Product`:: In [4]: product = epr.open(\ 'ASA_IMP_1PNUPA20060202_062233_000000152044_00435_20529_3110.N1') In [4]: product. product.bands product.get_mph product.close product.get_num_bands product.closed product.get_num_datasets product.datasets product.get_num_dsds product.file_path product.get_scene_height product.get_band product.get_scene_width product.get_band_at product.get_sph product.get_band_names product.id_string product.get_dataset product.meris_iodd_version product.get_dataset_at product.read_bitmask_raster product.get_dataset_names product.tot_size product.get_dsd_at In [5]: product.tot_size / 1024.**2 Out[5]: 132.01041889190674 In [6]: print(product) epr.Product(ASA_IMP_1PNUPA20060202_ ...) 7 datasets, 5 bands epr.Dataset(MDS1_SQ_ADS) 1 records epr.Dataset(MAIN_PROCESSING_PARAMS_ADS) 1 records epr.Dataset(DOP_CENTROID_COEFFS_ADS) 1 records epr.Dataset(SR_GR_ADS) 1 records epr.Dataset(CHIRP_PARAMS_ADS) 1 records epr.Dataset(GEOLOCATION_GRID_ADS) 11 records epr.Dataset(MDS1) 8192 records epr.Band(slant_range_time) of epr.Product(ASA_IMP_1PNUPA20060202_ ...) epr.Band(incident_angle) of epr.Product(ASA_IMP_1PNUPA20060202_ ...) epr.Band(latitude) of epr.Product(ASA_IMP_1PNUPA20060202 ...) epr.Band(longitude) of epr.Product(ASA_IMP_1PNUPA20060202 ...) epr.Band(proc_data) of epr.Product(ASA_IMP_1PNUPA20060202 ...) A short summary of product contents can be displayed simply printing the :class:`epr.Product` object as showed above. Being able to display contents of each object it is easy to keep browsing and get all desired information from the product:: In [7]: dataset = product.get_dataset('MAIN_PROCESSING_PARAMS_ADS') In [8]: dataset Out[8]: epr.Dataset(MAIN_PROCESSING_PARAMS_ADS) 1 records In [9]: record = dataset.[TAB] dataset.create_record dataset.get_dsd_name dataset.product dataset.description dataset.get_name dataset.read_record dataset.get_dsd dataset.get_num_records dataset.records In [9]: record = dataset.read_record(0) In [10]: record Out[10]: 220 fields In [11]: record.get_field_names()[:20] Out[11]: ['first_zero_doppler_time', 'attach_flag', 'last_zero_doppler_time', 'work_order_id', 'time_diff', 'swath_id', 'range_spacing', 'azimuth_spacing', 'line_time_interval', 'num_output_lines', 'num_samples_per_line', 'data_type', 'spare_1', 'data_analysis_flag', 'ant_elev_corr_flag', 'chirp_extract_flag', 'srgr_flag', 'dop_cen_flag', 'dop_amb_flag', 'range_spread_comp_flag'] In [12]: field = record.get_field('range_spacing') In [13]: field.get [TAB] field.get_description field.get_name field.get_unit field.get_elem field.get_num_elems field.get_elems field.get_type In [13]: field.get_description() Out[13]: 'Range sample spacing' In [14]: epr.data_type_id_to_str(field.get_type()) Out[14]: 'float' In [15]: field.get_num_elems() Out[15]: 1 In [16]: field.get_unit() Out[16]: 'm' In [17]: print(field) range_spacing = 12.500000 .. index:: iteration, iterable, record Iterating over :mod:`epr` objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :class:`epr.Record` objects are also iterable_ so one can write code like the following:: In [18]: for field in record: if field.get_num_elems() == 4: print('%s: %d elements' % (field.get_name(), len(field))) ....: nominal_chirp.1.nom_chirp_amp: 4 elements nominal_chirp.1.nom_chirp_phs: 4 elements nominal_chirp.2.nom_chirp_amp: 4 elements nominal_chirp.2.nom_chirp_phs: 4 elements nominal_chirp.3.nom_chirp_amp: 4 elements nominal_chirp.3.nom_chirp_phs: 4 elements nominal_chirp.4.nom_chirp_amp: 4 elements nominal_chirp.4.nom_chirp_phs: 4 elements nominal_chirp.5.nom_chirp_amp: 4 elements nominal_chirp.5.nom_chirp_phs: 4 elements beam_merge_sl_range: 4 elements beam_merge_alg_param: 4 elements .. index:: data, image Image data ~~~~~~~~~~ Dealing with image data is simple as well:: In [19]: product.get_band_names() Out[19]: ['slant_range_time', 'incident_angle', 'latitude', 'longitude', 'proc_data'] In [19]: band = product.get_band('proc_data') In [20]: data = band. [TAB] band.bm_expr band.read_raster band.create_compatible_raster band.sample_model band.data_type band.scaling_factor band.description band.scaling_method band.get_name band.scaling_offset band.lines_mirrored band.spectr_band_index band.product band.unit band.read_as_array In [20]: data = band.read_as_array(1000, 1000, xoffset=100, \ yoffset=6500, xstep=2, ystep=2) In [21]: data Out[21]: array([[ 146., 153., 134., ..., 51., 55., 72.], [ 198., 163., 146., ..., 26., 54., 57.], [ 127., 205., 105., ..., 64., 76., 61.], ..., [ 64., 78., 52., ..., 96., 176., 159.], [ 66., 41., 45., ..., 200., 153., 203.], [ 64., 71., 88., ..., 289., 182., 123.]], dtype=float32) In [22]: data.shape Out[22]: (500, 500) In [23]: imshow(data, cmap=cm.gray, vmin=0, vmax=1000) Out[23]: In [24]: title(band.description) Out[24]: In [25]: colorbar() Out[25]: .. figure:: images/ASA_IMP_crop.* :width: 100% Image data read from the "proc_data" band .. _iterable: http://docs.python.org/glossary.html#term-iterable .. index:: close, product Closing the epr.Product ~~~~~~~~~~~~~~~~~~~~~~~ Finally the :class:`epr.Product` can be closed using the :meth:`epr.Product.close` method:: In [26]: product.close() After a product is closed no more I/O operations can be performed on it. Any attempt to do it will raise a :exc:`ValueError`:: In [27]: product.tot_size / 1024.**2 ------------------------------------------------------------------------- ValueError Traceback (most recent call last) in () ----> 1 product.tot_size / 1024.**2 epr.so in epr.Product.tot_size.__get__ (src/epr.c:16534)() epr.so in epr.Product.check_closed_product (src/epr.c:16230)() ValueError: I/O operation on closed file At any time the user can check whenever a :class:`epr.Product` is closed or not using the :attr:`epr.Product.closed` property:: In [28]: product.closed Out[28]: True .. raw:: latex \clearpage pyepr-0.9.3/doc/make.bat000066400000000000000000000144431252122757300150500ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyEPR.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyEPR.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end pyepr-0.9.3/doc/ndvi_example.txt000066400000000000000000000130251252122757300166520ustar00rootroot00000000000000NDVI computation ---------------- .. index:: NDVI, MERIS This tutorial shows how to use PyEPR_ to open a MERIS_ L1B product, compute the *Normalized Difference Vegetation Index* (NDVI) and store it into a flat binary file. The example code (:download:`examples/write_ndvi.py`) is a direct translation of the C sample program `write_ndvi.c`_ bundled with the EPR API distribution. The program is invoked as follows: .. code-block:: sh $ python write_ndvi.py .. _PyEPR: https://github.com/avalentino/pyepr .. _MERIS: https://earth.esa.int/handbooks/meris/CNTR.html .. _`write_ndvi.c`: https://github.com/bcdev/epr-api/blob/master/src/examples/write_ndvi.c The code have been kept very simple and it consists in a single function (:func:`main`) that also performs a minimal command line arguments handling. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py The ENVISAT_ :class:`epr.Product` is opened using the :func:`epr.open` function. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py :lines: 49 .. index:: context, open pair: with; statement As usual in modern python programs the *with* statement has been used to ensure that the product is automatically closed as soon as the program exits the block. Of course it is possible to use a simple assignment form:: product = open(argv[1]) but in this case the user should take care of manually call:: product.close() when appropriate. The name of the product is in the first argument passed to the program. In order to keep the code simple no check is performed to ensure that the product is a valid L1B product. The NDVI is calculated using bands 6 and 8 (the names of these bands are "radiance_6" and "radiance_10"). :class:`epr.Band` objects are retrieved using the :meth:`epr.Product.get_band` method: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py :lines: 51-56 *band1* and *band2* are used to read the calibrated radiances into the :class:`epr.Raster` objects that allow to access data matrices with the radiance values. .. index:: raster, memory Before reading data into the :class:`epr.Raster` objects they have to be instantiated specifying their size and data type in order to allow the library to allocate the correct amount of memory. For sake of simplicity :class:`epr.Raster` object are created with the same size of the whole product (with no sub-sampling) using the :meth:`epr.Band.create_compatible_raster` method of the :class:`epr.Band` class: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py :lines: 58-66 Then data are actually loaded into memory using the :meth:`epr.Band.read_raster` method. Since :class:`epr.Raster` objects have been defined to match the whole product, offset parameters are set to zero (data are read starting from specified offset): .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py :lines: 68-76 .. note:: in this simplified example it is assumed that there is enough system memory to hold the two :class:`epr.Raster` objects. After opening (in binary mode) the stream for the output .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py :lines: 78-80 the program simply loops over all pixel and calculate the NDVI with the following formula: .. math:: NDVI = \frac{radiance_{10} - radiance_8}{radiance_{10} + radiance_8} .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py :lines: 82-95 This part of the code tries to mimic closely the original C code (`write_ndvi.c`_) .. code-block:: c out_stream = fopen(argv[2], "wb"); for (j = 0; j < height; ++j) { for (i = 0; i < width; ++i) { rad1 = epr_get_pixel_as_float(raster1, i, j); rad2 = epr_get_pixel_as_float(raster2, i, j); if ((rad1 + rad2) != 0.0) { ndvi = (rad2 - rad1) / (rad2 + rad1); } else { ndvi = -1.0; } status = fwrite( & ndvi, sizeof(float), 1, out_stream); } } epr_log_message(e_log_info, "ndvi was written success"); and uses the :meth:`epr.Raster.get_pixel` method to access pixel values and perform computation. The Python_ :func:`struct.pack` function together with :meth:`file.write` is used to write the NDVI of the pixel n the file in binary format. .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/write_ndvi.py :lines: 94 .. note:: the entire solution is quite not pythonic_. As an alternative implementation it could be used the :class:`numpy.ndarray` interface of :class:`epr.Raster` objects available via the :data:`epr.Raster.data` property. The NDVI index is computed on all pixels altogether using vectorized expressions:: # Initialize the entire matrix to -1 ndvi = numpy.zeros((height, width), 'float32') - 1 aux = raster2.data + raster1.data # indexes of pixel with non null denominator idx = numpy.where(aux != 0) # actual NDVI computation ndvi[idx] = (raster2.data[idx] - raster1.data[idx]) / aux[idx] Finally data can be saved to file simply using the :meth:`numpy.ndarray.tofile` method:: ndvi.tofile(out_stream) .. _ENVISAT: https://envisat.esa.int .. _Python: https://www.python.org .. _pythonic: http://www.cafepy.com/article/be_pythonic .. raw:: latex \clearpage pyepr-0.9.3/doc/pydoctheme/000077500000000000000000000000001252122757300155765ustar00rootroot00000000000000pyepr-0.9.3/doc/pydoctheme/LICENSE000066400000000000000000000307231252122757300166100ustar00rootroot00000000000000A. HISTORY OF THE SOFTWARE ========================== Python was created in the early 1990s by Guido van Rossum at Stichting Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands as a successor of a language called ABC. Guido remains Python's principal author, although it includes many contributions from others. In 1995, Guido continued his work on Python at the Corporation for National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) in Reston, Virginia where he released several versions of the software. In May 2000, Guido and the Python core development team moved to BeOpen.com to form the BeOpen PythonLabs team. In October of the same year, the PythonLabs team moved to Digital Creations (now Zope Corporation, see http://www.zope.com). In 2001, the Python Software Foundation (PSF, see http://www.python.org/psf/) was formed, a non-profit organization created specifically to own Python-related Intellectual Property. Zope Corporation is a sponsoring member of the PSF. All Python releases are Open Source (see http://www.opensource.org for the Open Source Definition). Historically, most, but not all, Python releases have also been GPL-compatible; the table below summarizes the various releases. Release Derived Year Owner GPL- from compatible? (1) 0.9.0 thru 1.2 1991-1995 CWI yes 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes 1.6 1.5.2 2000 CNRI no 2.0 1.6 2000 BeOpen.com no 1.6.1 1.6 2001 CNRI yes (2) 2.1 2.0+1.6.1 2001 PSF no 2.0.1 2.0+1.6.1 2001 PSF yes 2.1.1 2.1+2.0.1 2001 PSF yes 2.1.2 2.1.1 2002 PSF yes 2.1.3 2.1.2 2002 PSF yes 2.2 and above 2.1.1 2001-now PSF yes Footnotes: (1) GPL-compatible doesn't mean that we're distributing Python under the GPL. All Python licenses, unlike the GPL, let you distribute a modified version without making your changes open source. The GPL-compatible licenses make it possible to combine Python with other software that is released under the GPL; the others don't. (2) According to Richard Stallman, 1.6.1 is not GPL-compatible, because its license has a choice of law clause. According to CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 is "not incompatible" with the GPL. Thanks to the many outside volunteers who have worked under Guido's direction to make these releases possible. B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON =============================================================== PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 ------------------------------------------- BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization ("Licensee") accessing and otherwise using this software in source or binary form and its associated documentation ("the Software"). 2. Subject to the terms and conditions of this BeOpen Python License Agreement, BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use the Software alone or in any derivative version, provided, however, that the BeOpen Python License is retained in the Software, alone or in any derivative version prepared by Licensee. 3. BeOpen is making the Software available to Licensee on an "AS IS" basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 5. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 6. This License Agreement shall be governed by and interpreted in all respects by the law of the State of California, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between BeOpen and Licensee. This License Agreement does not grant permission to use BeOpen trademarks or trade names in a trademark sense to endorse or promote products or services of Licensee, or any third party. As an exception, the "BeOpen Python" logos available at http://www.pythonlabs.com/logos.html may be used according to the permissions granted on that web page. 7. By copying, installing or otherwise using the software, Licensee agrees to be bound by the terms and conditions of this License Agreement. CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 --------------------------------------- 1. This LICENSE AGREEMENT is between the Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 1.6.1 software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 1.6.1 alone or in any derivative version, provided, however, that CNRI's License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) 1995-2001 Corporation for National Research Initiatives; All Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 is made available subject to the terms and conditions in CNRI's License Agreement. This Agreement together with Python 1.6.1 may be located on the Internet using the following unique, persistent identifier (known as a handle): 1895.22/1013. This Agreement may also be obtained from a proxy server on the Internet using the following URL: http://hdl.handle.net/1895.22/1013". 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 1.6.1 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python 1.6.1. 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. This License Agreement shall be governed by the federal intellectual property law of the United States, including without limitation the federal copyright law, and, to the extent such U.S. federal law does not apply, by the law of the Commonwealth of Virginia, excluding Virginia's conflict of law provisions. Notwithstanding the foregoing, with regard to derivative works based on Python 1.6.1 that incorporate non-separable material that was previously distributed under the GNU General Public License (GPL), the law of the Commonwealth of Virginia shall govern this License Agreement only as to issues arising under or with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By clicking on the "ACCEPT" button where indicated, or by copying, installing or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and conditions of this License Agreement. ACCEPT CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 -------------------------------------------------- Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved. Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. pyepr-0.9.3/doc/pydoctheme/static/000077500000000000000000000000001252122757300170655ustar00rootroot00000000000000pyepr-0.9.3/doc/pydoctheme/static/pydoctheme.css000066400000000000000000000052671252122757300217520ustar00rootroot00000000000000@import url("default.css"); body { background-color: white; margin-left: 1em; margin-right: 1em; } div.related { margin-bottom: 1.2em; padding: 0.5em 0; border-top: 1px solid #ccc; margin-top: 0.5em; } div.related a:hover { color: #0095C4; } div.related:first-child { border-top: 0; border-bottom: 1px solid #ccc; } div.sphinxsidebar { background-color: #eeeeee; border-radius: 5px; line-height: 130%; font-size: smaller; } div.sphinxsidebar h3, div.sphinxsidebar h4 { margin-top: 1.5em; } div.sphinxsidebarwrapper > h3:first-child { margin-top: 0.2em; } div.sphinxsidebarwrapper > ul > li > ul > li { margin-bottom: 0.4em; } div.sphinxsidebar a:hover { color: #0095C4; } div.sphinxsidebar input { font-family: 'Lucida Grande',Arial,sans-serif; border: 1px solid #999999; font-size: smaller; border-radius: 3px; } div.sphinxsidebar input[type=text] { max-width: 150px; } div.body { padding: 0 0 0 1.2em; } div.body p { line-height: 140%; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { margin: 0; border: 0; padding: 0.3em 0; } div.body hr { border: 0; background-color: #ccc; height: 1px; } div.body pre { border-radius: 3px; border: 1px solid #ac9; } div.body div.admonition, div.body div.impl-detail { border-radius: 3px; } div.body div.impl-detail > p { margin: 0; } div.body div.seealso { border: 1px solid #dddd66; } div.body a { color: #0072aa; } div.body a:visited { color: #6363bb; } div.body a:hover { color: #00B0E4; } tt, code, pre { font-family: monospace, sans-serif; font-size: 96.5%; } div.body tt, div.body code { border-radius: 3px; } div.body tt.descname, div.body code.descname { font-size: 120%; } div.body tt.xref, div.body a tt, div.body code.xref, div.body a code { font-weight: normal; } .deprecated { border-radius: 3px; } table.docutils { border: 1px solid #ddd; min-width: 20%; border-radius: 3px; margin-top: 10px; margin-bottom: 10px; } table.docutils td, table.docutils th { border: 1px solid #ddd !important; border-radius: 3px; } table p, table li { text-align: left !important; } table.docutils th { background-color: #eee; padding: 0.3em 0.5em; } table.docutils td { background-color: white; padding: 0.3em 0.5em; } table.footnote, table.footnote td { border: 0 !important; } div.footer { line-height: 150%; margin-top: -2em; text-align: right; width: auto; margin-right: 10px; } div.footer a:hover { color: #0095C4; } .refcount { color: #060; } .stableabi { color: #229; } pyepr-0.9.3/doc/pydoctheme/static/sidebar.js000066400000000000000000000142141252122757300210360ustar00rootroot00000000000000/* * sidebar.js * ~~~~~~~~~~ * * This script makes the Sphinx sidebar collapsible and implements intelligent * scrolling. * * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds in * .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton used to * collapse and expand the sidebar. * * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden and the * width of the sidebar and the margin-left of the document are decreased. * When the sidebar is expanded the opposite happens. This script saves a * per-browser/per-session cookie used to remember the position of the sidebar * among the pages. Once the browser is closed the cookie is deleted and the * position reset to the default (expanded). * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ $(function() { // global elements used by the functions. // the 'sidebarbutton' element is defined as global after its // creation, in the add_sidebar_button function var jwindow = $(window); var jdocument = $(document); var bodywrapper = $('.bodywrapper'); var sidebar = $('.sphinxsidebar'); var sidebarwrapper = $('.sphinxsidebarwrapper'); // original margin-left of the bodywrapper and width of the sidebar // with the sidebar expanded var bw_margin_expanded = bodywrapper.css('margin-left'); var ssb_width_expanded = sidebar.width(); // margin-left of the bodywrapper and width of the sidebar // with the sidebar collapsed var bw_margin_collapsed = '.8em'; var ssb_width_collapsed = '.8em'; // colors used by the current theme var dark_color = '#AAAAAA'; var light_color = '#CCCCCC'; function get_viewport_height() { if (window.innerHeight) return window.innerHeight; else return jwindow.height(); } function sidebar_is_collapsed() { return sidebarwrapper.is(':not(:visible)'); } function toggle_sidebar() { if (sidebar_is_collapsed()) expand_sidebar(); else collapse_sidebar(); // adjust the scrolling of the sidebar scroll_sidebar(); } function collapse_sidebar() { sidebarwrapper.hide(); sidebar.css('width', ssb_width_collapsed); bodywrapper.css('margin-left', bw_margin_collapsed); sidebarbutton.css({ 'margin-left': '0', 'height': bodywrapper.height(), 'border-radius': '5px' }); sidebarbutton.find('span').text('»'); sidebarbutton.attr('title', _('Expand sidebar')); document.cookie = 'sidebar=collapsed'; } function expand_sidebar() { bodywrapper.css('margin-left', bw_margin_expanded); sidebar.css('width', ssb_width_expanded); sidebarwrapper.show(); sidebarbutton.css({ 'margin-left': ssb_width_expanded-12, 'height': bodywrapper.height(), 'border-radius': '0 5px 5px 0' }); sidebarbutton.find('span').text('«'); sidebarbutton.attr('title', _('Collapse sidebar')); //sidebarwrapper.css({'padding-top': // Math.max(window.pageYOffset - sidebarwrapper.offset().top, 10)}); document.cookie = 'sidebar=expanded'; } function add_sidebar_button() { sidebarwrapper.css({ 'float': 'left', 'margin-right': '0', 'width': ssb_width_expanded - 28 }); // create the button sidebar.append( '

' ); var sidebarbutton = $('#sidebarbutton'); // find the height of the viewport to center the '<<' in the page var viewport_height = get_viewport_height(); var sidebar_offset = sidebar.offset().top; var sidebar_height = Math.max(bodywrapper.height(), sidebar.height()); sidebarbutton.find('span').css({ 'display': 'block', 'position': 'fixed', 'top': Math.min(viewport_height/2, sidebar_height/2 + sidebar_offset) - 10 }); sidebarbutton.click(toggle_sidebar); sidebarbutton.attr('title', _('Collapse sidebar')); sidebarbutton.css({ 'border-radius': '0 5px 5px 0', 'color': '#444444', 'background-color': '#CCCCCC', 'font-size': '1.2em', 'cursor': 'pointer', 'height': sidebar_height, 'padding-top': '1px', 'padding-left': '1px', 'margin-left': ssb_width_expanded - 12 }); sidebarbutton.hover( function () { $(this).css('background-color', dark_color); }, function () { $(this).css('background-color', light_color); } ); } function set_position_from_cookie() { if (!document.cookie) return; var items = document.cookie.split(';'); for(var k=0; k wintop && curbot > winbot) { sidebarwrapper.css('top', $u.max([wintop - offset - 10, 0])); } else if (curtop < wintop && curbot < winbot) { sidebarwrapper.css('top', $u.min([winbot - sidebar_height - offset - 20, jdocument.height() - sidebar_height - 200])); } } } jwindow.scroll(scroll_sidebar); }); pyepr-0.9.3/doc/pydoctheme/theme.conf000066400000000000000000000010231252122757300175430ustar00rootroot00000000000000[theme] inherit = default stylesheet = pydoctheme.css pygments_style = sphinx [options] bodyfont = 'Lucida Grande', Arial, sans-serif headfont = 'Lucida Grande', Arial, sans-serif footerbgcolor = white footertextcolor = #555555 relbarbgcolor = white relbartextcolor = #666666 relbarlinkcolor = #444444 sidebarbgcolor = white sidebartextcolor = #444444 sidebarlinkcolor = #444444 bgcolor = white textcolor = #222222 linkcolor = #0090c0 visitedlinkcolor = #00608f headtextcolor = #1a1a1a headbgcolor = white headlinkcolor = #aaaaaa pyepr-0.9.3/doc/reference.txt000066400000000000000000001101371252122757300161370ustar00rootroot00000000000000API Reference ============= .. module:: epr :synopsis: Python bindings for ENVISAT Product Reader C API .. index:: bindings, ENVISAT, ESA, EPR-API pair: epr; module PyEPR_ provides Python_ bindings for the ENVISAT Product Reader C API (`EPR API`_) for reading satellite data from ENVISAT_ ESA_ (European Space Agency) mission. PyEPR_ is fully object oriented and, as well as the `EPR API`_ for C, supports ENVISAT_ MERIS, AATSR Level 1B and Level 2 and also ASAR data products. It provides access to the data either on a geophysical (decoded, ready-to-use pixel samples) or on a raw data layer. The raw data access makes it possible to read any data field contained in a product file. .. _PyEPR: https://github.com/avalentino/pyepr .. _Python: https://www.python.org .. _`EPR API`: https://github.com/bcdev/epr-api .. _ENVISAT: https://envisat.esa.int .. _ESA: https://earth.esa.int .. currentmodule:: epr Classes ------- Product ~~~~~~~ .. class:: Product ENVISAT product The Product class provides methods and properties to get information about an ENVISAT product file. .. seealso:: :func:`open` .. rubric:: Attributes .. attribute:: file_path The file's path including the file name .. attribute:: mode String that specifies the mode in which the file is opened Possible values: `rb` for read-only mode, `rb+` for read-write mode. .. attribute:: id_string The product identifier string obtained from the MPH parameter 'PRODUCT' The first 10 characters of this string identify the product type, e.g. "MER_1P__FR" for a MERIS Level 1b full resolution product. The rest of the string decodes product instance properties. .. attribute:: meris_iodd_version For MERIS L1b and RR and FR to provide backward compatibility .. attribute:: tot_size The total size in bytes of the product file .. rubric:: Methods .. method:: get_band(name) Gets the band corresponding to the specified name. :param name: the name of the band :returns: the requested :class:`Band` instance, or raises a :exc:`EPRValueError` if not found .. method:: get_band_at(index) Gets the :class:`Band` at the specified position within the :class:`product` :param index: the index identifying the position of the :class:`Band`, starting with 0, must not be negative :returns: the requested :class:`Band` instance, or raises a :exc:`EPRValueError` if not found .. method:: get_dataset(name) Gets the :class:`Dataset` corresponding to the specified dataset name :param name: the :class:`Dataset` name :returns: the requested :class:`Dataset` instance .. method:: get_dataset_at(index) Gets the :class:`Dataset` at the specified position within the :class:`Product` :param index: the index identifying the position of the :class:`Dataset`, starting with 0, must not be negative :returns: the requested :class:`Dataset` .. method:: get_dsd_at(index) Gets the :class:`DSD` at the specified position Gets the :class:`DSD` (:class:`Dataset` descriptor) at the specified position within the :class:`Product`. :param index: the index identifying the position of the :class:`DSD`, starting with 0, must not be negative :returns: the requested :class:`DSD` instance .. method:: get_num_bands() Gets the number of all :class:`Band`\ s contained in a :class:`Product` .. method:: get_num_datasets() Gets the number of all :class:`Dataset`\ s contained in a :class:`Product` .. method:: get_num_dsds() Gets the number of all :class:`DSD`\ s (:class:`Dataset` descriptors) contained in the :class:`Product` .. method:: get_scene_height() Gets the :class:`Product` scene height in pixels .. method:: get_scene_width() Gets the :class:`Product` scene width in pixels .. method:: get_mph() The :class:`Record` representing the main product header (MPH) .. method:: get_sph() The :class:`Record` representing the specific product header (SPH) .. method:: read_bitmask_raster(bm_expr, xoffset, yoffset, raster) Calculates a bit-mask raster Calculates a bit-mask, composed of flags of the given :class:`Product` and combined as described in the given bit-mask expression, for the a certain dimension and sub-sampling as defined in the given raster. :param bm_expr: a string holding the logical expression for the definition of the bit-mask. In a bit-mask expression, any number of the flag-names (found in the DDDB) can be composed with "(", ")", "NOT", "AND", "OR". Valid bit-mask expression are for example ``flags.LAND OR flags.CLOUD`` or ``NOT flags.WATER AND flags.TURBID_S`` :param xoffset: across-track co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param yoffset: along-track co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param raster: the raster for the bit-mask. The data type of the raster must be either :data:`E_TID_UCHAR` or :data:`E_TID_CHAR` :returns: zero for success, an error code otherwise .. seealso:: :func:`create_bitmask_raster` .. method:: close Closes the :class:`Product` product and free the underlying file descriptor. This method has no effect if the :class:`Product` is already closed. Once the :class:`Product` is closed, any operation on it will raise a :exc:`ValueError`. As a convenience, it is allowed to call this method more than once; only the first call, however, will have an effect. .. method:: flush() Flush the file stream .. rubric:: High level interface methods .. note:: the following methods are part of the *high level* Python API and do not have any corresponding function in the C API. .. attribute:: closed True if the :class:`Product` is closed. .. method:: get_dataset_names() Return the list of names of the :class:`Dataset`\ s in the :class:`Product` .. method:: get_band_names() Return the list of names of the :class:`Band`\ s in the :class:`Product` .. method:: datasets() Return the list of :class:`Dataset`\ s in the :class:`Product` .. method:: bands() Return the list of :class:`Band`\ s in the :class:`Product` .. rubric:: Special methods The :class:`Product` class provides a custom implementation of the following *special methods*: * __repr__ * __str__ * __enter__ * __exit__ .. index:: __repr__, __str__, __enter__, __exit__ pair: special; methods Dataset ~~~~~~~ .. class:: Dataset ENVISAT dataset The Dataset class contains information about a dataset within an ENVISAT product file which has been opened with the :func:`open` function. A new Dataset instance can be obtained with the :meth:`Product.get_dataset` or :meth:`Product.get_dataset_at` methods. .. rubric:: Attributes .. attribute:: description A short description of the :class:`Band` contents .. attribute:: product The :class:`Product` instance to which this :class:`Dataset` belongs to .. rubric:: Methods .. method:: get_name() Gets the name of the :class:`Dataset` .. method:: get_dsd() Gets the :class:`Dataset` descriptor (:class:`DSD`) .. method:: get_dsd_name() Gets the name of the :class:`DSD` (:class:`Dataset` descriptor) .. method:: get_num_records() Gets the number of :class:`Record`\ s of the :class:`Dataset` .. method:: create_record() Creates a new :class:`Record` Creates a new, empty :class:`Record` with a structure compatible with the :class:`Dataset`. Such a :class:`Record` is typically used in subsequent calls to :meth:`Dataset.read_record`. :returns: the new :class:`Record` instance .. method:: read_record(index[, record]) Reads specified :class:`Record` of the :class:`Dataset` The :class:`Record` is identified through the given zero-based :class:`Record` index. In order to reduce memory reallocation, a :class:`Record` (pre-)created by the method :meth:`Dataset.create_record` can be passed to this method. Data is then read into this given :class:`Record`. If no :class:`Record` (``None``) is given, the method initiates a new one. In both cases, the :class:`Record` in which the data is read into will be returned. :param index: the zero-based :class:`Record` index (default: 0) :param record: a pre-created :class:`Record` to reduce memory reallocation, can be ``None`` (default) to let the function allocate a new :class:`Record` :returns: the record in which the data has been read into or raises an exception (:exc:`EPRValueError`) if an error occurred .. versionchanged:: 0.9 The *index* parameter now defaults to zero .. rubric:: High level interface methods .. note:: the following methods are part of the *high level* Python API and do not have any corresponding function in the C API. .. method:: records() Return the list of :class:`Record`\ s contained in the :class:`Dataset` .. rubric:: Special methods The :class:`Dataset` class provides a custom implementation of the following *special methods*: * __repr__ * __str__ * __iter__ .. index:: __repr__, __str__, __iter__ pair: special; methods Record ~~~~~~ .. class:: Record Represents a record read from an ENVISAT dataset A record is composed of multiple fields. .. seealso:: :class:`Field` .. rubric:: Attributes .. attribute:: dataset_name The name of the :class:`Dataset` to which this :class:`Record` belongs to .. versionadded:: 0.9 .. attribute:: tot_size The total size in bytes of the :class:`Record` It includes all data elements of all :class:`Field`\ s of a :class:`Record` in a :class:`Product` file. *tot_size* is a derived variable, it is computed at run-time and not stored in the DSD-DB. .. versionadded:: 0.9 .. attribute:: index Index of the :class:`Record` within the :class:`Dataset` It is *None* for empty :class:`Record`\ s (created with :meth:`Dataset.create_record` but still not read) and for *MPH* (see :meth:`Product.get_mph`) and *SPH* (see :meth:`Product.get_sph`) :class:`Record`\ s. .. seealso:: :meth:`Dataset.read_record` .. versionadded:: 0.9 .. rubric:: Methods .. method:: get_field(name) Gets a :class:`Field` specified by name The :class:`Field` is here identified through the given name. It contains the :class:`Field` info and all corresponding values. :param name: the the name of required :class:`Field` :returns: the specified :class:`Field` or raises an exception (:exc:`EPRValueError`) if an error occurred .. method:: get_field_at(index) Gets a :class:`Field` at the specified position within the :class:`Record` :param index: the zero-based index (position within :class:`Record`) of the :class:`Field` :returns: the :class:`Field` or raises and exception (:exc:`EPRValueError`) if an error occurred .. method:: get_num_fields() Gets the number of :class:`Field`\ s contained in the :class:`Record` .. method:: print_([ostream]) Write the :class:`Record` to specified file (default: :data:`sys.stdout`) This method writes formatted contents of the :class:`Record` to specified *ostream* text file or (default) the ASCII output is be printed to standard output (:data:`sys.stdout`) :param ostream: the (opened) output file object .. note:: the *ostream* parameter have to be a *real* file not a generic stream object like :class:`StringIO.StringIO` instances .. method:: print_element(field_index, element_index[, ostream]) Write the specified field element to file (default: :data:`sys.stdout`) This method writes formatted contents of the specified :class:`Field` element to the *ostream* text file or (default) the ASCII output will be printed to standard output (:data:`sys.stdout`) :param field_index: the index of :class:`Field` in the :class:`Record` :param element_index: the index of element in the specified :class:`Field` :param ostream: the (opened) output file object .. note:: the *ostream* parameter have to be a *real* file not a generic stream object like :class:`StringIO.StringIO` instances .. method:: get_offset() :class:`Record` offset in bytes within the :class:`Dataset` .. versionadded:: 0.9 .. rubric:: High level interface methods .. note:: the following methods are part of the *high level* Python API and do not have any corresponding function in the C API. .. method:: get_field_names Return the list of names of the :class:`Field`\ s in the :class:`Record` .. method:: fields() Return the list of :class:`Field`\ s contained in the :class:`Record` .. rubric:: Special methods The :class:`Record` class provides a custom implementation of the following *special methods*: * __repr__ * __str__ * __iter__ .. index:: __repr__, __str__, __iter__ pair: special; methods Field ~~~~~ .. class:: Field Represents a field within a record A :class:`Field` is composed of one or more data elements of one of the types defined in the internal ``field_info`` structure. .. seealso:: :class:`Record` .. rubric:: Attributes .. attribute:: tot_size The total size in bytes of all data elements of a :class:`Field`. *tot_size* is a derived variable, it is computed at run-time and not stored in the DSD-DB. .. versionadded:: 0.9 .. method:: get_description() Gets the description of the :class:`Field` .. method:: get_name() Gets the name of the :class:`Field` .. method:: get_num_elems() Gets the number of elements of the :class:`Field` .. method:: get_type() Gets the type of the :class:`Field` .. method:: get_unit() Gets the unit of the :class:`Field` .. method:: get_elem([index]) :class:`Field` single element access This function is for getting the elements of a :class:`Field`. :param index: the zero-based index of element to be returned, must not be negative. Default: 0. :returns: the typed value from given :class:`Field` .. method:: get_elems() :class:`Field` array element access This function is for getting an array of field elements of the :class:`Field`. :returns: the data array (:class:`numpy.ndarray`) having the type of the :class:`Field` .. versionchanged:: 0.9 the returned :class:`numpy.ndarray` shares the data buffer with the C :c:type:`Field` structure so any change in its contents is also reflected to the :class:`Filed` object .. method:: set_elem(elem, [index]) Set :class:`Field` array element This function is for setting an array of field element of the :class:`Field`. :param elem: value of the element to set :param index: the zero-based index of element to be set, must not be negative. Default: 0. .. note:: this method does not have any corresponding function in the C API. .. versionadded:: 0.9 .. method:: set_elems(elems) Set :class:`Field` array elements This function is for setting an array of :class:`Field` elements of the :class:`Field`. :param elems: np.ndarray of elements to set .. note:: this method does not have any corresponding function in the C API. .. versionadded:: 0.9 .. method:: print_([ostream]) Write the :class:`Field` to specified file (default: :data:`sys.stdout`) This method writes formatted contents of the :class:`Field` to specified *ostream* text file or (default) the ASCII output is be printed to standard output (:data:`sys.stdout`) :param ostream: the (opened) output file object .. note:: the *ostream* parameter have to be a *real* file not a generic stream object like :class:`StringIO.StringIO` instances .. method:: get_offset() Field offset in bytes within the :class:`Record` .. versionadded:: 0.9 .. rubric:: Special methods The :class:`Field` class provides a custom implementation of the following *special methods*: * __repr__ * __str__ * __eq__ * __ne__ * __len__ [#]_ .. index:: __repr__, __str__, __eq__, __ne__, __len__ pair: special; methods .. rubric:: Footnotes .. [#] if the field is a :data:`E_TID_STRING` field then the :meth:`__len__` method returns the string length, otherwise the number of elements of the field is returned (same as :meth:`Field.get_num_elems`) DSD ~~~ .. class:: DSD :class:`Dataset` descriptor The DSD class contains information about the properties of a :class:`Dataset` and its location within an ENVISAT :class:`Product` file .. rubric:: Attributes .. attribute:: ds_name The :class:`Dataset` name .. attribute:: ds_offset The offset of :class:`Dataset` in the :class:`Product` file .. attribute:: ds_size The size of :class:`Dataset` in the :class:`Product` file .. attribute:: ds_type The :class:`Dataset` type descriptor .. attribute:: dsr_size The size of dataset record for the given :class:`Dataset` name .. attribute:: filename The filename in the DDDB with the description of this :class:`Dataset` .. attribute:: index The index of this :class:`DSD` (zero-based) .. attribute:: num_dsr The number of dataset records for the given :class:`Dataset` name .. rubric:: Special methods The :class:`DSD` class provides a custom implementation of the following *special methods*: * __repr__ * __eq__ * __ne__ .. index:: __repr__, __eq__, __ne__ pair: special; methods Band ~~~~ .. class:: Band The band of an ENVISAT :class:`Product` The Band class contains information about a band within an ENVISAT :class:`Product` file which has been opened with the :func:`open` function. A new Band instance can be obtained with the :meth:`Product.get_band` method. .. rubric:: Attributes .. attribute:: bm_expr A bit-mask expression used to filter valid pixels All others are set to zero .. attribute:: data_type The data type of the :class:`Band` pixels Possible values are: * ``*`` --> the datatype remains unchanged. * ``uint8_t`` --> 8-bit unsigned integer * ``uint32_t`` --> 32-bit unsigned integer * ``Float`` --> 32-bit IEEE floating point .. attribute:: description A short description of the :class:`Band` contents .. attribute:: lines_mirrored Mirrored lines flag If true (=1) lines will be mirrored (flipped) after read into a raster in order to ensure a pixel ordering in raster X direction from WEST to EAST. .. attribute:: product The :class:`Product` instance to which this :class:`Band` belongs to .. attribute:: sample_model The sample model operation The sample model operation applied to the source :class:`Dataset` for getting the correct samples from the MDS (for example MERIS L2). Possible values are: * ``*`` --> no operation (direct copy) * ``1OF2`` --> first byte of 2-byte interleaved MDS * ``2OF2`` --> second byte of 2-byte interleaved MDS * ``0123`` --> combine 3-bytes interleaved to 4-byte integer .. attribute:: scaling_factor The scaling factor Possible values are: * ``*`` --> no factor provided (implies scaling_method=*) * ``const`` --> a floating point constant * ``GADS.field[.field2]`` --> value is provided in global annotation :class:`Dataset` with name `GADS` in :class:`Field` `field`. Optionally a second element index for multiple-element fields can be given too .. attribute:: scaling_method The scaling method which must be applied to the raw source data in order to get the 'real' pixel values in geo-physical units. Possible values are: * ``*`` --> no scaling applied * ``Linear_Scale`` --> linear scaling applied:: y = offset + scale * x * ``Log_Scale`` --> logarithmic scaling applied:: y = log10(offset + scale * x) .. attribute:: scaling_offset Possible values are: * ``*`` --> no offset provided (implies scaling_method=*) * ``const`` --> a floating point constant * ``GADS.field[.field2]` --> value is provided in global annotation :class:`Dataset` with name ``GADS`` in :class:`Field` ``field``. Optionally a second element index for multiple-element fields can be given too .. attribute:: spectr_band_index The (zero-based) spectral :class:`Band` index -1 if this is not a spectral :class:`Band` .. attribute:: unit The geophysical unit for the :class:`Band` pixel values .. attribute:: dataset The source :class:`Dataset` The source :class:`Dataset` containing the raw data used to create the :class:`Band` pixel values. .. versionadded:: 0.9 .. rubric:: Methods .. method:: get_name() Gets the name of the :class:`Band` .. method:: create_compatible_raster([src_width, src_height, xstep, ystep]) Creates a :class:`Raster` which is compatible with the data type of the :class:`Band` The created :class:`Raster` is used to read the data in it (see :meth:`Band.read_raster`). The :class:`Raster` is defined on the grid of the :class:`Product`, from which the data are read. Spatial subsets and under-sampling are possible) through the parameter of the method. A :class:`Raster` is an object that allows direct access to data of a certain portion of the ENVISAT :class:`Product` that are read into the it. Such a portion is called the source. The complete ENVISAT :class:`Product` can be much greater than the source. One can move the :class:`Raster` over the complete ENVISAT :class:`Product` and read in turn different parts (always of the size of the source) of it into the :class:`Raster`. The source is specified by the parameters *height* and *width*. A typical example is a processing in blocks. Lets say, a block has 64x32 pixel. Then, my source has a width of 64 pixel and a height of 32 pixel. Another example is a processing of complete image lines. Then, my source has a widths of the complete product (for example 1121 for a MERIS RR product), and a height of 1). One can loop over all blocks read into the :clasS:`Raster` and process it. In addition, it is possible to defined a sub-sampling step for a :class:`Raster`. This means, that the source is not read 1:1 into the :class:`Raster`, but that only every 2nd or 3rd pixel is read. This step can be set differently for the across track (source_step_x) and along track (source_step_y) directions. :param src_width: the width (across track dimension) of the source to be read into the :class:`Raster`. Default: scene width (see :attr:`Product.get_scene_width`) :param src_height: the height (along track dimension) of the source to be read into the :class:`Raster`. Default: scene height (see :attr:`Product.get_scene_height`) :param xstep: the sub-sampling step across track of the source when reading into the :class:`Raster`. Default: 1. :param ystep: the sub-sampling step along track of the source when reading into the :class:`Raster`. Default: 1. :returns: the new :class:`Raster` instance or raises an exception (:exc:`EPRValueError`) if an error occurred .. note:: *src_width* and *src_height* are the dimantion of the of the source area. If one specifies a *step* parameter the resulting :class:`Raster` will have a size that is smaller that the specifies source size:: raster_size = src_size // step .. method:: read_raster([xoffset, yoffset, raster]) Reads (geo-)physical values of the :class:`Band` of the specified source-region The source-region is a defined part of the whole ENVISAT :class:`Product` image, which shall be read into a :class:`Raster`. In this routine the co-ordinates are specified, where the source-region to be read starts. The dimension of the region and the sub-sampling are attributes of the :class:`Raster` into which the data are read. :param xoffset: across-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region. Default 0. :param yoffset: along-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region. Default 0. :param raster: :class:`Raster` instance set with appropriate parameters to read into. If not provided a new :class:`Raster` is instantiated :returns: the :class:`Raster` instance in which data are read This method raises an instance of the appropriate :exc:`EPRError` sub-class if case of errors .. seealso:: :meth:`Band.create_compatible_raster` and :func:`create_raster` .. rubric:: High level interface methods .. note:: the following methods are part of the *high level* Python API and do not have any corresponding function in the C API. .. method:: read_as_array([width, height, xoffset, yoffset, xstep, ystep]) Reads the specified source region as an :class:`numpy.ndarray` The source-region is a defined part of the whole ENVISAT :class:`Product` image, which shall be read into a :class:`Raster`. In this routine the co-ordinates are specified, where the source-region to be read starts. The dimension of the region and the sub-sampling are attributes of the :class:`Raster` into which the data are read. :param src_width: the width (across track dimension) of the source to be read into the :class:`Raster`. If not provided reads as much as possible :param src_height: the height (along track dimension) of the source to be read into the :class:`Raster`, If not provided reads as much as possible :param xoffset: across-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region. Default 0. :param yoffset: along-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region. Default 0. :param xstep: the sub-sampling step across track of the source when reading into the :class:`Raster`. Default: 1 :param ystep: the sub-sampling step along track of the source when reading into the :class:`Raster`. Default: 1 :returns: the :class:`numpy.ndarray` instance in which data are read This method raises an instance of the appropriate :exc:`EPRError` sub-class if case of errors .. seealso:: :meth:`Band.create_compatible_raster`, :func:`create_raster` and :meth:`Band.read_raster` .. rubric:: Special methods The :class:`Band` class provides a custom implementation of the following *special methods*: * __repr__ .. index:: __repr__ pair: special; methods Raster ~~~~~~ .. class:: Raster Represents a raster in which data will be stored All 'size' parameter are in PIXEL. .. rubric:: Attributes .. attribute:: data_type The data type of the :class:`Band` pixels All ``E_TID_*`` types are possible .. attribute:: source_height The height of the source .. attribute:: source_width The width of the source .. attribute:: source_step_x The sub-sampling for the across-track direction in pixel .. attribute:: source_step_y The sub-sampling for the along-track direction in pixel .. rubric:: High level interface attributes .. note:: the following attributess are part of the *high level* Python API and do not have a counterpart in the C API. .. attribute:: data Raster data exposed as :class:`numpy.ndarray` object .. note:: this property shares the data buffer with the :class:`Raster` object so any change in its contents is also reflected to the :class:`Raster` object .. note:: the :class:`Raster` objects do not have a :class:`Field` named *data* in the corresponding C structure. The *EPR_SRaster* C structure have a :class:`Field` named *buffer* that is a raw pointer to the data buffer and it is not exposed as such in the Python API. .. rubric:: Methods .. method:: get_pixel(x, y) Single pixel access This function is for getting the values of the elements of a :class:`Raster` (i.e. pixel) :param x: the (zero-based) X coordinate of the pixel :param y: the (zero-based) Y coordinate of the pixel :returns: the typed value at the given co-ordinate .. method:: get_elem_size() The size in byte of a single element (sample) of this :class:`Raster` buffer .. method:: get_height() Gets the :class:`Raster` height in pixels .. method:: get_width() Gets the :class:`Raster` width in pixels .. rubric:: Special methods The :class:`Raster` class provides a custom implementation of the following *special methods*: * __repr__ .. index:: __repr__ pair: special; methods EPRTime ~~~~~~~ .. class:: EPRTime Convenience class for time data exchange. EPRTime is a :class:`collections.namedtuple` with the following fields: .. attribute:: days .. attribute:: seconds .. attribute:: microseconds .. index:: function Functions --------- .. function:: open(filename, mode='rb') Opens the ENVISAT product Opens the ENVISAT :class:`Product` file with the given file path, reads MPH, SPH and all :class:`DSD`\ s, organized the table with parameter of line length and tie points number. :param product_file_path: the path to the ENVISAT :class:`Product` file :param mode: string that specifies the mode in which the file is opened. Allowed values: `rb` for read-only mode, `rb+` for read-write mode. Default: mode=`rb`. :returns: the :class:`Product` instance representing the specified product. An exception (:exc:`exceptions.ValueError`) is raised if the file could not be opened. The :class:`Product` class supports context management so the recommended way to ensure that a product is actually closed as soon as a task is completed is to use the ``with`` statement:: with open('ASA_IMP_1PNUPA20060202_ ... _3110.N1') as product: dataset = product.get_dataset('MAIN_PROCESSING_PARAMS_ADS') record = dataset.read_record(0) print(record) .. seealso :class:`Product` .. function:: data_type_id_to_str(type_id) Gets the 'C' data type string for the given data type .. function:: get_data_type_size(type_id) Gets the size in bytes for an element of the given data type .. function:: get_numpy_dtype(type_id) Return the numpy data-type specified EPR type ID .. versionadded:: 0.9 .. function:: get_sample_model_name(model) Return the name of the specified sample model .. function:: get_scaling_method_name(method) Return the name of the specified scaling method .. function:: create_raster(data_type, src_width, src_height[, xstep, ystep]) Creates a :class:`Raster` of the specified data type This function can be used to create any type of raster, e.g. for later use as a bit-mask. :param data_type: the type of the data to stored in the :class:`Raster`, must be one of E_TID_*. .. seealso:: `Data type Identifiers`_ :param src_width: the width (across track dimension) of the source to be read into the :class:`Raster`. See description of :meth:`Band.create_compatible_raster` :param src_height: the height (along track dimension) of the source to be read into the :class:`Raster`. See description of :meth:`Band.create_compatible_raster` :param xstep: the sub-sampling step across track of the source when reading into the :class:`Raster`. Default: 1. :param ystep: the sub-sampling step along track of the source when reading into the :class:`Raster`. Default: 1. :returns: the new :class:`Raster` instance .. seealso:: description of :meth:`Band.create_compatible_raster` .. function:: create_bitmask_raster(src_width, src_height[, xstep, ystep]) Creates a :class:`Raster` to be used for reading bitmasks The :class:`Raster` returned always is of type ``byte``. :param src_width: the width (across track dimension) of the source to be read into the :class:`Raster` :param src_height: the height (along track dimension) of the source to be read into the :class:`Raster` :param xstep: the sub-sampling step across track of the source when reading into the :class:`Raster`. Default: 1. :param ystep: the sub-sampling step along track of the source when reading into the :class:`Raster`. Default: 1. :returns: the new :class:`Raster` instance or raises an exception (:exc:`EPRValueError`) if an error occurred .. seealso:: the description of :meth:`Band.create_compatible_raster` .. index:: exception, error Exceptions ---------- EPRError ~~~~~~~~ .. exception:: EPRError EPR API error .. attribute:: code EPR API error code .. method:: __init__([message[, code, *args, **kwargs]]) Initializer :param message: error message :pram code: EPR error code EPRValueError ~~~~~~~~~~~~~ .. exception:: EPRValueError Inherits both :exc:`EPRError` and standard :exc:`exceptions.ValueError` Data ---- .. data:: __version__ Version string of PyEPR .. data:: EPR_C_API_VERSION Version string of the wrapped `EPR API`_ C library Data type identifiers ~~~~~~~~~~~~~~~~~~~~~ .. data:: E_TID_UNKNOWN .. data:: E_TID_UCHAR .. data:: E_TID_CHAR .. data:: E_TID_USHORT .. data:: E_TID_SHORT .. data:: E_TID_UINT .. data:: E_TID_INT .. data:: E_TID_FLOAT .. data:: E_TID_DOUBLE .. data:: E_TID_STRING .. data:: E_TID_SPARE .. data:: E_TID_TIME .. index:: pair: sample; model Sample Models ~~~~~~~~~~~~~ .. data:: E_SMOD_1OF1 .. data:: E_SMOD_1OF2 .. data:: E_SMOD_2OF2 .. data:: E_SMOD_3TOI .. data:: E_SMOD_2TOF .. index:: pair: scaling; method Scaling Methods ~~~~~~~~~~~~~~~ .. data:: E_SMID_NON No scaling .. data:: E_SMID_LIN Linear pixel scaling .. index:: linear .. data:: E_SMID_LOG Logarithmic pixel scaling .. index:: logarithmic pyepr-0.9.3/doc/sphinxext/000077500000000000000000000000001252122757300154675ustar00rootroot00000000000000pyepr-0.9.3/doc/sphinxext/LICENSE.txt000066400000000000000000000072671252122757300173260ustar00rootroot00000000000000============================= The IPython licensing terms ============================= IPython is licensed under the terms of the Modified BSD License (also known as New or Revised BSD), as follows: Copyright (c) 2008-2010, IPython Development Team Copyright (c) 2001-2007, Fernando Perez. Copyright (c) 2001, Janko Hauser Copyright (c) 2001, Nathaniel Gray All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the IPython Development Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. About the IPython Development Team ---------------------------------- Fernando Perez began IPython in 2001 based on code from Janko Hauser and Nathaniel Gray . Fernando is still the project lead. The IPython Development Team is the set of all contributors to the IPython project. This includes all of the IPython subprojects. A full list with details is kept in the documentation directory, in the file ``about/credits.txt``. The core team that coordinates development on GitHub can be found here: http://github.com/ipython. As of late 2010, it consists of: * Brian E. Granger * Jonathan March * Evan Patterson * Fernando Perez * Min Ragan-Kelley * Robert Kern Our Copyright Policy -------------------- IPython uses a shared copyright model. Each contributor maintains copyright over their contributions to IPython. But, it is important to note that these contributions are typically only changes to the repositories. Thus, the IPython source code, in its entirety is not the copyright of any single person or institution. Instead, it is the collective copyright of the entire IPython Development Team. If individual contributors want to maintain a record of what changes/contributions they have specific copyright on, they should indicate their copyright in the commit message of the change, when they commit the change to one of the IPython repositories. With this in mind, the following banner should be used in any source code file to indicate the copyright and license terms: #----------------------------------------------------------------------------- # Copyright (c) 2010, IPython Development Team. # # Distributed under the terms of the Modified BSD License. # # The full license is in the file COPYING.txt, distributed with this software. #----------------------------------------------------------------------------- pyepr-0.9.3/doc/sphinxext/ipython_console_highlighting.py000066400000000000000000000101271252122757300240030ustar00rootroot00000000000000"""reST directive for syntax-highlighting ipython interactive sessions. XXX - See what improvements can be made based on the new (as of Sept 2009) 'pycon' lexer for the python console. At the very least it will give better highlighted tracebacks. """ #----------------------------------------------------------------------------- # Needed modules # Standard library import re # Third party from pygments.lexer import Lexer, do_insertions from pygments.lexers.agile import (PythonConsoleLexer, PythonLexer, PythonTracebackLexer) from pygments.token import Comment, Generic from sphinx import highlighting #----------------------------------------------------------------------------- # Global constants line_re = re.compile('.*?\n') #----------------------------------------------------------------------------- # Code begins - classes and functions class IPythonConsoleLexer(Lexer): """ For IPython console output or doctests, such as: .. sourcecode:: ipython In [1]: a = 'foo' In [2]: a Out[2]: 'foo' In [3]: print a foo In [4]: 1 / 0 Notes: - Tracebacks are not currently supported. - It assumes the default IPython prompts, not customized ones. """ name = 'IPython console session' aliases = ['ipython'] mimetypes = ['text/x-ipython-console'] input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") continue_prompt = re.compile(" \.\.\.+:") tb_start = re.compile("\-+") def get_tokens_unprocessed(self, text): pylexer = PythonLexer(**self.options) tblexer = PythonTracebackLexer(**self.options) curcode = '' insertions = [] for match in line_re.finditer(text): line = match.group() input_prompt = self.input_prompt.match(line) continue_prompt = self.continue_prompt.match(line.rstrip()) output_prompt = self.output_prompt.match(line) if line.startswith("#"): insertions.append((len(curcode), [(0, Comment, line)])) elif input_prompt is not None: insertions.append((len(curcode), [(0, Generic.Prompt, input_prompt.group())])) curcode += line[input_prompt.end():] elif continue_prompt is not None: insertions.append((len(curcode), [(0, Generic.Prompt, continue_prompt.group())])) curcode += line[continue_prompt.end():] elif output_prompt is not None: # Use the 'error' token for output. We should probably make # our own token, but error is typicaly in a bright color like # red, so it works fine for our output prompts. insertions.append((len(curcode), [(0, Generic.Error, output_prompt.group())])) curcode += line[output_prompt.end():] else: if curcode: for item in do_insertions(insertions, pylexer.get_tokens_unprocessed(curcode)): yield item curcode = '' insertions = [] yield match.start(), Generic.Output, line if curcode: for item in do_insertions(insertions, pylexer.get_tokens_unprocessed(curcode)): yield item def setup(app): """Setup as a sphinx extension.""" # This is only a lexer, so adding it below to pygments appears sufficient. # But if somebody knows that the right API usage should be to do that via # sphinx, by all means fix it here. At least having this setup.py # suppresses the sphinx warning we'd get without it. pass #----------------------------------------------------------------------------- # Register the extension as a valid pygments lexer highlighting.lexers['ipython'] = IPythonConsoleLexer() pyepr-0.9.3/doc/tutorials.txt000066400000000000000000000002711252122757300162240ustar00rootroot00000000000000Tutorials ========= .. index:: tutorial .. toctree:: :maxdepth: 2 interactive_use bands_example bitmask_example ndvi_example update_example gdal_export_example pyepr-0.9.3/doc/update_example.txt000066400000000000000000000126511252122757300172000ustar00rootroot00000000000000Update :class:`Field` elements ------------------------------ .. highlight:: ipython .. index:: update, field, read-only, EPR-API, MERIS pair: open; mode The EPR C API has been designed to provide read-only features. PyEPR_ provides and extra capability consisting in the possibility to modify (*update*) an existing ENVISAT_ :class:`Product`. Lets consider a MERIS Level 2 low resolution product ( `MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1`). It has a :class:`Band` named `water_vapour` containing the water vapour content at a specific position. One can load water vapour and compute an histogram using the following instructions: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/update_elements.py :language: python :lines: 10-15 .. index:: matplotlib The resulting histogram can be plot using Matplotlib_: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/update_elements.py :language: python :lines: 17-21 .. figure:: images/water_vapour_histogram_01.* :width: 60% Histogram of the original water vapour content The actual values of the water vapour content :class:`Band` are computed starting form data stored in the `Vapour_Content` :class:`Dataset` using scaling factors contained in the `Scaling_Factor_GADS` :class:`Dataset`. In particular :class:`Field`\ s `sf_wvapour` and `off_wvapour` are used:: In [21]: dataset = product.get_dataset('Scaling_Factor_GADS') In [22]: print(dataset) epr.Dataset(Scaling_Factor_GADS) 1 records sf_cl_opt_thick = 1.000000 sf_cloud_top_press = 4.027559 sf_wvapour = 0.100000 off_cl_opt_thick = -1.000000 off_cloud_top_press = -4.027559 off_wvapour = -0.100000 spare_1 = <> .. index:: band pair: scaling; factor Now suppose that for some reason one needs to update the `sf_wvapour` scaling factor for the water vapour content. Changing the scaling factor, of course, will change all values in the `water_vapour` :class:`Band`. The change can be performed using the :meth:`Field.set_elem` and :meth:`Field.set_elems` methods of :class:`Field` objects: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/update_elements.py :language: python :lines: 24-32 Now the `sf_wvapour` scaling factor has been changed and it is possible to compute and display the histogram of modified data in the `water_vapour` :class:`Band`: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/update_elements.py :language: python :lines: 34-48 .. figure:: images/water_vapour_histogram_02.* :width: 60% Histogram of the water vapour content (original and modified) Figure above shows the two different histograms, original data in blue and modified data in red, demonstrating the effect of the change of the scaling factor. The new map of water vapour is showed in the following picture: .. figure:: images/modified_water_vapour.* Modified water vapour content map .. important:: it is important to stress that it is necessary to close and re-open the :class:`Product` in order to see changes in the scaling factors applied to the `water_vapour`:class:`Band` data. This is a limitation of the current implementation that could be removed in future versions of the PyEPR_ package. It has been showed that changing the `sf_wvapour` scaling factor modifies all values of the `water_vapour` :class:`Band`. Now suppose that one needs to modify only a specific area. It can be done changing the contents of the `Vapour_Content` :class:`Dataset`. The :class:`Dataset` size can be read form the :class:`Product`:: In [44]: product.get_scene_height(), product.get_scene_width() Out[44]: (149, 281) while information about the fields in each record can be retrieved introspecting the :class:`Record` object:: In [49]: record = dataset.read_record(0) In [50]: record.get_field_names() Out[50]: ['dsr_time', 'quality_flag', 'wvapour_cont_pix'] In [51]: record.get_field('wvapour_cont_pix') Out[51]: epr.Field("wvapour_cont_pix") 281 uchar elements So the name of the :class:`Field` we need to change is the `wvapour_cont_pix`, and its index is `2`. It is possible to change a small box inside the :class:`Dataset` as follows: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/update_elements.py :language: python :lines: 60-68 Please note that when one modifies the content of a :class:`Dataset` he/she should also take into account id the corresponding band has lines mirrored or not:: In [59]: band = p.get_band('water_vapour') In [60]: band.lines_mirrored Out[60]: True Finally the :class:`Product` can be re-opened to load and display the modified :class:`Band`: .. raw:: latex \fvset{fontsize=\footnotesize} .. literalinclude:: examples/update_elements.py :language: python :lines: 71-82 .. figure:: images/modified_water_vapour_with_box.* Modified water vapour content map with zeroed box Of course values in the box that has been set to zero in the :class:`Dataset` are transformed according to the scaling factor and offset parameters associated to `water_vapour` :class:`Band`. The complete code of the example can be found at :download:`examples/update_elements.py`. .. _PyEPR: https://github.com/avalentino/pyepr .. _ENVISAT: https://envisat.esa.int .. _Matplotlib: http://matplotlib.org .. raw:: latex \clearpage pyepr-0.9.3/doc/usermanual.txt000066400000000000000000000400771252122757300163620ustar00rootroot00000000000000User Manual =========== .. index:: EPR-API, ENVISAT, MERIS, AASTR, ASAR, ESA single: product; dataset; record; close pair: epr; module Quick start ----------- PyEPR_ provides Python_ bindings for the ENVISAT Product Reader C API (`EPR API`_) for reading satellite data from ENVISAT_ ESA_ (European Space Agency) mission. PyEPR_, as well as the `EPR API`_ for C, supports ENVISAT_ MERIS, AATSR Level 1B and Level 2 and also ASAR data products. It provides access to the data either on a geophysical (decoded, ready-to-use pixel samples) or on a raw data layer. The raw data access makes it possible to read any data field contained in a product file. Full access to the Python EPR API is provided by the :mod:`epr` module that have to be imported by the client program e.g. as follows:: import epr The following snippet open an ASAR product and dumps the "Main Processing Parameters" record to the standard output:: import epr product = epr.Product( 'ASA_IMP_1PNUPA20060202_062233_000000152044_00435_20529_3110.N1') dataset = product.get_dataset('MAIN_PROCESSING_PARAMS_ADS') record = dataset.read_record(0) print(record) product.close() Since version 0.9 PyEPR_ also include *update* features that are not available in the EPR C API. The user can open a product in update mode ('rb+') and call the :meth:`epr.Field.set_elem` and :meth:`epr.Field.set_elems` methods of :class:`epr.Field` class to update its elements and write changes to disk. .. seealso:: `Update support`_ and :doc:`update_example` tutorial for details. .. index:: pair: open; mode .. _PyEPR: https://github.com/avalentino/pyepr .. _Python: https://www.python.org .. _`EPR API`: https://github.com/bcdev/epr-api .. _ENVISAT: https://envisat.esa.int .. _ESA: https://earth.esa.int .. index:: requirements, EPR-API, Python, numpy, cython, unittest2, gcc, extension Requirements ------------ In order to use PyEPR it is needed that the following software are correctly installed and configured: * Python2_ >= 2.6 or Python3_ >= 3.1 * numpy_ >= 1.5.0 * `EPR API`_ >= 2.2 (optional, since PyEPR 0.7 the source tar-ball comes with a copy of the PER C API sources) * a reasonably updated C compiler [#]_ (build only) * Cython_ >= 0.19 [#]_ (optional and build only) * unittest2_ (only required for Python < 2.7) .. [#] PyEPR_ has been developed and tested with gcc_ 4. .. [#] The source tarball of official releases also includes the C extension code generated by cython_ so users don't strictly need cython_ to install PyEPR_. It is only needed to re-generate the C extension code (e.g. if one wants to build a development version of PyEPR_). .. _Python2: Python_ .. _Python3: Python_ .. _numpy: http://www.numpy.org .. _gcc: http://gcc.gnu.org .. _Cython: http://cython.org .. _unittest2: https://pypi.python.org/pypi/unittest2 .. index:: download, PyPi, GitHub, project, git pair: git; clone Download -------- .. highlight:: sh Official source tar-balls can be downloaded form PyPi_: https://pypi.python.org/pypi/pyepr The source code of the development versions is available on the GitHub_ project page https://github.com/avalentino/pyepr To clone the git_ repository the following command can be used:: $ git clone https://github.com/avalentino/pyepr.git .. _PyPi: https://pypi.python.org/pypi .. _GitHub: https://github.com .. _git: http://git-scm.com .. index:: install, pip pair: install; user pair: install; option pair: install; prefix .. _installation: Installation ------------ The easier way to install PyEPR_ is using tools like pip_:: $ pip install pyepr For a user specific installation please use:: $ pip install --user pyepr To install PyEPR_ in a non-standard path:: $ pip install --install-option="--prefix=" pyepr just make sure that :file:`/lib/pythonX.Y/site-packages` is in the :envvar:`PYTHONPATH`. .. index:: single: sources; setup.py pair: standalone; mode pair: EPR-API; sources pair: dynamic; library pair: git; repository PyEPR_ can be installed from sources using the following command:: $ python setup.py install The :file:`setup.py` script by default checks for the availability of the EPR C API source code in the :file:`/epr-api-src` directory and tries to build PyEPR in *standalone mode*, i.e. without linking an external dynamic library of EPR-API. If no EPR C API sources are found then the :file:`setup.py` script automatically tries to link the EPR-API dynamic library. This can happen, for example, if the user is using a copy of the PyEPR sources cloned from a git_ repository. In this case it is assumed that the `EPR API`_ C library is properly installed in the system (see the Requirements_ section). It is possible to control which `EPR API`_ C sources to use by means of the :option:`--epr-api-src` option of the :file:`setup.py` script:: $ python setup.py install --epr-api-src=../epr-api/src Also it is possible to switch off the *standalone mode* and force the link with the system `EPR API`_ C library:: $ python setup.py install --epr-api-src=None .. _pip: https://pypi.python.org/pypi/pip .. index:: test, setup.py, download pair: test; suite pair: sample; product Testing ------- PyEPR_ package comes with a complete test suite. The test suite can be run using the following command in the :file:`tests` directory:: $ python test_all.py or from the package root directory:: $ python setup.py test The test script automatically downloads and decompresses the ENVISAT sample product necessary for testing, MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1__, if it is not already available in the :file:`tests` directory. .. note:: please note that, unless the user already have a copy of the specified sample product correctly installed, an **internet connection** is necessary the first time that the test suite is run. After the first run the sample product remains in the :file:`tests` directory so the internet access is no longer necessary. __ https://earth.esa.int/services/sample_products/meris/LRC/L2/MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1.gz .. index:: EPR-API Python vs C API --------------- The Python_ EPR API is fully object oriented. The main structures of the `C API`_ have been implemented as objects while C function have been logically grouped and mapped onto object methods. The entire process of defining an object oriented API for Python_ has been quite easy and straightforward thanks to the good design of the C API, Of course there are also some differences that are illustrated in the following sections. .. _`C API`: https://rawgithub.com/bcdev/epr-api/master/docs/epr_c_api/index.html .. index:: memory, product pair: allocation; de-allocation Memory management ----------------- .. highlight:: python Being Python_ a very high level language uses have never to worry about memory allocation/de-allocation. They simply have to instantiate objects:: product = epr.Product('filename.N1') and use them freely. Objects are automatically destroyed when there are no more references to them and memory is de-allocated automatically. Even better, each object holds a reference to other objects it depends on so the user never have to worry about identifiers validity or about the correct order structures have to be freed. For example: the C `EPR_DatasetId` structure has a field (`product_id`) that points to the *product* descriptor `EPR_productId` to which it belongs to. .. index:: dataset, record The reference to the parent product is used, for example, when one wants to read a record using the `epr_read_record` function: .. code-block:: c EPR_SRecord* epr_read_record(EPR_SDatasetId* dataset_id, ...); The function takes a `EPR_SDatasetId` as a parameter and assumes all fields (including ``dataset->product_id``) are valid. It is responsibility of the programmer to keep all structures valid and free them at the right moment and in the correct order. This is the standard way to go in C but not in Python_. In Python_ all is by far simpler, and the user can get a *dateset* object instance:: dataset = product.get_dataset('MAIN_PROCESSING_PARAMS_ADS') and then forget about the *product* instance it depends on. Even if the *product* variable goes out of :index:`scope` and it is no more directly accessible in the program the *dataset* object keeps staying valid since it holds an internal reference to the *product* instance it depends on. When *record* is destroyed automatically also the parent :class:`epr.Product` object is destroyed (assumed there is no other :index:`reference` to it). The entire machinery is completely automatic and transparent to the user. .. note:: of course when a *product* object is explicitly closed using the :meth:`epr.Product.close` any I/O operation on it and on other objects (bands, datasets, etc) associated to it is no more possible. .. index:: close .. index:: array, numpy, raster Arrays ------ PyEPR_ uses numpy_ in order to manage efficiently the potentially large amount of data contained in ENVISAT_ products. * :meth:`epr.Field.get_elems` return an 1D array containing elements of the field * the `Raster.data` property is a 2D array exposes data contained in the :class:`epr.Raster` object in form of :class:`numpy.ndarray` .. note:: :attr:`epr.Raster.data` directly exposes :class:`epr.Raster` i.e. shares the same memory buffer with :class:`epr.Raster`:: >>> raster.get_pixel(i, j) 5 >>> raster.data[i, j] 5 >>> raster.data[i, j] = 3 >>> raster.get_pixel(i, j) 3 * :meth:`epr.Band.read_as_array` is an additional method provided by the Python_ EPR API (does not exist any correspondent function in the C API). It is mainly a facility method that allows users to get access to band data without creating an intermediate :class:`epr.Raster` object. It read a slice of data from the :class:`epr.Band` and returns it as a 2D :class:`numpy.ndarray`. .. index:: read_as_aaray, data .. index:: enumeration pair: module; constant Enumerators ----------- Python_ does not have *enumerators* at language level (at least this is true for Python_ < 3.4). Enumerations are simply mapped as module constants that have the same name of the C enumerate but are spelled all in capital letters. For example: ============ ============ C Pythn ============ ============ e_tid_double E_TID_DOUBLE e_smod_1OF1 E_SMOD_1OF1 e_smid_log E_SMID_LOG ============ ============ .. index:: logging, error, exception, EPR-API Error handling and logging -------------------------- Currently error handling and logging functions of the EPR C API are not exposed to python. Internal library logging is completely silenced and errors are converted to Python_ exceptions. Where appropriate standard Python_ exception types are use in other cases custom exception types (e.g. :exc:`epr.EPRError`, :exc:`epr.EPRValueError`) are used. .. index:: library, module, APR-API pair: library; initialization Library initialization ---------------------- Differently from the C API library initialization is not needed: it is performed internally the first time the :mod:`epr` module is imported in Python_. .. index:: API pair: high-level; API High level API -------------- PyEPR_ provides some utility method that has no correspondent in the C API: * :meth:`epr.Record.fields` * :meth:`epr.Record.get_field_names` * :meth:`epr.Dataset.records` * :meth:`epr.Product.get_dataset_names` * :meth:`epr.Product.get_band_names` * :meth:`epr.Product.datasets` * :meth:`epr.Product.bands` Example:: for dataset in product.datasets(): for record in dataset.records(): print(record) print() Another example:: if 'proc_data' in product.band_names(): band = product.get_band('proc_data') print(band) .. index:: __str__, __repr__, print_, pair: special; methods Special methods --------------- The Python_ EPR API also implements some `special method`_ in order to make EPR programming even handy and, in short, pythonic_. The ``__repr__`` methods have been overridden to provide a little more information with respect to the standard implementation. In some cases ``__str__`` method have been overridden to output a verbose string representation of the objects and their contents. If the EPR object has a ``print_`` method (like e.g. :meth:`epr.Record.print_` and :meth:`epr.Field.print_`) then the string representation of the object will have the same format used by the ``print_`` method. So writing:: fd.write(str(record)) giver the same result of:: record.print_(fd) Of course the :meth:`epr.Record.print_` method is more efficient for writing to file. .. index:: __iter__ Also :class:`epr.Dataset` and :class:`epr.Record` classes implement the ``__iter__`` `special method`_ for iterating over records and fields respectively. So it is possible to write code like the following:: for record in dataset: for index, field in enumerate(record): print(index, field) .. index:: __eq__ :class:`epr.DSD` and :class:`epr.Field` classes implement the ``__eq__`` and ``__ne__`` methods for objects comparison:: if filed1 == field2: print('field 1 and field2 are equal') print(field1) else: print('field1:', field1) print('field2:', field2) .. index:: __len__ :class:`epr.Field` object also implement the ``__len__`` special method that returns the number of elements in the field:: if field.get_type() != epr.E_TID_STRING: assert field.get_num_elems() == len(field) else: assert len(field) == len(field.get_elem()) .. note:: differently from the :meth:`epr.Field.get_num_elems` method ``len(field)`` return the number of elements if the field type is not :data:`epr.E_TID_STRING`. If the field contains a string then the string length is returned. .. index:: __enter__, __exit__, context, with pair: context; manager pair: with; statement Finally the :class:`epr.Product` class acts as a `context manager`_ (i.e. it implements the ``__enter__`` and ``__exit__`` methods). This allows the user to write code like the following:: with epr.open('ASA_IMS_ ... _4650.N1') as product: print(product) that ensure that the product is closed as soon as the program exits the ``with`` block. .. index:: update, ENVISAT pair: open; mode Update support -------------- It is not possible to create new ENVISAT_ products for scratch with the EPR API. Indeed EPR means "**E**\ NVISAT **P**\ roduct **R**\ eaeder". Anyway, since version 0.9, PyEPR_ also include basic *update* features. This means that, while it is still not possible to create new :class:`Products`, the user can *update* existing ones changing the contents of any :class:`Field` in any record with the only exception of MPH and SPH :class:`Field`\s. The user can open a product in update mode ('rb+'):: product = epr.open('ASA_IMS_ ... _4650.N1', 'rb+') and update the :class:`epr.Field` element at a specific index:: field.set_elem(new_value, index) or also update all elements ol the :class:`epr.Field` in one shot:: field.set_elems(new_values) .. note:: unfortunately there are some limitations to the update support. Many of the internal structures of the EPR C API are loaded when the :class:`Product` is opened and are not automatically updated when the :meth:`epr.Field.set_elem` and :meth:`epr.Field.set_elems` methods are called. In particular :class:`epr.Band`\ s contents may depend on several :class:`epr.Field` values, e.g. the contents of `Scaling_Factor_GADS` :class:`epr.Dataset`. For this reason the user may need to close and re-open the :class:`epr.Product` in order to have all changes effectively applied. .. seealso:: :doc:`update_example` .. index:: pair: scaling; factor .. _`special method`: https://docs.python.org/3/reference/datamodel.html .. _pythonic: http://www.cafepy.com/article/be_pythonic .. _`context manager`: https://docs.python.org/3/library/stdtypes.html#context-manager-types pyepr-0.9.3/requirements.txt000066400000000000000000000000301252122757300161450ustar00rootroot00000000000000numpy>=1.5 cython>=0.19 pyepr-0.9.3/setup.py000077500000000000000000000164211252122757300144110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2011-2015, Antonio Valentino # # This file is part of PyEPR. # # PyEPR is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyEPR is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyEPR. If not, see . from __future__ import print_function import os import re import sys import glob def get_version(filename): with open(filename) as fd: data = fd.read() mobj = re.search( '''^__version__\s*=\s*(?P['"])(?P\d+(\.\d+)*.*)(?P=q)''', data, re.MULTILINE) return mobj.group('version') def get_use_setuptools(): use_setuptools = os.environ.get('USE_SETUPTOOLS', True) if str(use_setuptools).lower() in ('false', 'off', 'n', 'no', '0'): use_setuptools = False else: use_setuptools = True return use_setuptools try: if not get_use_setuptools(): raise ImportError from setuptools import setup, Extension HAVE_SETUPTOOLS = True except ImportError: from distutils.core import setup from distutils.extension import Extension HAVE_SETUPTOOLS = False print('HAVE_SETUPTOOLS: {0}'.format(HAVE_SETUPTOOLS)) try: from Cython.Build import cythonize HAVE_CYTHON = True except ImportError: HAVE_CYTHON = False print('HAVE_CYTHON: {0}'.format(HAVE_CYTHON)) # @COMPATIBILITY: Extension is an old style class in Python 2 class PyEprExtension(Extension, object): def __init__(self, *args, **kwargs): self._include_dirs = [] eprsrcdir = kwargs.pop('eprsrcdir', None) super(PyEprExtension, self).__init__(*args, **kwargs) self.sources.extend(self._extra_sources(eprsrcdir)) self.setup_requires_cython = False def _extra_sources(self, eprsrcdir=None): sources = [] # check for local epr-api sources if eprsrcdir is None: default_eprapisrc = os.path.join('epr-api-src') if os.path.isdir(default_eprapisrc): eprsrcdir = default_eprapisrc if eprsrcdir: print('using EPR C API sources at "{0}"'.format(eprsrcdir)) self._include_dirs.append(eprsrcdir) sources.extend(glob.glob(os.path.join(eprsrcdir, 'epr_*.c'))) else: print('using pre-built dynamic libraray for EPR C API') if 'epr_api' not in self.libraries: self.libraries.append('epr_api') sources = sorted(set(sources).difference(self.sources)) return sources @property def include_dirs(self): from numpy.distutils.misc_util import get_numpy_include_dirs includes = set(get_numpy_include_dirs()).difference(self._include_dirs) return self._include_dirs + sorted(includes) @include_dirs.setter def include_dirs(self, value): self._include_dirs = value # disable setuptools automatic conversion def _convert_pyx_sources_to_lang(self): pass def convert_pyx_sources_to_lang(self): lang = self.language or '' target_ext = '.cpp' if lang.lower() == 'c++' else '.c' sources = [] for src in self.sources: if src.endswith('.pyx'): csrc = re.sub('.pyx$', target_ext, src) if os.path.exists(csrc): sources.append(csrc) else: self.setup_requires_cython = True sources.append(src) else: sources.append(src) if not self.setup_requires_cython: self.sources = sources return self.setup_requires_cython def get_extension(): # command line arguments management eprsrcdir = None for arg in list(sys.argv): if arg.startswith('--epr-api-src='): eprsrcdir = os.path.expanduser(arg.split('=')[1]) if eprsrcdir.lower() == 'none': eprsrcdir = False sys.argv.remove(arg) break ext = PyEprExtension( 'epr', sources=[os.path.join('src', 'epr.pyx')], # libraries=['m'], # define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION'),], eprsrcdir=eprsrcdir, ) # @NOTE: uses the HAVE_CYTHON global variable if HAVE_CYTHON: extlist = cythonize([ext]) ext = extlist[0] else: ext.convert_pyx_sources_to_lang() return ext config = dict( name='pyepr', version=get_version(os.path.join('src', 'epr.pyx')), author='Antonio Valentino', author_email='antonio.valentino@tiscali.it', url='http://avalentino.github.com/pyepr', description='Python ENVISAT Product Reader API', long_description='''PyEPR provides Python_ bindings for the ENVISAT Product Reader C API (`EPR API`_) for reading satellite data from ENVISAT_ ESA_ (European Space Agency) mission. PyEPR, as well as the `EPR API`_ for C, supports ENVISAT_ MERIS, AATSR Level 1B and Level 2 and also ASAR data products. It provides access to the data either on a geophysical (decoded, ready-to-use pixel samples) or on a raw data layer. The raw data access makes it possible to read any data field contained in a product file. .. _Python: https://www.python.org .. _`EPR API`: https://github.com/bcdev/epr-api .. _ENVISAT: https://envisat.esa.int .. _ESA: https://earth.esa.int ''', download_url='http://pypi.python.org/pypi/pyepr', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Other Environment', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Cython', 'Topic :: Software Development :: Libraries', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: GIS', ], platforms=['any'], license='GPL3', requires=['numpy'], # XXX: check ) def setup_package(): ext = get_extension() config['ext_modules'] = [ext] if HAVE_SETUPTOOLS: config['test_suite'] = 'tests' config.setdefault('setup_requires', []).append('numpy>=1.5') config.setdefault('install_requires', []).append('numpy>=1.5') if ext.setup_requires_cython: config['setup_requires'].append('cython>=0.19') setup(**config) if __name__ == '__main__': setup_package() pyepr-0.9.3/src/000077500000000000000000000000001252122757300134575ustar00rootroot00000000000000pyepr-0.9.3/src/epr.pxd000066400000000000000000000271411252122757300147670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # PyEPR - Python bindings for ENVISAT Product Reader API # # Copyright (C) 2011-2015, Antonio Valentino # # This file is part of PyEPR. # # PyEPR is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyEPR is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyEPR. If not, see . from libc.stdio cimport FILE cdef extern from 'epr_api.h' nogil: char* EPR_PRODUCT_API_VERSION_STR ctypedef int epr_boolean ctypedef unsigned char uchar ctypedef unsigned short ushort ctypedef unsigned int uint ctypedef unsigned long ulong ctypedef EPR_Time EPR_STime #ctypedef EPR_FlagDef EPR_SFlagDef ctypedef EPR_PtrArray EPR_SPtrArray ctypedef EPR_FieldInfo EPR_SFieldInfo ctypedef EPR_RecordInfo EPR_SRecordInfo ctypedef EPR_Field EPR_SField ctypedef EPR_Record EPR_SRecord ctypedef EPR_DSD EPR_SDSD ctypedef EPR_Raster EPR_SRaster ctypedef EPR_BandId EPR_SBandId ctypedef EPR_DatasetId EPR_SDatasetId ctypedef EPR_ProductId EPR_SProductId ctypedef EPR_ErrCode EPR_EErrCode ctypedef EPR_LogLevel EPR_ELogLevel ctypedef EPR_SampleModel EPR_ESampleModel ctypedef EPR_ScalingMethod EPR_EScalingMethod ctypedef EPR_DataTypeId EPR_EDataTypeId ctypedef int EPR_Magic int EPR_MAGIC_PRODUCT_ID int EPR_MAGIC_DATASET_ID int EPR_MAGIC_BAND_ID int EPR_MAGIC_RECORD int EPR_MAGIC_FIELD int EPR_MAGIC_RASTER int EPR_MAGIC_FLAG_DEF enum EPR_ErrCode: e_err_none = 0 e_err_null_pointer = 1 e_err_illegal_arg = 2 e_err_illegal_state = 3 e_err_out_of_memory = 4 e_err_index_out_of_range = 5 e_err_illegal_conversion = 6 e_err_illegal_data_type = 7 e_err_file_not_found = 101 e_err_file_access_denied = 102 e_err_file_read_error = 103 e_err_file_write_error = 104 e_err_file_open_failed = 105 e_err_file_close_failed = 106 e_err_api_not_initialized = 201 e_err_invalid_product_id = 203 e_err_invalid_record = 204 e_err_invalid_band = 205 e_err_invalid_raster = 206 e_err_invalid_dataset_name = 207 e_err_invalid_field_name = 208 e_err_invalid_record_name = 209 e_err_invalid_product_name = 210 e_err_invalid_band_name = 211 e_err_invalid_data_format = 212 e_err_invalid_value = 213 e_err_invalid_keyword_name = 214 e_err_unknown_endian_order = 216 e_err_flag_not_found = 301 e_err_invalid_ddbb_format = 402 enum EPR_DataTypeId: e_tid_unknown = 0 e_tid_uchar = 1 e_tid_char = 2 e_tid_ushort = 3 e_tid_short = 4 e_tid_uint = 5 e_tid_int = 6 e_tid_float = 7 e_tid_double = 8 e_tid_string = 11 e_tid_spare = 13 e_tid_time = 21 enum EPR_LogLevel: e_log_debug = -1 e_log_info = 0 e_log_warning = 1 e_log_error = 2 enum EPR_SampleModel: e_smod_1OF1 = 0 e_smod_1OF2 = 1 e_smod_2OF2 = 2 e_smod_3TOI = 3 e_smod_2TOF = 4 enum EPR_ScalingMethod: e_smid_non = 0 e_smid_lin = 1 e_smid_log = 2 struct EPR_Time: int days uint seconds uint microseconds struct EPR_FlagDef: EPR_Magic magic char* name uint bit_mask char* description struct EPR_PtrArray: unsigned int capacity unsigned int length void** elems struct EPR_Field: EPR_Magic magic EPR_FieldInfo* info void* elems struct EPR_Record: EPR_Magic magic EPR_RecordInfo* info uint num_fields EPR_Field** fields struct EPR_DSD: EPR_Magic magic int index char* ds_name char* ds_type char* filename uint ds_offset uint ds_size uint num_dsr uint dsr_size struct EPR_Raster: EPR_Magic magic EPR_DataTypeId data_type uint elem_size uint source_width uint source_height uint source_step_x uint source_step_y uint raster_width uint raster_height void* buffer struct EPR_ProductId: EPR_Magic magic char* file_path FILE* istream uint tot_size uint scene_width uint scene_height char* id_string EPR_Record* mph_record EPR_Record* sph_record EPR_PtrArray* dsd_array EPR_PtrArray* record_info_cache EPR_PtrArray* param_table EPR_PtrArray* dataset_ids EPR_PtrArray* band_ids int meris_iodd_version struct EPR_DatasetId: EPR_Magic magic EPR_ProductId* product_id char* dsd_name EPR_DSD* dsd char* dataset_name #struct RecordDescriptor* record_descriptor EPR_SRecordInfo* record_info char* description struct EPR_DatasetRef: EPR_DatasetId* dataset_id int field_index # -1 if not used int elem_index # -1 if not used struct EPR_BandId: EPR_Magic magic EPR_ProductId* product_id char* band_name int spectr_band_index EPR_DatasetRef dataset_ref EPR_SampleModel sample_model EPR_DataTypeId data_type EPR_ScalingMethod scaling_method float scaling_offset float scaling_factor char* bm_expr EPR_SPtrArray* flag_coding char* unit char* description epr_boolean lines_mirrored # @TODO: improve logging and error management (--> custom handlers) # logging and error handling function pointers ctypedef void (*EPR_FLogHandler)(EPR_ELogLevel, char*) ctypedef void (*EPR_FErrHandler)(EPR_EErrCode, char*) # logging int epr_set_log_level(EPR_ELogLevel) void epr_set_log_handler(EPR_FLogHandler) void epr_log_message(EPR_ELogLevel, char*) # error handling void epr_set_err_handler(EPR_FErrHandler) EPR_EErrCode epr_get_last_err_code() const char* epr_get_last_err_message() void epr_clear_err() # API initialization/finalization int epr_init_api(EPR_ELogLevel, EPR_FLogHandler, EPR_FErrHandler) void epr_close_api() # DATATYPE uint epr_get_data_type_size(EPR_EDataTypeId) const char* epr_data_type_id_to_str(EPR_EDataTypeId) # open products EPR_SProductId* epr_open_product(char*) # PRODUCT int epr_close_product(EPR_SProductId*) uint epr_get_scene_width(EPR_SProductId*) uint epr_get_scene_height(EPR_SProductId*) uint epr_get_num_datasets(EPR_SProductId*) EPR_SDatasetId* epr_get_dataset_id_at(EPR_SProductId*, uint) EPR_SDatasetId* epr_get_dataset_id(EPR_SProductId*, char*) uint epr_get_num_dsds(EPR_SProductId*) EPR_SDSD* epr_get_dsd_at(EPR_SProductId*, uint) EPR_SRecord* epr_get_mph(EPR_SProductId*) EPR_SRecord* epr_get_sph(EPR_SProductId*) uint epr_get_num_bands(EPR_SProductId*) EPR_SBandId* epr_get_band_id_at(EPR_SProductId*, uint) EPR_SBandId* epr_get_band_id(EPR_SProductId*, char*) int epr_read_bitmask_raster(EPR_SProductId*, char*, int, int, EPR_SRaster*) # DATASET const char* epr_get_dataset_name(EPR_SDatasetId*) const char* epr_get_dsd_name(EPR_SDatasetId*) uint epr_get_num_records(EPR_SDatasetId*) EPR_SDSD* epr_get_dsd(EPR_SDatasetId*) EPR_SRecord* epr_create_record(EPR_SDatasetId*) EPR_SRecord* epr_read_record(EPR_SDatasetId*, uint, EPR_SRecord*) # RECORD void epr_free_record(EPR_SRecord*) uint epr_get_num_fields(EPR_SRecord*) void epr_print_record(EPR_SRecord*, FILE*) void epr_print_element(EPR_SRecord*, uint, uint, FILE*) void epr_dump_record(EPR_SRecord*) void epr_dump_element(EPR_SRecord*, uint, uint) EPR_SField* epr_get_field(EPR_SRecord*, char*) EPR_SField* epr_get_field_at(EPR_SRecord*, uint) # FIELD void epr_print_field(EPR_SField*, FILE*) void epr_dump_field(EPR_SField*) const char* epr_get_field_unit(EPR_SField*) const char* epr_get_field_description(EPR_SField*) uint epr_get_field_num_elems(EPR_SField*) const char* epr_get_field_name(EPR_SField*) EPR_EDataTypeId epr_get_field_type(EPR_SField*) char epr_get_field_elem_as_char(EPR_SField*, uint) uchar epr_get_field_elem_as_uchar(EPR_SField*, uint) short epr_get_field_elem_as_short(EPR_SField*, uint) ushort epr_get_field_elem_as_ushort(EPR_SField*, uint) int epr_get_field_elem_as_int(EPR_SField*, uint) uint epr_get_field_elem_as_uint(EPR_SField*, uint) float epr_get_field_elem_as_float(EPR_SField*, uint) double epr_get_field_elem_as_double(EPR_SField*, uint) const char* epr_get_field_elem_as_str(EPR_SField*) EPR_STime* epr_get_field_elem_as_mjd(EPR_SField*) const char* epr_get_field_elems_char(EPR_SField*) uchar* epr_get_field_elems_uchar(EPR_SField*) short* epr_get_field_elems_short(EPR_SField*) ushort* epr_get_field_elems_ushort(EPR_SField*) int* epr_get_field_elems_int(EPR_SField*) uint* epr_get_field_elems_uint(EPR_SField*) float* epr_get_field_elems_float(EPR_SField*) double* epr_get_field_elems_double(EPR_SField*) uint epr_copy_field_elems_as_ints(EPR_SField*, int*, uint) uint epr_copy_field_elems_as_uints(EPR_SField*, uint*, uint) uint epr_copy_field_elems_as_floats(EPR_SField*, float*, uint) uint epr_copy_field_elems_as_doubles(EPR_SField*, double*, uint) # BAND const char* epr_get_band_name(EPR_SBandId*) EPR_SRaster* epr_create_compatible_raster(EPR_SBandId*, uint, uint, uint, uint) int epr_read_band_raster(EPR_SBandId*, int, int, EPR_SRaster*) # RASTER void epr_free_raster(EPR_SRaster*) uint epr_get_raster_width(EPR_SRaster*) uint epr_get_raster_height(EPR_SRaster*) uint epr_get_raster_elem_size(EPR_SRaster*) uint epr_get_pixel_as_uint(EPR_SRaster*, int, int) int epr_get_pixel_as_int(EPR_SRaster*, int, int) float epr_get_pixel_as_float(EPR_SRaster*, int, int) double epr_get_pixel_as_double(EPR_SRaster*, int, int) void* epr_get_raster_elem_addr(EPR_SRaster*, uint) void* epr_get_raster_pixel_addr(EPR_SRaster*, uint, uint) void* epr_get_raster_line_addr(EPR_SRaster*, uint) EPR_SRaster* epr_create_raster(EPR_EDataTypeId, uint, uint, uint, uint) EPR_SRaster* epr_create_bitmask_raster(uint, uint, uint, uint) # @IMPORTANT: # # the following structures are not part of the public API. # It is not ensured that relative header files are available at build time # (e.g. debian does not install them), so structures are relìplicated here. # It is fundamental to ensure that structures defined here are kept totally # in sync with the one defined in EPR C API. # epr_field.h ctypedef struct EPR_FieldInfo: char* name EPR_EDataTypeId data_type_id uint num_elems char* unit char* description uint tot_size # epr_record.h ctypedef struct EPR_RecordInfo: char* dataset_name EPR_SPtrArray* field_infos uint tot_size pyepr-0.9.3/src/epr.pyx000066400000000000000000002476211252122757300150230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # PyEPR - Python bindings for ENVISAT Product Reader API # # Copyright (C) 2011-2015, Antonio Valentino # # This file is part of PyEPR. # # PyEPR is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyEPR is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyEPR. If not, see . '''Python bindings for ENVISAT Product Reader C API PyEPR_ provides Python_ bindings for the ENVISAT Product Reader C API (`EPR API`_) for reading satellite data from ENVISAT_ ESA_ (European Space Agency) mission. PyEPR_ is fully object oriented and, as well as the `EPR API`_ for C, supports ENVISAT_ MERIS, AATSR Level 1B and Level 2 and also ASAR data products. It provides access to the data either on a geophysical (decoded, ready-to-use pixel samples) or on a raw data layer. The raw data access makes it possible to read any data field contained in a product file. .. _PyEPR: http://avalentino.github.com/pyepr .. _Python: http://www.python.org .. _`EPR API`: https://github.com/bcdev/epr-api .. _ENVISAT: http://envisat.esa.int .. _ESA: http://earth.esa.int ''' __version__ = '0.9.3' from libc cimport errno from libc cimport stdio from libc.stdio cimport FILE from libc cimport string as cstring IF UNAME_SYSNAME == 'Windows': cdef extern from "stdio.h" nogil: int fileno "_fileno" (FILE*) ELSE: # posix cdef extern from "stdio.h" nogil: int fileno(FILE*) from epr cimport * from cpython.version cimport PY_MAJOR_VERSION from cpython.object cimport PyObject_AsFileDescriptor from cpython.weakref cimport PyWeakref_NewRef cimport numpy as np np.import_array() import os import sys from collections import namedtuple import numpy as np cdef bint PY3 = (PY_MAJOR_VERSION >= 3) cdef bint SWAP_BYTES = (sys.byteorder == 'little') # internal utils _DEFAULT_FS_ENCODING = sys.getfilesystemencoding() cdef inline bytes _to_bytes(s, encoding='UTF-8'): if hasattr(s, 'encode'): return s.encode(encoding) else: return s cdef inline str _to_str(b, encoding='UTF-8'): if PY3: return b.decode(encoding) else: return b # utils EPRTime = namedtuple('EPRTime', ('days', 'seconds', 'microseconds')) MJD = np.dtype([ ('days', 'i%d' % sizeof(int)), ('seconds', 'u%d' % sizeof(uint)), ('microseconds', 'u%d' % sizeof(uint)), ]) EPR_C_API_VERSION = _to_str(EPR_PRODUCT_API_VERSION_STR, 'ascii') # EPR_DataTypeId E_TID_UNKNOWN = e_tid_unknown E_TID_UCHAR = e_tid_uchar E_TID_CHAR = e_tid_char E_TID_USHORT = e_tid_ushort E_TID_SHORT = e_tid_short E_TID_UINT = e_tid_uint E_TID_INT = e_tid_int E_TID_FLOAT = e_tid_float E_TID_DOUBLE = e_tid_double E_TID_STRING = e_tid_string E_TID_SPARE = e_tid_spare E_TID_TIME = e_tid_time # EPR_SampleModel E_SMOD_1OF1 = e_smod_1OF1 E_SMOD_1OF2 = e_smod_1OF2 E_SMOD_2OF2 = e_smod_2OF2 E_SMOD_3TOI = e_smod_3TOI E_SMOD_2TOF = e_smod_2TOF # EPR_ScalingMethod E_SMID_NON = e_smid_non E_SMID_LIN = e_smid_lin E_SMID_LOG = e_smid_log # EPR magic IDs _EPR_MAGIC_PRODUCT_ID = EPR_MAGIC_PRODUCT_ID _EPR_MAGIC_DATASET_ID = EPR_MAGIC_DATASET_ID _EPR_MAGIC_BAND_ID = EPR_MAGIC_BAND_ID _EPR_MAGIC_RECORD = EPR_MAGIC_RECORD _EPR_MAGIC_FIELD = EPR_MAGIC_FIELD _EPR_MAGIC_RASTER = EPR_MAGIC_RASTER _EPR_MAGIC_FLAG_DEF = EPR_MAGIC_FLAG_DEF cdef np.NPY_TYPES _epr_to_numpy_type_id(EPR_DataTypeId epr_type): if epr_type == e_tid_uchar: return np.NPY_UBYTE if epr_type == e_tid_char: return np.NPY_BYTE if epr_type == e_tid_ushort: return np.NPY_USHORT if epr_type == e_tid_short: return np.NPY_SHORT if epr_type == e_tid_uint: return np.NPY_UINT if epr_type == e_tid_int: return np.NPY_INT if epr_type == e_tid_float: return np.NPY_FLOAT if epr_type == e_tid_double: return np.NPY_DOUBLE if epr_type == e_tid_string: return np.NPY_STRING return np.NPY_NOTYPE _DTYPE_MAP = { E_TID_UCHAR: np.uint8, E_TID_CHAR: np.int8, E_TID_USHORT: np.uint16, E_TID_SHORT: np.int16, E_TID_UINT: np.uint32, E_TID_INT: np.int32, E_TID_FLOAT: np.float32, E_TID_DOUBLE: np.float64, E_TID_STRING: np.bytes_, E_TID_TIME: MJD, E_TID_SPARE: None, E_TID_UNKNOWN: None, } _METHOD_MAP = { E_SMID_NON: 'NONE', E_SMID_LIN: 'LIN', E_SMID_LOG: 'LOG', } _MODEL_MAP = { E_SMOD_1OF1: '1OF1', E_SMOD_1OF2: '1OF2', E_SMOD_2OF2: '2OF2', E_SMOD_3TOI: '3TOI', E_SMOD_2TOF: '2TOF', } class EPRError(Exception): '''EPR API error''' def __init__(self, message='', code=None, *args, **kargs): '''__init__(self, message='', code=None, *args, **kargs)''' super(EPRError, self).__init__(message, code, *args, **kargs) #: EPR API error code self.code = code class EPRValueError(EPRError, ValueError): pass cdef pyepr_check_errors(): cdef int code cdef str msg code = epr_get_last_err_code() if code != e_err_none: msg = _to_str(epr_get_last_err_message(), 'ascii') epr_clear_err() # @TODO: if not msg: msg = EPR_ERR_MSG[code] if (e_err_invalid_product_id <= code <= e_err_invalid_keyword_name or code in (e_err_null_pointer, e_err_illegal_arg, e_err_index_out_of_range)): raise EPRValueError(msg, code) else: raise EPRError(msg, code) cdef pyepr_null_ptr_error(msg='null pointer'): cdef int code cdef str eprmsg = _to_str(epr_get_last_err_message(), 'ascii') code = epr_get_last_err_code() if not code: code = None epr_clear_err() raise EPRValueError('%s: %s' % (msg, eprmsg), code=code) cdef FILE* pyepr_get_file_stream(object ostream) except NULL: cdef FILE* fstream = NULL cdef int fileno if ostream is None: ostream = sys.stdout try: ostream.flush() except AttributeError, e: raise TypeError(str(e)) else: fileno = PyObject_AsFileDescriptor(ostream) if fileno == -1: raise TypeError('bad output stream') else: fstream = stdio.fdopen(fileno, 'w') if fstream is NULL: errno.errno = 0 raise TypeError('invalid ostream') return fstream cdef class _CLib: '''Library object to handle C API initialization/finalization .. warning:: this is meant for internal use only. **Do not use it**. ''' def __cinit__(self, *args, **kwargs): cdef bytes msg # @TODO: check #if EPR_C_API_VERSION != '2.2': # raise ImportError('C library version not supported: "%s"' % # EPR_C_API_VERSION) #if epr_init_api(e_log_warning, epr_log_message, NULL): if epr_init_api(e_log_warning, NULL, NULL): msg = epr_get_last_err_message() epr_clear_err() raise ImportError('unable to inizialize EPR API library: ' '%s' % _to_str(msg, 'ascii')) def __dealloc__(self): epr_close_api() def __init__(self): raise TypeError('"%s" class cannot be instantiated from Python' % self.__class__.__name__) # global _CLib instance cdef _CLib _EPR_C_LIB = None cdef class EprObject: cdef object epr_c_lib def __cinit__(self, *ars, **kargs): self.epr_c_lib = _EPR_C_LIB def __dealloc__(self): self.epr_c_lib = None def __init__(self): raise TypeError('"%s" class cannot be instantiated from Python' % self.__class__.__name__) cpdef get_numpy_dtype(EPR_EDataTypeId type_id): '''get_numpy_dtype(epr_type) Return the numpy datatype specified EPR type ID ''' try: return _DTYPE_MAP[type_id] except KeyError: raise ValueError('invalid EPR type ID: %d' % type_id) cpdef uint get_data_type_size(EPR_EDataTypeId type_id): '''get_data_type_size(type_id) Gets the size in bytes for an element of the given data type ''' return epr_get_data_type_size(type_id) cpdef str data_type_id_to_str(EPR_EDataTypeId type_id): '''data_type_id_to_str(type_id) Gets the 'C' data type string for the given data type ''' cdef char* type_id_str = epr_data_type_id_to_str(type_id) return _to_str(type_id_str, 'ascii') cpdef str get_scaling_method_name(EPR_ScalingMethod method): '''get_scaling_method_name(method) Return the name of the specified scaling method ''' try: return _METHOD_MAP[method] except KeyError: raise ValueError('invalid scaling method: "%s"' % method) cpdef str get_sample_model_name(EPR_SampleModel model): '''get_sample_model_name(model) Return the name of the specified sample model ''' try: return _MODEL_MAP[model] except KeyError: raise ValueError('invalid sample model: "%s"' % model) cdef class DSD(EprObject): '''Dataset descriptor The DSD class contains information about the properties of a dataset and its location within an ENVISAT product file ''' cdef EPR_SDSD* _ptr cdef object _parent # Dataset or Product cdef inline check_closed_product(self): if isinstance(self, Dataset): (self._parent).check_closed_product() else: #elif isinstance(self, Product): (self._parent).check_closed_product() property index: '''The index of this DSD (zero-based)''' def __get__(self): self.check_closed_product() return self._ptr.index property ds_name: '''The dataset name''' def __get__(self): self.check_closed_product() return _to_str(self._ptr.ds_name, 'ascii') property ds_type: '''The dataset type descriptor''' def __get__(self): self.check_closed_product() return _to_str(self._ptr.ds_type, 'ascii') property filename: '''The filename in the DDDB with the description of this dataset''' def __get__(self): self.check_closed_product() return _to_str(self._ptr.filename, 'ascii') property ds_offset: '''The offset of dataset-information the product file''' def __get__(self): self.check_closed_product() return self._ptr.ds_offset property ds_size: '''The size of dataset-information in dataset product file''' def __get__(self): self.check_closed_product() return self._ptr.ds_size property num_dsr: '''The number of dataset records for the given dataset name''' def __get__(self): self.check_closed_product() return self._ptr.num_dsr property dsr_size: '''The size of dataset record for the given dataset name''' def __get__(self): self.check_closed_product() return self._ptr.dsr_size # --- high level interface ------------------------------------------------ def __repr__(self): return 'epr.DSD("%s")' % self.ds_name def __richcmp__(self, other, int op): cdef EPR_SDSD* p1 = (self)._ptr cdef EPR_SDSD* p2 = (other)._ptr if isinstance(self, DSD) and isinstance(other, DSD): if op == 2: # eq if p1 == p2: return True (self).check_closed_product() return ((p1.index == p2.index) and (p1.ds_offset == p2.ds_offset) and (p1.ds_size == p2.ds_size) and (p1.num_dsr == p2.num_dsr) and (p1.dsr_size == p2.dsr_size)and (cstring.strcmp(p1.ds_name, p2.ds_name) == 0) and (cstring.strcmp(p1.ds_type, p2.ds_type) == 0) and (cstring.strcmp(p1.filename, p2.filename) == 0)) elif op == 3: # ne if p1 == p2: return False (self).check_closed_product() return ((p1.index != p2.index) or (p1.ds_offset != p2.ds_offset) or (p1.ds_size != p2.ds_size) or (p1.num_dsr != p2.num_dsr) or (p1.dsr_size != p2.dsr_size) or (cstring.strcmp(p1.ds_name, p2.ds_name) != 0) or (cstring.strcmp(p1.ds_type, p2.ds_type) != 0) or (cstring.strcmp(p1.filename, p2.filename) != 0)) else: raise TypeError('DSD only implements "==" and "!=" operators') else: return NotImplemented # --- low level interface ------------------------------------------------- property _magic: '''The magic number for internal C structure''' def __get__(self): self.check_closed_product() return self._ptr.magic cdef new_dsd(EPR_SDSD* ptr, object parent=None): if ptr is NULL: pyepr_null_ptr_error() cdef DSD instance = DSD.__new__(DSD) instance._ptr = ptr instance._parent = parent # Dataset or Product return instance cdef class Field(EprObject): '''Represents a field within a record A field is composed of one or more data elements of one of the types defined in the internal ``field_info`` structure. .. seealso:: :class:`Record` ''' cdef EPR_SField* _ptr cdef Record _parent cdef inline check_closed_product(self): self._parent.check_closed_product() cdef inline _check_write_mode(self): self._parent._check_write_mode() cdef long _get_offset(self, bint absolute=0): cdef bint found = 0 cdef int i = 0 cdef int num_fields_in_record = 0 cdef long offset = 0 cdef const char* name = NULL cdef const EPR_Field* field = NULL cdef const EPR_FieldInfo* info = NULL cdef const EPR_Record* record = NULL info = self._ptr.info name = info.name record = self._parent._ptr num_fields_in_record = epr_get_num_fields(record) for i in range(num_fields_in_record): field = epr_get_field_at(record, i) info = field.info if info.name == name: found = 1 break offset += info.tot_size if not found: offset = None elif absolute: offset += self._parent._get_offset(absolute) return offset def print_(self, ostream=None): '''print_(self, ostream=None) Write the field to specified file (default: :data:`sys.stdout`) This method writes formatted contents of the field to specified *ostream* text file or (default) the ASCII output is be printed to standard output (:data:`sys.stdout`) :param ostream: the (opened) output file object .. note:: the *ostream* parameter have to be a *real* file not a generic stream object like :class:`StringIO.StringIO` instances ''' cdef FILE* fstream = pyepr_get_file_stream(ostream) self.check_closed_product() with nogil: epr_print_field(self._ptr, fstream) stdio.fflush(fstream) pyepr_check_errors() #def dump_field(self): # epr_dump_field(self._ptr) # pyepr_check_errors() def get_unit(self): '''get_unit(self) Gets the unit of the field ''' cdef const char* unit = NULL self.check_closed_product() unit = epr_get_field_unit(self._ptr) if unit is NULL: return '' else: return _to_str(unit, 'ascii') def get_description(self): '''get_description(self) Gets the description of the field ''' cdef char* description = NULL self.check_closed_product() description = epr_get_field_description(self._ptr) return _to_str(description, 'ascii') def get_num_elems(self): '''get_num_elems(self) Gets the number of elements of the field ''' self.check_closed_product() return epr_get_field_num_elems(self._ptr) def get_name(self): '''get_name(self) Gets the name of the field ''' cdef char* name = NULL self.check_closed_product() name = epr_get_field_name(self._ptr) return _to_str(name, 'ascii') def get_type(self): '''get_type(self) Gets the type of the field ''' self.check_closed_product() return epr_get_field_type(self._ptr) def get_elem(self, uint index=0): '''get_elem(self, index=0) Field single element access This function is for getting the elements of a field. :param index: the zero-based index of element to be returned, must not be negative :returns: the typed value from given field ''' cdef EPR_STime* eprtime self.check_closed_product() etype = epr_get_field_type(self._ptr) if etype == e_tid_uchar: val = epr_get_field_elem_as_uchar(self._ptr, index) elif etype == e_tid_char: val = epr_get_field_elem_as_char(self._ptr, index) elif etype == e_tid_ushort: val = epr_get_field_elem_as_ushort(self._ptr, index) elif etype == e_tid_short: val = epr_get_field_elem_as_short(self._ptr, index) elif etype == e_tid_uint: val = epr_get_field_elem_as_uint(self._ptr, index) elif etype == e_tid_int: val = epr_get_field_elem_as_int(self._ptr, index) elif etype == e_tid_float: val = epr_get_field_elem_as_float(self._ptr, index) elif etype == e_tid_double: val = epr_get_field_elem_as_double(self._ptr, index) elif etype == e_tid_string: if index != 0: raise ValueError('invalid index: %d' % index) val = epr_get_field_elem_as_str(self._ptr) #elif etype == e_tid_spare: # val = epr_get_field_elem_as_str(self._ptr) elif etype == e_tid_time: if index != 0: raise ValueError('invalid index: %d' % index) # use casting to silence warnings eprtime = epr_get_field_elem_as_mjd(self._ptr) val = EPRTime(eprtime.days, eprtime.seconds, eprtime.microseconds) else: raise ValueError('invalid field type') pyepr_check_errors() return val def get_elems(self): '''get_elems(self) Field array element access This function is for getting an array of field elements of the field. :returns: the data array (:class:`numpy.ndarray`) having the type of the field ''' cdef void* buf = NULL cdef int nd = 1 cdef np.npy_intp shape[1] cdef np.ndarray out cdef EPR_Time* t = NULL cdef np.NPY_TYPES dtype self.check_closed_product() shape[0] = epr_get_field_num_elems(self._ptr) etype = epr_get_field_type(self._ptr) msg = 'Filed("%s") elems pointer is null' % self.get_name() if etype == e_tid_time: if shape[0] != 1: raise ValueError( 'unexpected number of elements: %d' % shape[0]) t = epr_get_field_elem_as_mjd(self._ptr) if t is NULL: pyepr_null_ptr_error(msg) out = np.ndarray(1, MJD) out[0]['days'] = t.days out[0]['seconds'] = t.seconds out[0]['microseconds'] = t.microseconds return out if etype == e_tid_uchar: dtype = np.NPY_UBYTE buf = epr_get_field_elems_uchar(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_char: dtype = np.NPY_BYTE buf = epr_get_field_elems_char(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_ushort: dtype = np.NPY_USHORT buf = epr_get_field_elems_ushort(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_short: dtype = np.NPY_SHORT buf = epr_get_field_elems_short(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_uint: dtype = np.NPY_UINT buf = epr_get_field_elems_uint(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_int: dtype = np.NPY_INT buf = epr_get_field_elems_int(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_float: dtype = np.NPY_FLOAT buf = epr_get_field_elems_float(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_double: dtype = np.NPY_DOUBLE buf = epr_get_field_elems_double(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) elif etype == e_tid_string: if shape[0] != 1: raise ValueError( 'unexpected number of elements: %d' % shape[0]) nd = 0 dtype = np.NPY_STRING buf = epr_get_field_elem_as_str(self._ptr) if buf is NULL: pyepr_null_ptr_error(msg) #elif etype == e_tid_unknown: # pass #elif etype = e_tid_spare: # pass else: raise ValueError('invalid field type') out = np.PyArray_SimpleNewFromData(nd, shape, dtype, buf) #np.PyArray_CLEARFLAG(out, NPY_ARRAY_WRITEABLE) # new in numpy 1.7 # Make the ndarray keep a reference to this object np.set_array_base(out, self) return out cdef _set_elems(self, np.ndarray elems, uint index=0): cdef Record record cdef Dataset dataset cdef Product product cdef FILE* istream cdef size_t ret cdef size_t nelems cdef size_t elemsize cdef size_t datasize cdef long file_offset cdef long field_offset cdef char* buf cdef EPR_DataTypeId etype = epr_get_field_type(self._ptr) dtype = _DTYPE_MAP[etype] elems = elems.astype(dtype) record = self._parent dataset = record._parent product = dataset._parent istream = product._ptr.istream nelems = elems.size elemsize = epr_get_data_type_size(etype) datasize = elemsize * nelems field_offset = index * elemsize file_offset = self._get_offset(absolute=1) buf = self._ptr.elems + field_offset cstring.memcpy(buf, elems.data, datasize) if SWAP_BYTES: elems = elems.byteswap() with nogil: stdio.fseek(istream, file_offset + field_offset, stdio.SEEK_SET) ret = stdio.fwrite(elems.data, elemsize, nelems, product._ptr.istream) if ret != nelems: raise IOError( 'write error: %d of %d bytes written' % (ret, datasize)) def set_elem(self, elem, uint index=0): '''set_elem(self, elem, index=0) Set Field array element This function is for setting an array of field element of the field. :param elem: value of the element to set :param index: the zero-based index of element to be set, must not be negative. Default: 0. ''' self.check_closed_product() self._check_write_mode() if self._parent.index is None: raise NotImplementedError( 'setting elements is not implemented on MPH/SPH records') elem = np.asarray(elem) if elem.size != 1: raise ValueError( 'invalid shape "%s", scalar value expected' % elem.shape) self._set_elems(elem, index) def set_elems(self, elems): '''set_elems(self, elems) Set Field array elements This function is for setting an array of field elements of the field. :param elems: np.ndarray of elements to set ''' cdef uint nelems self.check_closed_product() self._check_write_mode() if self._parent._index is None: raise NotImplementedError( 'setting elements is not implemented on MPH/SPH records') elems = np.ascontiguousarray(elems) nelems = epr_get_field_num_elems(self._ptr) if elems.ndim > 1 or elems.size != nelems: raise ValueError('invalid shape "%s", "(%s,)" value expected' % ( elems.shape, nelems)) self._set_elems(elems) property tot_size: '''The total size in bytes of all data elements of a field. *tot_size* is a derived variable, it is computed at runtime and not stored in the DSD-DB. ''' def __get__(self): cdef EPR_FieldInfo* info = self._ptr.info return info.tot_size # --- high level interface ------------------------------------------------ def __repr__(self): return 'epr.Field("%s") %d %s elements' % (self.get_name(), self.get_num_elems(), data_type_id_to_str(self.get_type())) def __str__(self): cdef object name = self.get_name() cdef EPR_DataTypeId type_ = self.get_type() cdef int num_elems = 0 if type_ == e_tid_string: return '%s = "%s"' % (name, self.get_elem().decode('utf-8')) elif type_ == e_tid_time: days, seconds, microseconds = self.get_elem() return '%s = {d=%d, j=%d, m=%d}' % (name, days, seconds, microseconds) else: num_elems = epr_get_field_num_elems(self._ptr) if type_ == e_tid_uchar: fmt = '%u' elif type_ == e_tid_char: fmt = '%d' elif type_ == e_tid_ushort: fmt = '%u' elif type_ == e_tid_short: fmt = '%d' elif type_ == e_tid_uint: fmt = '%u' elif type_ == e_tid_int: fmt = '%d' elif type_ == e_tid_float: fmt = '%f' elif type_ == e_tid_double: fmt = '%f' else: if num_elems > 1: data = ['<>'] * num_elems data = ', '.join(data) return '%s = {%s}' % (name, data) else: return '%s = <>' % name if num_elems > 1: data = ', '.join([fmt % item for item in self.get_elems()]) return '%s = {%s}' % (name, data) else: return '%s = %s' % (name, fmt % self.get_elem()) def __richcmp__(self, other, int op): cdef int ret cdef size_t n cdef EPR_SField* p1 = (self)._ptr cdef EPR_SField* p2 = (other)._ptr if isinstance(self, Field) and isinstance(other, Field): if op == 2: # eq if p1 == p2: return True (self).check_closed_product() if ((epr_get_field_num_elems(p1) != epr_get_field_num_elems(p2)) or (epr_get_field_type(p1) != epr_get_field_type(p2)) or (cstring.strcmp(epr_get_field_unit(p1), epr_get_field_unit(p2)) != 0) or (cstring.strcmp(epr_get_field_description(p1), epr_get_field_description(p2)) != 0) or (cstring.strcmp(epr_get_field_name(p1), epr_get_field_name(p2)) != 0)): return False n = epr_get_data_type_size(epr_get_field_type(p1)) if n != 0: n *= epr_get_field_num_elems(p1) #pyepr_check_errors() if n <= 0: # @TODO: check return True return (cstring.memcmp(p1.elems, p2.elems, n) == 0) elif op == 3: # ne if p1 == p2: return False (self).check_closed_product() if ((epr_get_field_num_elems(p1) != epr_get_field_num_elems(p2)) or (epr_get_field_type(p1) != epr_get_field_type(p2)) or (cstring.strcmp(epr_get_field_unit(p1), epr_get_field_unit(p2)) != 0) or (cstring.strcmp(epr_get_field_description(p1), epr_get_field_description(p2)) != 0) or (cstring.strcmp(epr_get_field_name(p1), epr_get_field_name(p2)) != 0)): return True n = epr_get_data_type_size(epr_get_field_type(p1)) if n != 0: n *= epr_get_field_num_elems(p1) #pyepr_check_errors() if n <= 0: # @TODO: check return False return (cstring.memcmp(p1.elems, p2.elems, n) != 0) else: raise TypeError( 'Field only implements "==" and "!=" operators') else: return NotImplemented def __len__(self): self.check_closed_product() if epr_get_field_type(self._ptr) == e_tid_string: return cstring.strlen(epr_get_field_elem_as_str(self._ptr)) else: return epr_get_field_num_elems(self._ptr) # --- low level interface ------------------------------------------------- property _magic: '''The magic number for internal C structure''' def __get__(self): self.check_closed_product() return self._ptr.magic def get_offset(self): '''Field offset in bytes within the Record''' self.check_closed_product() return self._get_offset() cdef new_field(EPR_SField* ptr, Record parent=None): if ptr is NULL: pyepr_null_ptr_error() cdef Field instance = Field.__new__(Field) instance._ptr = ptr instance._parent = parent return instance cdef class Record(EprObject): '''Represents a record read from an ENVISAT dataset A record is composed of multiple fields. .. seealso:: :class:`Field` ''' cdef EPR_SRecord* _ptr cdef object _parent # Dataset or Product cdef bint _dealloc cdef int _index def __dealloc__(self): if not self._dealloc: return if self._ptr is not NULL: epr_free_record(self._ptr) pyepr_check_errors() cdef inline check_closed_product(self): if isinstance(self._parent, Dataset): (self._parent).check_closed_product() else: #elif isinstance(self._parent, Product): (self._parent).check_closed_product() cdef inline _check_write_mode(self): if isinstance(self._parent, Dataset): (self._parent)._check_write_mode() else: #elif isinstance(self._parent, Product): (self._parent)._check_write_mode() cdef inline uint _get_offset(self, bint absolure=0): cdef EPR_RecordInfo* info = self._ptr.info cdef uint offset = self._index * info.tot_size # assert self._index is not None if absolure: offset += (self._parent)._get_offset() return offset def get_num_fields(self): '''get_num_fields(self) Gets the number of fields contained in the record ''' return epr_get_num_fields(self._ptr) def print_(self, ostream=None): '''print_(self, ostream=None) Write the record to specified file This method writes formatted contents of the record to specified *ostream* text file or (default) the ASCII output is be printed to standard output (:data:`sys.stdout`) :param ostream: the (opened) output file object .. note:: the *ostream* parameter have to be a *real* file not a generic stream object like :class:`StringIO.StringIO` instances ''' cdef FILE* fstream = pyepr_get_file_stream(ostream) self.check_closed_product() with nogil: epr_print_record(self._ptr, fstream) stdio.fflush(fstream) pyepr_check_errors() def print_element(self, uint field_index, uint element_index, ostream=None): '''print_element(self, field_index, element_index, ostream=None) Write the specified field element to file This method writes formatted contents of the specified field element to the *ostream* text file or (default) the ASCII output will be printed to standard output (:data:`sys.stdout`) :param field_index: the index of field in the record :param element_index: the index of element in the specified field :param ostream: the (opened) output file object .. note:: the *ostream* parameter have to be a *real* file not a generic stream object like :class:`StringIO.StringIO` instances ''' cdef FILE* fstream = pyepr_get_file_stream(ostream) self.check_closed_product() with nogil: epr_print_element(self._ptr, field_index, element_index, fstream) stdio.fflush(fstream) pyepr_check_errors() def get_field(self, name): '''get_field(self, name) Gets a field specified by name The field is here identified through the given name. It contains the field info and all corresponding values. :param name: the the name of required field :returns: the specified :class:`Field` or raises an exception (:exc:`EPRValueError`) if an error occurred ''' cdef EPR_SField* field_ptr cdef bytes cname = _to_bytes(name) self.check_closed_product() field_ptr = epr_get_field(self._ptr, cname) if field_ptr is NULL: pyepr_null_ptr_error('unable to get field "%s"' % name) return new_field(field_ptr, self) def get_field_at(self, uint index): '''get_field_at(self, index) Gets a field at the specified position within the record :param index: the zero-based index (position within record) of the field :returns: the field or raises and exception (:exc:`EPRValueError`) if an error occurred ''' cdef EPR_SField* field_ptr self.check_closed_product() field_ptr = epr_get_field_at(self._ptr, index) if field_ptr is NULL: pyepr_null_ptr_error('unable to get field at index %d' % index) return new_field(field_ptr, self) property dataset_name: '''The name of the dataset to which this record belongs to''' def __get__(self): self.check_closed_product() cdef EPR_RecordInfo* info = self._ptr.info if info.dataset_name == NULL: return '' else: return _to_str(info.dataset_name) property tot_size: '''The total size in bytes of the record It includes all data elements of all fields of a record in a product file. *tot_size* is a derived variable, it is computed at runtime and not stored in the DSD-DB. ''' def __get__(self): self.check_closed_product() cdef EPR_RecordInfo* info = self._ptr.info return info.tot_size property index: '''Index of the record within the dataset It is *None* for empty records (created with :meth:`Dataset.create_record` but still not read) and for *MPH* (see :meth:`epr.Product.get_mph`) and *SPH* (see :meth:`epr.Product.get_sph`) records. .. seealso:: :meth:`epr.Dataset.read_record` ''' def __get__(self): return self._index if self._index >= 0 else None # --- high level interface ------------------------------------------------ def get_field_names(self): '''get_field_names(self) Return the list of names of the fields in the product .. note:: this method has no correspondent in the C API ''' cdef EPR_SField* field_ptr cdef int idx cdef char* name cdef int num_fields self.check_closed_product() num_fields = epr_get_num_fields(self._ptr) names = [] for idx in range(num_fields): field_ptr = epr_get_field_at(self._ptr, idx) name = epr_get_field_name(field_ptr) names.append(_to_str(name, 'ascii')) return names def fields(self): '''fields(self) Return the list of fields contained in the record ''' return list(self) def __iter__(self): cdef int idx cdef int num_fields self.check_closed_product() num_fields = epr_get_num_fields(self._ptr) return (self.get_field_at(idx) for idx in range(num_fields)) def __str__(self): self.check_closed_product() return '\n'.join(map(str, self)) def __repr__(self): self.check_closed_product() return '%s %d fields' % (super(Record, self).__repr__(), self.get_num_fields()) # --- low level interface ------------------------------------------------- property _magic: '''The magic number for internal C structure''' def __get__(self): self.check_closed_product() return self._ptr.magic def get_offset(self): '''Record offset in bytes within the Dataset''' if self._index >= 0: self.check_closed_product() return self._get_offset() else: return None cdef new_record(EPR_SRecord* ptr, object parent=None, bint dealloc=False): if ptr is NULL: pyepr_null_ptr_error() cdef Record instance = Record.__new__(Record) instance._ptr = ptr instance._parent = parent # Dataset or Product instance._dealloc = dealloc instance._index = -1 return instance cdef class Raster(EprObject): '''Represents a raster in which data will be stored All 'size' parameter are in PIXEL. ''' cdef EPR_SRaster* _ptr cdef Band _parent cdef object _data def __dealloc__(self): if self._ptr is not NULL: epr_free_raster(self._ptr) property data_type: '''The data type of the band's pixels All ``E_TID_*`` types are possible ''' def __get__(self): return self._ptr.data_type property source_width: '''The width of the source''' def __get__(self): return self._ptr.source_width property source_height: '''The height of the source''' def __get__(self): return self._ptr.source_height property source_step_x: '''The sub-sampling for the across-track direction in pixel''' def __get__(self): return self._ptr.source_step_x property source_step_y: '''The sub-sampling for the along-track direction in pixel''' def __get__(self): return self._ptr.source_step_y def get_width(self): '''get_width(self) Gets the raster's width in pixels ''' return epr_get_raster_width(self._ptr) def get_height(self): '''get_height(self) Gets the raster's height in pixels ''' return epr_get_raster_height(self._ptr) def get_elem_size(self): '''get_elem_size(self) The size in byte of a single element (sample) of this raster's buffer ''' return epr_get_raster_elem_size(self._ptr) def get_pixel(self, int x, int y): '''get_pixel(x, y) Single pixel access This function is for getting the values of the elements of a raster (i.e. pixel) :param x: the (zero-based) X coordinate of the pixel :param y: the (zero-based) Y coordinate of the pixel :returns: the typed value at the given co-ordinate ''' if (x < 0 or x >= self._ptr.raster_width or y < 0 or y >= self._ptr.raster_height): raise ValueError('index out of range: x=%d, y=%d' % (x, y)) cdef EPR_EDataTypeId dtype = self._ptr.data_type if dtype == e_tid_uint: val = epr_get_pixel_as_uint(self._ptr, x, y) elif dtype == e_tid_int: val = epr_get_pixel_as_int(self._ptr, x, y) elif dtype == e_tid_float: val = epr_get_pixel_as_float(self._ptr, x, y) elif dtype == e_tid_double: val = epr_get_pixel_as_double(self._ptr, x, y) else: raise ValueError('invalid data type: "%s"' % epr_data_type_id_to_str(dtype)) pyepr_check_errors() # @TODO: check return val # --- high level interface ------------------------------------------------ cdef np.ndarray toarray(self): cdef np.NPY_TYPES dtype = _epr_to_numpy_type_id(self._ptr.data_type) cdef np.npy_intp shape[2] cdef np.ndarray result if dtype == np.NPY_NOTYPE: raise TypeError('invalid data type') else: shape[0] = self._ptr.raster_height shape[1] = self._ptr.raster_width result = np.PyArray_SimpleNewFromData(2, shape, dtype, self._ptr.buffer) # Make the ndarray keep a reference to this object np.set_array_base(result, self) return result property data: '''Raster data exposed as :class:`numpy.ndarray` object .. note:: this property shares the data buffer with the :class:`Raster` object so any change in its contents is also reflected to the :class:`Raster` object ''' def __get__(self): cdef object data if self._data is not None: data = self._data() if data is not None: return data if self._ptr.buffer is NULL: return np.ndarray(()) data = self.toarray() self._data = PyWeakref_NewRef(data, None) return data def __repr__(self): return '%s %s (%dL x %dP)' % (super(Raster, self).__repr__(), data_type_id_to_str(self.data_type), self.get_height(), self.get_width()) # --- low level interface ------------------------------------------------- property _magic: '''The magic number for internal C structure''' def __get__(self): return self._ptr.magic cdef new_raster(EPR_SRaster* ptr, Band parent=None): if ptr is NULL: pyepr_null_ptr_error() cdef Raster instance = Raster.__new__(Raster) instance._ptr = ptr instance._parent = parent # Band or None instance._data = None return instance def create_raster(EPR_EDataTypeId data_type, uint src_width, uint src_height, uint xstep=1, uint ystep=1): '''create_raster(data_type, src_width, src_height, xstep=1, ystep=1) Creates a raster of the specified data type This function can be used to create any type of raster, e.g. for later use as a bit-mask. :param data_type: the type of the data to stored in the raster, must be one of E_TID_* :param src_width: the width (across track dimension) of the source to be read into the raster. See description of :meth:`Band.create_compatible_raster` :param src_height: the height (along track dimension) of the source to be read into the raster. See description of :meth:`Band.create_compatible_raster` :param xstep: the sub-sampling step across track of the source when reading into the raster :param ystep: the sub-sampling step along track of the source when reading into the raster :returns: the new :class:`Raster` instance .. seealso:: description of :meth:`Band.create_compatible_raster` ''' if xstep == 0 or ystep == 0: raise ValueError('invalid step: xspet=%d, ystep=%d' % (xstep, ystep)) cdef EPR_SRaster* raster_ptr raster_ptr = epr_create_raster(data_type, src_width, src_height, xstep, ystep) if raster_ptr is NULL: pyepr_null_ptr_error('unable to create a new raster') return new_raster(raster_ptr) def create_bitmask_raster(uint src_width, uint src_height, uint xstep=1, uint ystep=1): '''create_bitmask_raster(src_width, src_height, xstep=1, ystep=1) Creates a raster to be used for reading bitmasks The raster returned always is of type ``byte``. :param src_width: the width (across track dimension) of the source to be read into the raster :param src_height: the height (along track dimension) of the source to be read into the raster :param xstep: the sub-sampling step across track of the source when reading into the raster :param ystep: the sub-sampling step along track of the source when reading into the raster :returns: the new raster instance or raises an exception (:exc:`EPRValueError`) if an error occurred .. seealso:: the description of :meth:`Band.create_compatible_raster` ''' if xstep == 0 or ystep == 0: raise ValueError('invalid step: xspet=%d, ystep=%d' % (xstep, ystep)) cdef EPR_SRaster* raster_ptr raster_ptr = epr_create_bitmask_raster(src_width, src_height, xstep, ystep) if raster_ptr is NULL: pyepr_null_ptr_error('unable to create a new raster') return new_raster(raster_ptr) cdef class Band(EprObject): '''The band of an ENVISAT product The Band class contains information about a band within an ENVISAT product file which has been opened with the :func:`open` function. A new Band instance can be obtained with the :meth:`Product.get_band` method. ''' cdef EPR_SBandId* _ptr cdef Product _parent cdef inline check_closed_product(self): self._parent.check_closed_product() property product: '''The :class:`Product` instance to which this band belongs to''' def __get__(self): return self._parent property spectr_band_index: '''The (zero-based) spectral band index -1 if this is not a spectral band ''' def __get__(self): self.check_closed_product() return self._ptr.spectr_band_index property sample_model: '''The sample model operation The sample model operation applied to the source dataset for getting the correct samples from the MDS (for example MERIS L2). Possible values are: * ``*`` --> no operation (direct copy) * ``1OF2`` --> first byte of 2-byte interleaved MDS * ``2OF2`` --> second byte of 2-byte interleaved MDS * ``0123`` --> combine 3-bytes interleaved to 4-byte integer ''' def __get__(self): return self._ptr.sample_model property data_type: '''The data type of the band's pixels Possible values are: * ``*`` --> the datatype remains unchanged. * ``uint8_t`` --> 8-bit unsigned integer * ``uint32_t`` --> 32-bit unsigned integer * ``Float`` --> 32-bit IEEE floating point ''' def __get__(self): return self._ptr.data_type property scaling_method: '''Scaling method The scaling method which must be applied to the raw source data in order to get the 'real' pixel values in geo-physical units. Possible values are: * ``*`` --> no scaling applied * ``Linear_Scale`` --> linear scaling applied:: y = offset + scale * x * ``Log_Scale`` --> logarithmic scaling applied:: y = log10(offset + scale * x) ''' def __get__(self): return self._ptr.scaling_method property scaling_offset: '''The scaling offset Possible values are: * ``*`` --> no offset provided (implies scaling_method=*) * ``const`` --> a floating point constant * ``GADS.field[.field2]` --> value is provided in global annotation dataset with name ``GADS`` in field ``field``. Optionally a second element index for multiple-element fields can be given too ''' def __get__(self): return self._ptr.scaling_offset property scaling_factor: '''The scaling factor Possible values are: * ``*`` --> no factor provided (implies scaling_method=*) * ``const`` --> a floating point constant * ``GADS.field[.field2]`` --> value is provided in global annotation dataset with name `GADS` in field `field``. Optionally a second element index for multiple-element fields can be given too ''' def __get__(self): return self._ptr.scaling_factor property bm_expr: '''A bit-mask expression used to filter valid pixels All others are set to zero ''' def __get__(self): if self._ptr.bm_expr is NULL: return None else: return _to_str(self._ptr.bm_expr, 'ascii') property unit: '''The geophysical unit for the band's pixel values''' def __get__(self): if self._ptr.unit is NULL: return None else: return _to_str(self._ptr.unit, 'ascii') property description: '''A short description of the band's contents''' def __get__(self): if self._ptr.description is NULL: return None else: return _to_str(self._ptr.description, 'ascii') property lines_mirrored: '''Mirrored lines flag If true (=1) lines will be mirrored (flipped) after read into a raster in order to ensure a pixel ordering in raster X direction from WEST to EAST. ''' def __get__(self): return self._ptr.lines_mirrored property dataset: '''The source dataset The source dataset containing the raw data used to create the band's pixel values. ''' def __get__(self): cdef EPR_SDatasetId* dataset_id = self._ptr.dataset_ref.dataset_id cdef const char* name = epr_get_dataset_name(dataset_id) return self.product.get_dataset(name) def get_name(self): '''get_name(self) Gets the name of the band ''' cdef char* name = NULL self.check_closed_product() name = epr_get_band_name(self._ptr) return _to_str(name, 'ascii') def create_compatible_raster(self, uint src_width=0, uint src_height=0, uint xstep=1, uint ystep=1): '''create_compatible_raster(self, src_width, src_height, xstep=1, ystep=1) Creates a raster which is compatible with the data type of the band The created raster is used to read the data in it (see :meth:`Band.read_raster`). The raster is defined on the grid of the product, from which the data are read. Spatial subsets and under-sampling are possible) through the parameter of the method. A raster is an object that allows direct access to data of a certain portion of the ENVISAT product that are read into the it. Such a portion is called the source. The complete ENVISAT product can be much greater than the source. One can move the raster over the complete ENVISAT product and read in turn different parts (always of the size of the source) of it into the raster. The source is specified by the parameters *height* and *width*. A typical example is a processing in blocks. Lets say, a block has 64x32 pixel. Then, my source has a width of 64 pixel and a height of 32 pixel. Another example is a processing of complete image lines. Then, my source has a widths of the complete product (for example 1121 for a MERIS RR product), and a height of 1). One can loop over all blocks read into the raster and process it. In addition, it is possible to defined a sub-sampling step for a raster. This means, that the source is not read 1:1 into the raster, but that only every 2nd or 3rd pixel is read. This step can be set differently for the across track (source_step_x) and along track (source_step_y) directions. :param src_width: the width (across track dimension) of the source to be read into the raster (default: scene_width) :param src_height: the height (along track dimension) of the source to be read into the raster (default: scene_height) :param xstep: the sub-sampling step across track of the source when reading into the raster (default=1) :param ystep: the sub-sampling step along track of the source when reading into the raster (default=1) :returns: the new raster instance or raises an exception (:exc:`EPRValueError`) if an error occurred .. note:: src_width and src_height are the dimantion of the of the source area. If one specifies a *step* parameter the resulting raster will have a size that is smaller that the specifies source size:: raster_size = src_size // step ''' cdef EPR_SRaster* raster_ptr = NULL cdef int scene_width cdef int scene_height self.check_closed_product() scene_width = epr_get_scene_width(self._parent._ptr) scene_height = epr_get_scene_height(self._parent._ptr) if src_width == 0: src_width = scene_width elif src_width > scene_width: raise ValueError( 'requeted raster width (%d) is too large for the Band ' '(scene_width=%d)' % (src_width, scene_width)) if src_height == 0: src_height = scene_height elif src_height > scene_height: raise ValueError( 'requeted raster height (%d) is too large for the Band ' '(scene_height=%d)' % (src_height, scene_height)) if xstep > src_width: raise ValueError('xstep (%d) too large for the requested width ' '(%d)' % (xstep, src_width)) if ystep > src_height: raise ValueError('ystep (%d) too large for the requested height ' '(%d)' % (ystep, src_height)) raster_ptr = epr_create_compatible_raster(self._ptr, src_width, src_height, xstep, ystep) if raster_ptr is NULL: pyepr_null_ptr_error('unable to create compatible raster with ' 'width=%d, height=%d xstep=%d, ystep=%d' % (src_width, src_height, xstep, ystep)) return new_raster(raster_ptr, self) cpdef read_raster(self, int xoffset=0, int yoffset=0, Raster raster=None): '''read_raster(self, xoffset=0, yoffset=0, Raster raster=None) Reads (geo-)physical values of the band of the specified source-region The source-region is a defined part of the whole ENVISAT product image, which shall be read into a raster. In this routine the co-ordinates are specified, where the source-region to be read starts. The dimension of the region and the sub-sampling are attributes of the raster into which the data are read. :param xoffset: across-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param yoffset: along-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param raster: :class:`Raster` instance set with appropriate parameters to read into. If not provided a new :class:`Raster` is instantiated :returns: the :class:`Raster` instance in which data are read This method raises an instance of the appropriate :exc:`EPRError` sub-class if case of errors .. seealso:: :meth:`Band.create_compatible_raster` and :func:`create_raster` ''' cdef int ret cdef int scene_width cdef int scene_height self.check_closed_product() if raster is None: raster = self.create_compatible_raster() scene_width = epr_get_scene_width(self._parent._ptr) scene_height = epr_get_scene_height(self._parent._ptr) if (xoffset + raster._ptr.source_width > scene_width or yoffset + raster._ptr.source_height > scene_height): raise ValueError( 'at lease part of the requested area is outside the scene') raster._data = None with nogil: ret = epr_read_band_raster(self._ptr, xoffset, yoffset, raster._ptr) if ret != 0: pyepr_check_errors() return raster # --- high level interface ------------------------------------------------ def read_as_array(self, width=None, height=None, uint xoffset=0, uint yoffset=0, uint xstep=1, uint ystep=1): '''read_as_array(self, width=None, height=None, xoffset=0, yoffset=0, xstep=1, ystep=1): Reads the specified source region as an :class:`numpy.ndarray` The source-region is a defined part of the whole ENVISAT product image, which shall be read into a raster. In this routine the co-ordinates are specified, where the source-region to be read starts. The dimension of the region and the sub-sampling are attributes of the raster into which the data are read. :param src_width: the width (across track dimension) of the source to be read into the raster. If not provided reads as much as possible :param src_height: the height (along track dimension) of the source to be read into the raster. If not provided reads as much as possible :param xoffset: across-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param yoffset: along-track source co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param xstep: the sub-sampling step across track of the source when reading into the raster :param ystep: the sub-sampling step along track of the source when reading into the raster :returns: the :class:`numpy.ndarray` instance in which data are read This method raises an instance of the appropriate :exc:`EPRError` sub-class if case of errors .. seealso:: :meth:`Band.create_compatible_raster`, :func:`create_raster` and :meth:`Band.read_raster` ''' cdef int w cdef int h cdef EPR_ProductId* product_id self.check_closed_product() product_id = self._parent._ptr if width is None: w = epr_get_scene_width(product_id) if w > xoffset: width = w - xoffset else: raise ValueError('xoffset os larger that he scene width') if height is None: h = epr_get_scene_height(product_id) if h > yoffset: height = h - yoffset else: raise ValueError('yoffset os larger that he scene height') raster = self.create_compatible_raster(width, height, xstep, ystep) self.read_raster(xoffset, yoffset, raster) return raster.data def __repr__(self): return 'epr.Band(%s) of epr.Product(%s)' % (self.get_name(), self.product.id_string) # --- low level interface ------------------------------------------------- property _magic: '''The magic number for internal C structure''' def __get__(self): self.check_closed_product() return self._ptr.magic property _field_index: '''Index or the field (within the dataset) containing the raw data used to create the band's pixel values. It is set to -1 if not used ''' def __get__(self): return self._ptr.dataset_ref.field_index property _elem_index: '''Index or the element (within the dataset field) containing the raw data used to create the band's pixel values. It is set to -1 if not used ''' def __get__(self): return self._ptr.dataset_ref.elem_index cdef new_band(EPR_SBandId* ptr, Product parent=None): if ptr is NULL: pyepr_null_ptr_error() cdef Band instance = Band.__new__(Band) instance._ptr = ptr instance._parent = parent return instance cdef class Dataset(EprObject): '''ENVISAT dataset The Dataset class contains information about a dataset within an ENVISAT product file which has been opened with the :func:`open` function. A new Dataset instance can be obtained with the :meth:`Product.get_dataset` or :meth:`Product.get_dataset_at` methods. ''' cdef EPR_SDatasetId* _ptr cdef Product _parent cdef inline check_closed_product(self): self._parent.check_closed_product() cdef inline _check_write_mode(self): self._parent._check_write_mode() cdef inline uint _get_offset(self): cdef const EPR_SDSD* dsd = epr_get_dsd(self._ptr) return dsd.ds_offset property product: '''The :class:`Product` instance to which this dataset belongs to''' def __get__(self): return self._parent property description: '''A short description of the band's contents''' def __get__(self): if self._ptr is not NULL: self.check_closed_product() if self._ptr.description is not NULL: return _to_str(self._ptr.description, 'ascii') return '' def get_name(self): '''get_name(self) Gets the name of the dataset ''' cdef char* name if self._ptr is not NULL: self.check_closed_product() name = epr_get_dataset_name(self._ptr) return _to_str(name, 'ascii') return '' def get_dsd_name(self): '''get_dsd_name(self) Gets the name of the DSD (dataset descriptor) ''' cdef char* name if self._ptr is not NULL: self.check_closed_product() name = epr_get_dsd_name(self._ptr) return _to_str(name, 'ascii') return '' def get_num_records(self): '''get_num_records(self) Gets the number of records of the dataset ''' if self._ptr is not NULL: self.check_closed_product() return epr_get_num_records(self._ptr) return 0 def get_dsd(self): '''get_dsd(self) Gets the dataset descriptor (DSD) ''' self.check_closed_product() return new_dsd(epr_get_dsd(self._ptr), self) def create_record(self): '''create_record(self) Creates a new record Creates a new, empty record with a structure compatible with the dataset. Such a record is typically used in subsequent calls to :meth:`Dataset.read_record`. :returns: the new record instance ''' self.check_closed_product() return new_record(epr_create_record(self._ptr), self, True) def read_record(self, uint index=0, Record record=None): '''read_record(self, index, record=None) Reads specified record of the dataset The record is identified through the given zero-based record index. In order to reduce memory reallocation, a record (pre-)created by the method :meth:`Dataset.create_record` can be passed to this method. Data is then read into this given record. If no record (``None``) is given, the method initiates a new one. In both cases, the record in which the data is read into will be returned. :param index: the zero-based record index (default: 0) :param record: a pre-created record to reduce memory reallocation, can be ``None`` (default) to let the function allocate a new record :returns: the record in which the data has been read into or raises an exception (:exc:`EPRValueError`) if an error occurred .. versionchanged:: 0.9 The *index* parameter now defaults to zero ''' cdef EPR_SRecord* record_ptr = NULL self.check_closed_product() if record: record_ptr = (record)._ptr with nogil: record_ptr = epr_read_record(self._ptr, index, record_ptr) if record_ptr is NULL: pyepr_null_ptr_error('unable to read record at index %d' % index) if not record: record = new_record(record_ptr, self, True) record._index = index return record # --- high level interface ------------------------------------------------ def records(self): '''records(self) Return the list of records contained in the dataset ''' return list(self) def __iter__(self): cdef int idx self.check_closed_product() return (self.read_record(idx) for idx in range(epr_get_num_records(self._ptr))) def __str__(self): lines = [repr(self), ''] lines.extend(map(str, self)) return '\n'.join(lines) def __repr__(self): return 'epr.Dataset(%s) %d records' % (self.get_name(), self.get_num_records()) # --- low level interface ------------------------------------------------- property _magic: '''The magic number for internal C structure''' def __get__(self): self.check_closed_product() return self._ptr.magic cdef new_dataset(EPR_SDatasetId* ptr, Product parent=None): if ptr is NULL: pyepr_null_ptr_error() cdef Dataset instance = Dataset.__new__(Dataset) instance._ptr = ptr instance._parent = parent return instance cdef class Product(EprObject): '''ENVISAT product The Product class provides methods and properties to get information about an ENVISAT product file. .. seealso:: :func:`open` ''' cdef EPR_SProductId* _ptr cdef str _mode def __cinit__(self, filename, str mode='rb'): cdef bytes bfilename = _to_bytes(filename, _DEFAULT_FS_ENCODING) cdef char* cfilename = bfilename cdef bytes bmode cdef char* cmode cdef int ret if mode not in ('rb', 'rb+', 'r+b'): raise ValueError('invalid open mode: "%s"' % mode) self._mode = mode with nogil: self._ptr = epr_open_product(cfilename) if '+' in mode: # reopen in 'rb+ mode bmode = _to_bytes(mode) cmode = bmode with nogil: self._ptr.istream = stdio.freopen(cfilename, cmode, self._ptr.istream) if self._ptr.istream is NULL: errno.errno = 0 raise ValueError( 'unable to open file "%s" in "%s" mode' % (filename, mode)) if self._ptr is NULL: # try to get error info from the lib pyepr_check_errors() raise ValueError('unable to open "%s"' % filename) def __dealloc__(self): if self._ptr is not NULL: if '+' in self._mode: stdio.fflush(self._ptr.istream) epr_close_product(self._ptr) pyepr_check_errors() self._ptr = NULL cdef inline check_closed_product(self): if self._ptr is NULL: raise ValueError('I/O operation on closed file') cdef inline _check_write_mode(self): if '+' not in self._mode: raise TypeError('write operation on read-only file') def __init__(self, filename, mode='rb'): # @NOTE: this method suppresses the default behavior of EprObject # that is raising an exception when it is instantiated by # the user. pass def close(self): '''close(self) Closes the ENVISAT :class:`epr.Product` product Closes the :class:`epr.Product` product and free the underlying file descriptor. This method has no effect if the :class:`Product` is already closed. Once the :class:`Product` is closed, any operation on it will raise a ValueError. As a convenience, it is allowed to call this method more than once; only the first call, however, will have an effect. ''' if self._ptr is not NULL: #if '+' in self.mode: # stdio.fflush(self._ptr.istream) epr_close_product(self._ptr) pyepr_check_errors() self._ptr = NULL def flush(self): '''Flush the file stream''' cdef int ret if '+' in self.mode: ret = stdio.fflush(self._ptr.istream) if ret != 0: errno.errno = 0 raise IOError('flush error') property file_path: '''The file's path including the file name''' def __get__(self): self.check_closed_product() if self._ptr.file_path is NULL: return None else: return _to_str(self._ptr.file_path, 'ascii') property _fileno: '''The fileno of the :class:`epr.Product` input stream To be used with care. ''' def __get__(self): if self._ptr.istream is NULL: return None else: return fileno(self._ptr.istream) property mode: def __get__(self): '''String that specifies the mode in which the file is opened Possible values: 'rb' for read-only mode, 'rb+' for read-write mode. ''' return self._mode property tot_size: '''The total size in bytes of the product file''' def __get__(self): self.check_closed_product() return self._ptr.tot_size property id_string: '''The product identifier string obtained from the MPH parameter 'PRODUCT' The first 10 characters of this string identify the product type, e.g. "MER_1P__FR" for a MERIS Level 1b full resolution product. The rest of the string decodes product instance properties. ''' def __get__(self): self.check_closed_product() if self._ptr.id_string is NULL: return None else: return _to_str(self._ptr.id_string, 'ascii') property meris_iodd_version: '''For MERIS L1b and RR and FR to provide backward compatibility''' def __get__(self): self.check_closed_product() return self._ptr.meris_iodd_version def get_scene_width(self): '''get_scene_width(self) Gets the product's scene width in pixels ''' self.check_closed_product() return epr_get_scene_width(self._ptr) def get_scene_height(self): '''get_scene_height(self) Gets the product's scene height in pixels ''' self.check_closed_product() return epr_get_scene_height(self._ptr) def get_num_datasets(self): '''get_num_datasets(self) Gets the number of all datasets contained in a product ''' self.check_closed_product() return epr_get_num_datasets(self._ptr) def get_num_dsds(self): '''get_num_dsds(self) Gets the number of all :class:`DSD`\ s Gets the number of all :class:`DSD`\ s (dataset descriptors) contained in the product ''' self.check_closed_product() return epr_get_num_dsds(self._ptr) def get_num_bands(self): '''get_num_bands(self) Gets the number of all bands contained in a product ''' self.check_closed_product() return epr_get_num_bands(self._ptr) def get_dataset_at(self, uint index): '''get_dataset_at(self, index) Gets the dataset at the specified position within the product :param index: the index identifying the position of the dataset, starting with 0, must not be negative :returns: the requested :class:`Dataset` ''' cdef EPR_SDatasetId* dataset_id dataset_id = epr_get_dataset_id_at(self._ptr, index) if dataset_id is NULL: pyepr_null_ptr_error('unable to get dataset at index %d' % index) return new_dataset(dataset_id, self) def get_dataset(self, name): '''get_dataset(self, name) Gets the dataset corresponding to the specified dataset name :param name: the dataset name :returns: the requested :class:`Dataset` instance ''' cdef EPR_SDatasetId* dataset_id cdef bytes cname = _to_bytes(name) dataset_id = epr_get_dataset_id(self._ptr, cname) if dataset_id is NULL: pyepr_null_ptr_error(r'unable to get dataset "%s"' % name) return new_dataset(dataset_id, self) def get_dsd_at(self, uint index): '''get_dsd_at(self, index) Gets the :class:`DSD` at the specified position Gets the :class:`DSD` (dataset descriptor) at the specified position within the product. :param index: the index identifying the position of the :class:`DSD`, starting with 0, must not be negative :returns: the requested :class:`DSD` instance ''' cdef EPR_SDSD* dsd_ptr self.check_closed_product() dsd_ptr = epr_get_dsd_at(self._ptr, index) if dsd_ptr is NULL: pyepr_null_ptr_error('unable to get DSD at index "%d"' % index) return new_dsd(dsd_ptr, self) def get_mph(self): '''get_mph(self) The main product header (MPH) :class:`Record` ''' cdef EPR_SRecord* record_ptr record_ptr = epr_get_mph(self._ptr) if record_ptr is NULL: pyepr_null_ptr_error('unable to get MPH record') return new_record(record_ptr, self, False) def get_sph(self): '''get_sph(self) The specific product header (SPH) :class:`Record` ''' cdef EPR_SRecord* record_ptr record_ptr = epr_get_sph(self._ptr) if record_ptr is NULL: pyepr_null_ptr_error('unable to get SPH record') return new_record(record_ptr, self, False) def get_band(self, name): '''get_band(self, name) Gets the band corresponding to the specified name. :param name: the name of the band :returns: the requested :class:`Band` instance, or raises a :exc:`EPRValueError` if not found ''' cdef EPR_SBandId* band_id cdef bytes cname = _to_bytes(name) band_id = epr_get_band_id(self._ptr, cname) if band_id is NULL: pyepr_null_ptr_error('unable to get band "%s"' % name) return new_band(band_id, self) def get_band_at(self, uint index): '''get_band_at(self, index) Gets the band at the specified position within the product :param index: the index identifying the position of the band, starting with 0, must not be negative :returns: the requested :class:`Band` instance, or raises a :exc:`EPRValueError` if not found ''' cdef EPR_SBandId* band_id band_id = epr_get_band_id_at(self._ptr, index) if band_id is NULL: pyepr_null_ptr_error('unable to get band at index "%d"' % index) return new_band(band_id, self) def read_bitmask_raster(self, bm_expr, int xoffset, int yoffset, Raster raster not None): '''read_bitmask_raster(self, bm_expr, xoffset, yoffset, raster) Calculates a bit-mask raster Calculates a bit-mask, composed of flags of the given product and combined as described in the given bit-mask expression, for the a certain dimension and sub-sampling as defined in the given raster. :param bm_expr: a string holding the logical expression for the definition of the bit-mask. In a bit-mask expression, any number of the flag-names (found in the DDDB) can be composed with "(", ")", "NOT", "AND", "OR". Valid bit-mask expression are for example ``flags.LAND OR flags.CLOUD`` or ``NOT flags.WATER AND flags.TURBID_S`` :param xoffset: across-track co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param yoffset: along-track co-ordinate in pixel co-ordinates (zero-based) of the upper right corner of the source-region :param raster: the raster for the bit-mask. The data type of the raster must be either e_tid_uchar or e_tid_char :returns: zero for success, an error code otherwise .. seealso:: :func:`epr.create_bitmask_raster` ''' cdef bytes c_bm_expr = _to_bytes(bm_expr) cdef int ret = 0 self.check_closed_product() ret = epr_read_bitmask_raster(self._ptr, c_bm_expr, xoffset, yoffset, (raster)._ptr) if ret != 0: pyepr_check_errors() return raster # --- high level interface ------------------------------------------------ property closed: '''True if the :class:`epr.Product` is closed.''' def __get__(self): return self._ptr is NULL def get_dataset_names(self): '''get_dataset_names(self) Return the list of names of the datasets in the product .. note:: this method has no correspondent in the C API ''' cdef EPR_SDatasetId* dataset_ptr cdef int idx cdef char* name cdef int num_datasets self.check_closed_product() num_datasets = epr_get_num_datasets(self._ptr) names = [] for idx in range(num_datasets): dataset_ptr = epr_get_dataset_id_at(self._ptr, idx) name = epr_get_dataset_name(dataset_ptr) names.append(_to_str(name, 'ascii')) return names def get_band_names(self): '''get_band_names(self) Return the list of names of the bands in the product .. note:: this method has no correspondent in the C API ''' cdef EPR_SBandId* band_ptr cdef int idx cdef char* name cdef int num_bands self.check_closed_product() num_bands = epr_get_num_bands(self._ptr) names = [] for idx in range(num_bands): band_ptr = epr_get_band_id_at(self._ptr, idx) name = epr_get_band_name(band_ptr) names.append(_to_str(name, 'ascii')) return names def datasets(self): '''datasets(self) Return the list of dataset in the product ''' cdef int idx cdef int num_datasets self.check_closed_product() num_datasets = epr_get_num_datasets(self._ptr) return [self.get_dataset_at(idx) for idx in range(num_datasets)] def bands(self): '''bands(self) Return the list of bands in the product ''' cdef int num_bands self.check_closed_product() num_bands = epr_get_num_bands(self._ptr) return [self.get_band_at(idx) for idx in range(num_bands)] # @TODO: iter on both datasets and bands (??) #def __iter__(self): # return itertools.chain((self.datasets(), self.bands())) def __repr__(self): return 'epr.Product(%s) %d datasets, %d bands' % (self.id_string, self.get_num_datasets(), self.get_num_bands()) def __str__(self): lines = [repr(self), ''] lines.extend(map(repr, self.datasets())) lines.append('') lines.extend(map(repr, self.bands())) return '\n'.join(lines) def __enter__(self): return self def __exit__(self, *exc_info): self.close() # --- low level interface ------------------------------------------------- property _magic: '''The magic number for internal C structure''' def __get__(self): self.check_closed_product() return self._ptr.magic def open(filename, mode='rb'): '''open(filename) Opens the ENVISAT product Opens the ENVISAT product file with the given file path, reads MPH, SPH and all DSDs, organized the table with parameter of line length and tie points number. :param product_file_path: the path to the ENVISAT product file :param mode: string that specifies the mode in which the file is opened. Allowed values: 'rb', 'rb+' for read-write mode. Default: mode='rb'. :returns: the :class:`Product` instance representing the specified product. An exception (:exc:`exceptions.ValueError`) is raised if the file could not be opened. .. seealso :class:`Product` ''' return Product(filename, mode) # library initialization/finalization _EPR_C_LIB = _CLib.__new__(_CLib) import atexit @atexit.register def _close_api(): # ensure that all EprObject(s) are collected before removing the last # reference to _EPR_C_LIB import gc gc.collect() global _EPR_C_LIB _EPR_C_LIB = None # clean namespace del atexit, namedtuple pyepr-0.9.3/tests/000077500000000000000000000000001252122757300140325ustar00rootroot00000000000000pyepr-0.9.3/tests/__init__.py000066400000000000000000000000001252122757300161310ustar00rootroot00000000000000pyepr-0.9.3/tests/checksetup.mak000066400000000000000000000135251252122757300166700ustar00rootroot00000000000000#!/usr/bin/make -f ROOT = CHECKSETUP_ROOT VERSION = $(shell grep "__version__ =" ../src/epr.pyx | cut -d \' -f 2) PKGDIST = pyepr-$(VERSION).tar.gz PYTHON = python PYVER = 3.4.2 CONDA = conda CONDAOPTS = --use-index-cache -m -y .PHONY: \ clean distclean cache check \ check-nosetuptools check-setuptoolsoff check-setuptools \ check-pip check-wheel check: \ check-nosetuptools \ check-setuptoolsoff \ check-setuptools \ check-pip \ check-wheel check-nosetuptools: \ check_nosetuptools_nonumpy_nocython \ check_nosetuptools_numpy_nocython_c \ check_nosetuptools_numpy_cython check-setuptoolsoff: \ check_setuptoolsoff_nonumpy_nocython \ check_setuptoolsoff_numpy_nocython_c \ check_setuptoolsoff_numpy_cython check-setuptools: \ check_setuptools_nonumpy_nocython \ #check_setuptools_nonumpy_nocython_c \ check_setuptools_numpy_cython_c check-pip: \ check_pip_nonumpy_nocython \ check_pip_nonumpy_nocython_c \ check_pip_numpy_cython_c check-wheel: check_wheel $(ROOT): mkdir -p $(ROOT) $(ROOT)/cache-done: $(CONDA) create -p $(ROOT)/dummyenv -m -y \ python=$(PYVER) setuptools pip numpy cython $(ROOT)/dummyenv/bin/pip install --download $(ROOT) --use-wheel wheel $(ROOT)/dummyenv/bin/pip install --download $(ROOT) numpy cython $(RM) -r $(ROOT)/dummyenv touch $@ $(ROOT)/$(PKGDIST): $(ROOT) #$(MAKE) -C .. distclean $(MAKE) -C .. sdist cp ../dist/$(PKGDIST) $(ROOT) cache: $(ROOT)/cache-done $(ROOT)/$(PKGDIST) clean: $(RM) -r $(ROOT)/check_* distclean: $(RM) -r $(ROOT) # no setuptools ############################################################### check_nosetuptools_nonumpy_nocython: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) rm $(ROOT)/$@/pyepr-$(VERSION)/src/*.c cd $(ROOT)/$@/pyepr-$(VERSION);\ ../bin/$(PYTHON) setup.py install;\ if [ ! $$? ]; then false; else true; fi @echo "EXPECTED FAILURE" check_nosetuptools_numpy_nocython_c: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) numpy tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) cd $(ROOT)/$@/pyepr-$(VERSION);\ ../bin/$(PYTHON) setup.py install check_nosetuptools_numpy_cython: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) numpy cython tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) rm $(ROOT)/$@/pyepr-$(VERSION)/src/*.c cd $(ROOT)/$@/pyepr-$(VERSION);\ ../bin/$(PYTHON) setup.py install # setuptools off ############################################################## check_setuptoolsoff_nonumpy_nocython: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) setuptools tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) rm $(ROOT)/$@/pyepr-$(VERSION)/src/*.c cd $(ROOT)/$@/pyepr-$(VERSION);\ env USE_SETUPTOOLS=FALSE ../bin/$(PYTHON) setup.py install;\ if [ ! $$? ]; then false; else true; fi @echo "EXPECTED FAILURE" check_setuptoolsoff_numpy_nocython_c: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) setuptools numpy tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) cd $(ROOT)/$@/pyepr-$(VERSION);\ env USE_SETUPTOOLS=FALSE ../bin/$(PYTHON) setup.py install check_setuptoolsoff_numpy_cython: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) setuptools numpy cython tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) rm $(ROOT)/$@/pyepr-$(VERSION)/src/*.c cd $(ROOT)/$@/pyepr-$(VERSION);\ env USE_SETUPTOOLS=FALSE ../bin/$(PYTHON) setup.py install # setuptools ################################################################## check_setuptools_nonumpy_nocython: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) setuptools tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) rm $(ROOT)/$@/pyepr-$(VERSION)/src/*.c cd $(ROOT)/$@/pyepr-$(VERSION);\ ../bin/$(PYTHON) setup.py install;\ if [ ! $$? ]; then false; else true; fi @echo "EXPECTED FAILURE" # @TODO: check #check_setuptools_nonumpy_nocython_c: $(ROOT) cache # $(RM) -r $(ROOT)/$@ # $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) setuptools # tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) # cd $(ROOT)/$@/pyepr-$(VERSION);\ # ../bin/$(PYTHON) setup.py install check_setuptools_numpy_cython_c: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) setuptools numpy cython tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) cd $(ROOT)/$@/pyepr-$(VERSION);\ ../bin/$(PYTHON) setup.py install # pip ######################################################################### check_pip_nonumpy_nocython: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) pip tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) rm $(ROOT)/$@/pyepr-$(VERSION)/src/*.c cd $(ROOT)/$@/pyepr-$(VERSION);\ $(ROOT)/$@/bin/pip install -v --no-index --find-links=file://$(PWD)/$(ROOT) $(ROOT)/$@/pyepr-$(VERSION);\ if [ ! $$? ]; then false; else true; fi @echo "EXPECTED FAILURE" check_pip_nonumpy_nocython_c: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) pip $(ROOT)/$@/bin/pip install -v --no-index --find-links=file://$(PWD)/$(ROOT) $(ROOT)/$(PKGDIST) check_pip_numpy_cython_c: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) pip cython numpy $(ROOT)/$@/bin/pip install -v --no-index --find-links=file://$(PWD)/$(ROOT) $(ROOT)/$(PKGDIST) # wheel ####################################################################### check_wheel: $(ROOT) cache $(RM) -r $(ROOT)/$@ $(CONDA) create $(CONDAOPTS) -p $(ROOT)/$@ python=$(PYVER) pip cython numpy $(ROOT)/$@/bin/pip install -v --no-index --find-links=file://$(PWD)/$(ROOT) wheel tar -C $(ROOT)/$@ -xvzf $(ROOT)/$(PKGDIST) cd $(ROOT)/$@/pyepr-$(VERSION);\ ../bin/$(PYTHON) setup.py bdist_wheel pyepr-0.9.3/tests/test_all.py000077500000000000000000003133111252122757300162200ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2011-2015, Antonio Valentino # # This file is part of PyEPR. # # PyEPR is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyEPR is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyEPR. If not, see . import io import os import re import sys import gzip import shutil import numbers import operator import tempfile import functools import contextlib from distutils.version import LooseVersion try: import resource except ImportError: resource = None try: from unittest import skipIf except ImportError: import unittest2 as unittest else: del skipIf import unittest try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen import numpy as np import numpy.testing as npt import epr EPR_TO_NUMPY_TYPE = { #epr.E_TID_UNKNOWN: np.NPY_NOTYPE, epr.E_TID_UCHAR: np.ubyte, epr.E_TID_CHAR: np.byte, epr.E_TID_USHORT: np.ushort, epr.E_TID_SHORT: np.short, epr.E_TID_UINT: np.uint, epr.E_TID_INT: np.int, epr.E_TID_FLOAT: np.float32, epr.E_TID_DOUBLE: np.double, epr.E_TID_STRING: np.str, #epr.E_TID_SPARE = e_tid_spare, #epr.E_TID_TIME = e_tid_time, } def has_epr_c_bug_pyepr009(): v = LooseVersion(epr.EPR_C_API_VERSION) if 'pyepr' in v.version: return v < LooseVersion('2.3dev_pyepr082') else: return v <= LooseVersion('2.3') EPR_C_BUG_PYEPR009 = has_epr_c_bug_pyepr009() EPR_C_BUG_BCEPR002 = EPR_C_BUG_PYEPR009 TESTDIR = os.path.abspath(os.path.dirname(__file__)) TEST_PRODUCT = 'MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1' def quiet(func): @functools.wraps(func) def wrapper(*args, **kwds): sysout = sys.stdout syserr = sys.stderr try: # using '/dev/null' doesn't work in python 3 because the file # object coannot be converted into a C FILE* #with file(os.devnull) as fd: with tempfile.TemporaryFile('w+') as fd: sys.stdout = fd sys.stderr = fd ret = func(*args, **kwds) finally: sys.stdout = sysout sys.stderr = syserr return ret return wrapper def equal_products(product1, product2): if type(product1) != type(product2): return False for name in ('file_path', 'tot_size', 'id_string', 'meris_iodd_version'): if getattr(product1, name) != getattr(product2, name): return False for name in ('get_scene_width', 'get_scene_height', 'get_num_datasets', 'get_num_dsds', 'get_num_bands', ): if getattr(product1, name)() != getattr(product2, name)(): return False return True def setUpModule(): filename = os.path.join(TESTDIR, TEST_PRODUCT) url = 'http://earth.esa.int/services/sample_products/meris/LRC/L2/MER_LRC_2PTGMV20000620_104318_00000104X000_00000_00000_0001.N1.gz' if not os.path.exists(filename): with contextlib.closing(urlopen(url)) as src: with open(filename + '.gz', 'wb') as dst: for data in src: dst.write(data) with contextlib.closing(gzip.GzipFile(filename + '.gz')) as src: with open(filename, 'wb') as dst: for data in src: dst.write(data) os.remove(filename + '.gz') class TestOpenProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) def test_open(self): product = epr.open(self.PRODUCT_FILE) self.assertTrue(isinstance(product, epr.Product)) self.assertEqual(product.mode, 'rb') def test_open_rb(self): product = epr.open(self.PRODUCT_FILE, 'rb') self.assertTrue(isinstance(product, epr.Product)) self.assertEqual(product.mode, 'rb') def test_open_rwb_01(self): product = epr.open(self.PRODUCT_FILE, 'r+b') self.assertTrue(isinstance(product, epr.Product)) self.assertTrue(product.mode in ('r+b', 'rb+')) def test_open_rwb_02(self): product = epr.open(self.PRODUCT_FILE, 'rb+') self.assertTrue(isinstance(product, epr.Product)) self.assertTrue(product.mode in ('r+b', 'rb+')) def test_open_invalid_mode_01(self): self.assertRaises(ValueError, epr.open, self.PRODUCT_FILE, '') def test_open_invalid_mode_02(self): self.assertRaises(ValueError, epr.open, self.PRODUCT_FILE, 'rx') def test_open_invalid_mode_03(self): self.assertRaises(TypeError, epr.open, self.PRODUCT_FILE, 0) if 'unicode' in dir(__builtins__): def test_open_unicode(self): filename = unicode(self.PRODUCT_FILE) product = epr.open(filename) self.assertTrue(isinstance(product, epr.Product)) else: def test_open_bytes(self): filename = self.PRODUCT_FILE.encode('UTF-8') product = epr.open(filename) self.assertTrue(isinstance(filename, bytes)) self.assertTrue(isinstance(product, epr.Product)) def test_product_constructor(self): product = epr.Product(self.PRODUCT_FILE) self.assertTrue(isinstance(product, epr.Product)) def test_open_failure(self): self.assertRaises(epr.EPRError, epr.open, '') def test_filename_type(self): self.assertRaises(TypeError, epr.open, 3) def test_open_failure_invalid_product(self): self.assertRaises(ValueError, epr.open, __file__) def test_product_constructor_failure(self): self.assertRaises(epr.EPRError, epr.Product, '') def test_product_constructor_failure_invalid_product(self): self.assertRaises(ValueError, epr.Product, __file__) class TestProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb' ID_STRING = 'MER_LRC_2PTGMV20000620_104318_00000104X000_00000' TOT_SIZE = 407461 DATASET_NAMES = [ 'Quality_ADS', 'Scaling_Factor_GADS', 'Tie_points_ADS', 'Cloud_Type_OT', 'Cloud_Top_Pressure', 'Vapour_Content', 'Flags', ] DATASET_NAME = 'Vapour_Content' DATASET_WIDTH = 281 DATASET_HEIGHT = 149 DATASET_NDSDS = 18 DATASET_NBANDS = 19 BAND_NAME = 'water_vapour' SPH_DESCRIPTOR = 'MER_LRC_2P SPECIFIC HEADER' MERIS_IODD_VERSION = 7 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE, self.OPEN_MODE) def tearDown(self): self.product.close() def test_close(self): self.product.close() def test_flush(self): self.product.close() def test_double_close(self): self.product.close() self.product.close() def test_file_path_property(self): self.assertEqual(self.product.file_path, self.PRODUCT_FILE.replace('\\', '/')) def test_mode_property(self): self.assertEqual(self.product.mode, self.OPEN_MODE) def test_tot_size_property(self): self.assertEqual(self.product.tot_size, self.TOT_SIZE) def test_id_string_property(self): self.assertEqual(self.product.id_string, self.ID_STRING) def test_meris_iodd_version_property(self): self.assertEqual(self.product.meris_iodd_version, self.MERIS_IODD_VERSION) def test_get_scene_width(self): self.assertEqual(self.product.get_scene_width(), self.DATASET_WIDTH) def test_get_scene_height(self): self.assertEqual(self.product.get_scene_height(), self.DATASET_HEIGHT) def test_get_num_datasets(self): self.assertEqual(self.product.get_num_datasets(), len(self.DATASET_NAMES)) def test_get_num_dsds(self): self.assertEqual(self.product.get_num_dsds(), self.DATASET_NDSDS) def test_get_num_bands(self): self.assertEqual(self.product.get_num_bands(), self.DATASET_NBANDS) def test_get_dataset_at(self): dataset = self.product.get_dataset_at(0) self.assertTrue(dataset) def test_get_dataset(self): dataset = self.product.get_dataset(self.DATASET_NAME) self.assertTrue(dataset) if 'unicode' in dir(__builtins__): def test_get_dataset_unicode(self): dataset = self.product.get_dataset(unicode(self.DATASET_NAME)) self.assertTrue(dataset) else: def test_get_dataset_bytes(self): filename = self.DATASET_NAME.encode('UTF-8') dataset = self.product.get_dataset(filename) self.assertTrue(isinstance(filename, bytes)) self.assertTrue(dataset) def test_datasets(self): datasets = [self.product.get_dataset_at(idx) for idx in range(self.product.get_num_datasets())] dataset_names = [ds.get_name() for ds in datasets] self.assertEqual(dataset_names, self.DATASET_NAMES) def test_get_dsd_at(self): self.assertTrue(isinstance(self.product.get_dsd_at(0), epr.DSD)) def test_get_mph(self): record = self.product.get_mph() self.assertTrue(isinstance(record, epr.Record)) product = record.get_field('PRODUCT').get_elem() self.assertEqual(product.decode('ascii'), os.path.basename(self.PRODUCT_FILE)) def test_get_sph(self): record = self.product.get_sph() self.assertTrue(isinstance(record, epr.Record)) sph_desct = record.get_field('SPH_DESCRIPTOR').get_elem() self.assertEqual(sph_desct.decode('ascii'), self.SPH_DESCRIPTOR) def test_get_band_id(self): self.assertTrue(isinstance(self.product.get_band(self.BAND_NAME), epr.Band)) if 'unicode' in dir(__builtins__): def test_get_band_id_unicode(self): band_name = unicode(self.BAND_NAME) self.assertTrue(isinstance(self.product.get_band(band_name), epr.Band)) else: def test_get_band_id_bytes(self): band_name = self.BAND_NAME.encode('UTF-8') self.assertTrue(isinstance(band_name, bytes)) self.assertTrue(isinstance(self.product.get_band(self.BAND_NAME), epr.Band)) def test_get_band_id_invalid_name(self): self.assertRaises(ValueError, self.product.get_band, '') def test_get_band_id_at(self): self.assertTrue(isinstance(self.product.get_band_at(0), epr.Band)) def test_get_band_id_at_invalid_index(self): self.assertRaises(ValueError, self.product.get_band_at, self.product.get_num_bands()) def test_read_bitmask_raster(self): bm_expr = 'l2_flags.LAND AND !l2_flags.CLOUD' xoffset = self.DATASET_WIDTH // 2 yoffset = self.DATASET_HEIGHT // 2 width = self.DATASET_WIDTH // 2 height = self.DATASET_HEIGHT // 2 raster = epr.create_bitmask_raster(width, height) raster = self.product.read_bitmask_raster(bm_expr, xoffset, yoffset, raster) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.get_width(), width) self.assertEqual(raster.get_height(), height) if 'unicode' in dir(__builtins__): def test_read_bitmask_raster_unicode(self): bm_expr = unicode('l2_flags.LAND AND !l2_flags.CLOUD') xoffset = self.DATASET_WIDTH // 2 yoffset = self.DATASET_HEIGHT // 2 width = self.DATASET_WIDTH // 2 height = self.DATASET_HEIGHT // 2 raster = epr.create_bitmask_raster(width, height) raster = self.product.read_bitmask_raster(bm_expr, xoffset, yoffset, raster) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.get_width(), width) self.assertEqual(raster.get_height(), height) else: def test_read_bitmask_raster_bytes(self): bm_expr = 'l2_flags.LAND AND !l2_flags.CLOUD'.encode('UTF-8') self.assertTrue(isinstance(bm_expr, bytes)) xoffset = self.DATASET_WIDTH // 2 yoffset = self.DATASET_HEIGHT // 2 width = self.DATASET_WIDTH // 2 height = self.DATASET_HEIGHT // 2 raster = epr.create_bitmask_raster(width, height) raster = self.product.read_bitmask_raster(bm_expr, xoffset, yoffset, raster) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.get_width(), width) self.assertEqual(raster.get_height(), height) def test_read_bitmask_raster_with_invalid_bm_expr(self): bm_expr = 'l5_flags.LAND AND !l2_flags.CLOUD' xoffset = self.DATASET_WIDTH // 2 yoffset = self.DATASET_HEIGHT // 2 width = self.DATASET_WIDTH // 2 height = self.DATASET_HEIGHT // 2 raster = epr.create_bitmask_raster(width, height) self.assertRaises(epr.EPRError, self.product.read_bitmask_raster, bm_expr, xoffset, yoffset, raster) try: self.product.read_bitmask_raster(bm_expr, xoffset, yoffset, raster) except epr.EPRError as e: self.assertEqual(e.code, 301) def test_read_bitmask_raster_with_wrong_data_type(self): bm_expr = 'l2_flags.LAND AND !l2_flags.CLOUD' xoffset = self.DATASET_WIDTH // 2 yoffset = self.DATASET_HEIGHT // 2 width = self.DATASET_WIDTH // 2 height = self.DATASET_HEIGHT // 2 raster = epr.create_raster(epr.E_TID_DOUBLE, width, height) self.assertRaises(epr.EPRError, self.product.read_bitmask_raster, bm_expr, xoffset, yoffset, raster) try: self.product.read_bitmask_raster(bm_expr, xoffset, yoffset, raster) except epr.EPRError as e: self.assertEqual(e.code, 7) def test_fileno(self): self.assertTrue(isinstance(self.product._fileno, int)) def test_fileno_read(self): os.lseek(self.product._fileno, 0, os.SEEK_SET) data = os.read(self.product._fileno, 7) self.assertEqual(data, b'PRODUCT') class TestProductRW(TestProduct): OPEN_MODE = 'rb+' class TestProductHighLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAMES = TestProduct.DATASET_NAMES BAND_NAMES = [ 'latitude', 'longitude', 'dem_alt', 'dem_rough', 'lat_corr', 'lon_corr', 'sun_zenith', 'sun_azimuth', 'view_zenith', 'view_azimuth', 'zonal_wind', 'merid_wind', 'atm_press', 'ozone', 'rel_hum', 'water_vapour', 'cloud_opt_thick', 'cloud_top_press', 'l2_flags', ] def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) def tearDown(self): self.product.close() def test_closed(self): self.assertFalse(self.product.closed) self.product.close() self.assertTrue(self.product.closed) def test_readonly_closed(self): self.assertFalse(self.product.closed) self.assertRaises(AttributeError, setattr, self.product, 'closed', True) def test_get_dataset_names(self): self.assertEqual(self.product.get_dataset_names(), self.DATASET_NAMES) def test_get_band_names(self): self.assertEqual(self.product.get_band_names(), self.BAND_NAMES) def test_datasets(self): datasets = self.product.datasets() self.assertTrue(datasets) self.assertEqual(len(datasets), self.product.get_num_datasets()) for index, dataset in enumerate(datasets): ref_dataset = self.product.get_dataset_at(index) self.assertEqual(dataset.get_name(), ref_dataset.get_name()) def test_bands(self): bands = self.product.bands() self.assertTrue(bands) self.assertEqual(len(bands), self.product.get_num_bands()) for index, band in enumerate(bands): ref_band = self.product.get_band_at(index) self.assertEqual(band.get_name(), ref_band.get_name()) # @TODO: not implemented #def test_iter(self): # pass def test_repr(self): pattern = ('epr\.Product\((?P\w+)\) ' '(?P\d+) datasets, ' '(?P\d+) bands') mobj = re.match(pattern, repr(self.product)) self.assertNotEqual(mobj, None) self.assertEqual(mobj.group('name'), self.product.id_string) self.assertEqual(mobj.group('n_datasets'), str(self.product.get_num_datasets())) self.assertEqual(mobj.group('n_bands'), str(self.product.get_num_bands())) def test_repr_type(self): self.assertTrue(isinstance(repr(self.product), str)) def test_str(self): lines = [repr(self.product), ''] lines.extend(map(repr, self.product.datasets())) lines.append('') lines.extend(map(str, self.product.bands())) data = '\n'.join(lines) self.assertEqual(data, str(self.product)) def test_str_type(self): self.assertTrue(isinstance(str(self.product), str)) def test_contect_manager(self): with epr.open(self.PRODUCT_FILE) as product: self.assertTrue(isinstance(product, epr.Product)) self.assertFalse(product.closed) self.assertTrue(str(product)) self.assertTrue(product.closed) class TestProductLowLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) def tearDown(self): self.product.close() def test_magic(self): self.assertEqual(self.product._magic, epr._EPR_MAGIC_PRODUCT_ID) class TestClosedProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = 'Vapour_Content' BAND_NAME = 'water_vapour' def setUp(self): self.product = epr.open(self.PRODUCT_FILE) self.product.close() def test_properties(self): for name in ('file_path', 'tot_size', 'id_string', 'meris_iodd_version'): self.assertRaises(ValueError, getattr, self.product, name) def test_get_get_scene_width(self): self.assertRaises(ValueError, self.product.get_scene_width) def test_get_get_scene_height(self): self.assertRaises(ValueError, self.product.get_scene_height) def test_get_num_datasets(self): self.assertRaises(ValueError, self.product.get_num_datasets) def test_get_num_dsds(self): self.assertRaises(ValueError, self.product.get_num_dsds) def test_get_num_bands(self): self.assertRaises(ValueError, self.product.get_num_bands) def test_get_mph(self): self.assertRaises(ValueError, self.product.get_mph) def test_get_sph(self): self.assertRaises(ValueError, self.product.get_sph) def test_get_dataset_at(self): self.assertRaises(ValueError, self.product.get_dataset_at, 0) def test_get_dataset(self): self.assertRaises(ValueError, self.product.get_dataset, self.DATASET_NAME) def test_get_dsd_at(self): self.assertRaises(ValueError, self.product.get_dsd_at, 0) def test_get_band_id(self): self.assertRaises(ValueError, self.product.get_band, self.BAND_NAME) def test_get_band_id_at(self): self.assertRaises(ValueError, self.product.get_band_at, 0) def test_read_bitmask_raster(self): bm_expr = 'l2_flags.LAND AND !l2_flags.CLOUD' raster = epr.create_bitmask_raster(12, 10) xoffset = 0 yoffset = 0 self.assertRaises(ValueError, self.product.read_bitmask_raster, bm_expr, xoffset, yoffset, raster) def test_get_dataset_names(self): self.assertRaises(ValueError, self.product.get_dataset_names) def test_get_band_names(self): self.assertRaises(ValueError, self.product.get_band_names) def test_datasets(self): self.assertRaises(ValueError, self.product.datasets) def test_bands(self): self.assertRaises(ValueError, self.product.bands) def test_repr(self): self.assertRaises(ValueError, repr, self.product) def test_str(self): self.assertRaises(ValueError, str, self.product) class TestDataset(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb' DATASET_NAME = 'Vapour_Content' DATASET_DESCRIPTION = 'Level 2 MDS Total Water vapour' NUM_RECORDS = 149 DSD_NAME = 'MDS Vapour Content' RECORD_INDEX = 0 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE, self.OPEN_MODE) self.dataset = self.product.get_dataset(self.DATASET_NAME) def tearDown(self): self.product.close() def test_product_property(self): self.assertTrue(equal_products(self.dataset.product, self.product)) def test_description_property(self): self.assertEqual(self.dataset.description, self.DATASET_DESCRIPTION) def test_get_name(self): self.assertEqual(self.dataset.get_name(), self.DATASET_NAME) def test_get_dsd_name(self): self.assertEqual(self.dataset.get_dsd_name(), self.DSD_NAME) def test_get_num_records(self): self.assertEqual(self.dataset.get_num_records(), self.NUM_RECORDS) def test_get_dsd(self): self.assertTrue(isinstance(self.dataset.get_dsd(), epr.DSD)) def test_create_record(self): record = self.dataset.create_record() self.assertTrue(isinstance(record, epr.Record)) def test_create_record_index(self): record = self.dataset.create_record() self.assertEqual(record.index, None) def test_read_record(self): record = self.dataset.read_record(self.RECORD_INDEX) self.assertTrue(isinstance(record, epr.Record)) def test_read_record_index(self): record = self.dataset.read_record(self.RECORD_INDEX) self.assertEqual(record.index, self.RECORD_INDEX) def test_read_record_passed(self): created_record = self.dataset.create_record() read_record = self.dataset.read_record(self.RECORD_INDEX, created_record) self.assertTrue(created_record is read_record) def test_read_record_passed_index(self): created_record = self.dataset.create_record() self.assertEqual(created_record.index, None) read_record = self.dataset.read_record(self.RECORD_INDEX, created_record) self.assertEqual(read_record.index, self.RECORD_INDEX) def test_read_record_passed_invalid(self): self.assertRaises(TypeError, self.dataset.read_record, 0, 0) class TestDatasetRW(TestDataset): OPEN_MODE = 'rb+' class TestDatasetHighLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = TestDataset.DATASET_NAME def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.dataset = self.product.get_dataset(self.DATASET_NAME) def tearDown(self): self.product.close() def test_records(self): records = self.dataset.records() self.assertTrue(records) self.assertEqual(len(records), self.dataset.get_num_records()) for index, record in enumerate(records): ref_record = self.dataset.read_record(index) self.assertEqual(record.get_field_names(), ref_record.get_field_names()) def test_iter(self): index = 0 for record in self.dataset: ref_record = self.dataset.read_record(index) self.assertEqual(record.get_field_names(), ref_record.get_field_names()) index += 1 self.assertEqual(index, self.dataset.get_num_records()) def test_repr(self): pattern = 'epr\.Dataset\((?P\w+)\) (?P\d+) records' mobj = re.match(pattern, repr(self.dataset)) self.assertNotEqual(mobj, None) self.assertEqual(mobj.group('name'), self.dataset.get_name()) self.assertEqual(mobj.group('num'), str(self.dataset.get_num_records())) def test_repr_type(self): self.assertTrue(isinstance(repr(self.dataset), str)) def test_str(self): lines = [repr(self.dataset), ''] lines.extend(map(str, self.dataset)) data = '\n'.join(lines) self.assertEqual(data, str(self.dataset)) def test_str_type(self): self.assertTrue(isinstance(str(self.dataset), str)) class TestDatasetOnClosedProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = 'Vapour_Content' def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.dataset = self.product.get_dataset(self.DATASET_NAME) self.record = self.dataset.create_record() self.product.close() def test_product_property(self): self.assertTrue(isinstance(self.dataset.product, epr.Product)) def test_description_property(self): self.assertRaises(ValueError, getattr, self.dataset, 'description') def test_get_name(self): self.assertRaises(ValueError, self.dataset.get_name) def test_get_dsd_name(self): self.assertRaises(ValueError, self.dataset.get_dsd_name) def test_get_num_records(self): self.assertRaises(ValueError, self.dataset.get_num_records) def test_get_dsd(self): self.assertRaises(ValueError, self.dataset.get_dsd) def test_create_record(self): self.assertRaises(ValueError, self.dataset.create_record) def test_read_record(self): self.assertRaises(ValueError, self.dataset.read_record, 0) def test_read_record_passed(self): self.assertRaises(ValueError, self.dataset.read_record, 0, self.record) def test_records(self): self.assertRaises(ValueError, self.dataset.records) def test_iter(self): self.assertRaises(ValueError, iter, self.dataset) def test_repr(self): self.assertRaises(ValueError, repr, self.dataset) def test_str(self): self.assertRaises(ValueError, str, self.dataset) class TestBand(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb+' DATASET_NAME = 'Vapour_Content' BAND_NAMES = ( 'latitude', 'longitude', 'dem_alt', 'dem_rough', 'lat_corr', 'lon_corr', 'sun_zenith', 'sun_azimuth', 'view_zenith', 'view_azimuth', 'zonal_wind', 'merid_wind', 'atm_press', 'ozone', 'rel_hum', 'water_vapour', 'cloud_opt_thick', 'cloud_top_press', 'l2_flags', ) BAND_NAME = 'water_vapour' BAND_DESCTIPTION = 'Water vapour content' XOFFSET = 30 YOFFSET = 20 WIDTH = 200 HEIGHT = 100 SCALING_FACTOR = 0.10000000149011612 SCALING_OFFSET = -0.10000000149011612 UNIT = 'g/cm^2' RTOL = 1e-7 DATA_TYPE = np.float32 TEST_DATA = np.asarray([ [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000], [0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.20000002, 0.10000000], ]) def setUp(self): self.product = epr.Product(self.PRODUCT_FILE, self.OPEN_MODE) self.band = self.product.get_band(self.BAND_NAME) def tearDown(self): self.product.close() def test_product_property(self): self.assertTrue(equal_products(self.band.product, self.product)) def test_spectr_band_index_property(self): self.assertEqual(self.band.spectr_band_index, -1) def test_sample_model_property(self): self.assertEqual(self.band.sample_model, 0) def test_data_type_property(self): self.assertEqual(self.band.data_type, epr.E_TID_FLOAT) def test_scaling_method_property(self): self.assertEqual(self.band.scaling_method, epr.E_SMID_LIN) def test_scaling_offset_property(self): self.assertEqual(self.band.scaling_offset, self.SCALING_OFFSET) def test_scaling_factor_property(self): self.assertEqual(self.band.scaling_factor, self.SCALING_FACTOR) self.assertTrue(isinstance(self.band.scaling_factor, float)) def test_bm_expr_property(self): self.assertEqual(self.band.bm_expr, None) def test_unit_property(self): self.assertEqual(self.band.unit, self.UNIT) def test_description_property(self): self.assertEqual(self.band.description, self.BAND_DESCTIPTION) def test_lines_mirrored_property(self): self.assertTrue(isinstance(self.band.lines_mirrored, bool)) self.assertEqual(self.band.lines_mirrored, True) def test_dataset_property(self): dataset = self.band.dataset self.assertTrue(isinstance(dataset, epr.Dataset)) self.assertEqual(dataset.get_name(), self.DATASET_NAME) def test_get_name(self): for index in range(len(self.BAND_NAMES)): b = self.product.get_band_at(index) self.assertEqual(b.get_name(), self.BAND_NAMES[index]) def test_create_compatible_raster(self): width = self.product.get_scene_width() height = self.product.get_scene_height() raster = self.band.create_compatible_raster(width, height) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.get_width(), width) self.assertEqual(raster.get_height(), height) # @NOTE: data type on disk is epr.E_TID_USHORT self.assertEqual(raster.data_type, epr.E_TID_FLOAT) def test_create_compatible_raster_with_invalid_size(self): width = self.product.get_scene_width() height = self.product.get_scene_height() self.assertRaises((ValueError, OverflowError), self.band.create_compatible_raster, -1, height) self.assertRaises((ValueError, OverflowError), self.band.create_compatible_raster, width, -1) self.assertRaises(ValueError, self.band.create_compatible_raster, self.product.get_scene_width() + 10, height) self.assertRaises(ValueError, self.band.create_compatible_raster, width, self.product.get_scene_height() + 10) def test_create_compatible_raster_with_step(self): src_width = self.product.get_scene_width() src_height = self.product.get_scene_height() xstep = 2 ystep = 3 width = (src_width - 1) // xstep + 1 height = (src_height - 1) // ystep + 1 raster = self.band.create_compatible_raster(src_width, src_height, xstep, ystep) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.get_width(), width) self.assertEqual(raster.get_height(), height) # @NOTE: data type on disk is epr.E_TID_USHORT self.assertEqual(raster.data_type, epr.E_TID_FLOAT) def test_create_compatible_raster_with_invalid_step(self): width = self.product.get_scene_width() height = self.product.get_scene_height() self.assertRaises((ValueError, OverflowError), self.band.create_compatible_raster, width, height, -1, 2) self.assertRaises((ValueError, OverflowError), self.band.create_compatible_raster, width, height, 2, -1) self.assertRaises((ValueError, OverflowError), self.band.create_compatible_raster, width, height, -2, -1) self.assertRaises((ValueError, ValueError), self.band.create_compatible_raster, width, height, width + 10, 2) self.assertRaises((ValueError, ValueError), self.band.create_compatible_raster, width, height, 2, height + 10) self.assertRaises((ValueError, ValueError), self.band.create_compatible_raster, width, height, width + 10, height + 10) def test_create_compatible_raster_with_default_size(self): src_width = self.product.get_scene_width() src_height = self.product.get_scene_height() raster = self.band.create_compatible_raster() self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.get_width(), src_width) self.assertEqual(raster.get_height(), src_height) # @NOTE: data type on disk is epr.E_TID_USHORT self.assertEqual(raster.data_type, epr.E_TID_FLOAT) def test_read_raster(self): rasterin = self.band.create_compatible_raster(self.WIDTH, self.HEIGHT) raster = self.band.read_raster(self.XOFFSET, self.YOFFSET, rasterin) self.assertTrue(isinstance(raster, epr.Raster)) self.assertTrue(raster is rasterin) self.assertEqual(raster.get_width(), self.WIDTH) self.assertEqual(raster.get_height(), self.HEIGHT) # @NOTE: data type on disk is epr.E_TID_USHORT self.assertEqual(raster.data_type, epr.E_TID_FLOAT) h, w = self.TEST_DATA.shape npt.assert_allclose(raster.get_pixel(0, 0), self.TEST_DATA[0, 0], rtol=self.RTOL) npt.assert_allclose(raster.get_pixel(w - 1, h - 1), self.TEST_DATA[h - 1, w - 1], rtol=self.RTOL) def test_read_raster_none(self): raster = self.band.read_raster() self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.get_width(), self.product.get_scene_width()) self.assertEqual(raster.get_height(), self.product.get_scene_height()) # @NOTE: data type on disk is epr.E_TID_USHORT self.assertEqual(raster.data_type, epr.E_TID_FLOAT) h, w = self.TEST_DATA.shape npt.assert_allclose( raster.get_pixel(self.XOFFSET, self.YOFFSET), self.TEST_DATA[0, 0], rtol=self.RTOL) npt.assert_allclose( raster.get_pixel(self.XOFFSET + w - 1, self.YOFFSET + h - 1), self.TEST_DATA[h - 1, w - 1], rtol=self.RTOL) def test_read_raster_default_offset(self): height = self.HEIGHT width = self.WIDTH raster1 = self.band.create_compatible_raster(width, height) raster2 = self.band.create_compatible_raster(width, height) r1 = self.band.read_raster(0, 0, raster1) r2 = self.band.read_raster(raster=raster2) self.assertTrue(r1 is raster1) self.assertTrue(r2 is raster2) self.assertEqual(raster1.get_pixel(0, 0), raster2.get_pixel(0, 0)) self.assertEqual(raster1.get_pixel(width - 1, height - 1), raster2.get_pixel(width - 1, height - 1)) h, w = self.TEST_DATA.shape npt.assert_allclose( raster1.get_pixel(self.XOFFSET, self.YOFFSET), self.TEST_DATA[0, 0], rtol=self.RTOL) npt.assert_allclose( raster1.get_pixel(self.XOFFSET + w - 1, self.YOFFSET + h - 1), self.TEST_DATA[h - 1, w - 1], rtol=self.RTOL) def test_read_raster_with_invalid_raster(self): self.assertRaises(TypeError, self.band.read_raster, 0, 0, 0) def test_read_raster_with_invalid_offset(self): raster = self.band.create_compatible_raster(self.WIDTH, self.HEIGHT) self.assertRaises(ValueError, self.band.read_raster, -1, 0, raster) self.assertRaises(ValueError, self.band.read_raster, 0, -1, raster) self.assertRaises(ValueError, self.band.read_raster, -1, -1, raster) invalid_xoffset = self.WIDTH invalid_yoffset = self.HEIGHT self.assertRaises(ValueError, self.band.read_raster, invalid_xoffset, 0, raster) self.assertRaises(ValueError, self.band.read_raster, 0, invalid_yoffset, raster) self.assertRaises(ValueError, self.band.read_raster, invalid_xoffset, invalid_yoffset, raster) def test_read_as_array_ref(self): data = self.band.read_as_array(self.WIDTH, self.HEIGHT, self.XOFFSET, self.YOFFSET) self.assertTrue(isinstance(data, np.ndarray)) self.assertEqual(data.shape, (self.HEIGHT, self.WIDTH)) self.assertEqual(data.dtype, self.DATA_TYPE) h, w = self.TEST_DATA.shape npt.assert_allclose(data[:h, :w], self.TEST_DATA, rtol=self.RTOL) def test_read_as_array_cross(self): data = self.band.read_as_array() box = self.band.read_as_array(self.WIDTH, self.HEIGHT, self.XOFFSET, self.YOFFSET) npt.assert_array_equal( data[self.YOFFSET:self.YOFFSET + self.HEIGHT, self.XOFFSET:self.XOFFSET + self.WIDTH], box) def test_read_as_array_default(self): data = self.band.read_as_array() self.assertTrue(isinstance(data, np.ndarray)) self.assertEqual( data.shape, (self.product.get_scene_height(), self.product.get_scene_width())) self.assertEqual(data.dtype, self.DATA_TYPE) h, w = self.TEST_DATA.shape npt.assert_allclose( data[self.YOFFSET:self.YOFFSET + h, self.XOFFSET:self.XOFFSET + w], self.TEST_DATA, rtol=self.RTOL) # @SEEALSO: https://www.brockmann-consult.de/beam-jira/browse/EPR-2 @unittest.skipIf(EPR_C_BUG_BCEPR002, 'buggy EPR_C_API detected') def test_read_as_array_with_step_2(self): step = 2 band = self.band data = band.read_as_array() box = band.read_as_array(self.WIDTH, self.HEIGHT, self.XOFFSET, self.YOFFSET, step, step) self.assertTrue(isinstance(box, np.ndarray)) self.assertEqual( box.shape, ((self.HEIGHT - 1) // step + 1, (self.WIDTH - 1) // step + 1)) self.assertEqual(box.dtype, self.DATA_TYPE) npt.assert_allclose( data[self.YOFFSET:self.YOFFSET + self.HEIGHT:step, self.XOFFSET:self.XOFFSET + self.WIDTH:step], box) h, w = self.TEST_DATA.shape npt.assert_allclose( box[:(h-1)//step+1, :(w-1)//step+1], self.TEST_DATA[::step, ::step], rtol=self.RTOL) @unittest.skipIf(EPR_C_BUG_BCEPR002, 'buggy EPR_C_API detected') def test_read_as_array_with_step_3(self): step = 3 band = self.band data = band.read_as_array() box = band.read_as_array(self.WIDTH, self.HEIGHT, self.XOFFSET, self.YOFFSET, step, step) self.assertTrue(isinstance(box, np.ndarray)) self.assertEqual( box.shape, ((self.HEIGHT - 1) // step + 1, (self.WIDTH - 1) // step + 1)) self.assertEqual(box.dtype, self.DATA_TYPE) npt.assert_allclose( data[self.YOFFSET:self.YOFFSET + self.HEIGHT:step, self.XOFFSET:self.XOFFSET + self.WIDTH:step], box) h, w = self.TEST_DATA.shape npt.assert_allclose( box[:(h-1)//step+1, :(w-1)//step+1], self.TEST_DATA[::step, ::step], rtol=self.RTOL) @unittest.skipIf(EPR_C_BUG_BCEPR002, 'buggy EPR_C_API detected') def test_read_as_array_with_step_4(self): step = 4 band = self.band data = band.read_as_array() box = band.read_as_array(self.WIDTH, self.HEIGHT, self.XOFFSET, self.YOFFSET, step, step) self.assertTrue(isinstance(box, np.ndarray)) self.assertEqual( box.shape, ((self.HEIGHT - 1) // step + 1, (self.WIDTH - 1) // step + 1)) self.assertEqual(box.dtype, self.DATA_TYPE) npt.assert_allclose( data[self.YOFFSET:self.YOFFSET + self.HEIGHT:step, self.XOFFSET:self.XOFFSET + self.WIDTH:step], box) h, w = self.TEST_DATA.shape npt.assert_allclose( box[:(h-1)//step+1, :(w-1)//step+1], self.TEST_DATA[::step, ::step]) @unittest.skipIf(EPR_C_BUG_BCEPR002, 'buggy EPR_C_API detected') def test_read_as_array_with_step_5(self): step = 5 band = self.band data = band.read_as_array() box = band.read_as_array(self.WIDTH, self.HEIGHT, self.XOFFSET, self.YOFFSET, step, step) self.assertTrue(isinstance(box, np.ndarray)) self.assertEqual( box.shape, ((self.HEIGHT - 1) // step + 1, (self.WIDTH - 1) // step + 1)) self.assertEqual(box.dtype, self.DATA_TYPE) npt.assert_allclose( data[self.YOFFSET:self.YOFFSET + self.HEIGHT:step, self.XOFFSET:self.XOFFSET + self.WIDTH:step], box) h, w = self.TEST_DATA.shape npt.assert_allclose( box[:(h-1)//step+1, :(w-1)//step+1], self.TEST_DATA[::step, ::step], rtol=self.RTOL) class TestBandRW(TestBand): OPEN_MODE = 'rb+' class TestAnnotationBand(TestBand): DATASET_NAME = 'Tie_points_ADS' BAND_NAME = 'sun_zenith' BAND_DESCTIPTION = 'Sun zenith angle' SCALING_FACTOR = 9.999999974752427e-07 SCALING_OFFSET = 0.0 UNIT = 'deg' RTOL = 1e-6 TEST_DATA = np.asarray([ [33.111412048339843750, 33.079044342041015625, 33.046680450439453125, 33.014350891113281250, 32.982025146484375000, 32.949695587158203125, 32.917366027832031250, 32.885074615478515625, 32.852783203125000000, 32.820491790771484375], [33.089057922363281250, 33.056671142578125000, 33.024288177490234375, 32.991939544677734375, 32.959594726562500000, 32.927246093750000000, 32.894897460937500000, 32.862583160400390625, 32.830276489257812500, 32.797962188720703125], [33.066703796386718750, 33.034297943115234375, 33.001895904541015625, 32.969532012939453125, 32.937164306640625000, 32.904792785644531250, 32.872428894042968750, 32.840099334716796875, 32.807769775390625000, 32.775436401367187500], [33.044349670410156250, 33.011924743652343750, 32.979503631591796875, 32.947116851806640625, 32.914733886718750000, 32.882343292236328125, 32.849956512451171875, 32.817611694335937500, 32.785259246826171875, 32.752910614013671875], [33.021995544433593750, 32.989555358886718750, 32.957111358642578125, 32.924705505371093750, 32.892299652099609375, 32.859893798828125000, 32.827487945556640625, 32.795120239257812500, 32.762748718261718750, 32.730381011962890625], [32.999782562255859375, 32.967323303222656250, 32.934860229492187500, 32.902431488037109375, 32.870010375976562500, 32.837581634521484375, 32.805160522460937500, 32.772773742675781250, 32.740379333496093750, 32.707992553710937500], [32.977569580078125000, 32.945091247558593750, 32.912605285644531250, 32.880161285400390625, 32.847717285156250000, 32.815273284912109375, 32.782829284667968750, 32.750423431396484375, 32.718013763427734375, 32.685604095458984375], [32.955352783203125000, 32.922855377197265625, 32.890354156494140625, 32.857891082763671875, 32.825424194335937500, 32.792964935302734375, 32.760498046875000000, 32.728076934814453125, 32.695644378662109375, 32.663219451904296875], [32.933139801025390625, 32.900619506835937500, 32.868103027343750000, 32.835620880126953125, 32.803138732910156250, 32.770652770996093750, 32.738170623779296875, 32.705722808837890625, 32.673278808593750000, 32.640830993652343750], [32.911067962646484375, 32.878528594970703125, 32.845993041992187500, 32.813491821289062500, 32.780990600585937500, 32.748485565185546875, 32.715984344482421875, 32.683513641357421875, 32.651054382324218750, 32.618587493896484375], ]) @unittest.skipIf(EPR_C_BUG_PYEPR009, 'buggy EPR_C_API detected') def test_read_raster(self): super(TestAnnotationBand, self).test_read_raster() @unittest.skipIf(EPR_C_BUG_PYEPR009, 'buggy EPR_C_API detected') def test_read_raster_default_offset(self): super(TestAnnotationBand, self).test_read_raster_default_offset() @unittest.skipIf(EPR_C_BUG_PYEPR009, 'buggy EPR_C_API detected') def test_read_as_array_ref(self): super(TestAnnotationBand, self).test_read_as_array_ref() @unittest.skipIf(EPR_C_BUG_PYEPR009, 'buggy EPR_C_API detected') def test_read_as_array_cross(self): super(TestAnnotationBand, self).test_read_as_array_cross() class TestBandHighLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) def tearDown(self): self.product.close() def test_repr(self): pattern = ('epr.Band\((?P\w+)\) of ' 'epr.Product\((?P\w+)\)') for band in self.product.bands(): mobj = re.match(pattern, repr(band)) self.assertNotEqual(mobj, None) self.assertEqual(mobj.group('name'), band.get_name()) self.assertEqual(mobj.group('product_id'), self.product.id_string) def test_repr_type(self): band = self.product.get_band_at(0) self.assertTrue(isinstance(repr(band), str)) def test_str_type(self): band = self.product.get_band_at(0) self.assertTrue(isinstance(str(band), str)) class TestBandLowLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) FIELD_INDEX = 3 ELEM_INDEX = -1 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.band = self.product.get_band_at(0) def tearDown(self): self.product.close() def test_magic(self): self.assertEqual(self.band._magic, epr._EPR_MAGIC_BAND_ID) def test_field_index(self): self.assertEqual(self.band._field_index, self.FIELD_INDEX) def test_elem_index(self): self.assertEqual(self.band._elem_index, self.ELEM_INDEX) class TestBandOnClosedProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) WIDTH = 12 HEIGHT = 10 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.band = self.product.get_band_at(0) self.raster = self.band.create_compatible_raster(self.WIDTH, self.HEIGHT) self.product.close() def test_product_property(self): self.assertTrue(isinstance(self.band.product, epr.Product)) def test_properties(self): # 'sample_model', 'data_type', 'scaling_method', 'scaling_offset', # 'scaling_factor', 'bm_expr', 'unit', 'description', 'lines_mirrored' for name in ('spectr_band_index',): self.assertRaises(ValueError, getattr, self.band, name) def test_sample_model_property(self): self.assertEqual(self.band.sample_model, 0) def test_data_type_property(self): self.assertEqual(self.band.data_type, 0) def test_scaling_method_property(self): self.assertEqual(self.band.scaling_method, epr.E_SMID_LIN) def test_scaling_offset_property(self): self.assertEqual(self.band.scaling_offset, 0) def test_scaling_factor_property(self): self.assertEqual(self.band.scaling_factor, 0) self.assertTrue(isinstance(self.band.scaling_factor, float)) def test_bm_expr_property(self): self.assertEqual(self.band.bm_expr, None) def test_unit_property(self): self.assertEqual(self.band.unit, None) def test_description_property(self): self.assertEqual(self.band.description, None) def test_lines_mirrored_property(self): self.assertTrue(isinstance(self.band.lines_mirrored, bool)) def test_get_name(self): self.assertRaises(ValueError, self.product.get_band_at, 0) def test_create_compatible_raster(self): self.assertRaises(ValueError, self.band.create_compatible_raster, self.WIDTH, self.HEIGHT) def test_read_raster(self): self.assertRaises(ValueError, self.band.read_raster) def test_read_as_array(self): self.assertRaises(ValueError, self.band.read_as_array, self.WIDTH, self.HEIGHT) def test_str(self): self.assertRaises(ValueError, str, self.band) def test_repr(self): self.assertRaises(ValueError, repr, self.band) class TestCreateRaster(unittest.TestCase): RASTER_WIDTH = 400 RASTER_HEIGHT = 300 RASTER_DATA_TYPE = epr.E_TID_FLOAT def test_create_raster(self): raster = epr.create_raster(self.RASTER_DATA_TYPE, self.RASTER_WIDTH, self.RASTER_HEIGHT) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.data_type, self.RASTER_DATA_TYPE) self.assertEqual(raster.get_width(), self.RASTER_WIDTH) self.assertEqual(raster.get_height(), self.RASTER_HEIGHT) def test_create_raster_with_step(self): src_width = 3 * self.RASTER_WIDTH src_height = 2 * self.RASTER_HEIGHT raster = epr.create_raster(self.RASTER_DATA_TYPE, src_width, src_height, 3, 2) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.data_type, self.RASTER_DATA_TYPE) self.assertEqual(raster.get_width(), self.RASTER_WIDTH) self.assertEqual(raster.get_height(), self.RASTER_HEIGHT) def test_create_raster_with_invalid_step(self): src_width = 3 * self.RASTER_WIDTH src_height = 2 * self.RASTER_HEIGHT self.assertRaises(ValueError, epr.create_raster, self.RASTER_DATA_TYPE, src_width, src_height, 0, 2) def test_create_raster_with_invalid_size(self): self.assertRaises((ValueError, OverflowError), epr.create_raster, self.RASTER_DATA_TYPE, -1, self.RASTER_HEIGHT) def test_create_bitmask_raster(self): raster = epr.create_bitmask_raster(self.RASTER_WIDTH, self.RASTER_HEIGHT) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.data_type, epr.E_TID_UCHAR) self.assertEqual(raster.get_width(), self.RASTER_WIDTH) self.assertEqual(raster.get_height(), self.RASTER_HEIGHT) def test_create_bitmask_raster_with_step(self): src_width = 3 * self.RASTER_WIDTH src_height = 2 * self.RASTER_HEIGHT raster = epr.create_bitmask_raster(src_width, src_height, 3, 2) self.assertTrue(isinstance(raster, epr.Raster)) self.assertEqual(raster.data_type, epr.E_TID_UCHAR) self.assertEqual(raster.get_width(), self.RASTER_WIDTH) self.assertEqual(raster.get_height(), self.RASTER_HEIGHT) def test_create_bitmask_raster_with_invalid_step(self): src_width = 3 * self.RASTER_WIDTH src_height = 2 * self.RASTER_HEIGHT self.assertRaises(ValueError, epr.create_bitmask_raster, src_width, src_height, 0, 2) def test_create_bitmask_raster_with_invalid_size(self): self.assertRaises((ValueError, OverflowError), epr.create_bitmask_raster, -1, self.RASTER_HEIGHT) class TestRaster(unittest.TestCase): RASTER_WIDTH = TestBand.WIDTH RASTER_HEIGHT = TestBand.HEIGHT RASTER_DATA_TYPE = epr.E_TID_FLOAT RASTER_ELEM_SIZE = 4 RTOL = 1e-7 TEST_DATA = np.zeros((10, 10)) def setUp(self): self.raster = epr.create_raster(self.RASTER_DATA_TYPE, self.RASTER_WIDTH, self.RASTER_HEIGHT) def test_get_width(self): self.assertEqual(self.raster.get_width(), self.RASTER_WIDTH) def test_get_height(self): self.assertEqual(self.raster.get_height(), self.RASTER_HEIGHT) def test_get_elem_size(self): self.assertEqual(self.raster.get_elem_size(), self.RASTER_ELEM_SIZE) def test_get_pixel(self): self.assertAlmostEqual(self.raster.get_pixel(0, 0), self.TEST_DATA[0, 0]) def test_get_pixel_invalid_x(self): self.assertRaises(ValueError, self.raster.get_pixel, -1, 0) self.assertRaises(ValueError, self.raster.get_pixel, self.RASTER_WIDTH + 1, 0) def test_get_pixel_invalid_y(self): self.assertRaises(ValueError, self.raster.get_pixel, 0, -1) self.assertRaises(ValueError, self.raster.get_pixel, 0, self.RASTER_HEIGHT + 1) def test_get_pixel_invalid_coor(self): self.assertRaises(ValueError, self.raster.get_pixel, -1, -1) self.assertRaises(ValueError, self.raster.get_pixel, self.RASTER_WIDTH + 1, self.RASTER_HEIGHT + 1) def test_get_pixel_type(self): self.assertEqual(type(self.raster.get_pixel(0, 0)), float) def test_data_type_property(self): self.assertEqual(self.raster.data_type, self.RASTER_DATA_TYPE) def test_source_width_property(self): self.assertEqual(self.raster.source_width, self.RASTER_WIDTH) self.assertTrue(isinstance(self.raster.source_width, numbers.Integral)) def test_source_height_property(self): self.assertEqual(self.raster.source_height, self.RASTER_HEIGHT) self.assertTrue(isinstance(self.raster.source_height, numbers.Integral)) def test_source_step_x_property(self): self.assertEqual(self.raster.source_step_x, 1) self.assertTrue(isinstance(self.raster.source_step_x, numbers.Integral)) def test_source_step_y_property(self): self.assertEqual(self.raster.source_step_y, 1) self.assertTrue(isinstance(self.raster.source_step_y, numbers.Integral)) def test_data_property(self): height = self.raster.get_height() width = self.raster.get_width() data = self.raster.data self.assertTrue(isinstance(data, np.ndarray)) self.assertEqual(data.ndim, 2) self.assertEqual(data.shape, (height, width)) self.assertEqual(data.dtype, EPR_TO_NUMPY_TYPE[self.raster.data_type]) ny, nx = self.TEST_DATA.shape npt.assert_allclose(data[:ny, :nx], self.TEST_DATA, rtol=self.RTOL) def test_data_property_two_times(self): data1 = self.raster.data data2 = self.raster.data self.assertTrue(data1 is data2) self.assertTrue(np.all(data1 == data2)) def test_data_property_shared_data_semantic(self): data1 = self.raster.data data1[0, 0] *= 2 data2 = self.raster.data self.assertTrue(np.all(data1 == data2)) def test_data_property_data_scope(self): data1 = self.raster.data self.assertTrue(isinstance(data1, np.ndarray)) data1 = None data2 = self.raster.data self.assertTrue(isinstance(data2, np.ndarray)) def test_data_property_raster_scope(self): data = self.raster.data self.assertTrue(isinstance(data, np.ndarray)) self.raster = None self.assertTrue(isinstance(data, np.ndarray)) ny, nx = self.TEST_DATA.shape npt.assert_allclose(data[:ny, :nx], self.TEST_DATA, rtol=self.RTOL) class TestRasterRead(TestRaster): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) BAND_NAME = TestBand.BAND_NAME RASTER_XOFFSET = TestBand.XOFFSET RASTER_YOFFSET = TestBand.YOFFSET TEST_DATA = TestBand.TEST_DATA def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.band = self.product.get_band(self.BAND_NAME) self.raster = self.band.create_compatible_raster(self.RASTER_WIDTH, self.RASTER_HEIGHT) self.band.read_raster(self.RASTER_XOFFSET, self.RASTER_YOFFSET, self.raster) def tearDown(self): self.product.close() def test_data_property_shared_semantics_readload(self): data1 = self.raster.data data1[0, 0] *= 2 self.band.read_raster(self.RASTER_XOFFSET, self.RASTER_YOFFSET, self.raster) data2 = self.raster.data self.assertEqual(data1[0, 0], data2[0, 0]) self.assertTrue(np.all(data1 == data2)) class TestAnnotatedRasterRead(TestRasterRead): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) BAND_NAME = TestAnnotationBand.BAND_NAME RASTER_XOFFSET = TestAnnotationBand.XOFFSET RASTER_YOFFSET = TestAnnotationBand.YOFFSET TEST_DATA = TestAnnotationBand.TEST_DATA RTOL = 1e-6 @unittest.skipIf(EPR_C_BUG_PYEPR009, 'buggy EPR_C_API detected') def test_get_pixel(self): super(TestAnnotatedRasterRead, self).test_get_pixel() @unittest.skipIf(EPR_C_BUG_PYEPR009, 'buggy EPR_C_API detected') def test_data_property(self): super(TestAnnotatedRasterRead, self).test_data_property() @unittest.skipIf(EPR_C_BUG_PYEPR009, 'buggy EPR_C_API detected') def test_data_property_raster_scope(self): super(TestAnnotatedRasterRead, self).test_data_property_raster_scope() class TestRasterHighLevelAPI(unittest.TestCase): RASTER_WIDTH = 400 RASTER_HEIGHT = 300 RASTER_DATA_TYPE = epr.E_TID_FLOAT def setUp(self): self.raster = epr.create_raster(self.RASTER_DATA_TYPE, self.RASTER_WIDTH, self.RASTER_HEIGHT) def test_repr(self): pattern = (' (?P\w+) ' '\((?P\d+)L x (?P\d+)P\)') mobj = re.match(pattern, repr(self.raster)) self.assertNotEqual(mobj, None) self.assertEqual(mobj.group('data_type'), epr.data_type_id_to_str(self.raster.data_type)) self.assertEqual(mobj.group('lines'), str(self.raster.get_height())) self.assertEqual(mobj.group('pixels'), str(self.raster.get_width())) def test_repr_type(self): self.assertTrue(isinstance(repr(self.raster), str)) def test_str_type(self): self.assertTrue(isinstance(str(self.raster), str)) class TestRasterLowLevelAPI(unittest.TestCase): RASTER_WIDTH = 400 RASTER_HEIGHT = 300 RASTER_DATA_TYPE = epr.E_TID_FLOAT def setUp(self): self.raster = epr.create_raster(self.RASTER_DATA_TYPE, self.RASTER_WIDTH, self.RASTER_HEIGHT) def test_magic(self): self.assertEqual(self.raster._magic, epr._EPR_MAGIC_RASTER) class TestRecord(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb' DATASET_NAME = 'Quality_ADS' NUM_FIELD = 21 FIELD_NAME = 'perc_water_abs_aero' TOT_SIZE = 32 RECORD_INDEX = 0 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE, self.OPEN_MODE) self.dataset = self.product.get_dataset(self.DATASET_NAME) self.record = self.dataset.read_record(self.RECORD_INDEX) def tearDown(self): self.product.close() def test_get_num_fields(self): self.assertEqual(self.record.get_num_fields(), self.NUM_FIELD) @quiet def test_print_(self): self.record.print_() @quiet def test_print_ostream(self): self.record.print_(sys.stderr) def test_print_invalid_ostream(self): self.assertRaises(TypeError, self.record.print_, 'invalid') @quiet def test_print_element(self): self.record.print_element(3, 0) @quiet def test_print_element_ostream(self): self.record.print_element(0, 0, sys.stderr) def test_print_element_invalid_ostream(self): self.assertRaises(TypeError, self.record.print_element, 0, 0, 'invalid') def test_print_element_field_out_of_range(self): index = self.record.get_num_fields() + 10 self.assertRaises(ValueError, self.record.print_element, index, 0) def test_print_element_element_out_of_range(self): self.assertRaises(ValueError, self.record.print_element, 0, 150) def test_get_field(self): field = self.record.get_field(self.FIELD_NAME) self.assertTrue(isinstance(field, epr.Field)) if 'unicode' in dir(__builtins__): def test_get_field_unicode(self): field = self.record.get_field(unicode(self.FIELD_NAME)) self.assertTrue(isinstance(field, epr.Field)) else: def test_get_field_bytes(self): field_name = self.FIELD_NAME.encode('UTF-8') field = self.record.get_field(self.FIELD_NAME) self.assertTrue(isinstance(field_name, bytes)) self.assertTrue(isinstance(field, epr.Field)) def test_get_field_invlid_name(self): self.assertRaises(ValueError, self.record.get_field, '') def test_get_field_at(self): self.assertTrue(isinstance(self.record.get_field_at(0), epr.Field)) def test_get_field_at_invalid_index(self): index = self.record.get_num_fields() + 10 self.assertRaises(ValueError, self.record.get_field_at, index) def test_dataset_name(self): self.assertEqual(self.record.dataset_name, self.DATASET_NAME) def test_dataset_name_new(self): record = self.dataset.create_record() self.assertEqual(record.dataset_name, self.DATASET_NAME) def test_tot_size(self): self.assertEqual(self.record.tot_size, self.TOT_SIZE) def test_index(self): self.assertEqual(self.record.index, self.RECORD_INDEX) class TestRecordRW(TestRecord): OPEN_MODE = 'rb+' class TestRecordHighLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = TestRecord.DATASET_NAME FIELD_NAMES = [ 'dsr_time', 'attach_flag', 'perc_water_abs_aero', 'perc_water', 'perc_ddv_land', 'perc_land', 'perc_cloud', 'perc_low_poly_press', 'perc_low_neural_press', 'perc_out_ran_inp_wvapour', 'per_out_ran_outp_wvapour', 'perc_out_range_inp_cl', 'perc_out_ran_outp_cl', 'perc_in_ran_inp_land', 'perc_out_ran_outp_land', 'perc_out_ran_inp_ocean', 'perc_out_ran_outp_ocean', 'perc_out_ran_inp_case1', 'perc_out_ran_outp_case1', 'perc_out_ran_inp_case2', 'perc_out_ran_outp_case2', ] def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.dataset = self.product.get_dataset(self.DATASET_NAME) self.record = self.dataset.read_record(0) def tearDown(self): self.product.close() def test_get_field_names_number(self): self.assertEqual(len(self.record.get_field_names()), self.record.get_num_fields()) def test_get_field_names(self): self.assertEqual(self.record.get_field_names()[:len(self.FIELD_NAMES)], self.FIELD_NAMES) def test_fields(self): fields = self.record.fields() self.assertTrue(fields) self.assertEqual(len(fields), self.record.get_num_fields()) names = [field.get_name() for field in fields] self.assertEqual(self.record.get_field_names(), names) def test_iter(self): index = 0 for field in self.record: ref_field = self.record.get_field_at(index) self.assertEqual(field.get_name(), ref_field.get_name()) index += 1 self.assertEqual(index, self.record.get_num_fields()) def test_repr_type(self): self.assertTrue(isinstance(repr(self.record), str)) def test_str_type(self): self.assertTrue(isinstance(str(self.record), str)) class TestRecordLowLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) RECORD_INDEX = 1 RECORD_SIZE = 32 RECORD_OFFSET = RECORD_INDEX * RECORD_SIZE def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) dataset = self.product.get_dataset_at(0) self.record = dataset.read_record(self.RECORD_INDEX) def tearDown(self): self.product.close() def test_magic(self): self.assertEqual(self.record._magic, epr._EPR_MAGIC_RECORD) def test_get_offset(self): self.assertEqual(self.record.get_offset(), self.RECORD_OFFSET) class TestMultipleRecordsHighLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = TestProduct.DATASET_NAME def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.dataset = self.product.get_dataset(self.DATASET_NAME) def tearDown(self): self.product.close() def test_repr(self): pattern = ' (?P\d+) fields' for record in self.dataset: mobj = re.match(pattern, repr(record)) self.assertNotEqual(mobj, None) self.assertEqual(mobj.group('num'), str(record.get_num_fields())) def test_str_vs_print(self): for record in self.dataset: with tempfile.TemporaryFile('w+') as fd: record.print_(fd) fd.flush() fd.seek(0) data = fd.read() if data.endswith('\n'): data = data[:-1] self.assertEqual(data, str(record)) class TestMphRecordHighLevelAPI(TestRecordHighLevelAPI): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = 'MPH' FIELD_NAMES = [ 'PRODUCT', 'PROC_STAGE', 'REF_DOC', 'ACQUISITION_STATION', 'PROC_CENTER', 'PROC_TIME', 'SOFTWARE_VER', 'SENSING_START', 'SENSING_STOP', 'PHASE', 'CYCLE', 'REL_ORBIT', ] def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.record = self.product.get_mph() def tearDown(self): self.product.close() def test_index(self): self.assertEqual(self.record.index, None) class TestRecordOnClosedProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = 'Quality_ADS' NUM_FIELD = 21 FIELD_NAME = 'perc_water_abs_aero' FIELD_NAMES = TestRecordHighLevelAPI.FIELD_NAMES def setUp(self): product = epr.Product(self.PRODUCT_FILE) dataset = product.get_dataset(self.DATASET_NAME) self.record = dataset.read_record(0) #self.mph = product.get_mph() product.close() def test_get_num_fields(self): self.assertEqual(self.record.get_num_fields(), self.NUM_FIELD) #def test_get_num_fields_mph(self): # self.assertEqual(self.mph.get_num_fields(), 34) def test_print_(self): self.assertRaises(ValueError, self.record.print_) def test_print_element(self): self.assertRaises(ValueError, self.record.print_element, 3, 0) def test_get_field(self): self.assertRaises(ValueError, self.record.get_field, self.FIELD_NAME) def test_get_field_at(self): self.assertRaises(ValueError, self.record.get_field_at, 0) def test_get_field_names(self): self.assertRaises(ValueError, self.record.get_field_names) def test_fields(self): self.assertRaises(ValueError, self.record.fields) def test_iter(self): self.assertRaises(ValueError, iter, self.record) def test_repr(self): self.assertRaises(ValueError, repr, self.record) def test_str(self): self.assertRaises(ValueError, str, self.record) class TestField(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb' DATASET_NAME = 'Quality_ADS' FIELD_NAME = 'perc_water_abs_aero' FIELD_DESCRIPTION = '% of water pixels having absorbing aerosols' FIELD_TYPE = epr.E_TID_UCHAR FIELD_TYPE_NAME = 'byte' FIELD_NUM_ELEMS = 1 FIELD_VALUES = (81,) FIELD_UNIT = '%' #FIELD_OFFSET = 13 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE, self.OPEN_MODE) dataset = self.product.get_dataset(self.DATASET_NAME) record = dataset.read_record(0) self.field = record.get_field(self.FIELD_NAME) def tearDown(self): self.product.close() @quiet def test_print_field(self): self.field.print_() @quiet def test_print_field_ostream(self): self.field.print_(sys.stderr) def test_print_fied_invalid_ostream(self): self.assertRaises(TypeError, self.field.print_, 'invalid') def test_get_unit(self): self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) def test_get_description(self): self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) def test_get_num_elems(self): self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) def test_get_name(self): self.assertEqual(self.field.get_name(), self.FIELD_NAME) def test_get_type(self): self.assertEqual(self.field.get_type(), self.FIELD_TYPE) def test_get_elem(self): self.assertEqual(self.field.get_elem(), self.FIELD_VALUES[0]) def test_get_elem_index(self): self.assertEqual(self.field.get_elem(0), self.FIELD_VALUES[0]) def test_get_elem_invalid_index(self): self.assertRaises(ValueError, self.field.get_elem, self.FIELD_NUM_ELEMS + 10) def test_get_elems(self): vect = self.field.get_elems() self.assertTrue(isinstance(vect, np.ndarray)) self.assertEqual(vect.shape, (self.field.get_num_elems(),)) self.assertEqual(vect.dtype, epr.get_numpy_dtype(self.FIELD_TYPE)) npt.assert_allclose(vect[:len(self.FIELD_VALUES)], self.FIELD_VALUES) def test_tot_size(self): elem_size = epr.get_data_type_size(self.FIELD_TYPE) tot_size = elem_size * self.FIELD_NUM_ELEMS self.assertEqual(self.field.tot_size, tot_size) class TestFieldRW(TestField): OPEN_MODE = 'rb+' class TestFieldWriteOnReadOnly(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb' DATASET_NAME = 'Vapour_Content' RECORD_INDEX = 10 FIELD_NAME = 'wvapour_cont_pix' FIELD_DESCRIPTION = 'Water Vapour Content pixel #1- #281' FIELD_TYPE = epr.E_TID_UCHAR FIELD_TYPE_NAME = 'uchar' FIELD_UNIT = '' FIELD_NUM_ELEMS = 281 def setUp(self): self.filename = self.PRODUCT_FILE + '_' shutil.copy(self.PRODUCT_FILE, self.filename) self.product = epr.Product(self.filename, self.OPEN_MODE) dataset = self.product.get_dataset(self.DATASET_NAME) record = dataset.read_record(self.RECORD_INDEX) self.field = record.get_field(self.FIELD_NAME) def tearDown(self): self.product.close() os.unlink(self.filename) def test_write_on_read_only_product(self): value = self.field.get_elem() + 10 self.assertRaises(TypeError, self.field.set_elem, value) class TestFieldWrite(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb+' REOPEN = False DATASET_NAME = 'Vapour_Content' RECORD_INDEX = 10 FIELD_NAME = 'wvapour_cont_pix' FIELD_DESCRIPTION = 'Water Vapour Content pixel #1- #281' FIELD_TYPE = epr.E_TID_UCHAR FIELD_TYPE_NAME = 'uchar' FIELD_UNIT = '' FIELD_INDEX = 2 FIELD_NUM_ELEMS = 281 FIELD_VALUES = ( 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, ) def setUp(self): self.filename = self.PRODUCT_FILE + '_' shutil.copy(self.PRODUCT_FILE, self.filename) self.product = None self.dataset = None self.record = None self.field = None self.reopen(self.OPEN_MODE) self.offset = self._get_offset() def _get_offset(self): offset = self.dataset.get_dsd().ds_offset offset += self.record.index * self.record.tot_size for i in range(self.FIELD_INDEX): offset += self.record.get_field_at(i).tot_size return offset def tearDown(self): self.product.close() os.unlink(self.filename) def reopen(self, mode='rb'): if self.product is not None: self.product.close() self.product = epr.Product(self.filename, mode) self.dataset = self.product.get_dataset(self.DATASET_NAME) self.record = self.dataset.read_record(self.RECORD_INDEX) self.field = self.record.get_field(self.FIELD_NAME) def read(self, offset=0, size=None): if size is None: size = self.field.tot_size os.lseek(self.product._fileno, self.offset + offset, os.SEEK_SET) return os.read(self.product._fileno, size) def test_set_elem_metadata(self): self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) value = self.field.get_elem() + 10 self.field.set_elem(value) if self.REOPEN: self.reopen() else: self.product.flush() self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) def test_set_elem_data(self): npt.assert_array_equal(self.field.get_elems(), self.FIELD_VALUES) self.assertEqual(self.field.get_elem(), self.FIELD_VALUES[0]) value = self.field.get_elem() + 10 self.field.set_elem(value) if self.REOPEN: self.reopen() else: self.product.flush() self.assertEqual(self.field.get_elem(), value) values = np.array(self.FIELD_VALUES) values[0] = value npt.assert_array_equal(self.field.get_elems(), values) def test_set_elem_rawdata(self): orig_data = self.read() value = self.field.get_elem() + 10 self.field.set_elem(value) if self.REOPEN: self.reopen() else: self.product.flush() data = self.read() self.assertNotEqual(data, orig_data) dtype = epr.get_numpy_dtype(self.FIELD_TYPE) data = np.fromstring(data, dtype) orig_data = np.fromstring(orig_data, dtype) orig_data[0] = value npt.assert_array_equal(data, orig_data) def test_set_elem0_metadata(self): self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) value = self.field.get_elem(0) + 10 self.field.set_elem(value) if self.REOPEN: self.reopen() else: self.product.flush() self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) def test_set_elem0_data(self): npt.assert_array_equal(self.field.get_elems(), self.FIELD_VALUES) self.assertEqual(self.field.get_elem(0), self.FIELD_VALUES[0]) value = self.field.get_elem(0) + 10 self.field.set_elem(value) if self.REOPEN: self.reopen() else: self.product.flush() self.assertEqual(self.field.get_elem(0), value) values = np.array(self.FIELD_VALUES) values[0] = value npt.assert_array_equal(self.field.get_elems(), values) def test_set_elem0_rawdata(self): orig_data = self.read() value = self.field.get_elem(0) + 10 self.field.set_elem(value) if self.REOPEN: self.reopen() else: self.product.flush() data = self.read() self.assertNotEqual(data, orig_data) dtype = epr.get_numpy_dtype(self.FIELD_TYPE) data = np.fromstring(data, dtype) orig_data = np.fromstring(orig_data, dtype) orig_data[0] = value npt.assert_array_equal(data, orig_data) def test_set_elem20_metadata(self): self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) value = self.field.get_elem(20) + 1 self.field.set_elem(value, 20) if self.REOPEN: self.reopen() else: self.product.flush() self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) def test_set_elem20_data(self): npt.assert_array_equal(self.field.get_elems(), self.FIELD_VALUES) self.assertEqual(self.field.get_elem(20), self.FIELD_VALUES[20]) value = self.field.get_elem(20) + 1 self.field.set_elem(value, 20) if self.REOPEN: self.reopen() else: self.product.flush() self.assertEqual(self.field.get_elem(20), value) values = np.array(self.FIELD_VALUES) values[20] = value npt.assert_array_equal(self.field.get_elems(), values) def test_set_elem20_rawdata(self): orig_data = self.read() value = self.field.get_elem(20) + 1 self.field.set_elem(value, 20) if self.REOPEN: self.reopen() else: self.product.flush() data = self.read() self.assertNotEqual(data, orig_data) dtype = epr.get_numpy_dtype(self.FIELD_TYPE) data = np.fromstring(data, dtype) orig_data = np.fromstring(orig_data, dtype) orig_data[20] = value npt.assert_array_equal(data, orig_data) def test_set_elems_metadata(self): self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) values = self.field.get_elems() + 1 self.field.set_elems(values) if self.REOPEN: self.reopen() else: self.product.flush() self.assertEqual(self.field.get_description(), self.FIELD_DESCRIPTION) self.assertEqual(self.field.get_num_elems(), self.FIELD_NUM_ELEMS) self.assertEqual(self.field.get_type(), self.FIELD_TYPE) self.assertEqual(epr.data_type_id_to_str(self.field.get_type()), self.FIELD_TYPE_NAME) self.assertEqual(self.field.get_unit(), self.FIELD_UNIT) def test_set_elems_data(self): npt.assert_array_equal(self.field.get_elems(), self.FIELD_VALUES) values = self.field.get_elems() + 1 self.field.set_elems(values) if self.REOPEN: self.reopen() else: self.product.flush() npt.assert_array_equal(self.field.get_elems(), values) def test_set_elems_rawdata(self): orig_data = self.read() values = self.field.get_elems() + 1 self.field.set_elems(values) if self.REOPEN: self.reopen() else: self.product.flush() data = self.read() self.assertNotEqual(data, orig_data) dtype = epr.get_numpy_dtype(self.FIELD_TYPE) data = np.fromstring(data, dtype) orig_data = np.fromstring(orig_data, dtype) orig_data += 1 npt.assert_array_equal(data, orig_data) def test_set_mph_elem(self): mph = self.product.get_mph() field = mph.get_field_at(3) self.assertRaises(NotImplementedError, field.set_elem, 5) class TestFieldWriteReopen(TestFieldWrite): REOPEN = True class TestTimeField(TestField): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = 'Quality_ADS' FIELD_NAME = 'dsr_time' FIELD_DESCRIPTION = 'Start time of the measurement' FIELD_TYPE = epr.E_TID_TIME FIELD_TYPE_NAME = 'time' FIELD_NUM_ELEMS = 1 FIELD_VALUES = (epr.EPRTime(days=171, seconds=38598, microseconds=260634),) FIELD_UNIT = 'MJD' def test_get_elems(self): vect = self.field.get_elems() self.assertTrue(isinstance(vect, np.ndarray)) self.assertEqual(vect.shape, (self.field.get_num_elems(),)) self.assertEqual(vect.dtype, epr.MJD) value = self.FIELD_VALUES[0] self.assertEqual(vect[0]['days'], value.days) self.assertEqual(vect[0]['seconds'], value.seconds) self.assertEqual(vect[0]['microseconds'], value.microseconds) class TestFieldWithMiltipleElems(TestField): DATASET_NAME = TestProduct.DATASET_NAME FIELD_NAME = 'wvapour_cont_pix' FIELD_DESCRIPTION = 'Water Vapour Content pixel #1- #281' FIELD_TYPE = epr.E_TID_UCHAR FIELD_TYPE_NAME = 'float' FIELD_NUM_ELEMS = 281 FIELD_VALUES = (1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2) FIELD_UNIT = '' class TestFieldHighLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = TestProduct.DATASET_NAME def setUp(self): product = epr.Product(self.PRODUCT_FILE) dataset = product.get_dataset(self.DATASET_NAME) self.record = dataset.read_record(0) def test_repr(self): pattern = ('epr\.Field\("(?P.+)"\) (?P\d+) ' '(?P\w+) elements') for field in self.record: mobj = re.match(pattern, repr(field)) self.assertNotEqual(mobj, None) self.assertEqual(mobj.group('name'), field.get_name()) self.assertEqual(mobj.group('num'), str(field.get_num_elems())) self.assertEqual(mobj.group('type'), epr.data_type_id_to_str(field.get_type())) def test_repr_type(self): field = self.record.get_field_at(0) self.assertTrue(isinstance(repr(field), str)) def test_str_type(self): field = self.record.get_field_at(0) self.assertTrue(isinstance(str(field), str)) def test_str_vs_print(self): for field in self.record: with tempfile.TemporaryFile('w+') as fd: field.print_(fd) fd.flush() fd.seek(0) data = fd.read() if data.endswith('\n'): data = data[:-1] self.assertEqual(data, str(field)) def test_eq_field1_field1(self): field = self.record.get_field_at(0) self.assertEqual(field, field) def test_eq_field1_field2(self): field1 = self.record.get_field_at(1) field2 = self.record.get_field_at(2) self.assertFalse(field1 == field2) def test_eq_field_record(self): field = self.record.get_field_at(0) self.assertFalse(field == self.record) def test_ne_field1_field1(self): field = self.record.get_field_at(0) self.assertFalse(field != field) def test_ne_field1_field2(self): field1 = self.record.get_field_at(1) field2 = self.record.get_field_at(2) self.assertNotEqual(field1, field2) def test_ne_field_record(self): field = self.record.get_field_at(0) self.assertNotEqual(field, self.record) class TestFieldHighLevelAPI2(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) def test_len_1(self): dataset = self.product.get_dataset_at(0) record = dataset.read_record(0) field = record.get_field('perc_water_abs_aero') self.assertEqual(len(field), field.get_num_elems()) def test_len_x(self): dataset = self.product.get_dataset_at(5) record = dataset.read_record(0) field = record.get_field('wvapour_cont_pix') self.assertEqual(len(field), field.get_num_elems()) def test_len_e_tid_unknown(self): dataset = self.product.get_dataset_at(1) record = dataset.read_record(0) field = record.get_field('spare_1') self.assertEqual(len(field), field.get_num_elems()) # @TODO: no e_tid_string field available #def test_len_e_tid_string(self): # dataset = self.product.get_dataset_at(0) # record = dataset.read_record(0) # field = record.get_field('???') # self.assertEqual(len(field), len(field.get_elem())) class TestFieldLowLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_INDEX = 0 RECORD_INDEX = 0 FIELD_NAME = 'perc_water_abs_aero' FIELD_OFFSET = 13 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) dataset = self.product.get_dataset_at(self.DATASET_INDEX) record = dataset.read_record(self.RECORD_INDEX) self.field = record.get_field(self.FIELD_NAME) def tearDown(self): self.product.close() def test_magic(self): self.assertEqual(self.field._magic, epr._EPR_MAGIC_FIELD) def test_get_offset(self): self.assertEqual(self.field.get_offset(), self.FIELD_OFFSET) class TestFieldOnClosedProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DATASET_NAME = 'Quality_ADS' FIELD_NAME = 'perc_water_abs_aero' def setUp(self): product = epr.Product(self.PRODUCT_FILE) dataset = product.get_dataset(self.DATASET_NAME) self.record = dataset.read_record(0) self.field = self.record.get_field(self.FIELD_NAME) self.field2 = self.record.get_field_at(4) product.close() def test_print_field(self): self.assertRaises(ValueError, self.field.print_) def test_get_unit(self): self.assertRaises(ValueError, self.field.get_unit) def test_get_description(self): self.assertRaises(ValueError, self.field.get_description) def test_get_num_elems(self): self.assertRaises(ValueError, self.field.get_num_elems) def test_get_name(self): self.assertRaises(ValueError, self.field.get_name) def test_get_type(self): self.assertRaises(ValueError, self.field.get_type) def test_get_elem(self): self.assertRaises(ValueError, self.field.get_elem) def test_get_elems(self): self.assertRaises(ValueError, self.field.get_elems) def test_repr(self): self.assertRaises(ValueError, repr, self.field) def test_str(self): self.assertRaises(ValueError, str, self.field) def test_eq(self): self.assertRaises(ValueError, operator.eq, self.field, self.field2) def test_ne(self): self.assertRaises(ValueError, operator.ne, self.field, self.field2) def test_len(self): self.assertRaises(ValueError, len, self.field) class TestDSD(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) OPEN_MODE = 'rb' DSD_INDEX = 0 DS_NAME = 'Quality ADS' DS_OFFSET = 12869 DS_TYPE = 'A' DS_SIZE = 160 DSR_SIZE = 32 NUM_DSR = 5 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE, self.OPEN_MODE) self.dsd = self.product.get_dsd_at(self.DSD_INDEX) def tearDown(self): self.product.close() def test_index(self): self.assertEqual(self.dsd.index, self.DSD_INDEX) self.assertTrue(isinstance(self.dsd.index, int)) def test_ds_name(self): self.assertEqual(self.dsd.ds_name, self.DS_NAME) self.assertTrue(isinstance(self.dsd.ds_name, str)) def test_ds_type(self): self.assertEqual(self.dsd.ds_type, self.DS_TYPE) self.assertTrue(isinstance(self.dsd.ds_type, str)) def test_filename(self): self.assertEqual(self.dsd.filename, '') self.assertTrue(isinstance(self.dsd.filename, str)) def test_ds_offset(self): self.assertEqual(self.dsd.ds_offset, self.DS_OFFSET) self.assertTrue(isinstance(self.dsd.ds_offset, numbers.Integral)) def test_ds_size(self): self.assertEqual(self.dsd.ds_size, self.DS_SIZE) self.assertTrue(isinstance(self.dsd.ds_size, numbers.Integral)) def test_num_dsr(self): self.assertEqual(self.dsd.num_dsr, self.NUM_DSR) self.assertTrue(isinstance(self.dsd.num_dsr, numbers.Integral)) def test_dsr_size(self): self.assertEqual(self.dsd.dsr_size, self.DSR_SIZE) self.assertTrue(isinstance(self.dsd.dsr_size, numbers.Integral)) def test_eq_dsd1_dsd1(self): self.assertEqual(self.dsd, self.dsd) def test_eq_dsd1_dsd2(self): dsd1 = self.product.get_dsd_at(1) dsd2 = self.product.get_dsd_at(2) self.assertFalse(dsd1 == dsd2) def test_eq_dsd_product(self): self.assertFalse(self.dsd == self.product) def test_ne_dsd1_dsd1(self): self.assertFalse(self.dsd != self.dsd) def test_ne_dsd1_dsd2(self): dsd1 = self.product.get_dsd_at(1) dsd2 = self.product.get_dsd_at(2) self.assertNotEqual(dsd1, dsd2) def test_ne_dsd_record(self): self.assertTrue(self.dsd != self.product) class TestDSDRW(TestDSD): OPEN_MODE = 'rb+' class TestDsdHighLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) def setUp(self): product = epr.Product(self.PRODUCT_FILE) self.dsd = product.get_dsd_at(0) def test_repr(self): pattern = 'epr\.DSD\("(?P.+)"\)' mobj = re.match(pattern, repr(self.dsd)) self.assertNotEqual(mobj, None) self.assertEqual(mobj.group('name'), self.dsd.ds_name) def test_repr_type(self): self.assertTrue(isinstance(repr(self.dsd), str)) def test_str_type(self): self.assertTrue(isinstance(str(self.dsd), str)) class TestDsdLowLevelAPI(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.dsd = self.product.get_dsd_at(0) def tearDown(self): self.product.close() def test_magic(self): #self.assertEqual(self.dsd._magic, epr._EPR_MAGIC_DSD_ID) self.assertTrue(isinstance(self.dsd._magic, int)) class TestDSDOnCloserProduct(unittest.TestCase): PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) DSD_INDEX = 0 def setUp(self): self.product = epr.Product(self.PRODUCT_FILE) self.dsd = self.product.get_dsd_at(self.DSD_INDEX) self.dsd2 = self.product.get_dsd_at(self.DSD_INDEX + 1) self.product.close() def test_index(self): self.assertRaises(ValueError, getattr, self.dsd, 'index') def test_ds_name(self): self.assertRaises(ValueError, getattr, self.dsd, 'ds_name') def test_ds_type(self): self.assertRaises(ValueError, getattr, self.dsd, 'ds_type') def test_filename(self): self.assertRaises(ValueError, getattr, self.dsd, 'filename') def test_ds_offset(self): self.assertRaises(ValueError, getattr, self.dsd, 'ds_offset') def test_ds_size(self): self.assertRaises(ValueError, getattr, self.dsd, 'ds_size') def test_num_dsr(self): self.assertRaises(ValueError, getattr, self.dsd, 'num_dsr') def test_dsr_size(self): self.assertRaises(ValueError, getattr, self.dsd, 'dsr_size') def test_eq(self): self.assertRaises(ValueError, operator.eq, self.dsd, self.dsd2) def test_ne(self): self.assertRaises(ValueError, operator.ne, self.dsd, self.dsd2) def test_repr(self): self.assertRaises(ValueError, repr, self.dsd) def test_str(self): self.assertRaises(ValueError, str, self.dsd) class TestDataypeFunctions(unittest.TestCase): TYPE_NAMES = { epr.E_TID_UNKNOWN: '', epr.E_TID_UCHAR: 'uchar', epr.E_TID_CHAR: 'char', epr.E_TID_USHORT: 'ushort', epr.E_TID_SHORT: 'short', epr.E_TID_UINT: 'uint', epr.E_TID_INT: 'int', epr.E_TID_FLOAT: 'float', epr.E_TID_DOUBLE: 'double', epr.E_TID_STRING: 'string', epr.E_TID_SPARE: 'spare', epr.E_TID_TIME: 'time', } TYPE_SIZES = { epr.E_TID_UNKNOWN: 0, epr.E_TID_UCHAR: 1, epr.E_TID_CHAR: 1, epr.E_TID_USHORT: 2, epr.E_TID_SHORT: 2, epr.E_TID_UINT: 4, epr.E_TID_INT: 4, epr.E_TID_FLOAT: 4, epr.E_TID_DOUBLE: 8, epr.E_TID_STRING: 1, epr.E_TID_SPARE: 1, epr.E_TID_TIME: 12, } TYPE_MAP = { epr.E_TID_UNKNOWN: None, epr.E_TID_UCHAR: np.uint8, epr.E_TID_CHAR: np.int8, epr.E_TID_USHORT: np.uint16, epr.E_TID_SHORT: np.int16, epr.E_TID_UINT: np.uint32, epr.E_TID_INT: np.int32, epr.E_TID_FLOAT: np.float32, epr.E_TID_DOUBLE: np.float64, epr.E_TID_STRING: np.bytes_, epr.E_TID_SPARE: None, epr.E_TID_TIME: epr.MJD, } def test_data_type_id_to_str(self): for type_id, type_name in self.TYPE_NAMES.items(): self.assertEqual(epr.data_type_id_to_str(type_id), type_name) def test_data_type_id_to_str_invalid(self): self.assertEqual(epr.data_type_id_to_str(500), '') def test_get_data_type_size(self): for type_id, type_size in self.TYPE_SIZES.items(): self.assertEqual(epr.get_data_type_size(type_id), type_size) def test_get_data_type_size_invalid(self): self.assertEqual(epr.get_data_type_size(500), 0) def test_epr_to_numpy_dtype(self): for epr_type in self.TYPE_MAP: #with self.subTest(epr_type=epr_type): # TODO: update (new in 3.4) # self.assertEqual( # epr.get_numpy_dtype(epr_type), self.TYPE_MAP[epr_type]) self.assertEqual( epr.get_numpy_dtype(epr_type), self.TYPE_MAP[epr_type]) class TestScalingMethodFunctions(unittest.TestCase): METHOD_NAMES = { epr.E_SMID_NON: 'NONE', epr.E_SMID_LIN: 'LIN', epr.E_SMID_LOG: 'LOG', } def test_get_scaling_method_name(self): for id_, name in self.METHOD_NAMES.items(): self.assertEqual(epr.get_scaling_method_name(id_), name) def test_get_scaling_method_name_invalid(self): self.assertRaises(ValueError, epr.get_scaling_method_name, 500) class TestSampleModelFunctions(unittest.TestCase): MODEL_NAMES = { epr.E_SMOD_1OF1: '1OF1', epr.E_SMOD_1OF2: '1OF2', epr.E_SMOD_2OF2: '2OF2', epr.E_SMOD_3TOI: '3TOI', epr.E_SMOD_2TOF: '2TOF', } def test_get_scaling_method_name(self): for id_, name in self.MODEL_NAMES.items(): self.assertEqual(epr.get_sample_model_name(id_), name) def test_get_scaling_method_name_invalid(self): self.assertRaises(ValueError, epr.get_sample_model_name, 500) class TestDirectInstantiation(unittest.TestCase): MSG_PATTERN = '"%s" class cannot be instantiated from Python' if sys.version_info[:2] >= (3, 2): # @COMPATIBILITY: python >= 3.2 pass elif sys.version_info[:2] in ((2, 7), (3, 1)): # @COMPATIBILITY: unittest2, python2.7, python3.1 assertRaisesRegex = unittest.TestCase.assertRaisesRegexp else: # @COMPATIBILITY: python < 2.7 def assertRaisesRegex(self, expected_exception, expected_regexp, callable_obj=None, *args, **kwargs): try: callable_obj(*args, **kwargs) except expected_exception as exc_value: import types if isinstance(expected_regexp, types.StringTypes): expected_regexp = re.compile(expected_regexp) if not expected_regexp.search(str(exc_value)): raise self.failureException( '"%s" does not match "%s"' % (expected_regexp.pattern, str(exc_value))) else: if hasattr(expected_exception, '__name__'): excName = expected_exception.__name__ else: excName = str(expected_exception) raise self.failureException("%s not raised" % excName) def test_direct_dsd_instantiation(self): pattern = self.MSG_PATTERN % epr.DSD.__name__ self.assertRaisesRegex(TypeError, pattern, epr.DSD) def test_direct_field_instantiation(self): pattern = self.MSG_PATTERN % epr.Field.__name__ self.assertRaisesRegex(TypeError, pattern, epr.Field) def test_direct_record_instantiation(self): pattern = self.MSG_PATTERN % epr.Record.__name__ self.assertRaisesRegex(TypeError, pattern, epr.Record) def test_direct_raster_instantiation(self): pattern = self.MSG_PATTERN % epr.Raster.__name__ self.assertRaisesRegex(TypeError, pattern, epr.Raster) def test_direct_band_instantiation(self): pattern = self.MSG_PATTERN % epr.Band.__name__ self.assertRaisesRegex(TypeError, pattern, epr.Band) def test_direct_dataset_instantiation(self): pattern = self.MSG_PATTERN % epr.Dataset.__name__ self.assertRaisesRegex(TypeError, pattern, epr.Dataset) def test_direct_Product_instantiation(self): self.assertRaises(epr.EPRError, epr.Product, 'filename') class TestLibVersion(unittest.TestCase): def test_c_api_version(self): self.assertTrue(isinstance(epr.EPR_C_API_VERSION, str)) @unittest.skipIf(resource is None, '"resource" module not available') class TestMemoryLeaks(unittest.TestCase): # See gh-10 (https://github.com/avalentino/pyepr/issues/10) PRODUCT_FILE = os.path.join(TESTDIR, TEST_PRODUCT) def test_memory_leacks_on_read_as_array(self): N = 10 for n in range(N): with epr.open(self.PRODUCT_FILE) as p: p.get_band('l2_flags').read_as_array() if n <= 1: m1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss m2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss self.assertLessEqual(m2 - m1, 8) if __name__ == '__main__': print('PyEPR: %s' % epr.__version__) print('EPR API: %s' % epr.EPR_C_API_VERSION) print('Python: %s' % sys.version) unittest.main()