pax_global_header00006660000000000000000000000064141517621070014516gustar00rootroot0000000000000052 comment=e6282b5a4751532cf02ac39505dd2f0bd2c6be4b zoph-v0.9.19/000077500000000000000000000000001415176210700127645ustar00rootroot00000000000000zoph-v0.9.19/COPYING000066400000000000000000000431331415176210700140230ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. zoph-v0.9.19/README.md000066400000000000000000000114421415176210700142450ustar00rootroot00000000000000# Zoph 0.9.19 Readme # http://www.zoph.org ## Introduction ## **Zoph** (**Z**oph **O**rganizes **Ph**otos) is a web based digital image presentation and management system. In other words, a photo album. It is built with PHP and MySQL. Many people store their photos in the digital equivalent of a shoe box: lots of directories with names like 'Holiday 2008', 'January 2005' or even 'Photos034'. Like shoe boxes, this is a great way to put your photos away, but not such a great way to find them back or even look at them. Zoph can help you to store your photos and keep them organized. While most photo album projects are primarily targeted at showing your photos to others, Zoph is primarily targeted at keeping your photos organized for yourself, giving you granular control over what you'd like to show to others, on a per-album or even a per-photo basis. If you just want to generate a gallery of thumbnails from a bunch of images, you may want to try one of the other numerous photo album projects. But if you want to also store additional information about your photos, search them, or control access to them, take a look at Zoph. ## Installation ## Read the the [Requirements](docs/REQUIREMENTS.md), [Installation guide](docs/INSTALL.md) docs. In order to customize your Zoph installation, read the [Configuration guide](docs/CONFIGURATION). If you are upgrading from a previous version, read the [Upgrade Instructions](docs/UPGRADE.md) document. For full documentation, see the [docs](docs/) directory. ## Copying ## Zoph is free software. It is released under the GPL license. Please read the [license](COPYING) file for more details ## Feedback ## Please report issues via https://gitlab.com/jeroenrnl/zoph/issues ## Thanks ## Zoph makes use of the following packages, for which I thank their authors for making available: * **HTML Mime Mail class** by Richard Heyes http://www.phpguru.org/mime.mail.html * **PHP Calendar class** by David Wilkinson http://www.cascade.org.uk/software/php/calendar/index.php * **Rycks Translation Project** by Eric Seigne (website no longer available) * **Leaflet** an open-source JavaScript library for mobile-friendly interactive maps http://leafletjs.com For a list of individuals who have contributed fixes, improvements or translations, click on the 'about' tab within Zoph. ## Troubleshooting ## ### GD library missing ### I'm trying to use the importer from the web but I get this error: Fatal error: Call to undefined function: imagecreatefromjpeg() To use the importer you need the GD 2 library for image creation support in PHP. See the [REQUIREMENTS](docs/REQUIREMENTS.md) doc for more info. ### Moving photos on disk ### I moved my photos around after I loaded them and now I see broken images. How can I fix them? If you move images to a different directory you'll start seeing broken images in Zoph unless you also update the 'path' field in the database. If you edit a photo, at the bottom of the page you'll see a 'show additional attributes' link. That will let you edit the path for a photo. If you're moving a bunch of photos, you may want to just create a list of their names as you are relocating them and then change all the paths at once from within MySQL: ```` mysql> update photos set path = 'new_path' where name in ('photo1.jpg', 'photo2.jpg'); ```` Why do I see some English phrases when I'm using a translation: [vo] that have been categorized Some language files are missing a few translations. Many, but not all, are shown in italics and preceded by [vo]. To fix this simply open the correct language file in the lang/ directory and add a transltions of the missing string (the English string should already be present in the file). Please share your changes, through an issue or fork + pull request. ### Change width of Zoph display ### Can I get Zoph to take up my whole browser window rather than that little rectangle? Try setting *Screen width* in the configuration screen (*admin* -> *configuration*) to "100%". ### Can I customize the name/title used in the interface? ### Change *Title* in the configuration screen (*admin* -> *configuration*) This is what appears on the logon page, on the home page, and in the title of every page. ### Changing text ### I don't like your welcome screen, your instructions on the import page, or your use of English in general. You could edit the templates so that Zoph says just what you want. A better alternative is perhaps to create your own custom translation. Create a file in the lang/ directory that maps English to English and tweak whatever phrases you want. For example: Welcome %s. %s currently contains=Go away %s. %s isn't for you. ## Miscellaneous ## ### How do you pronounce Zoph? ### I say Zoph with an O like in photos, some say Zoph like software ("Zophtware"), but you can pronounce it however you like. zoph-v0.9.19/cli/000077500000000000000000000000001415176210700135335ustar00rootroot00000000000000zoph-v0.9.19/cli/zoph000077500000000000000000000125441415176210700144470ustar00rootroot00000000000000#!/usr/bin/php getMessage() . "\n"; exit(EXIT_INSTANCE_NOT_FOUND); } catch (cliININotFoundException $e) { echo $e->getMessage() . "\n"; exit (EXIT_INI_NOT_FOUND); } require_once("log.inc.php"); require_once("config.inc.php"); require_once("autoload.inc.php"); require_once("settings.inc.php"); settings::parseINI($ini); try { require_once("include.inc.php"); } catch (cliUserNotValidException $e) { echo $e->getMessage() . "\n"; exit (EXIT_CLI_USER_NOT_VALID); } try { $cli=new cli\cli($user, CLI_API, $argv); $cli->run(); } catch (cliAPINotCompatibleException $e) { echo $e->getMessage() . "\n"; exit (EXIT_API_NOT_COMPATIBLE); } catch (cliUserNotAdminException $e) { echo $e->getMessage() . "\n"; exit (EXIT_CLI_USER_NOT_ADMIN); } catch (cliNoFilesException $e) { echo $e->getMessage() . "\n"; exit (EXIT_NO_FILES); } catch (cliUnknownErrorException $e) { echo $e->getMessage() . "\n"; exit (EXIT_UNKNOWN_ERROR); } catch (cliNotInCWDException $e) { echo $e->getMessage() . "\n"; exit (EXIT_PATH_NOT_IN_CWD); } catch (cliNotInCWDException $e) { echo $e->getMessage() . "\n"; exit (EXIT_PATH_NOT_IN_CWD); } catch (albumNotFoundException $e) { echo $e->getMessage() . "\n"; exit (EXIT_ALBUM_NOT_FOUND); } catch (categoryNotFoundException $e) { echo $e->getMessage() . "\n"; exit (EXIT_CATEGORY_NOT_FOUND); } catch (personNotFoundException $e) { echo $e->getMessage() . "\n"; exit (EXIT_PERSON_NOT_FOUND); } catch (placeNotFoundException $e) { echo $e->getMessage() . "\n"; exit (EXIT_PLACE_NOT_FOUND); } catch (cliNoParentException $e) { echo $e->getMessage() . "\n"; exit (EXIT_NO_PARENT); } catch (cliIllegalDirpatternException $e) { echo $e->getMessage() . "\n"; exit (EXIT_ILLEGAL_DIRPATTERN); } catch (configurationException $e) { echo $e->getMessage() . "\n"; exit (EXIT_CLI_CONFIG_ERROR); } } /** * See if the user specified an instance on the CLI or just pick the first one */ function getInstance() { global $argv; if ($argv) { $size=sizeof($argv); for ($i=0; $i<$size; $i++) { if ($argv[$i] == "-i" || $argv[$i] == "--instance") { return($argv[++$i]); } } } return null; } /** * Load the right instance from the INI file * @param string Name of the Zoph instance to be used */ function loadINI($instance) { if (!defined("INI_FILE")) { define("INI_FILE", "/etc/zoph.ini"); } if (file_exists(INI_FILE)) { $ini=parse_ini_file(INI_FILE, true); if (!empty($instance)) { if (isset($ini[$instance])) { $ini=$ini[$instance]; } else { throw new cliInstanceNotFoundException("Instance " . $instance . " not found in " . INI_FILE); } } else { // No instance given, taking the first $ini=array_shift($ini); } } else { throw new cliININotFoundException(INI_FILE . " not found."); } set_include_path(get_include_path() . PATH_SEPARATOR . $ini["php_location"]); return $ini; } ?> zoph-v0.9.19/cli/zoph.1.gz000066400000000000000000000116031415176210700152150ustar00rootroot00000000000000ENzoph.1;sƙ/&o(XrUzȶ3l%7IZ ٹ?>)(774 kWk1n719e/ӲK |3/v[4d5l}svy?/DP#b!Tn]RZ&xOon~G1}єJL0mulڪe2]u ($cE~EHK=M\߿D[Md:hu~F+ڍ5숟'm Ы׬++pNzۢ,7_Ͱ]DT8,?A) z Xx0Q+rʢ>JT}2YT\-n%~DƖrquxf__/2HQUZ5RuGhbJ~ plנ13;=V60{m.;kU!L+K+Su-VRm[7l eIr!p;Yg4sJR}#:]ǰaW'jhNK5P)VD#7@IWtN+ʦRͭЍm$D3)2i@[L^ _YxV;<ëR Ǝ`ob˹nxjrKad&7]'A4@=;z$c٠AʸlͰX|eѠ^e!_ۡjH`b=_cV2#NT-6&m{=2Hma410 ,gH.n^mt~K*v\oqЩM?Ge7pV콥cw=P=yn<-lp/=jz{7nn@-G}^ТP,VP AR!0[8Xő[Uݱ)e$ Z^dMWA| -zr?@Mgd&38XuU^lvʇ5SSZ(S y4QB >*?& Bz76 4d> !A:6bي%aur[@1(AܗE|zpbԢLh 3gAl@5tr:Qjr%m!ࠆJǨ :B &qO\gF-8b 'kd)O85V@>T7Bp*/KX#zţQF-Bl Q+L|k.0 (ˏ ĤG†o_p@H2͗y\jRPv;6.3 0l`2*`>D_ pS ۪aE9Gkv闗X1qp V-p A m?.Mg~2i7PzY#)\z'H1vd(YNQ1"[*B8?S6X;oFPSFBI_<8 EO*hp9$iQۍXP!˫ ;zef noQ>:\ygl\|k=6qǕ^l.N} twX-dHAo-5Z z+-\Uʖ.Wp<ڑSQB*kXvmW*)k]׶sp 0H3`OH"^L>%@<ѳc+8 w88)c"H UAj~vZIߍ|E0Rr^u …^@3>upTG9#XΑ/2X.5bm:;Sg`Us7Gc*Ёm~@m&[QxGęnҐJѝ"ƁK,ɡK1tdPgPrܤwܧn+)&I#|Y6I|duOM.rV6WMv6"}oosȥ :Z^⽱wz6%4c_GUgWUW߽O:9RZo`r!=D6[Y) cIY(&E̿QeǐN@D9Ɓi$Fuɞ~{kUq,{N(us!.4Vn0CğXXBm7Tt_k2~U'8i-~fy "F3J˽ Ź 59lk>c-@s#Ezg-8 :(#ureYNߺ7H#m"S+YPIO]F!jy~nF)u|*@t~r9Fx}):G3Zz0=_oJx[ǝN|>L)=ҩ?c¶p/ S˯H j`?ԇwK^$G/ө8Zނj5 9zY4o /OŻѻF@Y;hmko|6Wᙐ5c4sOn,%oO>M-))~=Ŭ|ZS/`=vuwuȤS=>:m;nA4Ա $Lxw_?ӏ qdۢi Y7FEw'^jM[ +eUW]%^I\v<$S\_iH|B?S?}ek }k98f5d k;9zoph-v0.9.19/cli/zoph.ini.5.gz000066400000000000000000000026631415176210700160050ustar00rootroot00000000000000Lzoph.ini.5WQo6~-iE<$xH/6eH~wGQ Æi*Q$K~3u(07ipk÷ UJ0M'W |ZO?ssXѐYn,[ϋr]ެ,締H_xAQ{K |!bqVcӇ:2x⢀\T PjRnO8#:#4uM/6YaK#xכ?~"p:ν=(gחQL4O8tن^ڜ.ܹxdӽN*hIx=4M i,SZv)1 tJ Lb}|8t}g*z0fs!t|wRdPXoX&I߾7;b+?R'zR;ҥ9bd!:|g?ļ\n7w/jU&v||L>$SCVzoph-v0.9.19/cli/zoph.ini.example000066400000000000000000000031101415176210700166410ustar00rootroot00000000000000;============================================================================= ; zoph.ini ;============================================================================= ; ; This file tells Zoph where it can find the database and it tells ; Zoph's CLI scripts where it can find your Zoph installation. ; ; You should place this file in /etc ; If you have no write access in /etc or have another reason to not put this ; file there, you should change the INI_FILE setting in config.inc.php ; *and* the 'zoph' CLI utility. ; ; You may have multiple Zoph installations, for example a production and a ; development version, like I have. You should define each of them in a named ; section like [production] or [development]. ; ; The webinterface of Zoph will use the "php_location" item to determine which ; settings it should use. The CLI will use the first one, unless you use the ; -i / --instance commandline argument with the name of the section below. ; ; If you have only one install, like most people, you should only create one ; section. (creating no section at all will *not* work). ; With only one instance, providing the -i / --instance commandline argument ; will not be necessary. ; ; If the settings contain characters other than a-z / A-Z / 0-9 you must ; enclose them in double quotes. ; [production] db_host = "localhost" db_name = "zoph" db_user = "zoph_rw" db_pass = "pass" db_prefix = "zoph_" php_location = "/var/www/html/zoph" [development] db_host = "localhost" db_name = "zophdev" db_user = "zoph_rw" db_pass = "pass" db_prefix = "zoph_" php_location = "/var/www/html/zophdev" zoph-v0.9.19/contrib/000077500000000000000000000000001415176210700144245ustar00rootroot00000000000000zoph-v0.9.19/contrib/copyright.gif000066400000000000000000000511761415176210700171350ustar00rootroot00000000000000GIF89a ! , ڋ޼H扦ʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZjz +;K[k{ ,N^n~/?O_o0 <0… :|1ĉ+Z1;z2ȑ$K<2ʕ,[| 3̙4kڼ3Ν<{ 4СD=4ҥL:} 5ԩTZ5֭\z 6رd˚=6ڵlۺ} 7ܹtڽ7޽| 8 >8Ō;~ 9ɔ+[9͜;{ :ѤK>:լ[~ ;ٴk۾;ݼ{ <ċ?<̛;=ԫ[=ܻ{>˛?>ۻ?ۿ?`H`` .`>aNHa^ana~b"Hb&b*b.c2Hc6ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fbIfffjfn grIgvމgzg~ hJhh.h> iNJi^ini~ jJjjj kJkފkk lKll.l> mNKm^mnm~ nKn枋nn oKoދoo pLpp /p? qOLq_qoq r"Lr&r*r. s2Ls6ߌs:s> tBMtFtJ/tN? uROMuV_uZou^ vbMvfvjvn wrMwvߍwzw~ xNxx/x?yONy_yoyz袏Nz馟zꪯz뮿{N{ߎ{;|O| { q|?}|C0}o}_ޏO~?|?~wߏ? y+*p3oJp |IF jp 'p Fp; p{ Epﻡ qE$*q;i(Jq{ME,jqԻ/0q E4glc8эt9qaʣG> J,!8D.鐌l$" 9rc$/)JjraĤ'MPr,FTjє|*_ ,r-o9EZ?/`,LsLb2fJsC5Clj3p pЛ伍8ωNsI;Cvs4=U8|b' Ps a@J*tDCЈJt(\&ьFТ-F? vt[ IOZBT*(mKҘ&4i eSt<_NZ uQWBԤ*U~GmH ը6ѩTňTUUuɪW= `-Y7ִlm+ zut^\ﺎux8 XuaKB㰌EbkJ/&YZvȬg5ЮⳤmhOKҪִm&V [ֺvm'K*ⶼŭn ׷-\wqHs3ԅtj׺ ^zwLy=KEz _w-ȯ+n a;x %L_x~9 {8!.G|](._4C[Əձwx@rZ"In{W'KYP딯L*7\βq0{#LfyME 6q3q,O;x>#ЄYDгThO>zҐt")iZ4h7MGOӠOmRլvX.KZzzy l.v=?c+߲f+ԞyWl[ڶӶm}{m ȍr[|nY}:wݼz{.5ߗJp|^M8q:\q'n8k?.Վ $VCS˕r|0h\m99Qw~6 }@ЏӢ LOҵWmJu]}5Ӹvzia/Kǎ4h' t?yV9ӻC} |xeo|F2K~7//ʏ 󜏨?/Jz~M=Xb?g0뾟O ķVR}=}tZ^9/?g ߦ~!!?韽5?GWF- Gג}} H- ϲ{ˠ{, zyh,7$(HDxk'+~zǂBw11x+j8hPu'v;(+WVu9tE*HG"tJsOH*9R4G2sZrY)*')煚RriArcFwQ†waqm)t(ppwF~ȇ"pp8(fo1oFfxnnh'&覉sBn1nhGHlmñ&ٖf"m8&8φ_l񋽸%(XRllh%ȌRktk%(h$kkۨ$DBj8$f @ rj(I!F:bix#!1iR"&"-%Bi "")h 9"IƑ!62!!#(iRh?Ғ+y!fhHB5!9zO?)vFHuF b͈IY M%PggSIlZfg•]f&_)qfog9d)'nɖfqI]'bfQ|җ{ixɈ\Ar Xxe☏(I1eYəN!e" )MњH&HdbYɛ4dy?+i1A˙Y+ Aci:+٩߉96,IaŲy94枓(R)i0Ѣ9-*bz/-ʠ -jbҡ&v." QbB%!.' bǢ/3*1J~a98j/=taC |JDzasMLBO*naCWj0[ʥ a:c0hjaiak z_AuZRrz1yWq:|1JQ`zyO!%ӨjyF Z2*B_0cS%3ʪ:3j Aj7_ʫw 4A 4Ǫ,Aϊ*4J%:_Z4Mӭ4犮9^R:5*AZZuʯz^+5k QKװ  !s;ᥱd^<!6K1l)+60`p#3+WE77Psó=:7?+]{cGW7K˴琴#7QUKeഃ[WZ[8_ еwe iϥ on8lK`y; }˷\3 k9ʠkȵ9+Ec+ o˹;\ Kk; K: к#k :[{nE;Z+;뼨ӫ +nڋ[s뽶m+i 뾙Lux[ ˿(qv]Z\ KOkXL󋷣  l»[=-l(,T6LÅ;3Q;= QKA,{?)IK<hO y@X *WĪ`zfAeLghnUZbZqKs,yl.rj{:[ǁLGEZ\ȏL\.lbTa*ɝ|JLa0MQ{4ʫ|_ >f|Su+glBUM R =]AzTMAҀ= "mP&}ҁ*B.02O6-=]:N}EB=DiF]@TJCPNmPRMTV]Z\]i^`=b]M,fe jMDn=u=r-Dрv}mz=t ~פFD$׆}L؎ Lٖؕƚٜj٢=ڤ=kݯK ڮ`-K۶}ۼ ܾU½J ۸ʽ}m= MJڀݾ ,ޝ  mmIPޞMٍ=-ߏm .lޭ nNF ~l !&#Fp/n4^13rT6nB?A~>nGNKIMOm~LNUntY~[G\+F]/+xD`gikv~Դs>lwymm*N|҈慞GCvf.ʓ莎&鋮ӑN^Hꩮ過G>l$N|N>얎9+Nl~nH+f>N~no.Hn̬ .>O/lWk6"/-O H.g9;~,/5OEGI(s%C>WR^\,QU^Y5~\\6ivq\-s !%&)"/*579;=?ACEG0MOHUWCP]_acegikmoqsu!Y{Ww|ᜧټ;k_3*`A&T8b` :8bE1f:9dM陔eK/aƔ93u4ԹgO q^Y.hQG&UtpL39:jUsPYMkW_C+FʦUz;n]wV+o̢/ 'VqcNJzL#re˗NfϟA=gM+VuHԂZy=vm۷ƶn߿ &xǑ'W\hq7̝G>s(X׾{wﰱ|yg'9O&}{ǧ^ t/!, P LPAkAP )lAP }UPI,(YDZi}b$Ay챪1 ,#s҃lɒ(+i2'#,1,D5 L|S9鬳8kS=ѡ3M; T(C]N<mGG,D)tETM9}/I- UT0SQMIW+SUif ]y\o Va^M6;Y$gXԔ~m6miGVq\oMWw wM1[V׭{ixwHsW %`PMA.!nD_]MX9a,YF?QF+cDnZWNYi0f]]ΙWGo矁^u.U{ZNZQK&--ޯElD!%[ن6pώ[ϴnU3fSS\p{$ßo G\11r'r?\ѭnQoqSIouY,Wqϝݥ m]቏OxON/k^vOС^够H//{IvϏ_~gl} 7E{BU@JG To S"$\.B.ŃH`¢Tep-tDJB$)RU _Cy!Bb6pE4Cp.UE)bYMs'q]bx+>}өUF5f3qu#,X7UbHA&c1[rtdC=Y$#IMzJ*[ur4%O@e$OKY[eu`Wr%Yxi[jF V-LeRY&%Ԗcsմ$)gI&Nq:}&7Kgase6x6*Â;KvEo`O |TP?Tu;G!x6!A;5MrшjĨ3=9P+Ky¤TtMKUSQ3%nEbO}K!yQCS5ըnu~OgUS@8ե\5QaL$+Q .ke+ְUʕ۫-*#eaW+VG3 XBO(e,FFv]eUy%1M8ͩgQ;КS4oMh۶•m1q\q,i\E]n]]GǗzyC V܁r5{ޯtxz_)u+"XfeR v[vɰ1Y.1+,_*Dڇt5-ʦ!ʳz1~d%Oy &(([] #>g +eObyP9,:N f9f3혆uљυ;̘~tЈ6Dhӑm!H; gMF7Ʌsԭ.Jba՘v_SZKq\:ѓLga/{w!YZn=Fqb(Ķ=nv@Mmu;'zn˛s|E@`9]k6q5X{iclog/ *S%,bv7˯ |͸a0Kk;Zq=' tmt)\UҿpW[:RuB'q~zB;qNw̝'{wپx/xJ*GHT|X _>]OI۽AO75.yџ~8gkx#yq ׋w }ؗo|bW|O/Fg [}an_OD?@w?xo߽(;P '{n 0g'%pONA0pI9p0IfꔧQpUc\om`Cy$BPo mP ӏ O yP z} oP -0P UP 0y0ϰ CP ? PYKP p Pӎ߰vq Q!q#{1q # POAq%OscPw`q5Sgqvp?OquO&O͐'Nݐq&MQ&ı5 %Ա͑%ޱ=s% M1u[!N!W!! "N""1R#4###A#D$a$ QR/n%3"a$e&N%upn'N'c%^('or"))'*N**!+%N+&rᄇ,M,-,ђԲ-5-7$2. ,-//}qG0.3..19N1ms 2[M2}2143m2')A]D400Q>5C4/a)T6qM63qt77wRr7o8R879M949:oM:89;L;:ij< M>=S?=o@W?=<ASA'>!ABLB7p0?CCCA4.DkLC];QTEE'AaDFFSnptdGJGoHgGD NIKIEJ'KJIKcJgK4LMLwLδMIMהHN{KNWMOwONUbP=KP@}Q%KQǓP!$RMݒB1.SSGADTTRQ~UKUOa5dV}V'QqctWcW-SV_XJXWWXYJYVUZZgT8[ JZgUZ\N\-\ Ե]HS#Z&ε^[ב[ ޵___v `L_k_aLal bGb7Y1$cMIc cA?d5Id^QAeIe-]aMNfMflpd_gUgG3nVdhhdVii=fjKhb~kskgvllG`Զm]j]j ޶nnhmŶoHnǁepVoiWqqk!n%r#r'vk0rsyJqlAq?tUs77jPt5uStitaWhdvLu-vqDwGpp!txGxmW~yyǒoNnzxws7{VzדqW|s7_ȷz|M}y~wk~ ~b;y} fa}| xU| x%%x߄h18~7zA^W׃æӂׄ]Rxv)af7!XyxLIXw)4Y8xO88LJU~8|xoW؋8x\ո~߸w׎~ x7x6痐yihxd93Kv!uCX}&X=9,y)Ë7vYq@D ;yQm9u#|y]9@8u3VySYwYGyVo|,?ș9a9yfyWY9J0x:٪YYg֞q(zl ?,'Qy9چ=zٟEzw89UZ/]:#y ZqR }w9tznzkzϸ'ЬzazoV7źF:ӬE4y9&vZzJۭw7Cڱ`Z{$W5;b8{yx:M]1zQJNں_5Zm;p{dKwcdԳ#+uۦA{'صmHw;㻽?[{;fy+9;;Ỿ9k7{}wؿJۻ!< Qz1ܾ{v5=͓;MX|[!ͻM:WiuqC|mGxKw…t<[˺;ɡ7P+u#a\w{ɽ|msHZM;||j|í{<З Л}BQ#$Abˏ-<V;{I=@0}3PU}ՕQeg\w=W ׁ=х}u-U՜֝}K]ʯڣ<֫;ۧ۹]r}y=?: !7ݲh ]G=޿lн\}=m]٩8` ^៦)]-~<=>|E^~to5>1c ~aCCmM x<}z~R1o}XPw~~>~ɾ>~>}>~=^ %_1? ?5=5?AI-_K?U9]aEhmq?uy}?O?_g?Tɿ???5G?ɿ >.?rjSz?#Y'+ۺ/3]7;?0(#20'4*5(4rޯ*3:^n8.;>?` a!b"a#"dd%f&g%hh'i)j*k+h+-n.b-o/p.q0r2s3tq45v6u7x8699z:{[y<{$>_,h „  le 'Fh"ƌ(r #Ȑ"Gd$)Pl@%̘2gti%͜:w͠T~nj!ѤJ;)ԨRR%ӫZU(-k,ab9f]XڸryK.޼zawbۿE-l 3n1ZK2t-39ۣL@ѪWD]5زg;sMδimQw6‡/4ȗW09:pSnzՖj>;oaۏv 8 B *8r J8NB!jpaw8b C")H}b+8#x#9ZS.șAbBy$YɋK$99JUZySᣖa%x]z&e)]p[g)Z9'ubgk⩕}~ :( U()DJNW[jl)Hvi*_Q*5뢯ڪݺ+*A+3 A*ޱdJ  L{-pV۞- yM嚫ٸX纫C;/{ @[//.&0µݛ0 Ep0ſ@\1tqR1j2%#Kd2˚21O*,3΁М3=#3O6#3w]4IrNB4e05U25MNGk5@-5auZ&6=`6mWLfO63]7ypwLrw78D~c8 8k`p&ycyᚏdyiMHseި:خSٰM{ٴO;ڼ?|w6ċm͇|ղS}֏&CS >Fk#>У>d~LSc?!%-0愀S` q(X1 j`06 $F*? J"y*A"ng,Z9 C2GCz>@ҕ 1"X ʴxi!a$T},35n e3&ѬgTDHӪlQ&:m3lB٦ѭo ^Xu5.{SJa]˼;W eQy;~UoW *uoy;O귿xoI,O6/Kq 8O.< _B9m0N]r.ψak0>vS<v' Nb,ޯ_@kq5qB,0'D.E / n2DLE>2jR֔58.×:v gKaUT]<uMHhA3ɀ^3LC+zэH9w|1-jhzӜ$RjtSOMALBs5 ZVwaMzu%3~AGYC!-8lu{mmm ϊ~twom}sWޟ>b(o}|=뽼>s/ӼpuǮGe3>K|߮^Lj ǿ7ц;8QK0w99p[r=Gu^y}Lё.HMӡu]lA՟$n=K verS|m fSu׸w6~C4#~?WO>眏}^^'Az_v}]y^ aصQzթWOm|qh7_BO_X}bG_N{f;1vEW#+giQ_=E ` L[2RE_Z:`jr`yZ Y R` ޙU x8] B v B b_b` X 2ZE&a]X#L[ va v\H!yaaVG; f[!![7G b# %J[$Jbo@H%j%&[cY'`(#`)bF*j#+\c,,b-r/_.¢h%/:"201c2(_3Z342R5&6~c7J4 c8)J(97F:R] 5;c<>b??ʝ?]YdA#AXBBDdZC6&k[rfgڦZ&ifmo^ap p&'Qga"gj*Pfq:UBgtJguZSbgljNry'Vwh2gsgLf'?\w~uzf'8gGg~gb'Z?( hg1D%*2hy:NH|J(5f\Zh.b(jfFhzeth3(}Fhkh)hrhwT~hi j&ii*()_8iB)JiZi RjhzT钒ii)t2iiariiioiH &(Bj!**i:JJiZjajjv&jzjrh{ۡjRjjGhrja j(&k_8k+J+E.ZUjjzkAk2k뎆j+k렆kF+b쮒k,*km9,GJkaXl!,jFax잁l쨦l,aȪrlǺ˚Gllfk,k-~!m*2m:v1KJʲlZ-tMSjO,z-o-׊,d^٪lb%"&mڭ]-ВlX5d m.Q!n*.+j9RImAY.LanjnzDnĦnߚ:AG2nѺN"l.8nn.)9J n/.!o*r.f8JnhY!aoj/x/o/Un/ RojK겯j0 o0_V*2o:0pJ^pZ0okކ)z0V0 ?p0l0 . pp'0p Gpp5Vm oՕ,+o ;1/#Ksp [񗁆 i/kxfG11q 1>b0gp۱qgq12 'r!Tq+x1:rSKq;=.{#3揾~~:>߾-?-C97??GO?V-_?+#9?mݮS+8?ɽ?QNZYCq$KDSue[cyks}{ÓxD&KfsBFSjzf[ncr|Fks~{~$,4TLT\dlt|8%-5=E%]emLqv%.6>F^fn~Jq'//FO6gowVP DPB>D-Q#=~RH66DJ-oӀK5męS',=OTE#ETRM0USUU]~VVĞE{ZgiݾW\6lW}]X`…9E b?Xdʕ-eΝ/i-hҥMZ5ԭ]Y{kڵmƭG$}[ōG|p桔?>gѭ_Ǟ4u*]͟Gvz{f{w)=~/@$p+0ADPA0BeNB 7CPG\)HD1EWYq!_Foı˱GvG!$F +2ɱMI'@&JbJ-2;,e2LML3D52LMJ\M9礓08U3?SO?+>C PPEeT-D3k4R,MTRK/Ŕ!J!˴6SQG%P+5UNMLUW_V[6fV]w\s_ Xcdػ%Ue6Zip֮i%k[*]B W\sErJum7^y%]x ^x_-J`ޖNR8`6J)b7Vc8A&KEdQIe_e`Ph9g7mYgyNg蠉hf"Zjfꍰ.Ok;lHl.lm>ᦻ~n[oWS G< ?\q' )0s4@WGtOQKY=v\OGhGvw wx߁'xAO^y_OJ>{oFg{/|aȧeg?_~7~Hu`俿0!@r 0 [A VAVhЃ,;rp$Da ia,T e8Q4 _CP:ܡ{шGLD\(QLb%JqLT$E2`\F21 b'4эoGɑtcw`G|9UR! HF6<#HJҒ%YI/hr%#=PVao(e* yJ7R l+eGXren7r6/LbQ, 9d6Sd3YMHӚPf5Yl"apLf9w&Yל&=q,&AЇYk%E/яY0Iҗr'}#M;ӟ~Yٚ"QoԧVrj!UկsG-%YַYɊ]׿r8a~pz6eNviOXYmv/jqsXZupywy}p#WIY/WOxūAWG8 'W' /OyCG_7GW?OWυN;=CG:䌾ts%xczԽtSMWzյ2w[ü>k"{ڥuv;Ko_{*ZswdX{w›H7V 7~b|?"GA|wuyγIUE*З>GMza c}]zڷLoru6>UT|?R#~|OTAWSg~1}R?Yoo?R?FΦ|Z7?$@+>tEa{ ܿ4> @ L=A@A$<dA?QAkt;B1D072D31rK64567CBCa;̗:5<˳$˲N^m.~/?/ OI<ؤ … f0ĉ+Zv6rĐ$KT6ʕ,[| sǘafҼ)&Ν< 4СD](ҔJRK4TMPZ5֭@ذd--6ت]ܹtȴ.z7`VI eNVÕ8iI^"{pIX)jcf1iAp KubeyY$w0*h !zZ(hJgAg$v飒vi>:˦Z* *1e*f뮾ѫ&0*R +V,> !Nbеn+\4("4.J~Wn>Юy*zSR&HNL>ۮ-^rq'V܏v0µ*Lu<1;(r2)12><#SY2==NԖN ?5؄-v$[u}Ki'׍xM:w#ڿmwNm}紸y) tw vG=uyzL'^bNd{Vtæ3zߎأ.#x|_{?H$sNվ}[}柏~c}9_zoMk/y^fzO+/ në-&쀱_5B(jp,l H0C!(0bDŽaDG qD,"-bؿ>!"(*[" h,jq\Ls$n`MtrhFQ-98q{숧56̣ (C}FmD*rJ@b=U8ƈ@K'@d'? 9Jd:&II {d8Pr%bNϕ=D%)K[ 1db&)8_T 6SZs7ͥMT!D4`~5l:שmF<ε.9G|^LXIq dį8P@B $eFkaJaSIa˼@U'HxDʀTK_pih&A>'ڨn 'S T}@9 RJ/%(1MiKl-RXXUuT%5g04!#~ +e0W?Ԫ \AԨj7Y=_ Ҿm`ղ3gϊط]h]+X+5(שԨӃCPg?&?:%g[m+Kfa9Tg ޳S-rY;Y(uhfS[1{څv@Tz\5,m% uk}p\ aIsK rEʦ]/,׭Ŋ+bŠ]49_W T![Ԃxޕoz,mհsaoWO7^Q\^W**.~W,cŒ3U+̫nEsaN%q($y81>pᢽxZ*x}}+,3* @sf+7)H|'C9N t ?oy2[fhi뚫\fwjYs OۧGY4Nu3[S^:-Z_3kjLtL:K|-Wgt6u"-+E| dE^hy;i)]pU){D>wj]k`/6=mr?\ἛqEܽ8oǀNw|_["!G/}QހXS{bSOAw֓,;UTQ׻]Gb~855ck7>b|ʀ6]Uv|&!'}~F}y|'~Ye}|}v\u~_H"@Wm y3'q%*h-w׶xe4{0w}> qG]?}g+:Ȁ瀨ǂ0{(T](`Xl'Hv1'u h%sW#؂PcoCDŽMHx=l(Ov7GVX/}ܖ{|xqpChyiwmxvP"(ix(Mv؇X'50^(~ hlxg_oDJ7pr8k389hȃx]wR`H苽Rxq4{DGFP1#mh(X(SGԈ@ x[ɨׅUX89QoGGWI'Îؑ)hnH5芶Z,(7> 2[8yָ?ِђ(x(A Ɍ6ؒ3Z@Y$I~UR@IW{`vxcGzXUX옕78}ypi[XQ)ى镞9UO&љ IHs) viiYdM9㗗%h ]dhIgIqReɖHP)~ǐ_yK؜)y8 f.cٕN H YEmi}oo<Y#ʙFGꝟi ]gJIqyK:J(߸ȡh#ږHٞyNV>:ia"FljIei ,ʞxY3E* ,dXݧ6ZhZ_NemʦY~3r:?* 噣hM:]~ 8ʤ:0 I!Vʁ q:G̹'3j9}:FWzZd: c [ jzQo $aŠ~ʝzj *YL9ZjȣvAuhsw=V:-V\ )xَړ[z~ zJ~ڰBz:';zhJ ˱δӊj&{J **^j4ziY([fCʟZ5_)Zg:Űʲ+;5碅J/K4{ƶaۭ*m˩i[>\ [ |{2"Z_|$T˛P's++%6BKszDɯ;I{onKF;*l;˴|jN[] a{g{r(3 Quz˲*q;ɻ{ѫR }ka)jJ)yQ˚9k.۾۲kR[ۼ+ )!{:EX隳یd澔A ܯh{ ݫڹ {*<۰-k'/^,w1x\C;̙_(Q/튬xPJ̸{bl&p<ǻn v * ̂9,kIK{Ĺ^dܬ!O<6rLe&WŞ 7zȎ\(|ǚ{T lƎpe,;^W-5.>ܞovߍ ~i9]]A^O+r^>]ވb6RTaY^,΍қ6nLnVD>r_l.-\ tuܬ택dvM }krr.ǺmnzހmQ->Ğ%o/[J_K2oD.CNb ?Bl,o-k0:O:}ؾN^X⎺^Ю@r2OEy+<j^%n~ e?mrm^E\bQU9}oVXO,*jO_?tˬ@bm/O_sOS/Ə?/َOa o?q?oPAϓ?|O1a){q֛wpHH<ѯ\dl&k=QX< M1Dh8e8C~q/W0&i5׼VQ>y_ۙ|TDش /% +35(1;w.KaڲL[;7a_+]oboigqG"StQwɒ9{!jCA7aݸeUyEmV3ӷi7'{[<|F,€%f4828duD -UDdI\Vp'ϜjP 9tFTZ-)r{I0RxW\9PxS(0`j rOFp늢ZRlIױ͹E6 r+'{X%}gYB˴fH[5g͚P~_Q3&RH,!ôTrCZtrJ27829O5P-Q TlTs/MQ167+[LQ )I'!Ӿ]O!0-YtV^(մֶB-RA'o=WF]\AUmԪ5/UWDϕvHkO]0[xe-Yh0]{e`m=_u%ݰ5@U^a#cEQx7dl>#Y؋=*v8c]g:TbtgCNg||-㐣gS)QNGfQThm iYn˕UæBɮk^:p^/[ƾefzif` Z[bM qs5O3nş=`ɓ>=-LXaeOt&r;mfm@yF^_3w]i㞼%5z嗟~-̭{gw؋u硷{yhvZ}WI/j;P;`@OH3KiO+W?|+' .\F0Yy^Ql$,GB.KDb^w(Q86nnw$E#ЊSFɘzsL#x f31F2#KFPd߈@r?FYK[!t WJ]r4$GhB,׳F3OԂ5=7CQse90lh)Ui?ӏ 4gzo |>YMnBӌ4`9DRєCcèc:cCAy{jt;M45Gt@MwEd+ʎ3C QP' 1Yӡ G & UV+)%Iaԙ EխF4W\(F 7iϪ5&SYTbQ)ݪU %R 5IqDNkSO249)b:Ŗ;lZ]jV ZƦBjYCr6~ lj ־VaZ˪`3 }g[Rv-gZt5S˜Iwv-Tց6xÚ󒷸}z5KSV\,9β/mGKBK_䎔c;4eO xk_6wƕpWtoӡ8>s]]U![/{3h]6:qs Bg{Pig .i>#l^;Ԃvuz܆ 1:U7;hgxPsq?ufIݗSיGjͿo6:q^|ޯҙnk|B]TO'IbV]W߯;6sMϽaOʃ-"Fn쑏|ZٱuKm8 ot:'Oy JJϹu4.}{P'̋{wۢ6{Ìq᧵x_Ȭf7=oB*wcKA 䱷]w~۟{?|8 |jp/8P)P I00 RQ!qN#q)v 2ag#Q$/pum RҚX\r1A$؁ ݡ#won#ip"QR m|Q*O$+L)C$-+)2t ?30@ң/O8r*78sٜR9p2"1\"K6WKb1MQAK6˳C5?QROES5PilLבGHRˢM+G IuHGN-%QVOPoSVX4'F{X5(Wu_XBII4'D85BY #\cU[y[rYׯuPU\u\k,MrP9`V*q5`QU^Y T!\ZY{F P&Sc_T7]MbonG6bWvX[PM#XsWuIdQTGH7.-V`٧Uqf_v9uAM6Su X'6aŵ;+iSտ)eYVkša2k@vlVIcO][r:m?YQ㔵lpfhKu L)Vc[`1Q/'̉88gW򳏑x0_\}%xnAօe8mv׆~xuigxoQ딗ؒ6i%%x҄~IMYx~}1 y˕Y8YWHWEnbgs\XYOٔ)9ՙUY9TI6H-ՠ}Y=m$ەwBUV&i.Eיqjh5D:JoYzcaFq-Zw?1Ѥ4uڥUҭbmu☨wZ{zҎ{+ z9N 9%Z;#my]8fS:`:BKk犂:7/ =qZ1u;_CH%orKKc+% ύ:q;绯wGZD[:;Z{۽\nqY)ڲY(}tua@ [y1ø<6ܻ : k;IɵS[{xX)?s61ۚA/sG9I; M텞ŇFo^g\ܾk| JNj:=[OAZƻy˧m]{ };˃ÑĽ-}1}f̜Ѽ$o'o1Ϸ[[̓ܘDEO?̝ϥ{ӻ<IH{w}]}[|Mp}Е\3٫CؿU흶[}9l|ZV+Dž<}}5޷=5\%E?;TK~I|۽wEbޥݒAc]Q]<=W!Is)Gʫ<5|=Y4o~|I3]Oԩ~ɔ/oW}U>eu~x1v[^Q^eݮ-Iޯ=]3>wnۺ?>)G)wڅ>֙wΛK>~xU?ߞSim_)_v~!]%CEIE۹Mמk?~!Ru_X~=o^ @>rg/t?\V'VBqXi=i'BYp%'TERy+6Umiמ3]<` ΠSO TB"ҡdd#EU奧%X&`iff+lhkl*fm*-0n))/*.0h55vopvbbr,*`8gb*+4WS(8?cHd YFed\Z)F~kBXYfk"Ɨ.`vf?HIy!QZsZeFzXn(9bOF8WfɉA镦ԹljBtB]ij}i Xn~i,,mv頕'Ҫ( QHZ̦&`wpzj{#ݾSf**KٹJ:Fg,Ym Gzå^&p֊|r;2˝rK 5՛)w3\4xzI;m&+rTyrOc,!{q߂ h}6ږ6m{\rGkӐ+90>2_n~ |:^>髿~g;{?|cO1/n׿Du-t4U&Ex{ #(o 6?/C(BoX\㨵fw,`=ڠhF(!2!V$2TZ6m_,^Zπ4$`më8fЉRToE+jDeTd;Kn4WPA-Td~6aAIϛ|&;;SLC=9q#ڥM,SMISr<(Ʉձ} < SMj}+\=誸fet+^O0~V5XbuDJ+bI*6ql,d#{?qﱒlE˳66{=+fC+Zu+iSڤ]-lّ0#uQc򶷾-pQ-ߐ{Ԣ׸}.t+R7.vۨ\$Զ.x+񒷼yӫZREz+ҷ$|_Һ5W8m~+>i6/ S0g$ sX-"<&߉S ,cRm[6qP7Q<?7Fw@>2V+4N?E-/ɪ\5 1aropK5Js3e8ә-3;:mM{jr{eю>2-imalҖgVҞ4# Qo>ZAh[Ѩ~5+Xz25a(V=5;`ʷ.u.Z~6c;hS}B=6;pY*J~7:x*V:73̀ܰ.8BW8e;S8nn3G8%9ָSo,9c.Ӽ|s>9m'F?:ғ*N:ԣ.up|V:ֳ7\:.ݼ^/;Ӯ.ӽv^~ ?<≗3<#/S<3s3>/Sֿ>s>/Fӯ_o?/;??_  &In. FNT= fnby ࣙ Ρ`Y ``  OY !!&!!1FNA`Y!n!v [h!GQUMQLF!֡2>eS!a˽!!""bv%"#6"!#F"aH%% WJ ^%J"#B"*6b&*b+z mJ('-*+2)$0"b,0c1Z,~V'(Y..&1/V*V#&&ch#r2N-^\L#Ҙ#HE#y6V;f#6c< :3 >=71#A*q"9:?P?JBNWEE&dFڨU9LDD^I:G>WJJv괤KaIfMHLWOOdA$H!d(%0%(@%oIeQP֍Un@"YVaSeNWYTWRJZZn_[^ּKn9~%>6He5%%&%&ff&\ b"bXc.%^2$y_`_cJf&Vg:g&Ʉfې}^!X >d\f]eM&[&]h n:Moo֊QeCllTprUss&DgP'xY&qN&c%'۰x)vyTzzJg|gtɧD8ay'gg!}€zTށgme%2(@: d'XBh$(E뉨:VʉVjSa_'x^dž֨6v͊ӎ^hBjHvR~h0)g("ƣbz(t)a)zfX N(R2zkO)*NiA:Wޜni,itiqj攁)m)gi0.9*AEN:NS*VymҪݮآ:-̦=' *N.B Lh-Zm)g~W9v$mv}FrY.b%n.&f+قnP-j=EB-ٹ-쮩ZbFK~b*,MB؞XRA֫+6/>J#f-jn{»ŃX>o^&.?:nX\ po$*0 "geF_0[Z;ğ^ o 즰 3Ʀip 6U+pLp*C/{.XFpZ G%vuqRc021'=171sko/;񞡭.l_#;)w$g0WrEr?1(&g(/`**\'`;&I_J;6*7˯7s4E'q-1@4Èbt(%t0H% 7D34O2v3g@u5B709+VwHgH/W{@I3M2P4N7gܞoe&"2orX-RUߩ0UO'^)B$I53z"+dgc5uZt>,hspj\LHS(PͲͦ/v7'@_oe DbS@pfvF)q_1aumcAqt^uc:vX%mKvg5sg?nkqn4g/ 7k#.IYl.m+r{wn6W lb7yG&8sPW6r#8e?828bj~pc*fxgLWFxl2;4i|vʬL4~.~4^vWqOsWA/K8@6h7Pw+4O7688 ~㗽=KԳw=O{={~?k<+)yԛ:p'~.y>3?swCkS仾v=w?{^~$c|j}-ԕQNZX޽B#DSOdE[6LC:C/pD&eȄFijuN[n4o|֎Ud,X_q~8o #,3$AddR|jlD<{C $%8e|k]mMUtM~ yAf0XNh~>tiF>) 6?MkECϚ5ğ݇^(]W^A1a `R4램uRx.$%hbJ/> Pf!rȉKQ^hfy.o1t RnH3yr待,4Ś&>uEEV̚,Fg㩅-slǽDJ-wا0C9w ރ8dC{UVtfa)l-+FHY`2eucFLG[W, 2q^n-<ŵ1.}cc6fc7Qv|@$y-<'n<wP/?.g](v6}XȜw?BG4'<8Pj !zE. '>_>?Pv8y>oB\Jlȸ6iu ,H! H,j"Ӧ)bH#7^"RX=6 RHLIDdAO12YbAV剄cHLoHL#(hEk,e%) 5F14d+:,Z"h:^.WTĞ,J5I-%aa1jjs.eqn[{A*2zg2WB3f''iɗ.e/c"~¦/M (#xCjj? a%ڦyЌBs,IяZ3ܦZR.th iB%JSԉ2*)OϖԣD89rv0'!?NDժ̘ QԣS1NuİmjMOޱnh1oyͲ2E#EͧW5[;osAkJ]zR26[bӻ2g*b}j}ZD}&Gi)ڿN6c 86r0{*c[իS=ejmktjݨ`C{&v.r85*igܿδ$.d; QdRֹ+jݕ7ML=f6u* ծHK_Rlv ߑҒow[2ؼ-l ䷮m",%v؜kNj|^ _wd{\c>qZUL2Ke\uq~+پٔ5Kذ .y8-KY_3], g{.8mLaEnki ]VQd+6nL9fБr= c#~rO#xNfqd:͙ p|b?qKFgnRoggX*mOJ߃'3+ᦧ޼ݮy^|gMz{옰goޱwo;YO1zwj'S޿V#| [B~o˯폾ZfX+3;==õÿ7 SSl>+ ? ?+=6<> [-'sh;<󓪥(AS6[p$ K8DA>#CC8=t?;Dv D Ź3+<3\B>lԿOLQ,C9 ?TEH#"dY|%DA,DV_B7=$STe̻RLC½FA iDG| D1EnEf<:Ŏv$BjdCsE; :DXyFiTFa\bw9g|QAF/D`jtH@`lj,D4FD̽{dB$IȴF@bG(GDD{8GI8;ǾDyoǷcAiơ<ȥrIUĊ'I4ŐĥH]=,dxlȤ|FLKCLH\ \KGG˺HzKd$K@uJLLFA c3NdBFR3eAcfPHLMn'JblNǵOaf^ciVf  fN_L܁sYamg-g]ha_FXR]6e |f+\|^.:bȬo&;nFhL-$gP6gEFg.vf-cDgK}g2߮gMn䮚ū;&^fc㙆6~jh~~ܨ]&ËIb^ oΐ2YnF&jTQ\ekecgijƨtj<v9쩖&e0Njvja귎l$딛 R>mh}V'vk뫮lu'~c]Fll¾Ǧͣ얦VF>eϖlAl4nn~fѾ]^?֦A]&CliݖKCV>o+rF脆6KOZpo~p otc%NnFkjNm$.VĶ_ݺjr¼;ޞ?.p> i'c GI :q^i o0< splV?k YŌs8o7d.X)jL$r3/Dz6Fp7t2'7d-x5Uo*sك,gG5Wgsus^m9sin!Qr;thLt\Ar)JtT'2vntV_uUPQr4 tNuYwq;Dn`}\{/|_Lv`G9iGiOaWx6yx~eiW*Vxn =TmWxOrKtF{pAXuuyz'&F/ѥxez zte_uƬ'7{lWpG{_p/wЯ_tg7gwwʳ|wƜwh}ݿhm!!OFl/OkY|xanN&؏k޴7}ګotv{_ovn'7b'#am%)0 4˻]}dihlA,t]xf|? Gl:g˨tJb (JN%'5)TVj3Kٖj魐> g+zd=x rjZ4T[#ꢤQKkhé2z.r{K'JE O/37GjVte>eR\ P0 {ǘ*q-7B͵8Vĝnjؒ2krsl5 Hn nlP @m$(CVpz: t}i@ܶ)r0+" 3pWroڴz-]tǽNF=.эb^E.-֤;zzbz٢y^.:r'wv8tf7[Ky?>(zCD}>w_ϻO"GO6P]5*Q48SۡN~1_W7%}D n]IK4&eDŽҋqo zZz&zpAHA}C  N(azzP=Ph2rqZ'fb<ρ^Ȧ23#%:R^3*b3!L^vӛtAYjv#5yVGA;W TjvMTɥ2%l&1p>(rot9XgB(@mN}51*GNsEir6(F+H;K-7hcu3b1_UOQj)dܨ`j}ܦTaS )Eqj̮3tVIVX5@L:V \mkAeͷQ(_UJ׻~=YWk]OOUkLf9e?+AQ!`mFR0b7xQdI[jŵ]`rd-nyWu,M-zj\7µuKe7tD[ ђ,Ni) }9卮tyJRo~|l[:v^\) eudk QVֵDXxY.vK#10;T\Ǻ]{GÂ̒fe>_wO 2IcfhgcpFt[Vnѳp5œ_3s&c>#u4Cdo(vcn&W_\UKV8 VٙDJ{\ҳK򬚈5Wtz?r&.MeV{ybu ax.MkZ ֮vڜ]n Vc9\UT8؊ zh3-Ά՛ғ%"rYn>/Ȋ[nsah%nl^eS4i69\g|L ^prq王y>$'~qzK?tQnxY_u>t/=𵟛h;\8mՃ\_'t#oH$y=쨛l6=/]mݫնv$<wYowL@[tX ]ɕ'33ʭ -Wvo[;2<Ͳg߬Gr ;vHLXuO־G~eٽB'zttC'7ާsIhp4q|}vET7nM2_gwr8vuDŽ Gpxǧ+gR}V6؄NXc؁OWiH^X"oxV|#}yWLJu-Ȇ ȅhu~؅Hd98_X?t?0X$_xJ_8s(d׉qx!VgXh8y4L1(kHtXjHChx1w{G[xȈ'jx?H~H@ThZmx4ca8xzg ~exwҨ}LCZ(|HmRX̘iocl 98fWV#9xhȐgcR"H}3IŒphHl;)h(_" 8}i') FK Y5VZS$YhGf%Y+9N钴Sh7ٖN~o6)9ItiOxSbِɀ|,([;VYw]ynAٔyI SȔvٓq{Dt5Yyq٘r$x }4s$z-^Svif k晤LHV +?Jh=Y!PpC)9I~S8}ؚלy ə9} V\)wjٙbȒkYyd u&Dzؠ5VY y}9Jy@B:/e۩ɚ Jىɞ 9X$syInsLJcڢ)xzxI٣t(kxgj?D|ڧ~ꦿIqڞu驦N P*% :sʨ)盅:<'zT9b{YRE{䥀* pW,omzWf UʫZ|#vJ jvj Wm֪2jT *غ: M z:E,-էYگzyɮɣ{ٛˡ~9*z'ʱZٮJ,۲.Z JL*Z^kV5[$ G7z;zfbjJkSQ!*) +f{h;~1[A˳.:`;˨Kz[1Ԋ>y]:HOIP4ĦTkRXH&(+a۴Y:e*;Vj#۳B ӓ}¯r >ǸEY[kbΚd׹[{F˽ߋkڼ{<ꓩ#㘳%;KRXbk4zʼ!˻[}j;8J۽ j۵L?kpXcϛvec[<iK;\ +HJ ڋ8\˶wzù+|j Vx%l5j[∹9,Nƿڱ[:m' e\W˷Et,GĂ<Ȅ@UB]cDMFNPRL=VH MU}^`b=d} 3bhpr=t~n]x]J w׀؂=؄]ئ[m؊A{ؔ]ٖ}٘q}ٞʍ ÉM٨ڪڬԖ,ٓڲ.Y]ãį-#<ۼ۾ Ͱ ڵ$}=Խù]Qj} ~]}= }M=79]n5܇, ~N+>fMmѨ@m MЇ:*,Υ.%x08:}<3>F~HJA5nT^V~S=XONRް[b>dG[bZnp|rj,Mu|~>`}+Ӂ.舞芾iڄ/^~阮SҔ>^(~ٞ.)-벞&>늽Q>ӵn'f.ظyÞʾsx]C~؞6+͠.>^nӬ.Τ-~ p l ?_"o&](ie-o,4_=6_;Q>B/@E?$qHJN?,P*XNMX\Z__>Vzba_h`}jo=Sr_v/tyKv|{ˇ_w/ӉӒ/?Mnו-])~d3"@MjoѸIǏ?9_<(/J?, Lx8VOޟ=KLOSQF9igy Ņ, MՕmPSNM'n]<*eVm{4fq0H1Qqэr(ί3R4S5UUt55Vvp V'Ww/rR8xK:Zv;[۱z;\\<<}=M]8~xn>ߧ? B g(dha)Xq</.qG#INYeJ+!#yeJ1f&H9m9{yZYiRaD6utQlRPUFVMf՚+͟cW.նuK'[s~ nr7`${8Y+CFUqd O|ʶ67KVcl:7aԈY'9v:jKd϶-mݻyS*wHPr㉔kܘ࡚/4zvյwΎ;}go|߹ ~g}!_+26Q@.4'>k1l 5, = 122k"D,B _4[DCu}1q>!w#1$P&q[.#(^\mLLD̷\cM;ؤd"*1L̓O@?y;BR F{B;G:L5ݔN=FR E-ԦJSQ:JTOeZmWTG]rWXpR FbMu,Xȕf}hR~,пlCv,er=t]x]M pgR~^6 ,ޕ~vlH\ 5ޘc>#;6d>ndJwb*em~ݑMǕ W)w^馝~nhG:yz2He ClBmmnޛoɤ¸*.E| *\/|ד[EM_HXtWR_A [u'qW}w.VYvyz҆g#>+)^Jxowco|H |oȟ'pϪ.}0[ZT?\Hun ' ꪀpۜ'8 J}'Fr AtPzM@h.` Bm`ď粪pDOT#:@0DaNJbC"jƅEɳ0P4x90Y:ؒqjtS ɱhmx='>Rm|hF;fz s>h#ρ5X |"|.w@_(Cm**))# RL\n)&GQ17\IT͌x2y @IQ)2a'jHNqZ$__+'.9C`BYY=l;pD4d%`ɴմ i6bH=GweM{J.U;F\-b<9ьD8YpNc*)3F&=#iІKXF-+M5D~NF%R/Z rFeFSi>MB*J*"7UYRsd+Sժxtnjo3E^miV8T4%*URƚ)W1(^ӫ~,J(ZkU]tlkֻLml_6敢%< {^Li+X0wc pٺunNeex;MESk^Q`+G6#a^ ]\|wI 8bUg ^/rw{ZZ. \1`} zXOXl ^b`۬{HaKx*yJu)0RM7|s- `*E:a e'tn20AO[pI'i,KBnc cef9^VmHMLs^R75{L^Ss9,e9Tѯ/{FcيN7XtO]Lր3v]ljۗ#1`r̷P淙99aWޖ6#aFVl O%dVvh-m ߌ : )nvPwu6WGPˢvdb=:ܦze$x%03{ߖsKQǻ|=x*/QkNOr÷g}|.z|<ܺg'{K[Xi;kqo':q3Ͼ:ˁ9:m߮˶k1OžǃsM}{ v+/kϼC/Z/Ov),L(rO{VNd0XB9L]/oPOhou/UO}ooR/"ߍׯ I(6PG 0ap PbT0 e %.upP ?/PPsD;+O;S>m0:-4?{eDM4E#DC4G-s$<FgS3Sඨ7Rk2F-t)CcR1JvB9H=K?G) CsKJe46HADSJ';N)8/1qA/!MPoMW@3K.CB4LuGUtPH7k;WJϳ-Ps,Qu 3(E Qo >'L4VOIqWRS#49VIStxFM38=UNtNRySPt[uW)V :XQHumTU[[NSsT_5@ttUT@)5(SMXӕ]-r9IW VUW\s`]-a`b\UWɣ "db9X:e+]SV&7eAW5c;5Xg[UZg#V]mcR^9sJg[5ii=lZRKVR%`wcZ6g fv=EVf[kGkfm9df6o1vm5m_vhF=la o;l kvptlro 6g??WKdVe=ag^7qc6nնf7qomD >vS'ri7Q4ZIyq/euWtjKg7q[Wrwyq5! lǕyGnskOi7$VoWgE_#0&U>Hi3,w|1w{6m-4CxWuupS724'|ZzFwA62JՆgY7z;uyHvu|7y^׀U|wKhq|oRTLm7k5>18XVSS;KtG'GbwsyIox{VEwAX]a؂7x#b~n9،7]7=؅7Ѥ^񶅅xk'Icz 8TgG։9y{C0dWi%sxXMY;Y39/Xe$;هØa}XhUyqYx&xiُ}6z嘑!]rIy3XsӘ]YVQ=؊38ل]96rewu8vwq YYYdxY8tuو yy Ɖ癏y98 ZYn9~)c*¦ٖXѷC'Z{7z1z-GV~؝C{nWYo9WŘA.#9:+XqZ)vuڛk}' KOmZvqړ/eZ->٧Y!7 j̯o" {1u"hIm鹨֏.+暨ٔU{iQ{aV!:Z:vyַv.[dzP;6:z{њyŮG;;;SK߻e[k3_c[ۥI[p_ҹyE;d[q۸vǫ <3|gݻ3ZY{׺s,ŻǏxuٻǜ[f{]* [u!en#\8a8BIWcǁ:isos;aεCDm\z{u|ۀ|cO=:ؐVy͑|]fu';=߼Q՛5}e]O7;;C]6/ŝu>]~~~ē~ e _qm=u㑹3*~`oT;H̋﹋|mJ2]mC_7^R\GE}/;h#=ǞD^}KU~_^ym+yc.(#V̬MC82 ~Ho6kO|:`Ph<ʥ$gOTjP,:튾W-5+63:ncҮs zvߡ *.%9E !ZJ8Vi]z1ʍ|..&pzƖV^v2.[%J 7Os=VFgrwk?zb`Z3Cҗcw@co9ɛEp!|ƐÊZq\)1Fd%3*h$1N)Qf6y1$ʞ"D2JCE=U@sjԠV MNk׿j۽;)1 Jv94Y1- D홒Ō12-to۵Fx7/XmByCh-=xsQ\yۑf6zž}>8lvo3S~]q VixfVr&n屗fqyi9a v߉*~">"#w22iGi.f%dk5--ItFb2= I>8z5Yϒ#L.t%hg~a[BIaigMVi-(%&暃+fЙV "`EJh9X"Z'y4 ji)JZV)idj keΚn";GZt+މ\8볂z)rݚV)k.^k* gmvo+0[z{.N5~+m*l+#ȯQn<J2;֚|m%gq![:{.6&_5pN|p p=,s|?[u9^՚5-u^ͳQB߲Mf[-=u#75'<3eK6N* rCʧSYMw 6ܷ|j N3'CǑs媻]鞋.ӎ8߸c=NvS{wBg]z(0s`<~Î}mZ{noO:ӣkž۬Ʃ/l+۱w%pgg*v{g@Boqk !gЀ< h+57a"+ ;/nEd/x=Ј0 Q: VP)^iHE0.db-jD\b,4ppl 'ݰmqWjh?X\L4|7 >V`8tt&7Jєd"UC %JT~f+mJ)v2eO]β_rT$,q߽1Is(ZotWr$ uHal&ר9=sD&78M-ԩ*yưddyoJӚT;9PўJ"&9_j\'Gψԍ%'i>&G5*қ)^wFy.meIQ|3 ͧ1IAd`b''DiӇ4KEқ)DZNԡDXIUf]PYSt(W3Z-A=/ CGUtk1 ׵))a_4Ρ]0*T "թO5$Y9kִzUCEqU)Ϗ`śZVО3(@N3 )p!a{ n_8\uX2 ,SEl [&vyU-zKn88ÜtRI|ˆ0U oBkX+q<ʼ9]SB?_0u%O7A~a;obMrf=]{csP="rDW}>8\0ꆑ:'x#+}pG:As| v~zz^=>mnyO_5 `vޟ^ -_b  m6 ʠn)~a j\2 r `%TXqݜɅa a !`!ZDaa^rcW^&v$ Q-& *!!)'^!+f"a] &\^"0b!"l!.c)ʢ'…/ ^"B#3bX$T'~*M]:,b,b-61=6b㸽cP !"n_ң c Jb=973^8>%0:"eI/R((b5EZ2zc?R4~H$#z!8j%r$CK"aNP^C>MN@.ZGaW*Y# ͤU[_$[%NB_ Uf )*1e0eT.&Z~VUFfdaeA:^O%e6&jFc~d"idblvNzZlp$#b^#;.$\d9'`R$hq'XV&aM~cQN'=͡d*gXBj%)g |n2Y w.h'iW>i'q9Fe&ZV#taS"fL' fC6g{ybme*&RrgF.jOXjF&dV^Mv}'xeOȠhHV'f:]6}ba%F(\hJh(nNi;[iVenvchhWu^vhSzTx>pg}N^"ă e o*>A-hP. &qB^)Vh)R:)r*BjuNji4q:~)ܱ)vc(ꚥj]*~ 2Ũ'l*KH=ijΪtޤR>Hm*jp"je6깶*):gR+rn+UkꚎW)]$&{+Ò: ^e1^+>lƮ+w–B`|b^yBk"n)J_Uj+?+ݩ+Rh>4>Xݬh:lʖ KYp%rr(2+oBqfp q0ֱm.R2 W(?2'Ѳ;,0 _ k/3^-3Ʋ+O2f333 &'FVO0'Z3h/ײ*;o#6Q#!p37[0?[᳓v zs so8nMbAE$3A/W1ft4;%=sǴC9u@oMۅN4 wtW4P7u"-5` Sv~`HBsV_uS7E/bt]8 Ofe+rwǯ'5joܚ1o6f^^Sq#4ZB O>vI6C3bSv!#DMq_6V5~g&HC4h2l݆+b5:4wI7E׶@4M0vW7][`_#qKdvls`_vs>t.x6N7%hT[1qKa/7xj8zsl'[6y놷WuCu}8U˷ugwf88r3RriWn$ٶ4ܫ:.oOhգvҳN}ėԓ}:<=S=ăzZ8~&V Yϻ7w~zm8\w|TW=>'~bjַ{}Pu~c~7y3`>S+4w/Ͼ$}y}co JzO~/'^~ӱ;W#;?|LEi5.>GrϫTp]P8g>kN6ᐘTc|Hg\R_pXs(t%7yPFmTBC \"48G3qJM EH;]S,M\UY: e UmUT\suW^{UWvXbM I5vY[eYhHSM8#iD eQl#5\tUw]e]x4WzwƶS7 @4DФL,=xb+1/xcʗ<{;63cK6r)%c#3Ρ3֖I{gSf6h,oM`ԹiMᕳR٩_R,#ApTg"P+XBP˜\8C d.xP'xD$ Kb{VDЇ`̈́WbȾj}Qt0Q}F8Q3툮28hT.$d! 9:RʣD.R8ILfR&=F>+'M9/uDPde+])/RN$F)XRQVK`S0IP/O1٠Ϥf5٣f^SlnSIyNt~Rg;k6HYݒwrg=VL{S'dtapz ehC.FԒ99hPfThG=ZO~TbgT bJ]Rav3@+7ʔ;}?ySNҧAi2':.ꅨKPT~QQ}QUNEV!R+}Ӧ]Uk[ IUepV5yk_v(!.؏be,pXE]c!ѽ^Vb8P'Rg%SӦVjum>Z[V ly[Yj%nq{\&7qiUns\FWa{]fWs]W%oyjYWeo{OWo}{_Lo_X&p |`'X fp`GXp-|a gXp=aX#&qM|bX+fq]bX3qm|cX;q}c YC&r|d$'YKfrd(GYSr|e,gY[re0Yc&s|f4YkfsԲ-2gc,\'k8ٴ]m#'$ݸs8FҺ/w/p7!{8Ѿ}.3ѭ  ^8v^,Ӧ*.b*M[@9@=Oa9| 1 dɢ@QA CAqAs@>4$A91AFA@2B68k8$'|1!A4B@*dһ<+,>TB/)3 4 dAFYCmBy9$B C1 $kڣCCD6\&DC9D|96,Ļ<\B *CBRD}DD?J AĐD-ræqG̢ErbEOFW|EXECţ!Gd]K+)+I4H]Ydc4%2]Q\ZEXPU.C[BFb8-%QJE%L}RW %SW mV+5VkEbTi`92**Rvl%;6WTV4k5p2XuABE~2 HR}Mnmɓ֥֑]ח "rKsrT[zXEsVU RT؈ Z؁]!{5\uLWV{W]X OeMW}s҉mٚ \[-Y8ZRw=[ϚYmۘZO=ZZ_i]U"@XNMd^X5[ARf/VVmd6;c#~p]5aV[y4e]PZvS~SU,WeW0f b4-rubN^<>~f7ޖI8gtάfcTZoވ%]cn=__j.cjid]c^ޢMNdy~abf&>K|v!f胦CBh\yd6 YU~,ipi(}V,L&e~ y^_bv&gfNhn6 gLViDV`/TtjIV꫆gFEjNi镩ji.Nj(xkV렾ak].˽-l>Z.gnlhlBpvP>&T^lf&0t.~cf׶k^k6ߖNl7ܦ>acNvm6ff΁ۦ*&kNm6llC^\]m~p%Vfo5-oonnOGpe PrF'2p`mV?kp mgldooq]oqq=lnVwx|qgprN7pxe/,kqs$r wo#f r!mo)6*;@}RTHW8ֽ7eKYV5YLt4:oJ{t"@;uKO>tTrCOَf=<PdWs&W&'sQ7Q]bVTag,_gwRev? GqU7`>_rMvWtvw'Ad(gvNfw/=C2q5T_ZGx4?Q旿vrWhot'H$ZuoyObptO1xsO?/7vaN{MP d_i?tff?su[.m|z&swnO{3'Y~O{O?jX3i{et"/x=voy{)'ĿIz\Xug{1u~7ugÏe<7}#LrfW_\IӯOPwǧyۿOD7z:g~{~wq}6xwoͧWޯ~?g'ܧ_oNG}ϏW~)힢ً޼FՈvX [6k Ge%݈NZjbE$244I yip.΅x߅E%g%uqxrhـWȆY)iy@ 6c;R{׻6j K\i9{w;d\m!FRxd7L-<~]MFN_&3]nL~ _6YTI:t/ࣂ*"CIFQ SXcԅ$x83U$eI/+ҘC~9d9LdS-"Dyj>YXq__tZ0'SkXCy6 0 %"Z{iEĈ6ygRP-|2[ѣ'2n52Uۍp78QHAB;8Z&a6~a|mw Nyhc_[O+|>leKk'~x6IxV_tU7 ڧwKz5^Aayg`>xQnH"1v[q$8!xr)m-hy"VN2H$D Tǡ!$z("Gމ4n(eFrV'< Yg&ťcicp~gSrZ_hyiIu.꥝9NtCߠRZvgn䫻3 +,*NY,Mlhz鱧^bhW:#KR+Uj ,²ZʨNkkۯ8:J.^;궈R0B|.3,rq;B뿜Ke+r.g [ܛ%{[#O{031-㛲CGqR 4bM4cuswֿռu٢big ԱFsK{ʶuv_6'kb- 6x9_i>}~ΐ7чOp-aB;4>~%7tNy۾7zz|߽N|>NӲ.:n_&_.-_zOOz2 ?_ݴo|g_{U7y|΃]:}񇼦x D~ǽ"}F??/$C6Bbpo)&,I!,! ;H(p1R(>NPġ*#$6Hr"}z$/h~Ta+h>wT]J\FYXe-=Rd.6ptDU<9LQ8 -Ify&5f}bUͤ:o7gg3N:e̸)Nw3鋒5;Ipqe@uP§_kjbVNP>:KxN!uByPfL)1iL*oL&KAhEQ:A)WJI4dDQTS&A]˄T%Ijf՜t$%:R^u)C{:Ti[ٺֆ"l]jүj/~qV_WLF+n?+e Ү*VSe7Դu`maLgJM=]6V5e-Z5W3m6 Yˎ4nnZuv5Y;Z>7Iq byvJZyJ7mj٢4SB.)uK_fWY{RgդsWZq~-MBw`78w aJx.Op KbWM)w%uk Ź/l[$ѿ,zFؗqYe,x6Qvx dOCo%by `rEivQkk#߷ѽ XD cDZڥsDn mfOЁu3&7 iO9ϡ泝y\bT8Ǵ鰘\Bq&,Yםt WYX/t}J:xvO9fi¦Y hSXv1O Lfۆ=rYۏ`h+O{ѸltKn606OMs7?Nq~O]QOz3^Mx:1u|suwrw.=I[8Q$tJ{ͥ[6t@bg󯓽^hwzǞyΓvҗ^Z;O˿.98^6~77'{۽)F>i-y;G6AN~~_Cߩ_/}/{uP7vWxg~W{'|GxxzgbzwxwGVFrQ}y__T!çzht+hd'"y7sԧ׃'(xćd |% 6gƃA(7x׀&{)H'KWG؂bHǂ|wf#l\+^6pnihHHgw3Hq5xp)eXWj}?HT7xVNhC1X{G!hah{j ^}貊 ԋ`Ry|Èx BSh@HX^s>SȂI;Ȋֈ7wgYXGgXڇ[s88(x|l@؇T8{ 93ørF(Biu'|xoHpX } U0+bȀl]'j9Yyx)ut#I}Ak9=9K9R~' YiHXJ|^)tuh4yg \T}וo䨑hH8pVa)n8Cٔ2K󨍻H=xC6tB9deYǕ}ؘِEȖUY7؍<y:I锝e阷mi7)ƹ ʹp9| ͹~t w) )9$邹9YɎXHiȔ闩IF"G5I領CY&ʘǘ Y㙙XFɜ߉vM7!ٛڞYT쉤9`q$JJ ڢ JAjxP#ڡ?JƘIznʣH XWryY: zfghR*zEʝ=pNd:i*ʡ5i94w@ ~hh%zR'z:zu艧ڪ ZR:hPȪC:fʧxJ%bye z٬F n 8Z9*{j}꬘ڔZگꭘ|܊ [JLڬJ:Yʰ h ; ኜʛJ[:0hy'D{Z/ˮ8 :m# jڭﺲ*n9[亵\;"yYK'+ڊL]Q+[{E@k @*A+[z(SKyBt[v4oyj |PKKs봞:N+{~اWO8U:kK1^벖ly+mlJ;i۲cڼ΋ūK$;;aۍ+A+9+c)l+\ڹ-l—+'ɻٿՋ;+dL{d \̽N<ֺhRh˽$LK(,Zfl˺llysȻ\XP<=|<ōϧCKk̶̬q{dȆ+}~ȽΪ/Mò%=l=1 G̹LdEZ,2˻=[ixЬF=^7 3-}/@xv{Mwլmm=e}EM֙B Έܗeѽi|ۗd ۘHepI[ܢ}H]A L~Pm<-+^mۨ3^fx;>}}Ղ{=\ ڽIemҺ;MET?ŏ@~-,4n2c0^,ۣ-j~G-=O=)i$iN˙5D|8%ߋP"N{.4.́ /~t- 2LKv> ~Fk*^6Up{蝞v$7@@|rN༞.e\ﭞ+O?(z!?-Ψ^װ>ΘM^OOkT;q_.9?݂燭H?ó1]NSNlOo޾qQl$o~B}]>A==/c^e?KW YI^ۻ/_̾pg`8E%I3ON1uV;a֛wK)T3!YKeXz#٦;9oyAO$4! 2\,^4Θ,ZXUC#wg[o9:Ow|  kLR!#%a.+25;'1 +T+k#ef _]?-z-hWQG,|SWgZ[]8lŤC7d_MzyM*;㷶]=xՇ=c:oƓ70ipLJ17C)Fdp0ע&lԎ4.Nv SPIfLν QĢq%ajRppDQ?E1On{Dt64Ӭ$Nڒn-xם>wwSq|- ˥wL/a׈}e_lSi#_! `fuO5ua 1[k}9c呿%IsEy[y0 VݤynVr jh:朽U`zV'kfNaC^e遼^e{{n;NpϽ;8hFr(eop.fڻV{$ֶ+ouΔv}syo:ўSٖQi1^ח~>?x{9?w;|]Ǔ>to:NF_|1 QTMk[ 8)JUWCOyRfJЂӥ+eU_hԧZ( FюVj=5*=ZԷ.Ф,$]:y*Lw}fMiNS7;AҰu-U;ԯrJ+sub~Fmi)0"cT]kZԾkd;܎ aY A5nN knDcՔ˽r- l FUbm5[RTI+\^4i/k[oxթ$VӕpcBwļau9RŒծT%I ?]xuZs@{ Wї #Z`(bmtg_H,Hd xH=l8\~Ib,[X&!ye%+1/x$_9<s&kϏlD뫘EZqRPv\F/fKG_4\5s3`:y )kwUpv<ծlv=#q+Rw--ny{<ߍM\0 }r<<{}o|k9^W#ܭ6]+] /:.r;k78-~.&[;.U}:=3!7ls`޹Ev]{C:՗F*hoqt&i{jQ,'\sOF4=e|;<Ǜ/1kS9mon|?X>d:UxV׷7>Ԡ {[-ܳ/* SG|ͧWY_W_cBfҏlhoOWN挎P蒏tΕB-B0PѬNo[n* kPoZ٪LR;+POP"MJ AI mM}OoЌOG,UЭO{0 QG nԼ| )ppp6qAQM0!0 I )onpe _[qݘԨ܏/yƀ cq QN /nw17,{ oQ q qqrP\%k1Qll GpWpnQ!1Q Qn;erF2 $?##Q2NR# i Mq Y2"- 3&7%)0?P1(2$RQ(g!m#+W%Ų,r@Ь2׏,(+0&*}Wq-32%"1 [ I20/1)$1393=3͞@s4q# .R/Io5p"%r!'5+2q3u$Us7߬7s8839/Q&S9&s3Q% Q5a :J8$S;G)-<3=ճ=?$7vr>M&Z32?:sI?q?)">5k@tAAAQ6!tB)tTGFK[52f}lfѕ/-du6hvhhCǵO06㚶P/+j6k35)kJcVOkl!NCk_5m6nIan?gQmGh@VŶBݶo p pmZvquU5%7s5ws pp9s+oVosA2UXt]ua0viWjV`}gMvf5w=ywxxyU7yvx%uwW\suV7z7{m{Z}zWW_1{q`5puvͷ}7v}YUvXw~6aw}M83uKdy+ xӥWx)8k1x_wl7}r-AQa/8Qqt;XX78o]DwuxyoXՇ8ŔfusX}#Xe7}튽u׊^/ׄ;[Upո5U)UXx4ـxX{wm^3!9t%y~ yW+=ϳ@E`UEyUyYQ\}/'cq9u9Mxwkփ֗_vU9y(Ky9yɹ9yٹ9y鹞9y:z :z!:%z)-1:5z9=A:EzIMQ:UzYa]Fa:im0qKgz}:y:u:zG0yzbd:EԫotyTzeњB::8z#UZanZA {43SE![H-2`XZ5;Iu5C%״S7UEm19ڵZu;a!Ը㖹%;Ӻtі-47|3Y[@;;Ѽؽ3U񗾕z={w NS{I7QV™Sc!|R9![ 5'KXE|E]<7 )u+{ܟA3ȅI|Jg\ UN,Zҷȹʋ<37 C4ț>q־54Y͵,ټͱ+v˥,|l͑{u[%=C9\!?}#ϱ-s'X@ɕW3}nQ}t4ԏ4 }eUO}ׁ3݈5a={}P +Z}}{ڭ=cIԻ)?-Ý==s.dYCm='szމt}>Ve6W]1}->+^+^1^o]0i'=;ޜO>U95<19:W~ͺmqy ٿaި癞}ysxckU1Z盾>76ױWg]KQA[ >=/>~Z 힆 ީ=>%)?Ý'[khm A?Eߙճ#~Y]_Kr'GW_?u95=QUyS7iߔߗ߻5;۳ʹ_]1??D{|% >?rj/z?'kD ˾37;?0(#2\2'4*R+6/8l){Kk-צJu`_!"Fb#$e%f&g'h(`di橋j&+[gmk-].lno3t4u5v6w(1m7L8y`z3,p#1N{2<>Z 9%سPBh"ƌ7r#HUB DP2$}|%% (N 3Gh*7<s}ϤrTht+**֬Zn"w`2lGvR>Ҏ8-%J4{+p7xs߃Ҿ<<۷TL6' 78q٩fDۼ5y͉65ZK ̕GiaXb$bbĺi=Qp߸!HQ?&}Eߤ?ڝui0#.;}x)R\RO%ElW  Ar1Gw$fSʙix҈Y^,֦ Wb(] Ww}7Y=KH%nRy,Q]he&Pi髦{9(q8*&=j s lꟳFvɃMXNi-z*{gƘ^+i{b& y.qN/>1n,w5z ڤ W@jm(f\7Z6,-z_QskƣrН k]k3u@-ݻlcK,wV2KųBWY:su;=ٕ]Kn+C.;.S>5~T_͓ g[CM{~j$;+_5Vs Wi}yԎ[κ{9zRƗnY}-,LwcAӎ(;F58[찥혮@uOpᶇU{sXJ|TAt]x(0 |ZTTTh&t^^8 k{ @^7D2dسvx׃aͰ Y2?Qo /w )QP"1F̑o4\\mQG ܱo]Eώ b2nY8>@:F!.ypX?ܣ#Sv%#0yG-(~}T/G< $)B&ttfw]fq m)'nYe 3)J 5KiJunäU´&OI4Ɉ|RК(A˃"3Y)pDt'CzMsqb6]ɒse%'zp# Ϙ& -;b{=9uZPNO)G ԎT> t撔HM(Gש+FW}-k͊RZWlt{QBy_>/1DBڕlYֽU/d~&מ fYz\RnLŚa|b7 ' "}tib +^mnŻ[Ay*nB[&~FmƊ2[:2a-Xzl\ WpE;WA&Y+,~Y+{d݅:F^rC1>1oӻN?aBSˇ3LJ)-Og<negMc_uUkm`X;Ud&:s aYF4YcF+;H*bרMi-I'۷pqOڊ3K悚~i]ɗ{ {saoC=Om7` ?'Ko{ ڼuz˃| UTiӑMZ}^_aI`ߊ5`` ``b^IՄ j^5_YiE%a >z!r!!1 6[>Rv^Ee  VQ!v !aa8]a#a_ >"$Vb.)%V Eb&"bb-b""2%"`",*.jQ.̡2r#N4N$F"Z3b)F26bu#.Rc }l)#8Vc3~921za=c81:zc*a+"-RB4bBr8dA0c9f#>ƣ=:CJF`FCEd>4F#;:^..dGB)*Mc1d=ܣId0V@]&@d@\H:&cNvs1bQQR$Ezr!<^# WdCrDΘZ"LzdSVUcPd\2%QX^J叩JJr\j%_>eXd*J[Ld_YB1K f%e-\v^%>rd*;/~feQeZd&]]V&Mc&hN&f&Cififp gnҦVq"gqF%FfK:2IrE5$o9`T&\gm>sZJpffX>LgcfgNea'B6&iΦj&v'|vVg#Il&zg2u^gA['~RzcbFh}^dv2gs:'gpb(b2|֧k¡o}*(Lhԍj.^Ǐeƨ.(}"猦6(k(fN?B:]&*'dJ)zhbR)(~'i>̱it.闚i~Zi ri)B)%) &S`eo6ji襂j'f~h 鐖窮B{j}鮒*krjOj*Rꔂ"b**.飮<*2j&^꩸kzjjB+O>^rj+~"+Fc:Fjþ"꾪*F빂dfedžZl*j)*뼮lVln,Ullll⬬R\hnC,^bd.,&˖C:l*,Іrmj]V-֎kb,>쇖lۚڙh,&,.m+,~m.l Fbjmj,lRm(u.^ZPަBݺm.,&Z-N+j.+- ,lkz$ӂν6/./f/.v/.Wo..Ӣ-檭NR36oop:nJr/rnn*GƯۊo*o .W˦/7poDZ #i010.:o/K_aB.+qxؑbZfW 7F/ qqz00f%S r c펰1 1 a~Wq4 q#_pbqȺj&w튺`[z)u2!kpg²%*r o~"q+G#229"-%C]ү"3+(w3`s"3+q01p+s*.p<ލ3Zs8?$:r[wƜ<42?3#g5.`nE4Cs2&g2^2 +K3C tA{CtҲ/C:C Jst*I p64[P+uZ*7P'-4"ruGE@g0IgRyQlVug=}еWT0Ln;N/5\o5p_/^aVcYg3SW{VAvGN-DbqSmcKl[^)Y5Zg4ovhsrXu^lpM6'vgD+6vT wdjs&Ʊ25ld[ps1B3I 64F7qV-FOfw?WvCs{ZN_s_n{,Qv{wIw>vKwyCuqKuo|Bx5(/rO,zk5{Cx44vBS۴IVJjOG8pGwpn1ɭx_kwKo,s4uZ؍'6='7G,9@Oyx֎28b#wK)+8wxWc9W,oc8/l#atkLCNc}9.*k8g:{xxl`6j[y6O:g|6|1bSmr)v7ax:~ߺ`}!+F'omooz;4 xĂ:9kK9m:6Һl:{9K!K;v1y;{׹uvk;{7Kǖ:gyvz 8[3O~_DUWw~'6jec? :U/{=z us| {z/? >OKsW/ ?WԔQNZYoCq$KseÔkwcg-Fm VAwD&a57hz}LXnC2r:9w-euv 4n^w=DTq{;DDKD#l1ds4k# 9,-"L2m]݄m{t<}K ]VMe6qfE͆: V6D6Vb'w'W_mBLFC|RO&c4?B?"tH36]pg2662L9'=S1EM`sFV OZmq_5zէ_EeؕEKA6Uͬ-ۊ\hi OoUP]VMV;wscwDxmEw|cU`^&WqO܂ ~SWMYs峋b{>4؀Y~75^F b;˓v2S,%3u_.g.לyIOc9if{Ѳpwwv?~7x5yq@ py&k\玗Ѐ 1Px#=@ i{jZŕ&L`x%|]EF$,?l%-4b PrDB|E hwEYxITݾEtPFc(JbGJC3j~9 r_0@nVߋC5CLnGϑQZ"eI,∓"e%C02L$*ɾXG[^0H%[Y_2$zAY^|͖Ib`$21HH.y3$d4?iMi?l娙(z 9QIg5K@b,Epj1hh󠤣C3#f2fG1ѝZDYȑm'7.TiAN->M'P&Us0&N>rLDJGĨ-ui5I}X F_ҵzaN vV\l ՟~%Q1)׀ t=}'KBRv%+.$dVk}b-{ ٥^٤i=KRwXUekrⶔ^\hjWb_l&xE+R=W)W%hGPѓӕDWJn+=辣zs+_DTmeyW+5{}{Two(wN/XJVTlv;^ݪ pz^z59j(ںU $ ,g`zw$*l:ؿ09c}"'fŒu d'7d|W4z3 G,v)S~Y5Bppk&1l9[2yh_V}]NVt,*Gӗqfz icδz֦>E+V5lS G5QD1=S5M`lluN Ek_JNͬfvٝjJ6]mjXӝ6M$;Վ7M|#k5ln\[Z^sI_͍sr6J\ ?;4Oanv=[Co JZx SWB>ρW: XQo\OTNLVgziJ'nl񊓝F嚏Zzߌgfmoy̑v͓/,=n=]zxɠiuśטWhw৆z1Mx.G~i#{5a6w&7yc~XEۏ1=>i==k+?K;苩˶9:@L 칫ûz :̀˼r+~bbݴ8W,7$[uL |ٓ܅s\%6֐MRWXj=;w3кk:YXF!uebF;M)ecf6G>Oj֢fOFRb,NQ~Ma'V][`ganFw[clج{gܽ^AcK2"Z&T=a]`ր&؁Nವg4>J2N2J.YoUFun[8nhhh/de"2Pa5f~)N揕smϖfirL/bejE6]g>jZl~cf&T.7vvff]z.II>~\ddCdbj>lFChC~Vkˢpbcu1V]gfacznjӨ-<N8JmNj0F&eb6nj&݂0HҖnKn^\>܎kdמi~]s ָ9զl oR ][h in6p.pm6=ovi.gz6o֭y.d_~멾i8\dGnmV?A?,'juh:Ҿq c gq#ޱJpVʞ6'qbZݶ겾pl"G4>.V7s4.Zs,)/mT;S* wYsHt9gIMNtw10WPOu3>^2?YtZJ^_u7N[(Grbuqpdj^XkfnopuYG^q#t g8!vsuyx3w|~O/''o7g`/fxsٍxyjvy3>gwZy~/yN~yyPy1j{'[G^K/TVyvo|b_Vigy%؇'w{S{ˎ{wn/Z'!lW|Nz^ǗSGbͷpз{-v[uԇؗ}?w yT}%SGsoWO?j_jdeWvO}77wdr'sϷo?x'0I8ͻ`(dihb@p xugG,i-@>ͺuԨ5۸s>[{AΒ2涑Do:ͣKNs~ճ7粒_!uӫ_y<o鋑_(˼ޥ_%V f΃FfNE(D hQ\cP<0j!61P0g3<&`DiddE>:1PZ>4E@VfO4>U;zoph-v0.9.19/contrib/topsecret.gif000066400000000000000000000312351415176210700171270ustar00rootroot00000000000000GIF89aXNdOdOeOe! ,XH0I8ͻ`(dihlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3j BHɓ^D ˗0cQI̛8sQSe?mJО!M ѧPrDJ5$KRjݚW\Ê[+S$˶[qf}KfU~U߿w!JSoac ̸Rr#˓$kFW(ϠCJm{><@װL>sލEnNM!y=a@ǣKlԚ/\>9Ş]ӧ=a~]ǽ&Z~z'yUy6] =hbEXS|Mٶs(bNt/Ti#ImPE50D$qpbL6R By3yXC?nTbDXdu]_ϡ5)hfbV*!$|Ii8&3硈R"rIdFuvFey(馜"aU|f"Tzɢ'湅O*묆0Y(x7ѪZQZUU4]V+K5;-ZWtURj YiFko_yd S+ {ork 7lR6~ KП$|/&J!_k*D'QCS!j֝x]\|r:smGrbpgǍJKXhǶg {١܀qM_w Iw(.cs{dv7sK-ړS͙NdEy.YL-gޱ!j+ݳ28U[]2!7^ڪK<ƫnwXd\}OEJQq?VrnW3 Wh̊ 6 {HACH"8@e V&KǼ0#!ĆBw/D|կꪧ".$mC̟D"fgN4CPMdbG)zq `eXjw<;ߥx&Bc/#yF3$!驍CIzI 7i8Mf*X 2)M0HR(Us(-RZTA*qIL@*ÃFGŌ/HؔՌRa3h#KഅF%n7d8'SS9ma0`0`0}ħ@+1 r@M[.KH  =юF4Bb29pԣ(EҒI%^ptєڔ>JKiXhLoJ93BĦ7 PmiؖT{2BhhT*9$Z/PxU#ؓhx⥭W]Pֺ:DaU&-UJXGY|Ū جϡl`BS(5% _O ?dWZ|H94>U>KveO];(Sj[1r lU,gd[&f] d]NFWjtj]͜,]^HTi^>YMJsلi}Y:`Ւos*[08[xb[, ջv}{W/q[`)sUK\,L.[;;u4.:uҢ:1F_ A+{d$'YUʶXQhA^٪$\0q|*OƲ PWKq\<3F=;orxЗdE4RzBlOlS>]RLZ+*.F|2ӌWzβ6bgP^N] `Vuy]F2L+l ~mmG9h\Jqs 5Mos7Tnf&~ߐOp{Phkcr&s2BEr@9/{8TޝH8ć*)#}Hrb*͜kw"t;˛5?/7&E0F^dtNK'hq*jʼ{j cLd4:jBXQw#1;06zu:q5wOQvSKPA$\x<珇~~]{%ٮ͘fELCO+ k)~t{w||3Px7O{32N5I`_A'|iaDYZox( ؟@t: s%'L3uet\yGO'P~juwn$,G@;'|px8 {eT}0$3kw!w{I'4t~B^Faߑq|r?D{,_"}sfPKF_T8gX( #7_WwEUxghxqczvYֆTcA&hw(U&OSF(hbkGDj{Xn* rHrڜuvz liqBڥ\y&Ԁ4Ʉf{l A|*bjwӠjLyH! ڝj?ʜzzp$S,LopvX:@Eꪀt0: E$O׺;pu6bPw*8j؃ T'C_0j˹C Hj [XV: +S79ZZKj`IR,{0eyZNȗ3,vAs; gwF{/KScD[J 2*J{ jZӌCy) fqT#-]GMK hψa[o;tb& Dz{V`qfgqMr꩹YKadsh+ iʹJkXW,K Dƺ ډHz lKҳùkÇ.kǫܙ۷x9p_kwܦ{ jkTW*by$ KX˷ĥ{RY+4Vf!Ki˿%r%+-) sܼaZ;*dUU-P*~, |&,Ô%yۧ9,ʏ+WTÅK: +2!Lk%lUETܮ xF 3SF|Zł|Q -Ȗ?|2.gJ(Ll|38M {fu1[s{ȁп^] Ƙ ^xx%7}~t`{j%+(F*˳0ʁ˹,iɾ<̥"*ilF)|L|6i 12Q7|Nm )ʊ|b۾, lGς||>le̮H il̴ʯL =_ ҼЏk9kXxұ TFZ'EI,*?ϥD|J %E8mRW=&mѯ kgC6\|'ʹT]̾{q-+ dh+~] ͅP_fy=-\y Kџ*kԾϗ-ڴud=Klֻl͈Zɭ- ]5\l}}5Ăkǿ]͆-jPgk'$.=}"{>bMЄ llЮe7d3]z}Wlx؝ݣѶ-wњ{ީ]f|]Ӷ}}nWf{\#-Ӭ:M⮈ߌ#NC:IϵA)$A=3ʼ+!c-%>+ yRLlh,^\2܃*q`C׀gn|;]} ]tR@ʪD6- 条6.;9,2PӨ=?Ĭ.ۙݶd)~^g4Z ݗ|JΡPNi.zq\mԭ->3J~ 8[^~~=^K"&Q NA쇑 <{ػODK8񥽞y;\'\ּ9IakZ Ù7G=cfq@IZxe- Ro*AU 618yo^@N7Ym ejdN.a ncHnp0БI5](6_ihlZ tmx|pH,Ȥr 'IEIP*-x쭑aʦvd Ci~="\Ijh'":&W24i`Q< V7Ou,-Ŵxprtl`/ȞȚb.H!n1 aBwiXoYJhC3`AVIHȱǏ Q$Â+2ztdEq.c")SH*Uh͟@ JTn:o ZpF3hX plKc@P(,`m W6V@ V 4L5X'$HܕfC"bT+vD ] ŷ$z)s@fPrqvȇRC3E0"7ƨcȂ?ڢwq怑+񑐬"ALJ$,"JAvRO/[vIL-va8ňO+)wK}-K\čY&ɶ`Ќf]0J#U&̤.r7CtΤIri\6-,ݝ?SF1${t=\'JpFLҒB[%6O +l5 "ִ*ɮ9:|l+Vv,\hE͞Yt׸l[3U:rxVϪ*5udCl&DCGXe0fTɷʮ Խ.u]^IY73z_m*;aꚔgl9bzs,iQz|ĉ.hW񴝛l]sr$XW+qƂM6gseD8 lwu: VB&rWVHC 8(gQȇJz/x2bsffr'v8g6wn Gva$ X1]fwx5J9%o,@>2CoqZD}AFd$2P]ȃ=8}h9 3gcT1ЀzvxB4P6{GbDq72fCaDYsby> zxUG_yHbY!/%[:I~(AIIS)]\\KYdit`G9A),W`ɗb҉I)Oٹdђ;D|$i[N~Va,' \)9aG!i Li[J\(SMx|f$,Bɖp\`QXwkƟ"ԩ_Fv?yh$n!y/ Z)IctrazBf@:aweAx.rqz)){/\ixVm5QHK R*a!ZmqdTt:UiɎ3;aW5ZYxj~ZW)NϰI^CywG*dfFZ٥gA2o꠨Tl~JꌋZh4Z]ʒaei՘OJ6M|*Q'׬*+: #B)׺e r+)j; 8qڛE52ƑZ谫$Uw:/*8 uzMvY";Њj:mبjG@d8jٚZ Kk3p{F{^P-;PJ-)@i֋ᨫjǴpFP r\ ji [p8+!KvGKDd +Nɬwi |Dy.";{zxknȮ;˹crAshwICHAከ+qH{yK+a#C)uOi;Zˏ؛$cX{t 2W ]Ěy{KǑ2ayuvMډƚ8@s B<_]d¼ %SU" b s7W,(:pM+g&AvqkwBӹuD" Ep.I">ޘժQCe 堾 HsEyvF~뛄ӎ.vSEָ0/뽮h;Ξy # %"nUb*^Sy,M.] eN^x=qj>ů~9_-v*W?P]-Od#>Mi},7Dz/m/mNqߜQJh/ߝG9ډ_"Nt:Xk/#?z6(v݅>С2Ե3]F^9?~%ӿ=|^[ _3Iݟ~d(Ի/0o"U$ͻ`(dihlp,tmx|pH,;#rD^ШӬZdB4xL.zn6O|~Z`ya|2Pby/Kw^!O\TWRPuȍ~ ΃ɈyQ᫿Xm "I`ԉX*\ȰÇ&A6@ŏ CI$i㿕0cʜI)zʥTiKz JѣC(r'%Y,իX@SAP-FֳhӪ]{o#/~SKE ˷_UVkI]8&ǐ#G|J'a3/-PPɠCMG`;1E1lc˞MS3) wl\a_<7u7УK)ɕ3gËO$'7،˟ύA/ rѿo`(}heE-(A:Vh0_\Յ ((zw$0J"auŊJ5T< Hcj6 rDBLxdžP5iX"(gуbbdi&xaLD(ؙp)gljxa8矀XuN.rR%8BFa",wnlgL#zIbc\%tif:s 64(r"̦6q/Zj 8YL pAvl,)Iz a˖=uXP\?JEv3`AP>s.lD':bh Lzw(HGJv(MiҖ0LgJӚ8ͩN ;zoph-v0.9.19/docs/000077500000000000000000000000001415176210700137145ustar00rootroot00000000000000zoph-v0.9.19/docs/CHANGELOG.md000066400000000000000000003205041415176210700155310ustar00rootroot00000000000000# Zoph Changelog # ================== ## Zoph 0.9.19 ## ### 01/12/2021 ### This release is a quality improvement release. A few months ago I went back to using [Sonarqube](https://www.sonarqube.org/) to monitor Zoph's code for quality issues. Whenever I changed something and there was an issue reported in that same file, I would go through the issues and fix them. However, with a large codebase (Zoph has about 25000 lines of code, not counting comments), this takes a long time, so I decided to dedicate a release to this and fixed about 600 issues. One issue that I had noticed some time ago, but thought was a configuration issue on my system, was that the "progress" bar on photo uploads no longer worked. I decided to look into this as well, it turned out to be an incompatibility between two configurations in PHP. See this [bug report](https://bugs.php.net/bug.php?id=64075), unfortunately this bug is not considered a bug by PHP. Fortunately, in the mean time (Zoph's upload code was writen 2010!), it has become possible to track upload progress in HTML and Javascript. This has the added advantage that some of Zoph's code here was greatly simplified *and* and that it's now possible to upload multiple files at the same time and use drag and drop! * [issue#167](https://gitlab.com/jeroenrnl/zoph/issues/167): * Refactor breadcrumb and added unittest * Refactor database code * fix return of method that does not return a value * Unittests for photo\controller * Fix namespace issue with Exception in photo\controller * Various fixes in HTML code * Whitespace cleanup * replace HTML4 doctypes by HTML5 * Add language to HTML tags * Cleanup, fix, modernize autocomplete javascript * Fix Unittests * Cleanup, fix, modernize autocomplete javascript * Fix many small issues as identified by Sonarqube * [issue#168](https://gitlab.com/jeroenrnl/zoph/issues/168): * Fix issues in autocomplete.js * Fix issues in error.js * Fix issues in import.js * Fix issue in json.js * Fix issues in locationLookup.js * Fix issues in photoPeople.js * Fix issues in maps.js * Fix issues in rating.js * Fix issues in slideshow.js * Fix issues in thumbview.js * Fix issues in util.js * Fix issues in xml.js * [issue#169](https://gitlab.com/jeroenrnl/zoph/issues/169): * Fix issues in album.php / category.php * Fix issues in auth::web * Refactor cli::arguments() for better readability * Refactor cli::arguments() for better readability * Add error to cli::arguments() in case of bug * Fix some style issues in calendar template * [issue#170](https://gitlab.com/jeroenrnl/zoph/issues/170): Rewrite of file upload code ## Zoph 0.9.18 ## ### 01/10/2021 ### Zoph 0.9.18 includes a change that has been on my personal wishlist for a very long time: no longer requiring manual changes to the database when performing an upgrade. It's something I tend to dislike and often forget about applications I use and it will open Zoph to much less computer-savvy users. Speaking about this, Zoph is now fully included in [FreedomBox](https://freedombox.org/), a project aiming to create a home server appliance that can be used by non-technical users. Check out their [Demo](https://demo.freedombox.org/). Zoph is now installable in the demo. If you were previously on Zoph v0.9.17, you can now upgrade to Zoph by simply copying the install files into you the 'webroot' of your webserver and the GUI will guide you through the upgrade process. Since it's always a good idea to create a backup before performing an upgrade, you can now also make a backup of Zoph's database, directly from the GUI. If you are not yet on v0.9.17, you should follow the manual upgrade instructions to get to v0.9.17 and after that log in and perform the upgrade to v0.9.18. The 'annotated photos' feature that was deprecated in v0.9.17 has been removed now. Furthermore, I made some changes to the import process. Most notably, a partial rewrite of the XMP import. It turned out I had made some erroneuous assumptions about the format of the XMP files and the import process would fail on certain data inserted by Photoshop. If you find photos that are incompatible with Zoph, please file a bug and I'll do my best to fix it. Shorty before the v0.9.17 release, I noticed some errors in the translation of 'actionlink', the small links, ususally on the top right of the page that let you for example [ edit ] or [ delete ] a photo or album. Unfortunately this was too late to be included in that release. I went through Zoph and found 35(!!) locations where there was something wrong with either a translation or the 'actionlinks'. These are all fixed now. Finally, there was a bug that was annoying enough to be reported by 3 (!!) different people. It would cause Zoph to show an error about not being able to change the size of the photo to a human readable format, but only in some specific cases, that had caused me to miss it. Anyway, this fixed in this release. #### Features #### * [issue#155](https://gitlab.com/jeroenrnl/zoph/issues/155): automatically apply database changes during upgrade * [issue#155](https://gitlab.com/jeroenrnl/zoph/issues/155): make databse backup from Zoph GUI * [issue#158](https://gitlab.com/jeroenrnl/zoph/issues/158): Zoph CLI: lookup filename without path when using --update * [issue#166](https://gitlab.com/jeroenrnl/zoph/issues/166): Remove annotated photo feature #### Bugs #### * [issue#156](https://gitlab.com/jeroenrnl/zoph/issues/156): fix actionlinks/translations * [issue#160](https://gitlab.com/jeroenrnl/zoph/issues/160): Error when trying to edit a photo as a non-admin user * [issue#163](https://gitlab.com/jeroenrnl/zoph/issues/163): Zoph cannot handle XMPdata written by Adobe Photoshop * [issue#164](https://gitlab.com/jeroenrnl/zoph/issues/164): Remove temporary directories after archive upload * [issue#165](https://gitlab.com/jeroenrnl/zoph/issues/165): Fix issue in logon.css (Remove PHP tags) * Fix missing label on config page #### Refactor #### * [issue#156](https://gitlab.com/jeroenrnl/zoph/issues/156): refactor group router/controller/view * [issue#158](https://gitlab.com/jeroenrnl/zoph/issues/158): Add unittests * [issue#164](https://gitlab.com/jeroenrnl/zoph/issues/164): Added unittest for archive import * Refactor: Change sizeof == 0 to empty * Whitespace fixes * Jenkins: switch to phpdox git version ## Zoph 0.9.17 ## ### 01/05/2021 ### Zoph v0.9.16 came out at the end of last year and I planned to release v0.9.17 on the first of April. However, I had quite a few things that were 'nearly' done at that time so I gave myself an extra month to finish those and still have some time to test it properly. This has resulted in a release with a lot of changes. I've extended the XMP feature with the possibility to load the XMP-data from a 'sidecar' file. This means that the XMP data for, for example IMAGE_001.JPG is stored in IMAGE_001.JPG.XMP, giving you the possibility to store information about your photos without actually modifying the file itself. The Zoph CLI `zoph` can now be used to create and display users. This can be handy to script Zoph installations or modifications. It was requested for the inclusion of Zoph in [FreedomBox](https://freedombox.org/), a project aiming to create a home server appliance that can be used by non-technical users. Check out their [Demo](https://demo.freedombox.org/) if you want to know more. (Unfortunately, at this moment, Zoph is visible there, but not yet installable). Zoph now allows the use of the '-' character as part of the path where photos are stored. Zoph stored the date and time a photo was taken (from EXIF data) and when it was last edited. However, as soon as a photo was edited at for the first time, the date and time a photo was imported was lost. As of this version, this data is stored, this will enable something like "recently imported" overviews in the future. During the change, the last edited date/time is stored as the 'imported' time, for all new photos imported as of the version, the 'real' import date will be stored. Zoph has had a possibility to create an 'annotated' version of a photo for a long time. This would enable you to create a photo where some data of the photo would be added to a text block at the bottom of the photo, mostly when mailing a photo. The result looks rather dated and doesn't really have a good use case. I highly doubt anyone uses it, so I've decided to deprecate the function and remove it in the next version. If you have this feature enabled, Zoph will warn you. If you actually use this feature, please let me know in [issue#150](https://gitlab.com/jeroenrnl/zoph/issues/150). Furtermore, a few bugs have been fixed and, invisible to most, a lot of work has been done in the 'background'. The ongoing project of moving all Zoph's frontend (HTML) code to separate templates has made a number of changes. I've also done work on unittests, tests used to automatically run hundreds of tests on Zoph's source code after every change. Finally, Leaflet, the library used add maps to the user interface, has been updated to the latest version. Unfortunately, due to changed licence terms from Google, I have had to remove Google Maps support. #### Features #### * [issue#13](https://gitlab.com/jeroenrnl/zoph/issues/13): Implement loading of XMP from sidecar files * [issue#142](https://gitlab.com/jeroenrnl/zoph/issues/142): Create users from support to CLI * [issue#151](https://gitlab.com/jeroenrnl/zoph/issues/151) Add - as allowed character in path * [issue#152](https://gitlab.com/jeroenrnl/zoph/issues/152): Add 'imported' field to photo * [issue#150](https://gitlab.com/jeroenrnl/zoph/issues/150): deprecating annotated photo feature #### Bugs #### * Fixed an issue where a PHP error was displayed when a photo was not found * Fixed an issue where a PHP error would be displayed when trying to edit someone elses comment * [issue#143](https://gitlab.com/jeroenrnl/zoph/issues/143): Sometimes searching for ratings gives an error * [issue#144](https://gitlab.com/jeroenrnl/zoph/issues/144): Possible to rate 'no rating' * [issue#147](https://gitlab.com/jeroenrnl/zoph/issues/147): Search not working when Zoph is used in a language other than English * Add some missing translations #### Refactor #### * [issue#146](https://gitlab.com/jeroenrnl/zoph/issues/146): Upgrade leaflet to 1.7.1 * [issue#148](https://gitlab.com/jeroenrnl/zoph/issues/148): Change license for Free-Javascript-Star-Rating-System ##### Templates #### * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Moved comments.php to template * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Move credits.html into template * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Changed color_schemes.php to use templates * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Move comment.php to template ##### Unittests ##### * Creating Jenkins CI/CD build * [issue#139](https://gitlab.com/jeroenrnl/zoph/issues/139): remove DBunit and rewrite all tests relying on it * [issue#139](https://gitlab.com/jeroenrnl/zoph/issues/139): assertRegExp() is deprecated ### Known issues ### * [issue#156](https://gitlab.com/jeroenrnl/zoph/issues/156): actionlinks are not always translated [ To be fixed in v0.9.18 ] ## Zoph 0.9.16 ## ### 31-12-2020 ## Just before the end of the year, a new Zoph release. I've been quite busy with a handful of new features, several bugfixes and a lot of internal changes. I've added some more XMP-support, Zoph can now read the rating from a photo and XMP support has also been added to the CLI import. This continues to be a work in progress and if there's anything you'd like to see in this regard, please do not hesitate to contact me. #### Features #### * [issue#13](https://gitlab.com/jeroenrnl/zoph/issues/13): XMP support: rating * [issue#13](https://gitlab.com/jeroenrnl/zoph/issues/13): Add XMP support to CLI * [issue#129](https://gitlab.com/jeroenrnl/zoph/issues/129): Added possibility to override automatically determined URL * Added text/xml as possible encoding for GPX tracks #### Bugs #### * [issue#135](https://gitlab.com/jeroenrnl/zoph/issues/135): Missing translations * [issue#136](https://gitlab.com/jeroenrnl/zoph/issues/136): Error on bulk edit page * [issue#137](https://gitlab.com/jeroenrnl/zoph/issues/137): issue#137 Deleting person leaves empty spot in circles * [issue#138](https://gitlab.com/jeroenrnl/zoph/issues/138): Error when importing JPG with no XML (XMP): data * [issue#140](https://gitlab.com/jeroenrnl/zoph/issues/140): --instance in Zoph CLI no longer works * [issue#13](https://gitlab.com/jeroenrnl/zoph/issues/13): Fixed an issue where the settings for the CLI user weren't always correctly handled. * [issue#141](https://gitlab.com/jeroenrnl/zoph/issues/141): Changed default CLI user from 'autodetect' to 'admin'. #### Refactoring #### * Update composer version numbers * Refactor: removed unused function from util.inc.php * Refactor: removing util.inc.php::update_query_string(): * Refactor: change util.inc.php::create_date_link to Time::getLink(): * Refactor: removed util.inc.php::create_field_html(): in favour of definitionlist template * Refactor: remove no longer used function rawurlencode_array from util.inc.php * Refactor: some small style fixes * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Refactor and templatify photos page * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Move color_scheme.php to templates * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): some refactor, small fixes + unittests * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Additional unittests * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Minor refactor album ## Zoph 0.9.15 ## ### 01-11-2020 ## This release fixes a few bugs, finalizes the update of the slideshow, bringing back the navigation buttons and added 'swiping' to aid into optimizing the working on touch devices. I've also started on a request that has been outstanding for a very long time: adding XMP support to Zoph, to aid into interworking with other applications. It's very basic now, please see [XMP](docs/xmp.md) in the documentation. If you are planning on using this in your workflow, please let me know what you're missing! #### Features #### * [issue#13](https://gitlab.com/jeroenrnl/zoph/issues/13) Issue#13 Basic XMP Reading * [issue#124](https://gitlab.com/jeroenrnl/zoph/issues/124) Issue#124 Add touch gestures to slideshow * [issue#124](https://gitlab.com/jeroenrnl/zoph/issues/124) Issue#124 Added navigation buttons to slideshow #### Bugs #### * [issue#133](https://gitlab.com/jeroenrnl/zoph/issues/133) Issue#133: ZIP download stops after first file * [issue#134](https://gitlab.com/jeroenrnl/zoph/issues/134) Issue#134 Actionlinks on organizer pages do not line up with the the organizer they belong to ## Zoph 0.9.14 ## ### 06-09-2020 ## This release mainly fixed a handful of minor bugs, introduced with the changes in the previous release. I also improved the way you can manage Zoph's configuration via the CLI, this could come in handy if you'd like to automate changes or have made a mistake that causes not te be able to logon anymore. Part of this was already there, just never documented. [It is now.](CLI.md#working-with-configuration-items-via-the-cli). I also did some work on the long running project to move all HTML code out of the source code and into templates. #### Features #### * [issue#130](https://gitlab.com/jeroenrnl/zoph/issues/130) Improved configuration changed via CLI #### Bugs #### * [issue#128](https://gitlab.com/jeroenrnl/zoph/issues/128) Error when adding a person to a photo with autocomplete switched off * [issue#132](https://gitlab.com/jeroenrnl/zoph/issues/132) First logon sometimes failed * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8) Fixed some small issues with the albums display #### Refactor #### * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8) Changed categories.php to use template instead of inline HTML * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8) Refactor category.inc.php in MVC + templated view ## Zoph 0.9.13 ## ### 15-07-2020 ## I have not yet managed to get back to my planned 6 week interval between releases, but at least it hasn't been a year since the last release. This release adds a few new features and fixes a few bugs. For the feature changes, I have also refactored the code somewhat to modernize Zoph's code and besided that, many more lines of code are now covered by unittests, which should have a positive impact on Zoph's stability. Zoph can now use external authentication by using REMOTE_USER authentication, this can be used to build a single signon system, where a front end performs the authentication and Zoph trusts the external authenication and logs the user in without presenting the logon screen. Obviously, this feature is off by default and you should only turn it on if you are aware of the consequences and trust the third party authenticator. This feature was requested to enable integration into [FreedomBox](https://freedombox.org). Anyone who has photos in their Zoph database with more than just a handful of people on it, knew that Zoph was limited on this point. This release enables you to specify rows of people on a photo and a much improved way to edit the order of people on a photo. This should make organizing photos with lots of people on it much easier. Finally, I updated leaflet and mapbox, which are used to display maps in Zoph to the most recent version. #### Features #### * [issue#123](https://gitlab.com/jeroenrnl/zoph/issues/123): Added REMOTE_USER authentication * [issue#117](https://gitlab.com/jeroenrnl/zoph/issues/117): Display people in rows, edit people on photos, move left, right and between rows * [issue#117](https://gitlab.com/jeroenrnl/zoph/issues/117): Added error when adding a person that is already on the photo #### Bug fixes #### * [issue#126](https://gitlab.com/jeroenrnl/zoph/issues/126): Edit link does not contain offset * [issue#126](https://gitlab.com/jeroenrnl/zoph/issues/126): Error displayed after deleting a photo #### Refactor #### * [issue#127](https://gitlab.com/jeroenrnl/zoph/issues/127): Updated leaflet and mapbox * [issue#123](https://gitlab.com/jeroenrnl/zoph/issues/123): Refactor authentication, improved unittest coverage for authentication * [issue#123](https://gitlab.com/jeroenrnl/zoph/issues/123): Started building PHP_CodeSniffer standard for Zoph * [issue#123](https://gitlab.com/jeroenrnl/zoph/issues/123): Fixes in documentation, some typos and whitespace fixes * [issue#123](https://gitlab.com/jeroenrnl/zoph/issues/123): Modified session class to make it testable * [issue#123](https://gitlab.com/jeroenrnl/zoph/issues/123): Slight modification in anonymousUser handling * [issue#123](https://gitlab.com/jeroenrnl/zoph/issues/123): Added PHP location to language file as in some cases (PHPUNIT) Zoph was not able to locate the files * [issue#117](https://gitlab.com/jeroenrnl/zoph/issues/117): some style, documentation and whitespace improvements * [issue#117](https://gitlab.com/jeroenrnl/zoph/issues/117): Refactor: move people-related methods from photo to photo\people object * [issue#117](https://gitlab.com/jeroenrnl/zoph/issues/117): Update unittests for 'people in rows' feature * [issue#117](https://gitlab.com/jeroenrnl/zoph/issues/117): Removed unused function ## Zoph 0.9.12 ## ### 06-04-2020 ### After almost a year, I finally managed to find some time to wrap up a new Zoph release. I've completely rebuilt the slideshow feature to make it more modern and feature rich. It's not yet complete, check [issue#124](https://gitlab.com/jeroenrnl/zoph/issues/124) if you have ideas on how to improve it or if you run into unexpected results, maybe on some devices. I also fixed a few bugs, among others, making Zoph compatible with PHP 7.4 and a few bugs with the lightbox feature. #### Features #### * [issue#118](https://gitlab.com/jeroenrnl/zoph/issues/118): Added fullscreen mode to slideshow #### Bugs #### * [issue#119](https://gitlab.com/jeroenrnl/zoph/issues/119): Unable to select lightbox album for user * [issue#119](https://gitlab.com/jeroenrnl/zoph/issues/119): Error when adding photo to lightbox * [issue#119](https://gitlab.com/jeroenrnl/zoph/issues/119): Not possible to remove photo from Lightbox * [issue#119](https://gitlab.com/jeroenrnl/zoph/issues/119): Zoph would sometimes incorrectly say 'lightbox' in title * [issue#120](https://gitlab.com/jeroenrnl/zoph/issues/120): No proper error when ZIP support missing * [issue#121](https://gitlab.com/jeroenrnl/zoph/issues/121): PHP 7.4 gives Notice on when no ratings in database * [issue#121](https://gitlab.com/jeroenrnl/zoph/issues/121): PHP 7.4: reverse order parameters for implode() is deprecated * [issue#121](https://gitlab.com/jeroenrnl/zoph/issues/121): PHP 7.4: reverse order parameters for implode() is deprecated * [issue#122](https://gitlab.com/jeroenrnl/zoph/issues/122): Error when making a rating or comment from an IPv6 address * [issue#125](https://gitlab.com/jeroenrnl/zoph/issues/125): Search for unrated photos and then rating them bug #### Refactoring #### * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8) Moved album to template + mvc * Moving to Jenkins, starting to use phpdoc, phpmd, phpstan ## Zoph 0.9.11 ## ### 13-04-2019 ### A not so spectacular release this time, with only a limited amount of changes: * Refactor: create_function is deprecated, replace with closure * [Issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Changed "person" into template and split controller view * [Issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): person: included number of photos of/by in links again * UnitTests: fixes for new phpunit version * whitespace fixes * Fixed an issue where an error was displayed when trying to delete a person with a coverphoto * Person view now also displays full name * locationLookup: whitespace fix ## Zoph 0.9.10 ## ### 25-01-2019 ### Here is the latest release of Zoph. For those who had hoped for a v1.0 after v0.9.9... I have to disappoint you as it has become v0.9.10. I still think Zoph is not ready to be v1.0 but we're moving closer and closer. This version brings two new features, both of which were ideas from John Lines, one of them was even partly implemented by him. Thanks John! #### Bugs #### * [Issue#116](https://gitlab.com/jeroenrnl/zoph/issues/116): 'Next' button on search results sometimes leads you back to the search page #### Features #### * [Issue#113](https://gitlab.com/jeroenrnl/zoph/issues/113): 'location lookup': lookup a location by GPS coordinates, Pluscode, OpenStreetMap URL or Zoph URL. * [Issue#112](https://gitlab.com/jeroenrnl/zoph/issues/112): improvements on calendar page #### Other #### * [Issue#115](https://gitlab.com/jeroenrnl/zoph/issues/115) Removed old password validation ## Zoph 0.9.9 ## ### 17-11-2018 ### It's been a long time since the last Zoph release. A lot has happened in between, Zoph has moved from Github to Gitlab and a lot of changes have been made to Zoph. Most of the changes are so called 'refactoring': changes to the code that do not change the functionality, so you shouldn't see anything of it. Many of these changes are necessary to keep Zoph's code up to date with current PHP best practices. Also, there is the long-running change to make Zoph's look fully managed by templates. In this release, I changed more parts of Zoph to use templates, instead of embedded HTML. There's one exception to the "no functionality changes": I dropped the possibility to redirect zoph back and forth between http and https. When this code was added, 12 years ago, many home-servers were not powerful enough to handle large photos over an SSL session, so Zoph included a system where you would logon via SSL and then redirect back to plain http. Nowadays webservers can easily handle the filesizes and this code only adds unnecessary complexion, so I removed it. Several bugs were fixed in this version, but none of them very major. #### Refactor #### * [Issue#100](https://gitlab.com/jeroenrnl/zoph/issues/100): remove ssl.force, url.http and url.https * Changing `and` and `or` into `&&` and `||` for readability and consistency * Changed capitalization of exception classes for consistency * Several fixes in namespace, capitalization and documentation * Moved part of `zophTable::update()` into `zophTreeTable::update()` * Updated navigation on edit photo page to match the photo page * [issue#8](https://gitlab.com/jeroenrnl/zoph/issues/8): Changed the display photo page to use a template * Documentation fixes, capatalization fixes, variable name fix, missing classname fix * Updated navigation on edit photo page to match the photo page * Changed the display photo screen to a template * Split photo.php into view and controller * Documentation improvements * Added forgotten default parameter to selection construct * whitespace fix * Removed rotate and thumbnail generation from display photo * Made some minor style and layout fixes in recent code * Moved creating the maps into photo views * Moved two functions from photo class to photo\collection class * Renamed some variables and functions that used "link" where "URL" was meant * Removed format_timestamp() and create_actionlinks() from util.inc.php * Moved create_zipfile() from util.inc.php into new file/archive class * Unittest fixes #### bugs #### * [issue#111](https://gitlab.com/jeroenrnl/zoph/issues/111): Not possible to edit album or category * Fixed an issue where a selection would sometimes display a warning. * Fix Exception call * Fixed an issue where a selection would sometimes display a warning. * Added missing ">" to edit photo template * Fixed an issue where sometimes not all info for a photo was displayed * Fixed two small issues in photo display template * Fix for description box always showing on photo page * Some small layout changes * fixed a forgotten reference to the old util.inc.php getZophURL() ## Zoph 0.9.8 ## ### 02 march 2018 ### I have moved all Zoph's documentation to Github. About 12 years ago, Zoph's documentation was hand-crafted HTML that was an ordeal to keep updated. I moved it to Wikibooks so I could update the docs through their webinterface. Nowadays, Github offers 'markdown' which is a text format that is both readable in plain text and can be rendered to a more pleasing look on the website. I was already keeping some documentation in this format and it caused a lot of extra work, because I was essentially maintaining two sets of documentation. So, as of this release, all documentation is back in one place: [Zoph's repository on Github](http://github.com/jeroenrnl/zoph/) Furthermore, quite a few bugfixes have been been made. Most of them related to the rewrite of the search page in the previous version. #### Bugs #### * [issue#102](https://github.com/jeroenrnl/zoph/issues/102): Error instead of thumbnail for empty circles * [issue#103](https://github.com/jeroenrnl/zoph/issues/103): each() is deprecated as of PHP 7.2 * [issue#104](https://github.com/jeroenrnl/zoph/issues/104): Search results for text-based 'LIKE' searches are reversed * [issue#105](https://github.com/jeroenrnl/zoph/issues/105), [issue#106](https://github.com/jeroenrnl/zoph/issues/106), [issue#108](https://github.com/jeroenrnl/zoph/issues/108): Several issues with the search results page * [issue#107](https://github.com/jeroenrnl/zoph/issues/107): ">", ">=", "<" and "<=" are pointless for text searches * [issue#109](https://github.com/jeroenrnl/zoph/issues/109): Autocomplete dropdown sometimes hidden behind map * [issue#110](https://github.com/jeroenrnl/zoph/issues/110): 'show all EXIF' button doesn't work #### Refactor #### * Removed unused functions in util.inc.php * Removing ancient scripts that are either redundant or no longer working * [issue#100](https://github.com/jeroenrnl/zoph/issues/100): Deprecate ssl.force, url.http and url.https As of **Zoph 0.9.9** these settings will be removed. Zoph warns you if you are using them as of **Zoph 0.9.8**. If you feel your Zoph installation can not do without these settings, please comment in this issue. * [issue#110](https://github.com/jeroenrnl/zoph/issues/110): Changed deprecated `read_exif_data()` to `exif_read_data()` ## Zoph 0.9.7 ## ### 19 jan 2018 ### I have had a very busy year and little time to spend on Zoph, but last december, I finally found time to finish what I had originally planned for 0.9.6: a complete rewrite of the search screen and the search engine. Most of the code in that part of Zoph was over 10 years old and had become quite messy over the years. The search engine is really the core of Zoph: if you open an album in Zoph, under the hood, Zoph really executes a search for all the photos in that album. This makes this code really important and I've made sure to cover all this by automated tests (UnitTests) before making any changes. * [issue#83](https://github.com/jeroenrnl/zoph/issues/83) Complete rewrite of the search page and the core functions of Zoph, including modernization of several other part of Zoph. * [issue#90](https://github.com/jeroenrnl/zoph/issues/90) Error displayed when adding a new place * [issue#99](https://github.com/jeroenrnl/zoph/issues/99) Geolocation doesn't work when using https * Documentation updates - not all files were correctly displayed using Github's Markdown interpreter ## Zoph 0.9.6 ## ### 14 apr 2017 ### Zoph 0.9.5 coincided with a significant change in MySQL, that caused a lot of bugs in Zoph and other open source projects. MySQL changed the way they process queries to handle them much more strictly. What makes things worse, is that MariaDB did not make this change, so at first I could not reproduce the issue. Because of the amount of work, I have decided to postpone the development that was planned for 0.9.6 and make this a bugfix-only release. In this release, I have included a few bugfixes by Pontus Fröding which is really great, thanks Pontus! ### Bugs ### * [issue#86](https://github.com/jeroenrnl/zoph/issues/86) Fixed an omission in the upgrade instructions for 0.9.5 * [issue#87](https://github.com/jeroenrnl/zoph/issues/87) error about class not found on add or edit * [issue#88](https://github.com/jeroenrnl/zoph/issues/88) Changes for MySQL 5.7 compatibility * Give timestamp a default value * Add field needed for MySQL 5.7 compatibility with SELECT DISTNCT .. ORDER BY * Adding "ORDER BY" fields to autocover query * More changes for MySQL 5.7 compatibility * Updated SQL scripts * Removed unused field from the database * [issue#91](https://github.com/jeroenrnl/zoph/issues/91) Changed PHPUnit classes to namespaced class naming * Fixed an issue in a UnitTest that caused a failed test * [Pull Request#94](https://github.com/jeroenrnl/zoph/pull/94) Add namespace to template showJSwarning in edit_person (by Pontus Fröding) * [Pull Request#95](https://github.com/jeroenrnl/zoph/pull/95) Add template namespace on two more places. (by Pontus Fröding) * [issue#92](https://github.com/jeroenrnl/zoph/issues/92) Fixed database connection to utf-8 * [issue#93](https://github.com/jeroenrnl/zoph/issues/93) [Pull Request#95](https://github.com/jeroenrnl/zoph/pull/95) Fix for "Class pager not found" when using pagesets (by Pontus Fröding) ### Refactor ### * Some modifications to backtrace printing, for easier debugging * Moved album view into template * [issue#89](https://github.com/jeroenrnl/zoph/issues/89) Changed look of next and previous buttons on photo page and increased size of actionlinks * Small style change ## Zoph 0.9.5 ## ### 4 feb 2017 ### Zoph 0.9.5 is the new stable release. It is recommended for everyone to upgrade to this release ### Features ### * [Issue#68](https://github.com/jeroenrnl/zoph/issues/68) Changed from Mapstraction to Leaflet as mapping abstraction - with GoogleMaps, OpenStreetMap and MapBox (OpenStreetMap) support The code for this was based on code provided by Jason (@JiCiT) * [Issue#80](https://github.com/jeroenrnl/zoph/issues/80) You can now edit permissions from the album screen, without the need to go to the group edit. * [Issue#82](https://github.com/jeroenrnl/zoph/issues/82) Zoph now gives a proper error message if a photo can not be found ### Bugs ### * Fixed a bug where in some cases it was possible for an admin to unintentionally delete albums ### Refactor ### * Lots of internal changes to move to an MVC-architecture * Several more parts of Zoph moved into templates * Added more unittests - to automatically test Zoph ## Zoph 0.9.4 ## ### 18 Sept 2016 ### Zoph 0.9.4 is the new stable release. It is recommended for everyone to upgrade to this release ### Features ### * Geocoding: Zoph now also searches Wikipedia * [Issue#67](https://github.com/jeroenrnl/zoph/issues/67) Changed the colour scheme definition to use a nice interface to select the colour * [Issue#23](https://github.com/jeroenrnl/zoph/issues/23) An admin user can now define default prefences for new users * [Issue#24](https://github.com/jeroenrnl/zoph/issues/24) Added an option to automatically propagate permissions to newly created albums * [Issue#78](https://github.com/jeroenrnl/zoph/issues/78) Removed Yahoo, Cloudmade mapping as they no longer offer their services to the public * [Issue#78](https://github.com/jeroenrnl/zoph/issues/78) Removed Openlayers mapping, as Zophs implementation was buggy and did not work anymore. * [Issue#47](https://github.com/jeroenrnl/zoph/issues/47) Photos can now be deleted from disk (moved to a trash dir) * [Issue#67](https://github.com/jeroenrnl/zoph/issues/67) Added some new colour schemes ### Bugs ### * Fixed an issue with album pulldown when editing group access rights * Fixed an issue where the circles page would sometimes report $title not found * Fixed an issue with changing views on circle page * Fixed an issue that caused errors in Firefox when using the configuration page * fixed collapsable details for time and rating * [Issue#78](https://github.com/jeroenrnl/zoph/issues/78) Fixed a case where an admin user was sometimes not allowed to see a person or a place ### Other improvements ### * [Issue#77](https://github.com/jeroenrnl/zoph/issues/77) Lots of fixes in the German translation by Thomas Weiland (@HonkXL) * Moved group display to template * Moved group delete (confirm) into template * Moved group edit to a template * [Issue#79](https://github.com/jeroenrnl/zoph/issues/79) Modify recursive creation of directories, so Zoph can function in an open_basedir enverironment. * [Issue#66](https://github.com/jeroenrnl/zoph/issues/66) Cleanup of CSS * Some modernization of the looks of Zoph * [Issue#85](https://github.com/jeroenrnl/zoph/issues/85) Modified import process to show clearer error message * [Issue#66](https://github.com/jeroenrnl/zoph/issues/66) Added a reset CSS * [Issue#81](https://github.com/jeroenrnl/zoph/issues/81) Documentation updates * Some fixes for UnitTests * Additional tests * Refactor of group_permissions class into permissions class * Refactor prefs class * Moved preferences page to template * Modified prefs template to use labels instead of definition lists ## Zoph 0.9.3 ## ### 10 jun 2016 ### Zoph 0.9.3 is the new stable release. It is recommended for everyone to upgrade to this release ### Features ### * [Issue #72](https://github.com/jeroenrnl/zoph/issues/72) Zoph now has a new logon screen. The logon screen has background photos. Two of them are already included in Zoph. You can place your own backgrounds in ```templates/default/images/backgrounds```. Or, you can (on the config screen) define an album from which the images will be used as background images. Zoph will display a random image as background. * [Issue #76](https://github.com/jeroenrnl/zoph/issues/76) The logon screen now gives a message about the username and/or password being wrong instead of just returning to the same screen * [Issue #75](https://github.com/jeroenrnl/zoph/issues/75) Zoph now uses PHP's password hashing algorithm instead of MySQL's. This includes a random 'salt' added to each password. This will make it much, much harder to decrypt your passwords, if your database would ever fall into the wrong hands. The old hashes will be updated with the new ones as soon the the user logs in. Zoph will continue to support the old password hashes at least until v0.9.5. * [Issue #26](https://github.com/jeroenrnl/zoph/issues/26) It is now possible to define the cookie expirement time. In previous versions of Zoph, a user would be logged out when closing the browser. Is now possible to extend the time to 1 hour, 4 hours, 8 hours, 1 day, 1 week or 1 month. This means a user will not need to re-login for that period of time, even when the browser is closed in the mean time. This can be very convenient, but it could mean that a user leaves Zoph logged in on a public PC. Therefore, the default is still 'session', which means a user will be logged out when closing the browser. * "new" pages now show up in breadcrumbs * It is now possible to give a user "can see all photos" access rights. This means you can give a user access to all photos, without giving him/her admin rights and without having to update user rights whenever an album is added. * [Issue #22](https://github.com/jeroenrnl/zoph/issues/22) It is now possible to allow a user to create albums, categories, people, circles and places. The user automatically has access rights to place photos in the albums, categories, people, circles and places he or she has created. * [Issue #21](https://github.com/jeroenrnl/zoph/issues/21) It is now possible to allow a user to delete photos. The user will have to have "write" access to at least one album a photo is in. * Remove the rather ugly trailing space on the links on zoph.php ### Bugs ### * [Issue #73](https://github.com/jeroenrnl/zoph/issues/73) Fixed sharing feature * [Issue #74](https://github.com/jeroenrnl/zoph/issues/74) Fixed Canadian English, Dutch and German translation files ### Other improvements ### * Added a way to disable a setting on the configuration page depending on the state of another configuration item. (This was created because the photo album as a logon background relies on the sharing feature to be enabled). * Moved user page to template * Moved form into a separate class * Some cleanup of the places and categories pages * Refactor HTML for actionlinks * Modified createTestData script to only require password once * Rearranged order of unittests * Added translations for German, Canadian English and Dutch ## Zoph 0.9.2 ## ### 1 apr 2016 ### Zoph 0.9.2 is the new stable release. I have decided to drop the separation between 'stable' and 'unstable' or 'feature' releases. This means that it is recommended for everyone to upgrade to this release. ### Features ### * [Issue #44](https://github.com/jeroenrnl/zoph/issues/44) : Added 'circles': a way to group people in Zoph. This is especially handy if you have a large amount of people in your Zoph, and the 'person' page is becoming confusing or cluttered. * [Issue #46](https://github.com/jeroenrnl/zoph/issues/46) A circle and it's members can be surpressed in the overview page, so you can, for example, hide people that you added only for a small set of photos. * [Issue #20](https://github.com/jeroenrnl/zoph/issues/20) Zoph has switched to the PDO classes for database access. This ensures compatibility with PHP in the future, because the old mysql libs will be dropped soon. * [Issue #32](https://github.com/jeroenrnl/zoph/issues/32) It is now possible to set more properties of a photo, including map zoom from the web import. * [Issue #60](https://github.com/jeroenrnl/zoph/issues/60) The link text for "next" and "previous" as well as page numbers has been increased in size for better usability esp. on mobile devices * Added a script for fixing filename case (by Jason Taylor [@JiCit] ) * Access Google maps via https (Jason Taylor [@JiCiT]) * As of this version, the language files are in the php dir, and no longer need to be copied or moved separately ### Bugs ### * [Issue #49](https://github.com/jeroenrnl/zoph/issues/49) Zoph now supports MySQL strict mode * [Issue #55](https://github.com/jeroenrnl/zoph/issues/55) Autocomplete not working for people * [Issue #58](https://github.com/jeroenrnl/zoph/issues/58) Sort order for albums and categories can not be changed * CLI: Fixed an issue where Zoph would try to import to the current directory when double spaces were present in CLI * Better handling of file not found problems during import * Fixed two bugs that caused maps not to display * Fixed an issue where breadcrumbs wouldn't be removed correctly in some cases * Changed erronous extension of Exception class * Fixed slow login times for non-admin users * Improved performance on people page * Fixed: zoom buttons are missing from Google Maps * Remove duplicate files from import (if you would specify the same file twice on CLI import, you would get an error, this is now filtered out) * Fixed an issue where the person pulldown on the add user page appeared to be empty * Remove a user from a group when a the user is deleted * Fixed a warning about unknown variable on places page * Allow apostropes in place names when creating map markers (Jason Taylor [@JiCiT]) ### Refactor ### * A complete new query builder has been created * Many more parts of Zoph can be (and are being) tested automatically now, this should improve overall quality and reduce bugs * Many parts of Zoph have been cleaned up to modernize code to the current state of PHP - dropping PHP 5.3 and 5.4 compatibility * Dropped MSIE6/7 compatibility * Added documentation to many parts of Zoph's source code * Many changes to readability of source code, such as more consistent use of whitespace * Added some more debugging possibilities to easier troubleshoot in case of problems * Changed logging so less logging is displayed when set to log::NONE * Changed all self:: references into static:: references * Added function scope to many methods * Started using namespaces to better organize the classes * Updated version numbers in REQUIREMENTS readme. * [Issue #8](https://github.com/jeroenrnl/zoph/issues/8) (partial) Changed several parts of Zoph to use templates * Added improvements to templating system * Modified query for photo access rights to a view for performance reasons * Changed logging so SQL query log to file can be done without displaying * Performance improvement on place page * Added a posibility to debug queries including parameters ## Zoph 0.9.1 ## ### 21 Feb 2014 ### Zoph 0.9.1 is the first feature release for Zoph 0.9, it shows a preview of some of the new features for Zoph 0.10. Most important change is the move of most configuration items from config.inc.php into the Web GUI. #### Features #### * [Issue #28](https://github.com/jeroenrnl/zoph/issues/28) Configuration through webinterface * Removed display desc under thumbnail feature * Removed MIXED_THUMBNAILS and THUMB_EXTENSION settings * removed DEFAULT_SHOW_ALL setting * Removed LANG_DIR configuration item * Changed the looks of fields a bit * Removed alternative password validators * Removed checks for PHP 5.1 * Adding CLI support for configuration * [Issue #7](https://github.com/jeroenrnl/zoph/issues/7) Added a favicon * [Issue #18](https://github.com/jeroenrnl/zoph/issues/18) Added "return" link on bulk edit page * Added a script to migrate config to new db-based system * [Issue #8](https://github.com/jeroenrnl/zoph/issues/8) Made template selectible from webinterface * Removed MAX_CRUMBS #### Bugs #### * Simplified CLI code & fixed bug in --autoadd * [Issue #34](https://github.com/jeroenrnl/zoph/issues/34) Rows and columns swapped on photos page * [Issue #36](https://github.com/jeroenrnl/zoph/issues/36) Webimporter does not import description * [Issue #37](https://github.com/jeroenrnl/zoph/issues/37) Can not add position on map using the mouse * Fixed a bug that caused EXIF information in some (rare) cases to report the aperture wrong. * Strict standards warning * [Issue #45](https://github.com/jeroenrnl/zoph/issues/45) Pagebreak inside HTML tags causes browser to render incorrectly * [Issue #45](https://github.com/jeroenrnl/zoph/issues/45) Added selectArray cache to zophTable * [Issue #48](https://github.com/jeroenrnl/zoph/issues/48) Repair photo ratings during import * [Issue #50](https://github.com/jeroenrnl/zoph/issues/50) Geonames project has changed URL and requires username * [Issue #51](https://github.com/jeroenrnl/zoph/issues/51) Fixed depth in tree display when autocorrect is off * [Issue #39](https://github.com/jeroenrnl/zoph/issues/39) Added support for session.upload_progress as APC replacement (PHP 5.4 compatibility) * [Issue #38](https://github.com/jeroenrnl/zoph/issues/38) CLI tries to lookup previous argument's value when looking up photographer #### Improvements #### I have made quite a few improvements on the "inside" of Zoph. I have refactored many parts of Zoph to create cleaner, less duplicated and more robust code. I have introduced UnitTests (resulting in about 20% of Zoph's sourcecode now tested fully automatic for bugs). As a help to that, I am now using Sonar to automatically run these tests and also analyse Zoph code for other problems. * [Issue #29](https://github.com/jeroenrnl/zoph/issues/29) First step in creating unittests for Zoph * Sonar Support * Refactor of PHP part of Mapping implementation * Move timezone-related global functions into class * TimeZone object improvements * Small change in way template is called on photo page (Full page templates are now "templates" and partial pages are "blocks") * Refactor of htmlMimeMail.php * Refactor of Mail_mimePart * Refactor annotate photo, watermark photo, image.php * Removed several global variables * Finished refactor of MIME classes * Refactor album, category, place, person, photo * Refactor: getEditArray() + unittests * Further refactor of photo, album, person, place, category * Refactor: move ratings out of photo object * Refactor: moved relations from photo object to new photoRelations object * Refactor: photo object * Got rid of adding session_id to URL * Modified internal database references to static * Removed brackets from require and include statements * Replaceed a die() with exception * Changed self-references in objects to use self:: * Removed unused class smtp * Made autoload a little more robust * Changes to autoload so it works in unittests too. * Removed unused RFC822 class * Changed line-endings in mailMimePart.inc.php to unix-style * Removed various unused variables * Removed duplicate templates * Removed unused $user from createPulldown() calls. * [Issue #40](https://github.com/jeroenrnl/zoph/issues/40) Change documentation to Markdown * Modified some queries to improve performance ## Zoph 0.9.0.1 ## ### 18 oct 2012 ### Zoph 0.9.0.1 is the first maintenance release for Zoph 0.9. It adds compatibility with MySQL 5.4.4 and later and PHP 5.4 support. Several bugs were fixed. #### Bugs #### * [Issue #1](https://github.com/jeroenrnl/zoph/issues/1) Changed TYPE=MyISAM to ENGINE=MyISAM for MySQL > 5.4.4 compatibility * [Issue #1](https://github.com/jeroenrnl/zoph/issues/1) Fixed: PHP Notice: Array to string conversion * [Issue #2](https://github.com/jeroenrnl/zoph/issues/2) Changed timestamp(14) into timestamp * [Issue #3](https://github.com/jeroenrnl/zoph/issues/3) Removed pass-by-reference for PHP 5.4 compatibility * [Issue #6](https://github.com/jeroenrnl/zoph/issues/6) Missing French translation * [Issue #30](https://github.com/jeroenrnl/zoph/issues/30) Remove warning about undefined variables * [Issue #31](https://github.com/jeroenrnl/zoph/issues/31) Fixed several errors in geotagging code * [Issue #33](https://github.com/jeroenrnl/zoph/issues/33) Fixed: no error message when rotate fails Fixed a small layout issue on the prefs page ## Zoph 0.9 ## ### 23 jun 2012 ### Zoph 0.9 is a stable release. It's equal to v0.9pre2, except for an updated Italian translation. #### Translations #### Updated Italian translation, by Francesco Ciattaglia There are no known bugs in this version. ## Zoph 0.9pre2 ## ### 20 Feb 2012 ### Zoph 0.9pre2 is the second release candidate for Zoph 0.9. Zoph is now completely feature-frozen for the 0.9 release, only bugfixes will be made. #### Bugs #### * Bug#3471099: Map not displaying when looking at photo in edit mode * Bug#3471100: On some pages, title contains PHP warning ## Zoph 0.9pre1 ## ### 26 Nov 2011 ### Zoph 0.9pre1 is the first release candidate for Zoph 0.9. Zoph is now completely feature-frozen for the 0.9 release, only bugfixes will be made. #### Bugs ### * Bug#3420574: When using --autoadd, zoph CLI import sometimes tries to create new locations or photographers even though they already exist in the database. * Bug#3427517: Share this photo feature does not work * Bug#3427518: Not possible to remove and album or category from a photo * Bug#3433687: Not possible to remove album or category from photo (bulk) * Bug#3431130: Share this photo doesn't show links in photo edit mode * Bug#3433810: Popup for albums, categories, people and places doesn't always disappear when moving mouse away. * Removed a warning that in some cases caused images not to be displayed. #### Translations #### * Added a few missing strings, reported by Pekka Kutinlahti. * Updated Italian translation, by Francesco Ciattaglia * Updated Dutch, German, Canadian English and Finnish #### Other #### * Got rid of a lot of PHP warnings * Got rid of a lot of PHP strict messages * Cut down on the number of global variables * Removed support for magic_quotes * Removed (last traces of) PHP4 support * Bug#3435181: Variable inside quotes * Updated wikibooks documentation ## Zoph 0.9 ## ### 23 jun 2012 ### Zoph 0.9 is a stable release. It's equal to v0.9pre2, except for an updated Italian translation. ### Translations ### * Updated Italian translation, by Francesco Ciattaglia There are no known bugs in this version. ## Zoph 0.9pre2 ## ### 20 feb 2012 ### Zoph 0.9pre2 is the second release candidate for Zoph 0.9. Zoph is now completely feature-frozen for the 0.9 release, only bugfixes will be made. ### Bugs ### * Bug#3471099: Map not displaying when looking at photo in edit mode * Bug#3471100: On some pages, title contains PHP warning ## Zoph 0.9pre1 ## ### 26 nov 2011 ### Zoph 0.9pre1 is the first release candidate for Zoph 0.9. Zoph is now completely feature-frozen for the 0.9 release, only bugfixes will be made. ### Bugs ### * Bug#3420574: When using --autoadd, zoph CLI import sometimes tries to create new locations or photographers even though they already exist in the database. * Bug#3427517: Share this photo feature does not work * Bug#3427518: Not possible to remove and album or category from a photo * Bug#3433687: Not possible to remove album or category from photo (bulk) * Bug#3431130: Share this photo doesn't show links in photo edit mode * Bug#3433810: Popup for albums, categories, people and places doesn't always disappear when moving mouse away. * Removed a warning that in some cases caused images not to be displayed. ### Translations ### * Added a few missing strings, reported by Pekka Kutinlahti. * Updated Italian translation, by Francesco Ciattaglia * Updated Dutch, German, Canadian English and Finnish ### Other ### * Got rid of a lot of PHP warnings * Got rid of a lot of PHP strict messages * Cut down on the number of global variables * Removed support for magic_quotes * Removed (last traces of) PHP4 support * Bug#3435181: Variable inside quotes * Updated wikibooks documentation ## Zoph 0.8.4 ## ### 9 Sept 2011 ### Zoph 0.8.4 is the final pre-release for Zoph 0.9. This version adds several feature improvements. More features have been added the new CLI import, which was introduced in v0.8.2. The 'bulk edit' page has been improved, both in features as in loading speed (100x faster in some cases!). The 'tree view' and 'thumb view' overview pages have been improved. Several coding style modernisation changes have been made. ### Features ### * Req#1985439: Adding albums, categories, places and people via the CLI * Req#1985439: Automatically adding albums, categories, places and people via the CLI * Req#3042674: Recursive import of directories * Req#1985439: Setting album, category, person, photographer, path from import dir. * Req#1756507: photocount in tree view. * Req#1491208: Show more info in thumbnail overview * REQ#2813979: Added date & time fields to bulk edit page * Added autocomplete support to bulk edit page * Changed the photo edit page to automatically add new dropdowns to albums, categories and people. * Removed 'people_slots' functionality * Changed add people on bulk photo edit page to use multiple dropdowns * Add multiple albums, categories, persons on both single and bulk photo edit. * Req#2871210: Added 'share photo' feature. * Zoph now stores a hash of a photo in the database * zoph CLI: Added -D as shorthand for `--path` ### Bugs ### * Bug#3312029: `MAGIC_FILE` cannot be empty * Fixed an issue that caused the 'search' button for geocoding on the edit location page to be misplaced. * Fixed a typo that caused the 'track' screen to no longer work ### Translations ### * Updated translations * Added some previously forgotten translations ### Refactoring ### Zoph has started it's life in the era of PHP3, while the current version of PHP is version 5.3. In between a lot has been changed in PHP. I have started to adopt PHP5-style programming some time ago for new development. I have now also started to refactor the other code to a new coding style. Currently, Zoph still has ''a lot'' of global functions and I am slowly moving almost all of them to static methods. * Made several changes to function names to accommodate new coding style * Refactored `photo->update_relations()` to merge with the similar `photo->updateRelations()` that the new import introduced. * Moved `get_root_...()` functions into static functions. * Refactor of `zoph_table` object (now called `zophTable`) * Renamed function `photo->get_image_href()` to `photo->getURL()` * Made some changes to the `delete()` methods so PHP strict standards are followed. ### Other ### * Inline documentation improvements * Improved expand/collapse Javascript robustness * Some eyecandy (esp expand/collapse) * Changed the date and time field to type 'date' and type 'time', which are new types for HTML5. Tested in Chromium. * Removed deprecated IMAGE_SERVICE setting. IMAGE_SERVICE is now always on. * Renamed image_service.php to image.php * Improved loading speed of the 'tracks' page by using a different, better cachable SQL query ## Zoph 0.8.3 ## ### April 3, 2011 ### Zoph 0.8.3 is a pre-release for Zoph 0.9. This version adds several feature improvements, mostly related to mapping. The most important addition is the support for geotagging. This version also fixes several bugs. Zoph 0.8.3 is beta release, I tested it as well as possible on my system, but it should not be considered a "stable" version. I would, however, very much appreciate if people could test and give feedback on this release and the updated documentation, in this way I can make sure that the stable (v0.9) version will be as bug-free as possible. ### Features ### * Geotagging support * Req#2974014: Search for location * Geocoding: finding lat/lon location from city, county. * Req#2974016: Additional mapping resources * Req#3077944: When adding a new place, or editting a place with no location (lat/lon) set, zoph will zoom the map to the parent location. If a photo is editted, and the photo has no lat/lon, but it's location does, the map is zoomed to the location's lat/lon. ### Bugs ### * Getting rid of a NOTICE regarding unset `DB_PREFIX` constant * Several small changes to decrease the number of NOTICE messages. * In photo edit mode, moved maps to bottom of page, to fix a bug with Openlayers maps * Better error handling when `UPLOAD_DIR` does not exist. * Zoph.ini: Added quotes around values, PHP fails if they contain special characters. As suggested by scantron. * Bug#3237112: Rating counts are incorrect with new import * Bug#3237012: There is no "next" link on the bulk edit page, although a "previous" link is present. ### Other ### * Switched from Mapstraction 1.x to Mapstraction 2.0.15 * Namespacing in mapping Javascript. * Some changes in templating system * Bug#3104632: Various changes for PHP 5.3 compatibility * Refactor of zophcode, tag, smiley and replace objects to new coding style, including added PHPdoc comments. * Added a copyright note to Openlayers maps * Refactor of the admin class & move admin page to a template. * Getting rid of some warning messages ### Translations ### * Dutch and Canadian English have been updated and are completely up to date ## Zoph 0.8.2.1 ## ### November 20, 2010 ### Zoph 0.8.2.1 is a bugfix release for Zoph 0.8.2. Many changes were made in Zoph 0.8.2 and with so many changed lines of code, a few bugs is almost inevitable. This release fixes all known bugs in v0.8.2. ### Bugs ### * Bug#3064940: HTML in dropdown menus. (This bug was previously fixed in Zoph 0.8.0.5, but the fix was not correctly ported to the development branch) * Bug#3094182: New CLI does not store location and photographer * Bug#3094198: New CLI does not always look up location name correctly. * Bug#3094201: New CLI does not exit when it encounters an error (album, category, ... not found) * Bug#3102078: Webimport of archives fails with no error * Bug#3102080: New CLI `--update` can not set location and photographer * Bug#3102148: New CLI `--field` gives an error * Fix for an issue that caused javascript errors when an apostroph would appear in a title of a place. * Bug#3108196: Translation not working in Zoph 0.8.2 ## Zoph 0.8.2 ## ### October 20, 2010 ### Zoph 0.8.2 is the second pre-release for Zoph 0.9. Zoph 0.8.2 features a completely rewritten import system. The webinterface has been modernized. Error handling and user-friendliness have been improved. The CLI interface prior to v0.8.2 was written in Perl, because the rest of Zoph was written in PHP, a lot of duplicate work needed to be done whenever something needed to be changed in the import system. As of this version, the CLI interface has been rewritten in PHP as well. Zoph 0.8.2 is beta release, I tested it as well as possible on my system, but it should not be considered a "stable" version. I would, however, very much appreciate if people could test and give feedback on this release and the updated documentation, in this way I can make sure that the stable (v0.9) version will be as bug-free as possible. ### Features ### * New webimport * New CLI-import ### Bugs ### * Bugfixes from v0.8.0.5 have been included in this release. ### Other changes ### * Configuration of database connection has been moved from `config.inc.php` (webinterface) and `.zophrc` (CLI interface) to `/etc/zoph.ini`, for both the webinterface and the CLI interface. * `bin` and `man` directories in release tarball have been combined into the `cli` directory * HTML documentation (`docs` directory) is no longer included in the release. Maintaining this documentation cost a lot of time. The scripts I wrote to convert the Wikibooks documentation into offline documentation could not handle images and the documentation I wrote for the new webimport contains a lot of pictures. ## Zoph 0.8.0.5 ## ### October 20, 2010 ### Zoph 0.8.0.5 is a bugfix release that fixes a few bugs in Zoph 0.8.0.4 ### Bugs ### * Bug#3049203: Rating links on search page do not work. * Bug#3054562: HTML in rating dropdown on search page * Bug#3054566: Search for albums/categories/places/people/photographers is broken after 0.8.0.2 update. * Bug#3066174: Rotation not working in auto edit mode * Bug#3064937: SQL error when inserting a place with no timezone. * Bug#3064940: HTML in dropdown menu's. * Bug#3072586: Latitude is misspelled as "lattitude" ## Zoph 0.8.1.2 ## ### July 15, 2010 ### Zoph 0.8.1.2 is a bugfix release that fixes a few bugs in Zoph 0.8.1.1. ### Bugs ### * A few cases of duplicate encoding, causing HTML code to appear instead of being interpreted by the browser * A bug that caused markers not to work correctly * A bug that caused Zoph to loose timezone information when using the 'assign timezone to children' functionality. ## Zoph 0.8.0.4 ## ### July 15, 2010 ### Zoph 0.8.0.4 is a bugfix release that fixes a few bugs in Zoph 0.8.0.3. ### Bugs ### * A few cases of duplicate encoding, causing HTML code to appear instead of being interpreted by the browser ## Zoph 0.8.1.1 ## ### July 1, 2010 ### Zoph 0.8.1.1 is a security release that fixes a number of Cross Site Scripting (XSS) issues of which most were found by [VUPEN Security](http://www.vupen.com). I would like to thank VUPEN for reporting these bugs. Zoph 0.8.1.1 does not fix any other bugs. ### Bugs ### * Several XSS scripting issues found by VUPEN Security * Several XSS scripting issues found during fixing of the above bugs ## Zoph 0.8.0.3 ## ### July 1, 2010 ### Zoph 0.8.0.3 is a security release that fixes a number of Cross Site Scripting (XSS) issues of which most were found by [VUPEN Security](http://www.vupen.com). I would like to thank VUPEN for reporting these bugs. This release also fixes all the bugs found since the 0.8.0.2 release. ### Bugs ### * Several XSS scripting issues found by VUPEN Security * Several XSS scripting issues found during fixing of the above bugs * Bug#2901852: Fatal error when a photo without a photographer is displayed on the map * Bug#2902011: zophImport.pl cannot find people with no last name. * Bug#2925030: Last modified time is not displayed correctly * Bug#2925498: NULL entries in the database change to 0.000 after rotating an image causing fake map entries to appear. Fix by Jason Taylor. * Bug#2925508: Thumbnail covers actionlinks on people page. Fix by Jason Taylor. * Bug#2925506: Count of places is wrong. Fix by Jason Taylor. * Bug#2982051: editting photo does not work when using "auto edit". * Bug#3002691: Next/prev links lost after update. ## Zoph 0.8.1 ## ### 3 Jan 2010 ### Zoph 0.8.1 is the first feature release for v0.9. This release introduces a new logging system, that should allow users and developers to control more granular which debugging messages Zoph displays. The other major change is that Zoph is now completely UTF-8 based, this should fix issues users had with international characters. This last change requires some manual changes to the MySQL database. Zoph 0.8.1 is beta release, I tested it as well as possible on my system, but especially the UTF-8 conversion is very dependent on specific situations on your system; therefore it should not be considered a "stable" version. I would, however, very much appreciate if people could test and give feedback on this release and the upgrade documentation, in this way I can make sure that the stable (v0.9) version will be as bug-free as possible. ### Features ### * New logging/debugging system ### Bugs ### * Bug#1985449: Zoph should be UTF-8 * Bug#2901852: Fatal error when a photo without a photographer is displayed on the map * Bug#2902011: zophImport.pl cannot find people with no last name. * Bug#2925030: Last modified time is not displayed correctly * All the bugfixes from Zoph 0.8.0.1 and 0.8.0.2 ## Zoph 0.8.0.2 ## ### 1 Nov 2009 ### Zoph 0.8.0.2 is a bugfix release for Zoph 0.8. ### Bugs ### * Bug#2876282: Not possible to create new pages. * Bug#2873171: fatal error when autocomplete is switched off. * Bug#2873171: Javascript error in MSIE when trying to change the parent place using the autocomplete dropdown. * Bug#2873171: Timezone autocomplete does not work in MSIE * Bug#2881212: Not possible to unset timezone. * Bug#2889934: No icons in admin menu when using MSIE8 * Bug#2888263: Unintuative working of bulk edit page could lead to dataloss * Bug#2890387: Saved search does not remember the "include sub-albums/categories/places" checkbox and the state of the "AND/OR" dropdown. ### Translations ### * Added a Russion translation created by Sergey Chursin and Alexandr Bondarev ### Various ### * Changed deprecated mysql_escape_string() into new mysql_real_escape_string(). ## Zoph 0.7.0.8 and Zoph 0.8.0.1 ## ### 23 Sept 2009 ### Security fixes for 0.7 and 0.8. ### Bugs ### * Fixes a security bug that caused a user to be able to execute admin-only pages. ## Zoph 0.8 ## ### 9 Sept 2009 ### Final 0.8 release. Only small changes compared to 0.8pre3: ### Bugs ### * Fixed a bug that caused users of PHP 5.1.x get an error about non-existant DateTime class. ### Documentation ### * Added a few long-existing but overlooked and therefore not documented configuration settings * Added a troubleshooting section ("Solving Problems") ## Zoph 0.8pre3 ## ### 28 August 2009 ### This is the third pre-release for 0.8, it fixes the bugs discovered since v0.8pre2, including the security bug. It also updates several translations. ### Bugs ### * Bug#2841196: PHP error when logging in as non-admin user * zophImport.pl: Perl error due to missing quote and indentation fixes * Bug#2841296: Not possible to download 4.2GB ZIP files * Bug#2841357: Save search fails without an error in some cases * Bug#2841373: Saved search does not always work correctly when saving a photo collection that was not the result of a search action. * Fix for a cross site scripting bug (the same as the 0.7.0.7 release) * Bug#2845750: zophImport.pl fails when `--path` contains multiple dirs ### Translations ### * Dutch, Danish, French, Italian, Norwegian Bokmål and Swedish chef have been updated and are fully up to date. ### Documentation ### * Various updates * Removing very old changelog and upgrade instructions. They can still be read in the online (wikibooks) version. * Adding long existing but until now not documented options `DEFAULT_ORDER` and `DEFAULT_DIRECTION` * Completely rewritten requirements page ## Zoph 0.7.0.7 ## ### 24 Aug 2009 ### Zoph 0.7.0.7 is an update of the stable 0.7 branch and fixes a cross site scripting security bug. ### Bugs ### * Fix for a cross site scripting bug that found during development of Zoph v0.8 ## Zoph 0.8pre2 ## ### 8 July 2009 ### This is the second pre-release for 0.8, it fixes the bugs discovered since v0.8pre1, including the security bug. ### Bugs ### * Bug#2813464: Date link on photo page links to the wrong year * Bug#2813467: '+' links to expand date/time, ratings and tree view do not work anymore after a Googlemaps update * Fix for a cross site scripting bug that was reported by "y3nh4ck3r". * Fix for a bug that caused manually entered dates with webimport not to be used ## Zoph 0.7.0.6 ## ### 2 July 2009 ### Zoph 0.7.0.6 is an update of the stable 0.7 branch and fixes a cross site scripting security bug. ### Bugs ### * Fix for a cross site scripting bug that was reported by "y3nh4ck3r". ## Zoph 0.8pre1 ## ### 27 June 2009 ### Zoph 0.8pre1 is a prerelease (release candidate) for Zoph 0.8. It fixes a number of bugs from 0.7.5. ### Bugs ### * Fix for a bug that would give an error (or not execute without an error, depending on the situation) when an album is added to a photo. Bug found and fixed by Pekka Kutinlati. * Bug#2687577: Download link does not work in some cases * Bug#2720782: edit does not work after using back and forward buttons * Bug#2720807: Layout glitch on slideshow * Fixed two small issues in saved searches * Bug#2718812: Cannot assign someone as a father/mother/spouse when person does not yet appear on a photo. * Bug#2724768: Error in timezone code * Bug#2750454: Fatal error: Call to undefined function `get_photographer_search_array()` in person.inc.php * Bug#2775190: Dropdown menu with people is not sorted by name. * Bug#2718814: Not possible to unset a relationship between persons. * Fixed a bug where the average rating would become 0 when the last rating for that photo was removed * Bug#2794052: Syntax error in timezone.inc.php when using PHP4 * Bug#2803133: Making a category/album or place it's own parent causes out of memory error. * Bug#2804335: Division by zero error when importing JPG with zeroes in some of the EXIF fields. * Fix for a bug where the map on the photo page did not show the location of the photo if it is set on the location and not on the photo itself. * Fix for a bug where the map on the photo page did not show if the user is not using the 'auto-edit' feature. * Fix for a bug that caused a javascript error when title or address of a place contained quotes. ### Translations ### * German, Canadian English, Danish, Dutch, Italian and Swedish Chef have been updated. * Added Finnish translation by Pekka Kutinlati. * Removed some empty translations from outdated translations ### Other ### * Removed `zoph-0.3.3.postgress.diff` from the contrib dir. It was too outdated to serve any purpose. ## Zoph 0.7.5 ## ### 14 March 2009 ### Zoph 0.7.5 is the last "feature release" before v0.8. This version introduces a few new features that will be present in the next "stable" version, 0.8. This release fixes a number of bugs from the earlier 0.7.x releases. ### Bugs ### * Bug#2465009, wrong counters for rating * Fixed a bug where a translated version of Zoph would not make a breadcrumb for search results. * Fixed: Timezone calculations are using local timezone instead of configured `CAMERA_TZ`. * Bug#2671365 Can not leave comments * Fixed a bug in `zophImport.pl` where `--update` could in some cases move a photo to a wrong location. ### Features ### * Added a feature where an admin user can check out the ratings a certain user has given, adds a graph similar to the one on the reports page to the user's page. * Admins can now see who has rated what per photo * Admins can delete ratings * IP address and date/time are now stored when rating * An admin can control wheter a user can rate photos or not. * Req#2126915: Allow a user to rate the same photo multiple times, but only once per IP addres, you can use this for the `DEFAULT_USER` or a user account that is shared among multiple people. * Improved error handling on erronous time or date. (timezone calculations) * Move all MySQL calls into `database.inc.php`, making adjusting to other db's easier, partly resolves Req#2464455 * Req#1480136: Save search results * A list of all comments by a user is now shown in user profile. ### Known issues ### * The translations have not yet been updated * Not all documentation is up to date ## Zoph 0.7.4 ## ### 22 December 2008 ### Zoph 0.7.4 is a "feature release", that introduces a few new features that will be present in the next "stable" version, 0.8. This release fixes a number of bugs from the earlier 0.7.x releases and specifically deals with some performance issues. ### Bugs ### * Bug#2044965: Assign timezone to all children only assigns timezone to direct children. * Bug#2044967: Better error handling for guess timezone functionality * Bug#1820234: Zoph shows places, categories and people for restricted users. * Bug#2059210: Overal bad performance: long loading times, autocomplete boxes taking forever, etc. This fix gives a giant improvement on zoph performance. * Simplified several SQL queries * Changed SQL queries so unused rows are now longer requested from the db * Changed SQL queries so records are no longer sorted when it is not needed * Changed autocomplete code so it was no longer necessary to load both autocomplete and legacy dropdowns (major improvement on loading the seachpage!) * Bug#2125858: table headers on user page swapped * Bug#2097894: Layout failure in bulk edit page when using MSIE * Bug#1706366: People slots feature is incompatible with autocomplete Also adds autocomplete support to several pages that did not have it before (only bulk edit page does not have autocomplete support yet) * Bug#2274989: When changing user, password is overwritten. * Bug#2275005: Photographers not in people list. People list not showing all people for admin users. * Bug#2373633: Counter on zoph.php wrong for non-admin users * Bug#2373609: Tree view shows all albums/categories/places * Bug#2315870: Layout glitch when using non-standard size thumbnails. * Bug#2438062: Zoph does not pick a different coverphoto for people if the assigned one is not visible for the user. ### Features ### * Req#2097906: Add "next" and "prev" links to edit photo page, when not using auto-edit feature * Req#1467095: Group access rights ### Translation ### * Fixed some errors in translations (mostly extra spaces) * Updated Canadian English, Dutch and German translations ### Various ### * Removed 'smart_pulldown' code that was not used in most of the cases anyway, especially since autocomplete was added. * Created a script to automatically migrate from user-rights to group-rights. To be used with 0.7.3 to 0.7.4 or 0.7 to 0.8 migrations. ## Zoph 0.7.3 ## ### 24 July 2008 ### Zoph 0.7.3 is a "feature release", that introduces a few new features that will be present in the next "stable" version, 0.8. It also fixes some bugs from 0.7.1 and 0.7.2 Finally, it includes the bugfixes from 0.7.0.5. This includes the security update. ### Bugs ### * Bug#1985434: a-z index for people doesn't work anymore. * Bug#2006151: one of the '+' buttons on the search page does not work * Bug#1987338: ZIP downloading feature does not work with PHP4 * Bug#2006154: Case insensitive search for description field doesn't work * Bug#1985432: two different meanings for 'home' * Bug#1986847: wrong charset for French translation * Bug#1983556: It is not possible to unset many attributes once they have been set. Fix by Charles Brunet. * Bug#2015802: SQL error when inserting a new place * Bug#2012300: Missing linefeed on places page. * Bug#2015312: Wrong layout for 'work' field on person page. * Bug#2015346: Home location does not display the title * Bug#2015340: Deleting a person does not delete all references * Bug#2015348: Deleting a place does not remove all references to it * Bug#2021272: Crash when changing the parent of the root album * Bug#2022777: [person] tag is missing from pages feature * Bug#2021272: Crash when changing the parent of the root album ### Features ### * Req#1505552: Mapping support. You can now use maps to show the location of your photos, using the mapstraction api. There is support for Google, Yahoo and Openstreetmap maps. * Req#1586463: Time zone support. You can store information about the timezone where a photo was taken and have Zoph automatically compute the correct time for you. * Req#2006156: Increase length of title field for albums and categories * Req#2021275 Expand all button for tree view ### Translations ### Translations for Dutch, French, German and Polish have been updated ## Zoph 0.7.0.5 ## ### 20 July 2008 ### Zoph 0.7.0.5 is a security fix that repairs several SQL injections. Although most are not exploitable or only exploitable by an admin user, I recommend upgrading to 0.7.0.5. This release also includes a number of extra 'safety nets' that will make exploiting any future SQL injections a lot harder. It also fixes a number of bugs in the 0.7 release: * Bug#1813293: import is not compatible with PHP < 5 * Bug#2006151: one of the '+' buttons on the search page does not work * Bug#2012300: Missing linefeed on places page. * Bug#2015312: Wrong layout for 'work' field on person page. * Bug#2015346: Home location does not display the title * Bug#2021272: Crash when changing the parent of the root album ## Zoph 0.7.2.1 ## ### 3 June 2008 ### Zoph 0.7.2.1 is a bugfix release for Zoph 0.7.2 it fixes the following issues: * Bug#1981910: Some files in the distribution for 0.7.2 are not the latest version * Bug#1820229: Some thumbs not displayed when user has no right to see them. * Bug#1813293: web import is not compatible with PHP < 5 ## Zoph 0.7.2 ## ### 1 June 2008 ### Zoph 0.7.2 is a "feature release", that introduces a few new features that will be present in the next "stable" version, 0.8. It also fixes some bugs from 0.7.1. Finally, it includes the bugfixes from 0.7.0.3 and 0.7.0.4. ### Bugs ### * Bug#1819755: User that cannot see all albums does not always see all the albums he *is* allowed to see. * Bug#1820225: Restricted user can see the list of people. * Bug#1820229: User does not see all thumbs if he has not the right to see the manually assigned thumb. * Sometimes not all albums were shown and sortorder was not always correct. ### Features ### * Zophcode: Possibility to add markup and smileys to comments. Smileys were taken from PHPBB. (they are under GPL) * Patch#1923522 and Patch#1923525 Default language now configurable and logon screen translated. Thanks to Francisco Javier Félix for providing these patches. * Req#1928328: Use an alternating colour scheme to make it easier to keep the overview on the list of people. Thanks to Francisco Javier Félix for providing this. * Added Licence and some extra security to selection.inc.php (although there was no security isssue with this file, in case there will be one discovered in the future, it will be harder to exploit). * Moved the functionality from `pager.inc.php` to `util.inc.php`, so it is easier to re-use. * Added an admin page where administrator can manage settings. Replaced 'users' in the main menu with 'admin'. * Req#1506959: Zoph Pages feature that allows customization of the first page of an album/category/person/place. ### Translations ### * Spanish was updated by Francisco Javier Félix * Canadian English, German and Dutch were updated ## Zoph 0.7.0.4 ## ### 26 May 2008 ### This is a bugfix release that fixes a few bugs in the 0.7 release. * Bug#1923507: pleasewait.gif missing * Bug#1926107 SQL error because of dashed line in zoph.sql * Bug#1923955: photo x of y is not correctly translated * Bug#1928150: tree view shows a "+" even though the branch is already open * Bug#1928671: Notify mail doesn't work * Perl chokes when the .zophrc file ends with a negative assignment (" = 0"), adding "1;" to make sure it always ends "positively". * Bug#1964408 Garbled layout on prefs page. Very small new feature: the photo is now shown when asking for confirmation of deletion ## Zoph 0.7.0.3 ## ### 15 March 2008 ### This is a bugfix release that fixes a few bugs in the 0.7 release. * Bug#1856587: CSS fixes for MSIE rendering problems * Bug#1859100: `zophImport.pl` moves files to wrong dir when path is specified in filename * Bug#1840352: Ratings and Favourites do not always work correctly. ## Zoph 0.7.1 ## ### 21 Oct 2007 ### Zoph 0.7.1 is a "feature release", that introduces a few new features that will be present in the next "stable" version, 0.8. It also includes the bugfixes from 0.7.0.1 and 0.7.0.2. * It is now possible to define the position of the watermark. * Req#1713938: Zoph can now be configured to move an imported image instead of copying it. This saves you from having to clean up later. Default is to move the photo. * Req#1504375 You can now download a set (album, category, search result, ..) of photos in a ZIP file. The size of the ZIP file and the number of photos are configurable. * Req#1500560: For albums and categories, you can now set the desired sort order through preferences. (newest/oldest photo, first/last change, lowest/highest/avg rating, name, sortname). Sortname is a new field that you can use to sort on. * Req#1742672 Albums/Categories/Places now also have a thumbnail when the album itself does not have any photos, it picks a photo from one of it's subalbums/c/p * Info table now displays total size of photos in the most appropriate unit (KiB, MiB, GiB) instead of always in MiB ## Zoph 0.7.0.2 ## ### 25 July 2007 ### * Bug#1756660: Admin can not see details of places * Admin can not see details of people * Bug#1755325: Not possible to unset a coverphoto * Bug#1598437 A user can now only put photos into an album he has write permission to. * Bug#1760100: SQL script for new installations doesn't work. * Italian translation is now up to date ## Zoph 0.7.0.1 ## ### 14 July 2007 ### * Fix for a (non-exploitable) SQL injection error. ## Zoph 0.7 ## ### 1 July 2007 ### ### Bugfixes ### * Bug#1745803: Layout problem on annotate photo page * Bug#1745795: Autocompletion navigation with keyboard did not handle "enter" right * Fixed a bug that caused auto thumbnail not to when user was not logged in as admin * Fixed a bug where a non-admin user would get the same thumbnail for ALL categories, regardless of whether this photo would actually be in that category. * Bug#1742676: Thumbnails show unexpected behaviour with insufficent rights. * Bug#1742674: An autocomplete field now advances to next field when "enter" is pressed. ### Cleanup and various ### * Made several (small) changes to Dutch, German, Canadian English, French, Norwegian and Swedish Chef. * Updated Turkish and Danish ## Zoph 0.7pre2 ## ### 24 June 2007 ### ### Bugfixes ### * Bug#1738931 View selection does not work for people * Capitalization error in `places.php`, `albums.php`, `categories.php` that caused translations not to work * Bug#1738592 Pressing enter in autocomplete field did not work * Bug#1738307: In some cases `zophImport.pl` would try to connect to the database before the db connection was made. * Fixed a layout-issue where in some cases the photo description would end up on an odd place on the page. ### Cleanup and various ### * All languages have been updated. All duplicate and unused strings have been removed from the translation files. Dutch, German, Canadian English, French, Norwegian and even Swedish Chef (Bork! Bork! Bork!) are completely up to date now. Danish, Italian and Turkish are almost up to date. ## Zoph 0.7pre1 ## ### 02 June 2007 ### ### New Features ### * Req#722617: read/display/handle more/full exif data * Req#1260584: Javascript-based autocompletion for select-boxes. * Req#1478748 Now possible to search albums/categories/photographers/people by text instead of selecting from list. * Req#1491208: In albums/categories/places each link now shows the number of photos in that album and the number of photos in the album and the ones below it. * In albums and categories you now see the number of photos in the current album, as well as the number of photos in the current album and all albums below it (which was the only one shown up until now) - just like places has had since the previous version of Zoph * Req#1506959 (partly): Specify a coverphoto for albums, categories, people and places * Req#1511961: There are now 3 views for albums/categories/people/places: list (the "old" view), tree and thumbnail. * Automatically pick a coverphoto in thumbnail view for a/c/p/p when none has been picked. * Req#1709390: zophImport.pl: You can now set the defaults for dateddirs, copy, hierarchical and verbose through the .zophrc file. Thanks to Peter Farr for the patch. * Patch#1647439: zophImport.pl can now resolve symlinks before importing. Thanks to Peter Farr for the patch. ### Bugfixes ### * Bug#1564548, Bug#1725811: Bugs with slideshows showing an error * Bug#1568418: Pager links do not work in bulk edit page when no search criteria are used. * Bug#1571227: Webimport of ZIP files not working * Bug#1571577: Cannot login with `DEBUG` set * Bug#1571682: extra '/' in URL after logon * Bug#1574205: No "return" from edit page * Bug#1574206: Removing crumbs when on edit page does not correctily return * in some cases the second page of a search would change ">=" or "<=" into "=". * urls for places could not be longer than 32 chars. * Fix for a bug that made search behave incorrectly when text-search for a person did not return any people. * Bugfix for layout problem - sometimes the main window on the people page was not large enough to display all * Bug#1713946 Missing localized strings * Bug#1592560 Import fails when "path" field is empty * Bug#1598437 Import does not check if user can write to the selected album. * Patch#1713924: EXIF date/time priority, patch by Antoine Delvaux. ### Cleanup and various ### * Lots of cleanout of HTML and CSS code. Now all unnecessary tables have been replaced by semantic HTML/CSS combinations. * Removed duplicate spaces in translation files. * Cleanout and getting rid of lots of (but not yet all) PHP warning messages. * Updated info page with new mailadress for Zoph * Changed "view" to "display" on the people page for consistancy reasons and to remove a translation problem (the word "view" is also used on the photo page, and has a different meaning there) * Dutch, German and French translation updated * changed some SQL syntax for speedup ## Zoph 0.6 ## ### 21 September 2006 ### * Removed mailaddress of original Dutch translator on his request * Fixed: Rating links on reports page not working in translated Zoph version. * Updated Danish language file * Fixed: issues with LIKE searches (Bug#1541763) * Improved error handling in imports * Fixed an issue with imports not working on Windows systems (Bug#1527333) * Fixed: slideshow not working on search results (Bug#1562419) ## Zoph 0.6pre2 ## ### 13 July 2006 ### * Updated translations: Dutch, English, German, Danish and Canadian English should be completely up to date now. * Fixed a layout glitch in the edit screen for places * Fixed missing translations in relation and selection features. * Fixed some incompatibilities with PHP4 * Fixed an issue that caused guest users to be unable to logon. * Fixed an issue with trying to logon after a session timeout * Fixed an issue with search not working for translated Zoph versions * Fixed some issues in the SQL installation script, thanks to Ed P. for the patch. * Added partial Turkish translation, thanks to Mufit Eribol * Fixed and issue with auto-edit mode where you would not return to the correct photo after making a change. * Updated man pages for zophImport.pl and zophExport.pl * In the userlist, changed "view" to "display" for consistancy reasons and to remove a translation problem (the word "view" is also used on the photo page, and has a different meaning there) ## Zoph 0.6pre1 ## ### 4 June 2006 ### ### New features ### * It is now possible to leave comments with photos * You can select a photo to do certain actions with that selection. * You can now create links between photos. (Req#778845 (partly), Req#828750) (for now, this is the only feature that makes use of "selections") * Using external links to Zoph will now go to the login page and then to the requested URL. (Req#1443574) * Image service is now on by default * Possibility to overide sort order of photos in album (Req#665237) * Possibility to overide sort order of photos in category (similar to Req#665237) * Possibility to call albums and categories by name in URL instead of id. (Req#778024) * Made a small change to the menu: when hovering a menu-option, the layout changes to emulate a "tab"-like display (let me know if you like this!) * It is now longer required to be in the image dir to import a photo. (Req#853091) * ZophImport.pl and zophExport.pl now use and external file to store the configuration (like the Debian version of Zoph). * Quick navigation through locations. (Req#1417305) * The search page now has a "no children" checkbox next to albums, categories and places. (Req#1416195) * Add URL to places, so a link to -for example- a map can be made. (Req#1466069) ### Bugfixes ### * Include URL to Zoph in e-mails (Req#655957) * Tranlation fixes in `define_annotated_photo.php`, `edit_person.inc.php` and `edit_place.inc.php` * `zoph_table.inc.php`: small layout fix in debug code * Fixed: a string would not be correctly translated if it starts with a "special character". * Fixed a few html encoding issues. (Bug#1467146 and some not reported bugs) * Button text not correct when php.ini setting is short_open_tag = Off (Bug#1459175) * Ratings being truncated (Bug#1466551) * Fixed a bug where logging in without SSL would redirect you to the wrong page. * Next/prev buttons lost after editting/deleteing a photo when using 'auto-edit' mode. (Bug#1467143, Bug#1463947) * CSS style is not applied when mid prefix is changed in config.inc.php (Bug#1466068) * Added missing space in photo.inc.php * Specifying the `DEFAULT_TABLE_WIDTH` as a percentage doesn't work (Bug#1446202) * HTML tag missing for all pages. * MySQL >4.1 conversion doesn't work with default user feature. (Bug#1500325) * Object syntax in `comment.inc.php not` compatible with PHP4.(Bug#1500582) ### Cleanup and various ### * Updated Danish, Italian, Dutch, German and Canadian English language files * Cleanup of all language files (removed no longer used strings) * Removed `zoph_update-0.4pre1.sql` * In photo.php, the actionlinks are now built using an array. To make life a bit easier for people using the auto-edit feature, the edit page now displays more links. * Cleaned out the code of the search page: Removed lots of messy and redundant code and added whitespace for readability. Functionality should be unchanged. * Fixed code layout in `util.inc.php` * Updated HTML for the edit page of places to use semantic HTML and not tables. ## Zoph 0.5.1 ## ### 12 March 2006 ### * Updated Richard Heyes mailclass to newest version. Should partly solve Req#655957 * Fixed: Quotes and apostrophes do not display correctly (Bug#1443235) * Fixed: Places are sorted by id instead of alphabetically. (Bug#1443427) * Fixed: Loosing context after editing (Bug#1333428) * Fixed: Clicking on the thumbnail of a randomly chosen photo would pick a new random photo instead of showing a larger version of the thumb (Bug#1443927) * Fixed: field with double quotes are truncated (Bug#1443235) * Fixed: photo.php: the `_rows`, `_cols` etc. fields are added to the url, instead of replaced, whenever they are changed. (did not cause any functionality issues) * Fixed: error at the end of a slideshow (Bug#1446200) * Removed extra space in `create_text_input` * Fixed installation SQL file: some missing changes needed for Zoph 0.5, (Bug#1447727) * Resolved duplicate subject header in mail sent from Zoph * Translation fixes in German translation, thanks to Ulrich Wiederhold * Added missing translation to Dutch and Canadian English and updated zoph_strings.txt * Fixed: search page does not show results when using a translated Zoph version (Bug#1448346) ## Zoph 0.5 ## ### 1 March 2006 ### * v0.5 is equal to v0.5-pre4 ## Zoph 0.5 pre4 ## ### 18 February 2006 ### * Solved a bug that caused an error on the bulk edit page if you would add some people to a photo and consequently made another edit (Bug#1422741) * Fixed an issue where the pager links on the bulk edit page would cause errors after an edit has been made. * Additional anti-SQL injection code in the search page. * When updating user permissions with a high number of albums, a "URL too long" error occurred. (Bug#1434235) * Fixed a bug that caused some albums permissions not to be properly updated when making a change. ## Zoph 0.5-pre3 ## ### 30 January 2006 ### * Solved a typo in upgrade documentations * Solved a bug that caused an Admin user not to be able to browse people * zophImport.pl: `--verbose` combined with `--path` would not correctly tell where the file was copied. * zophImport.pl: now exits with a non-0 status code when something goes wrong * updated man-pages for `zophImport.pl` and `zophExport.pl` (thanks to Edelhard Becker) * Solved a bug that caused the bulk-edit page not to work when called from search-results (Bug#1415457) * Added brackets to some queries to make the search page react better on "not in" queries. * Fixed a bug that caused some changes made on the bulk-edit page to be ignored. * Added an extra Update button to the bulk edit paged (Req#1416184) * Made a change to the db lookup for the place dropdown that dramatically increases the performance of the bulk edit page. ## Zoph 0.5-pre2 ## ### 24 January 2006 ### * Logging on with non-admin user in Zoph-0.5pre1 does not work (Bug#1413557) * Rating links do not work in v0.5pre1 (Bug#1413244) ## Zoph 0.5-pre1 ## ### 21 January 2006 ### * Changed typos in `logon.php` and `credits.html` * Fixed php errors when user is not logged in (bug#1325547) * Added compatibility with MySQL=>4.1, and code to automatically convert passwords from MySQL pre-4.1 to 4.1 and later format. * Many updates to HTML and CSS, most to improve HTML semantics. (Less tables used for layout). * Resolved some inconsistencies in config.inc.php (some defines used quotes and some not) * `zoph_table.inc.php` now gives some more debug info when `DEBUG` is on. * Locations are now hierarchical. The necessary database updates for this are done by the SQL update script; an unsopported script is included in the contrib dir that will try to change your locations to a real hierarchical list. Use at your own risk! * Dated_dirs can now be made hierarchical (instead of a directory called `2006.01.20` you will have a directory-tree `2006/01/20`). Thanks to Oliver Seidel (Req#656472) * Immediate editting of color schemes and possibility to copy them (Req#715104) * Dated dirs in webimporter (Req#739557) * Imported tar and zip files can be removed automatically (Req#739267) * Change of error message in import.php to ease translation. * People without "browse people" rights can now no longer see people's names. (Req#749503) * Use the file date and time if there is no date in exif header. (Req#752404) * Option to open the fullsize image in a new window. (Req#1252457) * Watermarking for high quality images. (Req#1250028) * Forced SSL login, thanks to Aaron Parecki. (Patch#1253265) * Forced SSL usage * `zophImport.pl`: Now fails when album/location/category/person does not exist. (Can be turned off by setting `$ignoreerror`). Partly solves Debian bug #284539. * `zophImport.pl`: A friendly error is now displayed when a photo is added to an album/cat/person it is already in. (partly solves Debian bug #284539) * Changed default permissions in `config.inc.php` as requested in Debian bug#326649 * `zophImport.pl`: Added `--copy` and `--verbose` options. Solves Debian bug#211312 and partly #218491. * Major improvements to the search page. Thanks to Roy Bonser. (Req#685269 and Patch#1395052). * Fixed some possible SQL-injection issues. * Adding multiple people to a photo at once, thanks to Neil McBride. (Patch#1406959) * Fixed Date Field set inconsistently when using files with no EXIF info. (Bug#1402492) * Updated Canadian English, German and Dutch translations. ## Zoph 0.4 ## ### 4 September 2005 ### * Removed "float" in CSS breadcrumb definition, this was a workaround for a very small layout issue in Firefox, but caused some ugly behaviour in Konqueror and Safari. * Fixed incorrect 'Next' URL after editing photos. (bug#1252455) * Moved edit button to right side in `edit_photo.php` * Updated Dutch, English, Canadian English and German translation * `zoph_strings.txt` (translation skeleton file) was updated for 0.4 * The "root category" on the categories page is now translated * Fixed a layout issue when pressing pause during a slideshow * "Up" button now takes you to the last page you were looking at, instead of the first (bug#1259152) * Added a warning to check for maximum file size when uploading fails (bug#739546) * Added Polish translation (thanks Krzysztof Kajkowski) * Swedish translation was updated by Johan Linder * Increased `DEFAULT_WIDTH` to 600, for layout reasons ## Zoph 0.4pre2 ## ### 1 August 2005 ### * Changed layout to use CSS (thanks Jeroen Roos) * Added Traditional Chinese translation (thanks Mat Lee) * Fixed translation of update and submit buttons * Added a "Contrib" directory in which some user-contributed tools are distributed. * Contrib: Diff to use Postgres as database (for zoph 0.3.3) (thanks Chris Beauchamp) * Contrib: ZophEdit Python script to edit photo metadata in a zoph database (thanks Nils Decker) * Contrib: ZophClean Perl script to find and solve differences between database and files on disk. * Fixed a bug where only Admin users could rate photos and add photos to a lightbox album (pat#1179920) (thanks Jason Taylor) * Added a check to prevent album names, category names, location, people names, user names and color schemes to have empty names (bug#846417) * Added a fix for `zophImport.pl`, it failed in looking up people that have a name with multiple spaces (pat#830236) (thanks Hans Verbrugge) * Contrib: Added a script to add movies to Zoph (pat#1176317) (thanks Giles Morant) * Fixed bug: a deleted album could still be a lightbox album (bug#1193347) * Fixed an url-encoding bug in relation to breadcrumbs (bug#1194722) * Fixed a problem with deleting a photo: returning to the photos after the delete was inconsistent when auto-edit is on or off. (bug#772403) * Added an error message when file cannot be unzipped (#1193351) * Changed the licence from BSD to GPL. * Changed default width in `config.inc.php` to be slidely wider to solve a layout glitch ## Zoph 0.4pre1 ## Never released * Created a validator class to allow different types of authentication * Added a function to `validator.inc.php` to allow htpasswd authentication (req#656449) (thanks Asheesh Laroia) * Added $host param to `zophImport.pl` (bug#656438) * Fixed it's vs its grammar (bug#656444) * Changed ` --password ```` You should keep in mind that this will store the password in your shell's history file. Both username and password are required. To make the user an admin user, add `--admin` ```` zoph --user add --username --password --admin ```` ### Delete a user ### *not implemented yet* ```` zoph --user delete --username ```` or ```` zoph --user delete --userid ```` Either username or userid are required. ### Modify a user ### *not implemented yet* ```` zoph --user update --username ```` or ```` zoph --user update --userid ```` Either username or userid are required. ### Show a user ### ```` zoph --user show --username ```` or ```` zoph --user show --userid ```` Either username or userid are required. Shows information about the user requested. ```` zoph --user show --userid 1 User ID: 1 Username: admin User class: Admin Groups: Last login: 2021-04-05 15:53:39 Access rights: view_all_photos : 1 delete_photos : 1 browse_people : 1 browse_places : 1 browse_tracks : 1 edit_organizers : 1 detailed_people : 1 see_hidden_circles : 1 detailed_places : 1 import : 1 download : 1 leave_comments : 1 allow_rating : 1 allow_multirating : 0 allow_share : 0 ```` Note that due to technical reasons, the access rights for an admin user may show "0" for some rights even though the admin user has these rights implicitly. zoph-v0.9.19/docs/CLI.md000066400000000000000000000400331415176210700146450ustar00rootroot00000000000000# The `zoph` CLI tool # `zoph` is the commandline interface (CLI) of Zoph 0.8.2 and later. You can use the CLI to import photos in Zoph and make (bulk) changes to photos already in Zoph. ## Multiple Zoph installations ## ### `--instance` ### You can have multiple Zoph installations on one system. For example a Zoph installation for yourself and one for a family member or friend, or if you are a Zoph developper, a *production* and a *development* version. The webinterface can determine which installation your are using by the URL you are using. The command line interface does not have an URL thus it needs a different way to find out which instance of Zoph is used. **Aliases:** `-i` **Default:** First instance in zoph.ini **Options:** Instance defined in zoph.ini **Example:** zoph --instance production photo.jpg ## Commands ## You can only supply one "command" type option to Zoph, if you supply more, Zoph will take the last one. ### `--import` ### The list of photos given will be imported in Zoph **Aliases:** `-I` **Default:** --import is the default command, it doesn't need to be given. **Options:** **Example:** zoph --import photo.jpg ### `--update` ### Zoph will try to find the given list of photos in the database and apply the options to those photos. You can either give a list of filenames or a list of id's, see [--useIds](#--useIds). **Aliases:** `-u` **Default:** `--import` is the default command **Example:** `zoph --update photo.jpg` ### `--new` ### Create albums, categories, places and people from CLI **Aliases:** `-N` **Default:** **Options:** Use `--album "new album"`, `--category "new category"`, `--person "new person"`, `--place "new location"`. The new object will be created directly under the root unless [--parent](#--parent) is specified. See [--person](#--person) for details on how Zoph determines what's the first and second name. **Example:** `zoph --new --parent "Holidays" --album "Summer 2011"` ### `--version` ### Show the current Zoph version. **Aliases:** `-V` **Default:** `--import` is the default command **Options:** All other options will be ignored if `--version` is specified **Example:** `zoph --version` ### `--help` ### Display help. **Aliases:** `-h` **Default:** --import is the default command **Options:** All other options will be ignored if `--help` is specfied **Example:** zoph --help ### Working with configuration items via the CLI ### The following commands are available to display or modify Zoph's configuration via the CLI. This could come in handy when you've somehow locked yourself out of Zoph by making an erroneous change to the configuration or when you want to script changes to Zoph's configuration. #### `--config` #### Modify configuration. **Aliases:** `-C` **Default:** change the configuration item to the default. **Options:** configuration item to change and the value to set it to, if the value contains spaces, use quotes. For boolean items (checkboxes in the Web GUI), use "true" or "false". **Example:** Change the name of your Zoph installation to the default ("Zoph"): ```` zoph --config interface.title ```` Change the name of your Zoph installation to "My photos": ```` zoph --config interface.title "My photos" ```` #### `--dumpconfig` #### Get full configuration dump **Aliases:** `--dump-config` **Example:** ```` zoph --dumpconfig interface.title: Zoph interface.width: 800px interface.template: default interface.autocomplete: true interface.language: en [...] ```` #### `--getconfig` #### Get the value of an individual configuration item **Aliases:** `--get-config` `-g` **Example:** ```` zoph --getconfig interface.title My photos ```` ### Adding and changing users using the CLI ### As of Zoph v0.9.17 it is possible to add and modify users via the CLI, see [CLI-USERS](CLI-USERS.md) ## Organizers ## Organizers is what Zoph is all about, these are the ways you can organize your photos by. ### `--album` ### Specify one or multiple albums Zoph should add the given list of photos to. You can specify `--album` multiple times. **Aliases:** `-a` `--albums` **Options:** The name of an album or multiple, separated by commas. The album must pre-exist in the database. **Example:** ```` zoph --album "Summer, Holiday" photo.jpg zoph -a "Summer" -a "Holiday" photo.jpg ```` ### `--category` ### Specify one or multiple categories Zoph should add the given list of photos to. You can specify `--category` multiple times. **Aliases:** `-c` `--categories` **Options:** The name of a category or multiple, separated by commas. The category must pre-exist in the database. **Example:** ```` zoph --category "sun, water" photo.jpg zoph -c "sun" -c "water" photo.jpg ```` ### `--person` ### Specify one or multiple persons that appear on the photos specified. You can specify `--person` multiple times. **Aliases:** `-p` `--persons` `--people` **Options:** The name of a person or a list of persons separated by commas. The person must pre-exist in the database. When using [--new](#--new) to add new persons to the database, Zoph will try to determine which parts of the name are first, middle and last. If a name is a single word ("John"), Zoph assumes this is the first name. If a name is two words ("John Doe"), Zoph will assume this is the first and last name. If a name is 3 or more words, Zoph will assume the first word is the first name, the second is a middle name and all remaining words are the last name. If this does not give the correct results, you can choose to separate by colon (":") instead of space. Zoph will then set the part before the first colon to first name, then middle, then last and finally 'called'. **Example:** ```` zoph --person "Linus Torvalds, Mark Shuttleworth" photo.jpg zoph -p "Linus Torvalds" -p "Mark Shuttleworth" photo.jpg zoph --new --person "Linus Torvalds" zoph --new --person "John Fitzgerald Kennedy" zoph --new --person "Johnny B.::Goode" zoph --new --person "John::Doe:Average Joe" ```` ### `--location` ### Specify the location where the photos specified were taken. You can specify `--location` only one time. **Aliases:** `-l` `--place` **Options:** The name of a place. The place must pre-exist in the database. **Example:** ```` zoph --location "Rotterdam" photo.jpg zoph -l "Rotterdam" photo.jpg ```` ### `--photographer` ### Specify the photographer of the photos specified. You can specify `--photographer` only one time. **Aliases:** `-P` **Options:** The name of a person. The person must pre-exist in the database. **Example:** ```` zoph --photographer "Alan Cox" photo.jpg zoph -P "Alan Cox" photo.jpg ```` ### `--fields` ### **Aliases:** `-f` `--field` Specify fields that should be filled for the photos specified. You can specify `--field` multiple times. **Options:** The following fields can be used: * date * time * camera_make * camera_model * flash_used * focal_length * exposure * compression * aperture * iso_equiv * metering_mode * ccd_width * focus_dist * comment * lat * lon * rating * description * level * view * title **Example:** ```` zoph --field "rating=10" photo.jpg zoph -f "description=self portrait" photo.jpg ```` ## Options ## ### `--thumbs` / `--no-thumbs` ### Specify whether thumbnails should be created. **Aliases:** `-t` / `--nothumbs` `-n` **Default:** When importing ([--import](#--import)): create thumbs. When updating ([--update](#--update)): do not create thumbs. **Options:** Use these commands to overrule the defaults. If you want to recreate thumbs for already imported photos, use `--thumbs`. If you do not want to create thumbnails while importing, use `--no-thumbs`. **Example:** ```` zoph --import --no-thumbs photo.jpg zoph --update -t photo.jpg ```` ### `--exif` / `--no-exif` ### Specify whether EXIF date should be read. **Aliases:** `--EXIF` / `--noexif` `--no-EXIF` `--noEXIF` **Default:** When importing ([--import](#--import)): read EXIF data. When updating ([--update](#--update)): do not read EXIF data. **Options:** Use these commands to overrule the defaults. If you want to reread the EXIF date of already imported photos, use `--exif`. If you do not want to read EXIF data while importing, use `--no-exif`. **Example:** ```` zoph --import --no-exif photo.jpg zoph --update --exif photo.jpg ```` ### `--size` / `--no-size` ### Specify whether Zoph should update the dimensions of the photo stored in the database. **Aliases:** *(none)* / `--nosize` **Default:** When importing ([--import](#--import)): update database with dimensions of the image. When updating ([--update](#--update)): do not update the size information. **Options:** Use these commands to overrule the defaults. If you want to update the information stored in the database when updating, use `--size`. If you do not want store size information while importing (although I see no real use for this), use `--no-size`. **Example:** ```` zoph --import --no-size photo.jpg zoph --update --size photo.jpg ```` ### `--useids` ### When updating photos it can be useful to be able to specify database ids instead of filenames. **Aliases:** `--useIds` `--use-ids` `--useid` `--use-id` **Default:** Filenames are used. Using `--useids` implies `--update` **Options:** You can specify a list of ids instead of a list of filenames. You can either specify a single id or a range of ids. Keep in mind that the list of filenames or ids are the **last** options of the command and do not necessarily follow the `--useids` option. **Example:** ```` zoph --update --useids 2 5 11-20 56 zoph --update --useids --album "Summer" 15-60 ```` ### `--move` / `--copy` ### When importing photos, you can either import a copy of the photo or move the photo into the Zoph imagedirectory. **Default:** Files are moved. **Options:** If the file imported is a symlink, in case of `--move`, a copy of the file the symlink points to is imported and the symlink is deleted. In case of `--copy`, the symlink is not deleted. **Example:** ```` zoph --move photo.jpg zoph --copy photo.jpg ```` ### `--dateddirs` / `--no-dateddirs` ### With dated dirs, Zoph automatically creates directories based on the (EXIF-)date of a photo. For example a photo taken on March 15, 2010, will automatically be places in a directory called 2010.03.15 **Aliases:** `--datedDirs` `--dated` `-d` / `--no-datedDirs` `--nodateddirs` `--nodatedDirs` **Default:** No dated dirs are used. **Options:** **Example:** `zoph --dateddirs photo.jpg` ### `--hierarchical` / `--no-hierarchical` ### Hierarchical dated dirs are similar to [--dateddirs](#--dateddirs----no-dateddirs), Zoph automatically creates directories based on the (EXIF-)date of a photo, the difference is that with hierarchical dated dirs, a separtate directory is create for year, month and day. For example a photo taken on March 15, 2010, will automatically be places in the directory tree `2010/03/15`. **Aliases:** `-H` `--hier` / `--no-hierarchical` `--no-hier` `--nohierarchical` `--nohier` **Default:** No hierarchical dated dirs are used. **Example:** `zoph --hierarchical photo.jpg` ### `--XMP` ### Zoph can read XMP metadata embedded in photos. By default Zoph does not process XMP data during import. Specify `--XMP` to enable this feature. For more information about XMP support, see [XMP](XMP.md). **Aliases:** `-X` `--xmmp` **Default:** XMP data is not used. **Example:** `zoph --XMP photo.jpg` ### `--XMP` ### ### `--hash` / `--no-hash` ### As of v0.8.4 Zoph stores a hash of each photo in the database. This is currently only used for the 'share photo' feature. In the future other features will use this, as it will allow Zoph to detect whether a photo has been changed. **Default:** Generate a hash or update the hash when `--update` is used. **Options:** **Example:** `zoph --no-hash photo.jpg` ### `--parent` ### **Default:** If you do not specify a parent, the new object will be placed directly under the root. When adding new objects to the database using the [--new](#--new) option, you can determine where in the tree an album, category or place will be placed by specifying `--parent`. **Options:** `--parent` **must precede** the actual album, category or place. The parent is only set for the next [--album](#--album), [--category](#--category) or [--place](#--place). **Example:** Create a new album called 'summer 2011' under the root album: ```` zoph --new --album "Summer 2011" ```` Create new albums called 'Summer 2011' and 'Winter 2011' under the 'Holidays' album: ```` zoph --new --parent "Holidays" --album "Summer 2011, Winter 2011" ```` Create new albums called 'Summer 2011' and 'Winter 2011' under the 'Holidays' album and an album 'Trees' under the root album: ```` zoph --new --parent "Holidays" --album "Summer 2011, Winter 2011" --album "Trees" ```` Create new albums called 'Summer 2011' and 'Winter 2011' under the 'Holidays' album and an album "Trees" under the "Nature" album: ```` zoph --new --parent "Holidays" --album "Summer 2011, Winter 2011" --parent "Nature" --album "Trees" ```` Create a new album called 'Summer 2011' under the 'Holidays' album and a cateogory "Trees" under the "Nature" category: ```` zoph --new --parent "Holidays" --album "Summer 2011" --parent "Nature" --category "Trees" ```` ### `--autoadd` ### You can use [--new](--new) to add albums, categories, places and people from CLI, with autoadd you can add them in the same run as you are importing photos. Zoph will add any album, category, etc. you have specified, but does not exist. However, to protect you from every typo to be automatically added to the database, only items preceded with [--parent](#--parent) will be added, unless you specify [--addalways](#--addalways). Of course this only works for albums, categories and locations, and not for persons and photographers. **Aliases:** `-A` `--auto-add` **Example:** ```` zoph --autoadd --album "Summer 2011" IMG_1234.JPG No parent album for "Summer 2011" ```` `zoph --autoadd --parent "Holidays" --album "Summer 2011" IMG_1234.JPG` ### `--addalways` ### When using [--autoadd](#--autoadd), zoph protects you from every typo to be automatically added to the database by only adding albums, categories and location preceded with [--parent](--parent). To overrule this behaviour, use `--addalways`, which causes them to be added under the root album, category or location. **Aliases:** `-w` `--add-always` **Default:** Do not add albums, categories or locations unless a parent has been specified. **Example:** `zoph --autoadd --addalways --album "Summer 2011" IMG_1234.JPG` ### `--recursive` ### With `--recursive`, Zoph will recursively go through directories added to the file list and import photos found in those dirs as well. **Aliases:** `-r` **Default:** Zoph will error if you try to import a directory. **Example:** Import image IMG_1234.JPG and any photos in the directory 'Photos', or any directory below that. ```` zoph -r IMG_1234.JPG Photos/ ```` ### `--dirpattern` ### With `--dirpattern`, you can automatically assign albums, categories, people, photographer, location or path based on the directories the photos are in. You do this by specifying a pattern, based on which Zoph will use directory names to assign to correct organizer. This pattern consists of a list of letters, where each letter is a directory. This option makes no sense if you do not specify [--recursive](#--recursive) as well. **Default:** No default. **Options:** **a** (album), **c** (category), **l** (location), **p** (person), **P** (photographer) and **D** (path) **Example:** `zoph -r --dirpattern "Paccc" *` Import all files in the current directory **and** the directories below. For each path, assign the name of the first directory as photographer, the second as album, and the third, fourth and fifth as categories. For a more detailed example, see [Using dirpatterns](IMPORT-CLI.md#Using_dirpatterns) ### `--path` ### You may want to manually organize your photos in directories. You can use `--path` for that. The path is inserted between the image directory and (in case they are enabled) dated or hierarchical dated directories. **Aliases:** `-D` **Default:** Photos are imported directly under the image dir. **Options:** Valid path, relative to image dir. **Example:** ```` zoph --path "holiday" photo.jpg zoph --path "travel/business" --dateddirs photo.jpg ```` zoph-v0.9.19/docs/CONFIGURATION.md000066400000000000000000000243261415176210700162540ustar00rootroot00000000000000# CONFIGURATION # ## Database connection ## Access to the database needs to be configured through `/etc/zoph.ini`. The `zoph.ini` files tells Zoph where it can find the database and it tells Zoph's CLI scripts where it can find your Zoph installation. Normally, `zoph.ini` will be placed in `/etc`. If you have no write access in `/etc` or have another reason to not put this file there, you should change the `INI_FILE` setting in `config.inc.php` and the 'zoph' CLI utility. **Never, _ever_, place it in the same directory as the Zoph PHP files. This will enable _everyone_ to download it and read your passwords.** An example `zoph.ini` file called **`zoph.ini.example`** is included in the `cli` dir of the Zoph tarball. ### Contents of `zoph.ini` ### `zoph.ini` consists of one or more *sections*. A section starts with the name of the section between square brackets. `[zoph]` You should create a section for each Zoph installation on your system. The section name is a descriptive name that you can choose yourself. Each section must contain the following settings: `db_host` The hostname of the system that is running your MySQL server, usually "`localhost`". `db_name` The name of the database. If you have followed the installation instructions closely, this will be `zoph`, but of course you are free to use any other name. `db_user` The user to connect to your Zoph database. If you have followed the installation instructions closely, this will be `zoph_rw`, but of course you are free to use any other name. `db_pass` Password to connect to the database. This is what you have set while creating users for Zoph in MySQL. `db_prefix` Zoph can prefix all MySQL table names with a prefix string. This is especially useful for people who only have a single database to use and want to use multiple applications on, for example, a shared hosting environment. By default, this is "`zoph_`". `php_location` With the `php_location` setting, you define where the PHP-files for your Zoph installation are located. This is necessary for the Zoph CLI scripts to locate the rest of your Zoph installation. All values that contain non-alphanumeric characters must be enclosed in double quotes. It won't hurt to use quotes even if the values are purely alphanumeric. #### Examples #### ##### Single installation #### Most Zoph users will have only one Zoph installation on their system. This is how a `zoph.ini` for a single installation looks: ```` [zoph] db_host = "localhost" db_name = "zoph" db_user = "zoph_rw" db_pass = "pass" db_prefix = "zoph_" php_location = "/var/www/html/zoph" ```` ##### Multiple installations #### You can have multiple Zoph installations on one system. For example, one for yourself and one for a family member or friend; or, if you are a Zoph developper, a development and a productions environment. If you have more than one Zoph installation, simply create a section *per installation*. For example: ```` [production] db_host = "localhost" db_name = "zoph" db_user = "zoph_rw" db_pass = "pass" db_prefix = "zoph_" php_location = "/var/www/html/zoph" [development] db_host = "localhost" db_name = "zophdev" db_user = "zoph_rw" db_pass = "pass" db_prefix = "zoph_" php_location = "/var/www/html/zophdev" ```` The webinterface of Zoph will be able to determine which settings it should use with the `php_location` setting. The CLI scripts need the `--instance` parameter to determine that. If you omit the `--instance` parameter, it will use the first one in `zoph.ini`. ## Web GUI ## Most of Zoph can be configured from the Web GUI. Log in as a user with admin rights. If you haven't created a user for yourself, you can login with the user `admin`. Go to "admin" in the top menu and then choose "config". The configuration items should be self-explanatory. The configuration in the Web GUI can also be done via CLI, see the [CLI documentation](CLI.md) for details. When you first get started with Zoph, you should at least change the following: ### Images path ### **Images directory** under **paths**. This is the directory where your photos are stored. It should be an _absolute path_ (that is: referenced from the root) and it should not be in your webroot. See the [installation documentation](INSTALLATION.md) for how to set the correct access rights for this directory. ### Sharing Salt ### **Salt for sharing full size images** and **Salt for sharing mid size images** under **Sharing**. You should set these salts to unique values. You can do so by clicking the generate buttons. Even though you will not need these unless you enable **Sharing**, it is a good idea to make sure you have a unique salt set. (and Zoph will refuse to save your configuration if you don't). ### Enable import and upload ### **Import through webinterface** and **Upload through webinterface** under **Import**. Unless you plan to use the CLI import exclusively, you should enable import through the web interface here. ### Interface title ### **Title** under **Interface settings**. You probably want to change the name Zoph will show on the login page and in the title bar. ## `config.inc.php` ## There are a few configuration settings that can only be changed in `config.inc.php`. Most users will never need to change anything here. ### `LOG_ALWAYS` ### **Description:**: This option controls how much debug information is showed. Zoph will show you the severity you configure and everything worse than that. This setting configures a log level that is always displayed, no matter which subject the message is in. By default this is set to `log::FATAL`, which means that any message that has a severity of FATAL or *worse* is displayed. Since `log::FATAL` is the worst kind of message, only `log::FATAL` messages will be displayed. If you configure `log::ERROR`, you will see `ERROR` and `FATAL` messages and if you configure `log::DEBUG`, you will see all messages. A special severity level has been added to suppreses *all* messages: **log::NONE** **Default:** `log::FATAL` **Options:** See [Log Severity](#log-severity) below **Example:** `define('LOG_ALWAYS', log::ERROR);` ### `LOG_SEVERITY` ### **Description:** This setting works in the same way as the previous one, except that only messages for a specific severity will be displayed; it is used in combination with [`LOG_SUBJECT`](#log_subject) to achieve this. These two option enable you to have granular control over which messages are displayed. With `LOG_SEVERITY` you configure how much debug information is showed. The difference with [`LOG_ALWAYS`](#log_always) is, that the messages are only shown for the subject you have configured in [`LOG_SUBJECT`](#log_subject). Zoph will show you the severity you configure and everything worse than that. So if you configure `log::ERROR`, you will see `ERROR` and `FATAL` messages and if you configure `log::DEBUG`, you will see all messages. **Default:** `log::NONE` **Options:** See [Log Severity](#log-severity) below **Example:** `define('LOG_SEVERITY', log::NOTIFY);` ### `LOG_SUBJECT` ### **Description:** With this setting you can control for which subjects you want to see the messages. There is a special subject to show all messages: `log::ALL`. You can also combine multiple subjects, using the | (or) sign and the ~ (not) sign. This option, together with [`LOG_SEVERITY`](#log_severity) enables you to have granular control over which messages are displayed. **Default:** `log::NONE` **Options:** See [Log Subjects](#log-subjects) below **Example:** Display all messages which indicate an error or a fatal error, regarding the translation of Zoph or images: ````php define('LOG_SEVERITY', log::ERROR); define('LOG_SUBJECT', log::LANG | log::IMG); ```` Display all messages, except debug-level messages, except those regarding SQL queries: ````php define('LOG_SEVERITY', log::NOTIFY); define('LOG_SUBJECT', log::ALL | ~log::SQL); ```` Display all messages, except those regarding redirects or the database connection: ````php define('LOG_SEVERITY', log::DEBUG); define('LOG_SUBJECT', log::ALL ~(log::REDIRECT | log::DB)); ```` ### Log Severity ### Severity | Meaning --------------|--------------------- log::DEBUG | Debugging messages, Zoph gives information about what it's doing. log::NOTIFY | Notification about something that is happening which is influencing Zoph's program flow log::WARN | Warning about something that is happening log::ERROR | Error condition, something has gone wrong, but Zoph can recover log::FATAL | Fatal error, something has gone wrong and Zoph needs to stop execution of the current script. log::NONE | Do not display any messages ### Log Subjects ### Subject | Type of messages in this subject --------------|--------------------- log::ALL | All messages log::VARS | Messages regarding setting of variables log::LANG | Messages regarding the translation of Zoph log::LOGIN | Messages regarding the Login procedure log::REDIRECT | Messages regarding redirection log::DB | Messages regarding the database connection log::SQ | Messages regarding SQL Queries log::XML | Messages regarding XML creation log::IMG | Messages regarding image creation log::IMPORT | Messages regarding the import functions log::GENERAL | Other messages log::NONE | No messages. ## Resized image generation ## Zoph automatically creates thumbnails and medium-sized ('mid') images during import. To influence this process, you can edit the parameters below. It is not recommended to change these, especially not after you have imported some photos. In the near future there will be an option to change this in the webinterface. ### `THUMB_SIZE` ### **Description:** Maximum width or height of thumbnails **Default:** `120` **Options:** Maximum width/height in pixels. **Example:** `define('THUMB_SIZE', 120);` ### `MID_SIZE` ### **Description:** Maximum width or height of midsized images **Default:** `480` **Options:** Maximum width/height in pixels. **Example:** `define('MID_SIZE', 480);` ### `THUMB_PREFIX` ### **Description:** Prefix for filenames of thumbnails **Default:** `thumb` **Options:** **Do not** make this string empty! **Example:** `define('THUMB_PREFIX', 'thumb');` ### `MID_PREFIX` ### **Description:** Prefix for filenames of thumbnails **Default:** `mid` **Options:** **Do not** make this string empty! **Example:** `define('MID_PREFIX', 'mid');` zoph-v0.9.19/docs/IMPORT-CLI.md000066400000000000000000000273161415176210700156660ustar00rootroot00000000000000# Import using the CLI # Many users will use the Zoph webinterface almost exclusively to work with Zoph. However, more advanced users may prefer the commandline interface (CLI) for some tasks. Zoph has a CLI client called `zoph` that can be used to import photos and make (bulk) changes to photos already in the database. A detailed overview of all the options can be found in [The Zoph CLI tool](CLI.md). ## configuration ## ### zoph.ini ### First of all, you will need a valid [zoph.ini](CONFIGURATION.md#contents-of-zophini) file to work with the CLI client. If you have multiple Zoph installations on your system, an important difference with the webinterface is the fact that the CLI cannot automatically determine which Zoph installation (instance) you are trying to import photos to. By default it will take the first instance, otherwise you need to specify the [--instance](CLI.md#--instance) CLI option. See the [CONFIGURATION](CONFIGURATION.md) documentation for some examples. ### Webinterface ### ![Screenshot: import settings on configuration page](img/Config_import.png) There are several option related to import in the configuration page (under admin). See the screenshot for an overview. The options should be self-explanatory. ## Specifying which photos to import ## Of course you want to tell Zoph which photos it should import. The list of photos is **always** specified **last**. You can simply specify filenames, but you can also use your shell's "globbing" feature to specify multiple photos at once. ### example ### Let's say you have a bunch of photos plus a text file in a directory: ```` zoph@zoph $ ls IMG_1203.JPG IMG_1207.JPG IMG_1211.JPG IMG_1215.JPG IMG_1219.JPG IMG_1204.JPG IMG_1208.JPG IMG_1212.JPG IMG_1216.JPG IMG_1220.JPG IMG_1205.JPG IMG_1209.JPG IMG_1213.JPG IMG_1217.JPG IMG_1221.JPG IMG_1206.JPG IMG_1210.JPG IMG_1214.JPG IMG_1218.JPG photos.txt ```` If you want to import all photos, you could do ```` zoph@zoph $ zoph * ```` However, this will cause an error when zoph tries to import a textfile, so it's better to do: ```` zoph@zoph $ zoph *.JPG ```` Or even ```` zoph@zoph $ zoph IMG_12*.JPG ```` But, what if you would like to import only some of these photos, for example, 1203 to 1205, 1210 to 1219 (except 1213), 1220 and 1221. You could of course specify every file individually: ```` zoph@zoph $ zoph IMG_1203.JPG IMG_1204.JPG IMG_1205.JPG IMG_1210.JPG IMG_1211.JPG IMG_1212.JPG IMG_1214.JPG IMG_1215.JPG IMG_1216.JPG IMG_1217.JPG IMG_1218.JPG IMG_1219.JPG IMG_1220.JPG IMG_1221.JPG ```` Well, I don't know about you, but *I* certainly didn't buy a computer to do things by myself, so why not let the computer take care of that? ```` zoph@zoph $ zoph IMG_120[3-5].JPG IMG_121[^3]*.JPG IMG_122*.JPG ```` That saved a lot of typing, didn't it? This is not a zoph feature, by the way, it is a feature of your shell (probably [Bash](http://www.gnu.org/software/bash/bash.html)). ## Organizing photos ## You could just import your photos like described above and then use the Zoph webinterface to organize them, but why not organize them right away? ### Albums, categories and people ### You can put your photo in one or more albums and one or more categories (well, that's actually zero or more, since you don't *have* to put them in an album or category). Use the [--album](CLI.md#--album) and [--category](CLI.md#--category) commandline option for this. If there are any people on the photo, you can add people to the photo using the [--person](CLI.md#--person) option. Remember that the list of photos *always* comes *after* the other options. It's important to realize that the album, category or person must already be in the database. #### Examples #### Import `IMG_1300.JPG` and place it in the album **Summer** and category **Landscapes**: ```` zoph@zoph $ zoph --album "Summer" --category "Landscapes" IMG_1300.JPG ```` Import `john.jpg` and place it in the album **Family**, category **Portraits** and specify **John Doe** is in this picture: ```` zoph@zoph $ zoph --album "Family" --category "Portraits" --person "John Doe" john.jpg ```` Import `family.jpg` and place it in the albums **Family** and **Summer** and specify **John Doe**, **Johnny Doe** and **Jane Doe** are in this picture: ```` zoph@zoph $ zoph --album "Family" --album "Summer" --category "Portraits" --person "John Doe, Johnny Doe, Jane Doe" family.jpg ```` Import `guitarists.jpg` and place it in the categories **Music** and **Musicians** and specify **Hank Marvin**, **Jimi Hendrix** and **Brian May** are in this picture: ```` zoph@zoph $ zoph --category "Music, Musicians" --person "Hank Marvin" --person "Jimi Hendrix" --person "Brian May" guitarists.jpg ```` As you can see, you can add multiple albums, categories or people by repeating the [--album](CLI.md#--album), [--category](CLI.md#--category) or [--person](CLI.md#--person) option multiple times, or by specifying it only once and give it a list of albums, categories or people, separated by commas. ### Photographer and location ### Of course, you also want to record *where* and *by whom* the your photos were taken. This works almost the same als albums, categories and people, *except* that you can only store *one of each*. Again, the person and place must be in the database prior to using it via the CLI. Specify the photographer using the [--photographer](CLI.md#--photographer) option and the location using the [--location](CLI.md#--location) option. #### Examples #### Import `IMG_1400.JPG` and set **John Doe** as the photographer: ```` zoph@zoph $ zoph --photographer "John Doe" IMG_1400.JPG ```` Import `IMG_1401.JPG` and set **Berlin** as the location where the photo was taken: ```` zoph@zoph $ zoph --location "Berlin" IMG_1401.JPG ```` ### Other fields ### There are a lot more attributes Zoph can store about your photos. Many of them will be automatically read from the photo's EXIF information. You can also set these fields manually using the [--field](CLI.md#--field) option. #### Examples #### Import `IMG_1416.JPG` and set the title: ```` zoph@zoph $ zoph --field "Title=A nice photo" IMG_1416.JPG ```` ## Import directory ## During the import, Zoph moves (or copies) your photos to a directory that you have set to be the `image_dir`. You set this in the configuration screen. Under this directory, Zoph can create subdirectories. This is controlled by the [--path](CLI.md#--path), [--dateddirs](CLI.md#--dateddirs) and [--hierarchical](CLI.md#--hierarchical) options. With [--path](CLI.md#--path), you can manually set a path that will be inserted between the `image_dir` and the filename. With [--dateddirs](CLI.md#--dateddirs) and [--hierarchical](CLI.md#--hierarchical), Zoph will create directories based on the (EXIF-)date of the photo. If you specify both a pathname and [--dateddirs](CLI.md#--dateddirs) or [--hierarchical](CLI.md#--hierarchical), the location will contain the path first and the dated directory second. ### Examples ### Assume IMG_1480.JPG was taken on 5 May 2010 and IMG_1481.JPG was taken on 13 May 2010 and image_dir is set to `/data/photos`. ```` zoph@zoph $ zoph IMG_1480.JPG IMG_1481.JPG zoph@zoph $ ls /data/photos mid thumb IMG_1480.JPG IMG_1481.JPG ```` Ok, now let's add a `--path`: ```` zoph@zoph $ zoph --path "family" IMG_1480.JPG IMG_1481.JPG zoph@zoph $ ls /data/photos family zoph@zoph $ ls /data/photos/family mid thumb IMG_1480.JPG IMG_1481.JPG ```` And `--dateddirs`: ```` zoph@zoph $ zoph --dateddirs IMG_1480.JPG IMG_1481.JPG zoph@zoph $ ls /data/photos 2010.05.05 2010.05.13 zoph@zoph $ ls /data/photos/2010.05.05 mid thumb IMG_1480.JPG ```` This is of course nice if you only have a few photos, but when your collection grows and you have taken photos spread over several years, you will end up with hundreds of dated directories. For this reason, there is hierarchical dated directories: ```` zoph@zoph $ zoph --hierarchical IMG_1480.JPG IMG_1481.JPG zoph@zoph $ ls /data/photos 2010 zoph@zoph $ ls /data/photos/2010 05 zoph@zoph $ ls /data/photos/2010/05 05 13 zoph@zoph $ ls /data/photos/2010/05/05 mid thumb IMG_1480.JPG ```` You could, of course, also use both a path and dated directories: ```` zoph@zoph $ zoph --path "family" --hierarchical IMG_1480.JPG zoph@zoph $ zoph --path "family" --dateddirs IMG_1481.JPG zoph@zoph $ ls /data/photos family zoph@zoph $ ls /data/photos/family 2010 2010.05.13 ```` Although mixing `--dateddirs` and `--hierarchical` is probably not a good idea if you want to keep your collection organized and also, at least somewhat accessible directly from the OS (as opposed to from Zoph). (By the way, when specifying both `--dateddirs` *and* `--hierarchical`, hierarchical will take precedence). ## Using dirpattern ## With the [--dirpattern](CLI.md#--dirpattern) CLI option, you can automatically assign albums, categories, people, photographer, location or path based on the directories the photos are in. You do this by specifying a pattern, based on which Zoph will use directory names to assign to correct organizer. This pattern consists of a list of letters, where each letter is a directory. The letters you can use are: **a** (album), **c** (category), **l** (location), **p** (person), **P** (photographer) and **D** (path). Let's say you have the following directory structure: ```` |- John Doe | |- Walk in the park | | |- Trees | | | |- IMG_2001.JPG | | | |- IMG_2002.JPG | | | |- Flowers | | | |- IMG_2003.JPG | | |- Flowers | | |- IMG_2004.JPG | |- A day in the forest | | |- Trees | | | |- IMG_2005.JPG | | | |- IMG_2006.JPG | | | |- Birds | | | |- IMG_2007.JPG | | |- Animals | | |- IMG_2008.JPG | |- Summer Holiday | |- IMG_2009.JPG | |- IMG_2010.JPG |- Jane Doe |- A day in the forest | |- Trees | | |- DSC_1000.JPG | | |- DSC_1001.JPG | | |- Birds | | |- DSC_1002.JPG | |- Animals | |- DSC_1003.JPG |- Summer Holiday |- DSC_1004.JPG |- DSC_1005.JPG ```` Now, you can go into the top directory and run a Zoph import with the `--dirpattern` option, to automatically assign a photographer, an album and a few categories to each photo: ```` zoph --import -r --dirpattern "Pacc" * ```` Zoph will now import the entire directory structure, using the first level directory name to assign the photographer (the **P** in the dirpattern), the second level to assign an album (**a**) and the third and fourth to assign categories (**cc**). In this example, `IMG_2001.JPG` to `IMG_2010.JPG` will be stored with "John Doe" as photographer and the photos `DSC_1000.JPG` to `DSC_1005.JPG` will be stored with "Jane Doe" as photographer. `IMG_2001.JPG` to `IMG_2004.JPG` will have album "A walk in the park". `IMG_2005.JPG` to `IMG_2008.JPG` as well as `DSC_1000.JPG` to `IMG_1003.JPG` will be in the album "A day in the forest". `IMG_2009.JPG`, `IMG_2010.JPG`, `DSC_1004.JPG` and `DSC_1005.JPG` will be in the album "Summer Holiday". `IMG_2001.JPG`, `IMG_2002.JPG`, `IMG_2003.JPG`, `IMG_2005.JPG` to `IMG_2007.JPG` and `DSC_1000.JPG` to `IMG_1002.JPG` will be in the category Trees. `IMG_2003.JPG` and `IMG_2004.JPG` will be in the category Flowers. Which means that `IMG_2003.JPG` will be assigned to *both* Trees and Flowers. In the same way, `DSC_1002.JPG` will be assigned to both Trees and Birds. `IMG_2008.JPG` and `DSC_1003.JPG` will be in the category Animals. Finally, the photos in the "Summer Holiday" album, will not have any categories assigned. ## Controlling the way `zoph` works ## The Zoph CLI client has several options that control how it works, an example of `--dateddirs` and `--hierarchical` has been given above. More settings can be found in [The Zoph CLI tool](CLI.md). zoph-v0.9.19/docs/IMPORT-WEB.md000066400000000000000000000251161415176210700156700ustar00rootroot00000000000000# Importing photos - through the web interface # A photo album is of little use without photos. This page describes how to import photos into Zoph with the webinterface. You could also use the [Zoph/Using the commandline tools](CLI.md). The import process in Zoph consists of 2 steps: **uploading** and **importing**. If you have access to the filesystem of the server, you can skip the uploading and manually move the files you wish to import into the directory specified as `upload dir` on the [configuration screen](CONFIGURATION.md). You can then use the webinterface to import them into Zoph. If you don't have access to the server's filesystem or do not wish to use it, you can upload photos to the `upload dir` and then use the same process. You can even mix the two: copy some photos directly into the `upload dir` and upload others and then continue as if it is one set of photos (which it actually is now). ## Configuration ## ![Enabling import and upload in the configuration screen](img/Zoph_enable_import.png) There are several configuration options that are related to importing and uploading photos. At least, make sure `Import through webinterface` is enabled and if you also want to enable uploading photos through the webinterface, also enable `Upload through webinterface`. Furthermore, make sure that the `upload dir` under `paths` is set correctly. On some systems it might be needed to set the `magic file` in order for Zoph to be able to figure out the filetypes of the imported photos. ## Uploading ## To import photos in Zoph, first click on "Import" in the main menu. If you do not have that option, your useraccount is not permitted to perform imports or importing is disabled. Once you have clicked on the option, you will be taken to the Import page. ![Import page](img/ZophImport001.png) Next, click on "Browse..." to select an image on your local disk. Due to browser/operating system restrictions, you can only select one image at a time. Once you have selected an image, click "Import" to start the upload. Your browser will now upload the photo to the webserver that is running your Zoph installation and place it in the upload directory you have specified in the configuration. Once you have clicked "Import", Zoph will display a progress bar indicating how the import is progressing. At the same time, it will create a new upload form, enabling you to start another upload. There is no limit on the number of uploads you can do simultaneously (although of course each additional upload will slow down all the other uploads and use resources on both the uploading system and the server running Zoph). ![A few busy uploads](img/ZophImport002.png) As soon as the first upload is finished, Zoph will create a new 'window' on the page, "Uploaded Photos" and will place the newly uploaded photo in this window. At first, the photo will be displayed using an icon, but at the same time, Zoph will create a thumbnail for this image. Each additional finished upload will be placed in this window and thumbnails will be created. Zoph will show you with an icon which image is being resized and which images are waiting. By default, Zoph will resize one image at a time. You can use the `resize parallel` setting (on the configuration page) to enable multiple parallel resize jobs. ![Image resize in progress](img/ZophImport003.png) Once the thumbnail is created, the icon will be replaced by a thumbnail image. If you hold your mouse still over the thumbnail, you will see a "midsize" image, which will enable you to take a closer look to the photos. ![Zoph showing thumbnails of uploaded photos](img/ZophImport004.png) ## Setting properties of photos ## So, now the photos are on the server, we can import them into Zoph. We could just hit the "Import" button at the bottom of the screen and add the photos to albums, categories, etc. later, but it's probably easier to do it now. Keep in mind that all categories, albums, etc. will have to exist before you can use them on the import screen, it is (currently) not possible to add them from the import screen. Once all uploads have finished, it is no problem to leave the import page and go create albums, categories, etc. The photos are all stored on the server, so when you return to the import page, the photos will still be there. ### Album ### The photos I have just uploaded were all taken during a summer vacation in Canada, so I have created an album "Canada" under the "Summer vacation" album. To assign this album to imported photos, I click on the white box next to "albums" to display a dropdown menu that shows all the albums I have created. When I click on "Canada", the white box will display "Canada" and a new, empty field is created in case I want to add more. ![Choosing an album](img/ZophImport005.png) ![Chosen an album](img/ZophImport006.png) ### Photographer and Location ### All photos are taken by me, so I'm adding myself as the photographer and all photos were taken in Canada, so I set the location to "Canada" (In a real-world situation, you'd probably want to be a bit more precise about the location where your photos were taken, but for this example this will do). Note that, since a photo can only have one location and one photographer, no empty field is added. ### Categories ### Now, I would like to add categories. I use categories to describe what is on the photo. But, in this case, there are many different subjects on these photos, how are we going to do that? Let's start with flowers. First, I select the category "flowers" and then I 'tick' each photo with flowers. ![The photos to be imported have been 'ticked'](img/ZophImport007.png) As of Zoph v0.9.15, Zoph has basic support for XMP during import. Please see [XMP](XMP.md) for more information. ## Importing ## Finally, click "Import" and Zoph starts importing the photos into the database. Have a moment of patience and the 3 selected photos will disappear from the "Uploaded photos" window because they are no longer in the "upload" directory. At the same time a "details" window is added, showing you any messages the import process generated. If something goes wrong, the error message will also be displayed. In that case, the photos will not disappear from the "Uploaded photos" window, so you can easily try again after you have resolved the problem. ![Photos have been imported](img/ZophImport008.png) ### Using autocomplete ### So, let's import a few more photos. As you can see, the album, category, location and photographer we have chosen before, are still there, so we only need to change the category. The next picture shows a plane and no flowers at all, so click the little red "x" next to "Flowers" to remove that category and then click the empty field next to categories. The list of categories is quite long, so why search the list yourself, if you can let the computer do that for you? We are looking for a category named "planes", so we'll type a "p", the list will now be significantly shorter, only showing categories starting with "p". We select "planes" by clicking on it. Finally, we'll tick the photo we want to import and then hit "Import" to start the import. ![Using autocomplete on a dropdown box](img/ZophImport009.png) ### Multiple categories ### Now we only have 4 photos left. They all feature 'mountains', so let's remove the category "Planes" and add "Mountains". The first two mostly feature mountains while the others also feature other subjects. We'll just tick the first two, click import, and wait for the photo's to be imported. This time, we don't click the little red "x" next to "Mountains", but instead, add a second category. "Roads", for the first one. Again, we tick the photo we want to import, click "Import" and wait for the photo to be imported. The last photo features "Mountains" and "Snow", so, we want to remove "Roads" and add "Snow". Instead of using the red "x", you can also simply re-open the dropdown box and choose "Snow" (or type "snow" or a part of that). Tick the last photo and wait for the "Uploaded photos" window to disappear, since all photos have now been imported. ## Uploading an archive ## Uploading photos one-by-one can take a lot of time. Even worse, once you're uploading 5 or 10 photos simultaneously, your browser could get a bit slow. Wouldn't it be great if you could upload a bunch of photos in one go? Well, you can! Just put them in a ZIP or TAR archive and upload that! For this exercise, I have put a few of my photos from a vacation on Cuba in a zipfile called cuba.zip. First, I am uploading the file to the server. Just click on "Browse...", find the ZIP-file on my local disk and then "Import". ![Uploading a ZIP file](img/ZophImport010.png) ### Correcting problems ### Depending on the speed of the connection to the server, the upload may take a long time, since ZIPing a photo usually doesn't make it any smaller. We use the ZIP file only to be able to upload multiple files in one go. Once the upload is finished, it will appear in the "Uploaded photos" window: ![Oops!](img/ZophImport011.png) Oops! Something went wrong there! I forgot to configure `unzip command` on the configuration screen. After I have changed that (in a separate browser window), I simply click on "retry" to give it another go. I don't need to re-upload the file. As you can see, Zoph now starts unpacking the archive (notice the 'unpack' icon). The error from the previous action is still there, that could be confusing; fortunately you can remove it by clicking "clear" in the "Details" window. ![Problem solved](img/ZophImport012.png) After Zoph has finished decompressing the ZIP file, the photos in it will appear in the same way as if they were uploaded one-by-one. If Zoph encounters another archive inside the archive, it will unpack it as well. If you upload a .tar.gz file, this is exactly what happens: Zoph un-gzips it, resulting in a .tar file, which will be unTARred. Of course, you must have defined `ungzip command` and `untar command` in the configuration screen for this to work. Besides ZIP, TAR and GZ, Zoph supports BZIP files as well. Define `unbzip command` to enable that. (But remember, it makes little sense to Gzip or Bzip a bunch of photos, it will not make the file any smaller!) ![Archive unpacked](img/ZophImport013.png) ## Importing local files ## In many cases, the server running Zoph, may be the same machine that you are working on, or it may be sitting next to that machine. In those cases, uploading photos via the webinterface may be an unnecessary inconvenience. In that case, you may simple move or copy the files into the upload directory and then visit the webinterface. Zoph will find the photos and start resizing them. After that, you can proceed with importing just like the two previous cases. zoph-v0.9.19/docs/INSTALL.md000066400000000000000000000141731415176210700153520ustar00rootroot00000000000000Zoph Installation ================= Requirements ------------ See the [requirements](REQUIREMENTS.md) document. Creating the database --------------------- ### Create a database and import the tables ### ``` $ mysql -u root -p -e "CREATE DATABASE zoph CHARACTER SET utf8 COLLATE utf8_general_ci" $ mysql -u root -p zoph < sql/zoph.sql ``` ### Create users for zoph ### I created two users: ```zoph_rw``` is used by the application and ```zoph_admin``` is used when I work directly in mysql so I don't have to use root. ``` $ mysql -u root -p mysql> grant select, insert, update, delete on zoph.* to zoph_rw@localhost identified by 'PASSWORD'; mysql> grant all on zoph.* to zoph_admin identified by 'PASSWORD'; ``` Create zoph.ini --------------- In Zoph 0.8.2 and later, you need to create a zoph.ini file, usually in /etc. zoph.ini is where you define database settings. A simple example: ``` [zoph] db_host = "localhost" db_name = "zoph" db_user = "zoph_rw" db_pass = "pass" db_prefix = "zoph_" php_location = /var/www/html/zoph ``` An example zoph.ini file, called zoph.ini.example is included in the cli directory. See the man page for zoph.ini(5) or the [documentation](docs/) for more details Install the templates --------------------- ### Pick a location to put Zoph ### Create a zoph/ directory off the doc root of your web server, or create a Virtual Host with a new doc root. ``` $ mkdir /var/www/html/zoph ``` ### Copy the templates ### ``` $ cp -r php/* /var/www/html/zoph/ ``` ### Set accessrights ### For better security, you probably want to set accessrights on your Zoph files. (You may want to do this after testing whether Zoph works, in that case you know what caused it when it seizes working after this change) First, you need to figure out which user Apache is running under. Usually this is apache for both user and group. To determine this, check httpd.conf or use ``` ps -ef | grep httpd ``` You should probably make all files owned by the user apache and the group apache. You can do than with ``` chown -R apache:apache /var/www/html/zoph ``` You can either make them only readable by this user/group (more security): *440*, readable by all users: *444*, or readable and writable by all users: *666*. The last case means that you don't need root access to edit config.inc.php or to make changes to the other php files (such as upgrades to a new version). Keep in mind that giving write access to the .php files effectively gives control over Zoph. If you have other users on your system, you should choose the first option. Also, your mysql password is in `/etc/zoph.ini`, so if you've users on your system that are not allowed to know it, you should protect it against reading as well. The directories should have execute rights: *550* for max security or *777* for access for all users. To do this, first go to the directory directly above your Zoph directory, in this example /var/www/html ``` cd /var/www/html chmod [dir] zoph cd zoph find -type f | xargs chmod [file] find -type d | xargs chmod [dir] ``` replace [dir] with the accesspattern you've chosen for directories above and replace [file] with the one for files. > :exclamation: Warning :exclamation: > Double check whether you are using the correct directory and if you have typed it correctly, if you would > accidently type `/[space]var/www/html/zoph` or something, you would change all files on your entire system to > apache/apache as owner - not good). ### Access rights for your photos ### In many cases you can simply leave the access rights on you photo directories on default. However, if you use both the CLI and the webinterface to access your photos, you may want to change to a more advanced way of managing accessrights, using the [setgid](https://en.wikipedia.org/wiki/Setgid#setgid_on_directories]) feature in Linux and most other POSIX Operating Systems. * Create a new Unix group (in example "photo") ```` groupadd photo ```` * Add all users that use the CLI and/or are allowed to modify the photos on disk to this group (in this example, the user is called 'jeroen') ```` useradd -g photo jeroen ```` * Additionally, the apache user is added to this group, on my system, this user is called 'apache', but 'www-data' is also often used. ```` useradd -g photo apache ```` * Change the ownership of the photo directory to your user and the group photo ```` chown jeroen:photo /data/images ```` * Set the permissions on this directory as you wish, for example *775* (full rights for user and group, read rights for other) or *770* (full rights for user and group, no access for others). ```` chmod 775 /data/images ```` * Now set 'setgid' on the dir, this causes new files and directories to be created with the group 'photo'. ```` chmod g+s /data/images ```` Configure the PHP templates --------------------------- Some configuration options can be set in `php/config.inc.php file`. Usually you will not have to change anything there. Most configuration can be done from the web interface of Zoph. For more information, see the [Configuration documentation](CONFIGURATION.md). Install the CLI scripts ----------------------- ### Check the path to PHP ### The CLI script points to `/usr/bin/php`. If your PHP installation is in a different place, edit the first line of the script. ### Copy cli/zoph to /bin ### Or some other directory in your `PATH`. ### Install the man page ### Man pages for zoph and `zoph.ini` are in the `cli`/ directory. Copy these to the `man1` and `man5` directoies in your manpath, `/usr/local/man/man1` and `/usr/local/man/man5` for example. Test it ------- Try hitting http://localhost/zoph/logon.php. You should be presented with the logon screen. You can log in with admin / admin. It is recommended to change this. If you get a 404 error... make sure the zoph/ folder and templates can be seen by the web server. If you see a bunch of code... make sure Apache is configured to handle PHP (see the [requirements file](REQUIREMENTS.md) file) If you see a MySQL access denied error... make sure the `db_user` you specified in `zoph.ini` actually has access to the database. If your database is not on localhost, you will need to grant permissions to `zoph_rw@hostname` for that host. zoph-v0.9.19/docs/README.md000066400000000000000000000007151415176210700151760ustar00rootroot00000000000000# Zoph Documentation # http://www.zoph.org 1. [Requirements](REQUIREMENTS.md) 2. [Installation guide](INSTALL.md) 3. [Configuration Instructions](CONFIGURATION.md) 4. [Using the web interface](WEBINTERFACE.md) 5. [Importing photos through the web interface](IMPORT-WEB.md) 6. [Importing photos using the CLI interface](IMPORT-CLI.md) 7. [XMP Support](XMP.md) 8. [Using the CLI tool](CLI.md) 9. [Upgrade Instructions](UPGRADE.md) 10. [Changelog](CHANGELOG.md) zoph-v0.9.19/docs/REQUIREMENTS.md000066400000000000000000000071251415176210700161660ustar00rootroot00000000000000# REQUIREMENTS # Zoph is being developed on Linux, but it should be able to run on any OS that can run Apache, MySQL and PHP. Users have reported succesful installations on MacOSX, several BSD flavours and even Windows. Zoph requires the following: * Apache 2.4 * PHP 8.0 * MariaDB 10.x or MySQL 8.x * ImageMagick 7.0 Other versions may work as well, see below for more details. How to install these applications and get them to work together is depending on your OS and distribution. Check the documentation of the application and/or your distribution for details. ## Apache ## * Current versions of Zoph are developed on Apache 2.4.x ## PHP ## Current versions of Zoph are developed on PHP 8.0 * 7.4 probably also works, but is no longer actively tested. ### Required features ### The following features (extensions) to PHP are required for Zoph. Not all distributions automatically install all of them. * session * pcre * gd2 * exif * xml * pear (if you want to use the e-mail features) * FileInfo ## php.ini settings ## Settings you may need to change in php.ini: ### max_input_time ### This is the time Zoph is allowed by PHP to spend waiting for the file to be uploaded. Depending on the size of your files and the speed of your server's connection, 30 seconds (the default) is usually enough to process single images, if you are uploading zip or tar files, you may want to increase this to 60 or 120 seconds. ### max_execution_time ### This is the time Zoph is allowed by PHP to run. Depending on the speed of your webserver, Zoph could spend quite a lot of time resizing an image. 30 seconds may not be enough, especially if you have a camera with a lot of megapixels. ### memory_limit ### This is the amount of memory PHP allows Zoph to use. Especially if you have large images, the default (8 or 16 Megabyte) may not be enough. If you have sufficient memory in your server, setting it to 128M is perfectly safe. * If you are using the web importer you may need to increase the `max_execution_time`, `upload_max_filesize`, `post_max_size` and `max_input_time` defined in php.ini. * If you are using the watermarking feature, you probably need to increase the `memory_limit` setting. Please note that enabling this function uses a rather large amount of memory on the webserver. PHP by default allows a script to use a maximum of 8MB memory. You should probably increase this by changing `memory_limit` in php.ini. A rough estimation of how much memory it will use is 6 times the number of megapixels in your camera. For example, if you have a 5 megapixel camera, change the line in php.ini to `memory_limit=30M` * The e-mail photo feature may require increasing the `memory_limit` setting. Since Zoph needs to convert the photo into Base64 encoding for mail, it requires quite a large amount of memory if you try to send full size images and you may need to adjust `memory_limit` in php.ini, you should give it at least about 4 times the size of your largest image. ## MySQL ## * Current versions are developed with MariaDB 10.6 * MySQL 8.0 has not been tested ## ImageMagick ## * Current Zoph versions have been tested against ImageMagick 7.0.x ## Browser ## In order to be able to use Zoph, you will need a browser. * Zoph is being developed and thoroughly tested with a recent Firefox build * Zoph should work with all recent browser versions * Please report a bug if it doesn't. * Older versions usually work, but layout may not be 100% ok. * Some features require Javascript support * Most of Zoph should work when Javascript is turned off in the browser, but this is decreasing, Javascript is required for more and more functions! zoph-v0.9.19/docs/UPGRADE.md000066400000000000000000000211161415176210700153260ustar00rootroot00000000000000# UPGRADE INSTRUCTIONS # ## Zoph 0.9.18 and up ## As of Zoph 0.9.18, database upgrades are included in the release. Simply copy the contents of the `php` directory into the webroot. Then log in to Zoph with an admin user and the GUI will guide you through the upgrade process. If you are running an older version of Zoph, please make sure you follow the upgrade instructions to v0.9.17 (below) prior to logging in to Zoph and performning the upgrade from there. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH`. ``` cp cli/zoph /usr/bin ``` ### Database changes ### ## Zoph 0.9.15 or 0.9.16 to 0.9.17 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.15. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH`. ``` cp cli/zoph /usr/bin ``` ### Database changes ### Execute zoph-update-0.9.17.sql: ``` mysql -u zoph_admin -p zoph < sql/zoph_update-0.9.17.sql ``` Changes this script makes: * Add a `imported` field to `zoph_photos`, so Zoph stores the date and time a photo was imported * Copy the date and time in `timestamp` to `imported`, so photos imported prior to this release have some data stored in here ## Zoph 0.9.13 or 0.9.14 to 0.9.15 or 0.9.16 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.14. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH`. The CLI tool has undergone some changes in v0.9.13, so if you copied it previously, you should copy the new version in place. ``` cp cli/zoph /usr/bin ``` ### Database changes ### * There are no database changes in v0.9.13 or 0.9.14. ## Zoph 0.9.12 to 0.9.13 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.12. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH` ``` cp cli/zoph /usr/bin ``` ### Database changes ### Execute zoph-update-0.9.13.sql: ``` mysql -u zoph_admin -p zoph < sql/zoph_update-0.9.13.sql ``` Changes this script makes: * Add a `row` field to `zoph_photo_people`, allowing you to specify multiple rows of people on a photo. ## Zoph 0.9.11 to 0.9.12 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.11. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH` ``` cp cli/zoph /usr/bin ``` ### Database changes ### Execute zoph-update-0.9.12.sql: ``` mysql -u zoph_admin -p zoph < sql/zoph_update-0.9.12.sql ``` Changes this script makes: * Resize fields that store IP addresses to give them enough space to store IPv6 addresses ## Zoph 0.9.6-0.9.9 to 0.9.7-0.9.11 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.6. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH` ``` cp cli/zoph /usr/bin ``` ### Database changes ### * There are no database changes in v0.9.7, v0.9.8, v0.9.9, v0.9.10 and v0.9.11 ### Deprecated configuration ### I will be removing the `ssl.force`, `url.http` and `url.https` configuration option in v0.9.9. As of v0.9.8, Zoph will show a warning. If your setup requires setting these functions, please comment on [issue#100](http://github.com/jeroenrnl/zoph/issues/100) ![screenshot of the deprecated options](img/zoph-ssl-config.png) ## Zoph 0.9.5 to 0.9.6 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.5. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH` ``` cp cli/zoph /usr/bin ``` ### Database changes ### Execute zoph-update-0.9.6.sql: ``` mysql -u zoph_admin -p zoph < sql/zoph_update-0.9.6.sql ``` Changes this script makes: * Give several timestamp fields a default value, because as of MySQL 5.7.4 "0000-00-00 00:00:00" is no longer a valid date in the default configuration (this was reverted in MySQL 5.7.8) * Set `person_id` in the `zoph_users` table to have a default of `NULL` instead of `"0"` * Drop the `column contact_type` from `zoph_places`, as it was not used as of Zoph 0.3.3 (!) ## Zoph 0.9.4 to 0.9.5 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.4. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` If you use the CLI client, you should copy it to a path that's in your `$PATH` ``` cp cli/zoph /usr/bin ``` ### Database changes ### There are no database changes for 0.9.5 ## Zoph 0.9.3 to 0.9.4 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.3. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` ### Database changes ### * Execute zoph-update-0.9.4.sql: ``` mysql -u zoph_admin -p zoph < sql/zoph_update-0.9.4.sql ``` Changes this script makes: * Add a field that stores whether or not new subalbums should be automatically granted permission * Add new colour schemes ## Zoph 0.9.2 to 0.9.3 ## * If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.2. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below. ### Copy files ### Copy the contents of the php directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` ## Database changes ## * Execute zoph-update-0.9.3.sql: ``` mysql -u zoph_admin -p zoph < sql/zoph_update-0.9.3.sql ``` Changes this script makes: * Resize the `password` field to allow store bigger hashes * Add fields to the `user` table to allow for new access rights * Add `created_by` fields to the albums, categories, places, people and circles tables ## Zoph 0.9.1 to 0.9.2 ## * *If you want to upgrade from an older version, first follow the instructions to upgrade to 0.9.1. It is not necessary to install older versions first, you can just install the current version and follow the upgrade instructions below.* ### Copy files ### Copy the contents of the `php` directory, including all subdirs, into your webroot. ``` cp -a php/* /var/www/html/zoph ``` ### Database changes ### * Execute zoph-update-0.9.2.sql: ``` mysql -u zoph_admin -p zoph < sql/zoph_update-0.9.2.sql ``` Changes this script makes: * Add previously missing 'random' sortorder to preferences * Resize Last IP address field so IPv6 addresses can be stored * Database changes for 'circles' feature * Create a VIEW on the database to speed up queries for non-admin users zoph-v0.9.19/docs/WEBINTERFACE.md000066400000000000000000000551031415176210700161000ustar00rootroot00000000000000# Using the Web Interface # This manual will describe how to start using Zoph. It assumes you have successfully installed all the components and are presented the logon screen when you visit http://localhost/zoph/logon.php (if you are not running your web browser on the same machine as where you are running Zoph, you will need to replace `localhost` with the hostname or IP address of that machine. ## Logging In For The First Time ## An admin user was created when you loaded the database. You should be able to login as `admin` using the password `admin`. You will be presented with Zoph's home page where you will be greeted as 'Unknown Person'. There will be a blank square where a random photo would normally appear. The first thing you should do is change the admin's password: 1. Click on the 'prefs' tab. 2. Click the 'change password' link. 3. Enter a new password and click 'submit'. Next, create a 'person' record for yourself: 4. Click on the 'people' tab. 5. Click on the [ new ] link on the right. 6. Fill in your first name, last name and whatever other fields you wish. 7. Click the 'Insert' button. Now create a user for yourself: 8. Click on the 'admin' tab. 9. Click on the 'users' icon. 10. Click on [ new ]. 11. Pick a username and password. 12. Select yourself from the person pulldown. 13. Change your class to Admin. 14. Click the 'Insert' button. An admin has permission to do anything so you don't need to grant the specific 'browse' or 'view details' permissions, nor grant permissions to individual albums. Now that you have created a user: 15. Click on the 'logout' tab. 16. Login as your user. ## Creating An Album, Category and Place ## Before importing some images we'll create an album to put them in. Note that you do not need to assign photos to an album if you don't want to. To create an album: 1. Click on the 'albums' tab. 2. Click on [ new ]. 3. Pick an album name. I'm using "Zoph Tutorial" for this example. 4. Add an album description if you wish. This can be left blank. 5. Click the 'Insert' button. To create a category, click on the 'category' tab and do exactly as above. To create a place: 6. Click on the 'places' tab. 7. Click on [ new ]. 8. Enter a title at minimum. 9. Click the 'Insert' button. Albums, categories and places are hierarchical in nature (each entry can have children entries as well). When you view photos in an album, category or place within Zoph you can choose whether you see just the photo in that album, category or place or also automatically see photos placed in their children. Once you are creating more albums, categories and places, you can change the hierarchy by choosing the 'parent' album, category or place. ## Importing photos ## Once you have your Zoph installation up and running, the next step is to add photos to it. There are 3 ways to import photos in Zoph: 1. Uploading and importing them using the webinterface 2. Placing the photos in the upload directory and importing them using the webinterface 3. Importing the photos using the Command Line Interface (CLI) tool. The first 2 are described in [Using the webinterface to import photos](IMPORT-WEB.md), the last one is described in [using the CLI](CLI.md). ## Viewing Photos ## Now that there are some photos in the database, I'll go over some features of the web interface. The UI is fairly self-explanatory so you probably don't really have to read this section. ### Finding Photos ### There are numerous ways to find the photos you just entered, a few of which are: - Browse to the album you created and click [ view photos ]. - Browse to yourself in 'people' and click [ photos by ]. - Browse to the place you took of photo and click [ photos at ]. - Use the search form. Specifying no criteria will bring up all photos. - Use the 'photos' tab and you can browse every photo in Zoph. ### The Results Page ### By default the results page will show thumbnails of up to 12 photos (you can change this from the 'prefs' page). On the prefs page you can also choose whether or not to display descriptions under the thumbnails. From this page you can: - Click on a thumbnail to go to the photo page. - Click on [ Prev ] or [ Next ], or use the pager, to page through the photos (if your results span more than one page). - Click on [ Slideshow ] to enter slideshow mode. - Reorder the photos by choosing a field from the pulldown. - Change the direction of ordering by clicking on one of the triangles. The white triangle/arrow represents the current direction. - Change the number of rows and columns displayed. ### The Photo Page ### Once you click on a thumbnail, you will go to a page for that photo. Here you'll see a mid sized version of the photo plus all the information about the photo available. From this page you can: - Click the name of the file or the image itself to bring up the full sized photo. - Click on a person, place, album, category or date to take you to the corresponding pages. - Click on [ Prev ] or [ Next ] to move through the photos as they appeared on the results page. - Click on [ email ] to email the photo (If this function is enabled in the configuration). - Click on [ lightbox ] to add the photo to your [lightbox](#lightboxes) (if you have one) - Click on [ edit ] or [ delete ]. (if you are an admin or have been given write permission on the album in which the photo appears). If you set the auto edit pref, you will automatically be presented with fields to edit the info about a photo whenever you click on a thumbnail. Clicking [ return ] takes you back to the regular view. ### Slideshows ### The slideshow mode (entered from the results page) will cycle through the current set of photos by refreshing the browser window. The interval between refreshes can be set on the 'prefs' page. From the slideshow page you can: - Click on [ pause ] to pause the slideshow. - Click on [ stop ] to return to the results page. - Click on [ open ] to view the current photo on the normal photo page. ### The Search Page ### You can search for photos using multiple criteria on the search page. Note that when you select an album or category, all descendant albums or categories will also automatically be chosen. By default all criteria are joined by "and". You can choose "or" instead from the pulldowns in the left most column. Since you can not specify groupings of the conditions, you might not get the results you want if you try to construct complex queries using different conjunctions. Note that for non-admin users, the album, category, location, person and photographer menus are pruned so that only those entries that actually appear in a photo that the user has access to are shown. ### Random Photos ### The thumbnail that appears on the 'home' page is randomly selected. I tend to like this photo to be somewhat good so I created a setting on the 'prefs' page that lets you specify the minimum rating to be used when selecting a random photo. If you want to create a never ending slideshow of random photos, you can do this by clicking the "randomly chosen photo" link on the home page. If you then click the [ Slideshow ] link on the results page a new random photo will be chosen for each slide. ### Lightboxes ### Lightboxes give someone a place to gather and share their favorite photos. In Zoph, lightboxes are simply albums. When editing a user, an admin can declare a certain album to be the lightbox for that user. The admin must grant the user permission to view the lightbox album as well. IMPORTANT: Don't give a user write permission on their lightbox album. The user could then add any photo to their lightbox and be able to edit it. Whenever the user views a photo they can click a 'lightbox' link which will add that photo to their lightbox. When a user is viewing their lightbox, each photo will have an 'x' below it. Clicking this will remove that photo from the lightbox. Lightboxes allow users to gather photos of interest to them without requiring the ability to edit a photo to place it in an album. Lightboxes are only activated for a user by specifying a lightbox album. The setup I used was to create a "Lightbox" album, under which I created separate albums for each user's lightbox (e.g. "Jason's Lightbox"). To share a lightbox an admin must grant permission to that album to other users. ## Managing Users, Groups & Restricting Access ## One of the features of Zoph is its system of access privileges. You have already created an admin class user for yourself. If you desire, you can create other users as well. ### Users ### Creating a new user account 1. Log on with an admin account. 2. Click "admin" in the menubar. 3. Click "users". 4. Click "new" in the right top of the screen. 5. Specify a user name (e.g. first initial + last name). 6. If the user is also represented by a "person" in your database (for example because he or she has taken photos or appears on them), specify a person, otherwise leave it on "Unknown Person". 7. Specify a password 8. Select a user class: an Admin can do anything. A User has restrictions. In that case, you can specify what a user can see, besides photos: - **browse people**: the user can view the 'people' page in which all the people stored in the database can be seen. - **browse places**: the user can view the 'places' page in which all the places stored in the database can be seen. - **details of people**: if "Yes" the user can see all information about any person. If "No" the user will only see a person's name. - **details of places**: if "Yes" the user can see all information about any place. If "No" the user will only see a place's title and city. - **import**: if "Yes" the user can import photos - **download zipfiles**: if "Yes", this user can download a set of photos (album, category, search result, etc.) as a zip file. - **leave comments**: if "Yes", this user can leave comments with photos. - **rate photos**: if "Yes", this user can rate photos. - **rate photos multiple times**: if "Yes", this user can rate the same photo more than once. Use this if you create an account that is used by multiple people. Each photo can be rated once per IP address. lightbox: the user's lightbox album 9. Click "insert" 10. Add the user to one or more groups. See [groups](#groups) how to do that. ### Modifying or deleting a user account ### 1. Log on with an admin account. 2. Click "admin" in the menubar. 3. Click "users". 4. Click the user you want to modify 5. Click "edit" or "delete" 6. Make the desired changes or confirm in case of a deletion. For a description of the fields, see creating a new user account. ### Groups ### In order to give users access to photos, you must create one or more groups, you can then give accessrights to albums to those groups, you could for example create a group 'family', a group 'colleagues' and a group 'friends'. A user can be member of multiple groups and Zoph will combine the accessrights for all the groups. #### Creating a group #### 1. Log on with an admin account. 2. Click "admin" in the menubar. 3. Click "groups". 4. Enter a group name 5. If desired, enter a description. 6. Click "insert" You will automatically be taken to the edit group screen. See [Modifying a group](#modifying-a-group) for an explanation. #### Modifying a group #### 1. Log on with an admin account. 2. Click "admin" in the menubar. 3. Click "groups". 4. Click on "display" next to the group name you wish to edit 5. Click on "edit" 6. You can change the group name or description and add or remove a member. - If you modify the group name, all the members will automatically change to the new group. - Select a user from the list to add them to the group - To remove a member from the group, tick the box in front of the username 7. Click "update". - This screen has two "update" buttons, be sure to click the top one for these modifications. 8. To add albums to the list this user can view, use the "grant access to all existing albums" or add a specific album. For a more detailed explanation, see [albums](#albums). 9. Click "update". - This screen has two "update" buttons, be sure to click the bottom one for these modifications. Be sure to modify either the top or the bottom half of this screen and not both. ### Albums ### For non-Admin users, permissions to view photos are granted on a per album basis. For each album you grant a group access to, you must specify: - An access level - A Watermark level (if you have enabled watermarking in the configuration) - Whether that album will be writable Zoph determines whether a user has access to a photo checking two things: 1. Is this photo in an album that at least one of the groups the user is member of has access to? 2. If so, is the group's access level for that album greater than or equal to the level of the photo? If both of these conditions are met, the user has access to the photo. If you have enabled watermarking, a photo with an watermark level lower than the photo's level, will see full-sized photos with a watermark on them. If a photo appears in more than one album, a user needs only to have permission on one of them to see the photo. If the user has access to multiple albums in which a photo is found, or the user has multiple groups that give access to the photo, the least restrictive (highest access level) permission is used. The level of a photo (and therefore of access levels) can range from 0 to 10. The default access level is 5. Zoph is designed so that these privileges should work transparently. It should appear to any user that they have access to all photos. There should be no reference to any album or photo (including photo counts) to which a user does not have access. If an album is marked writable, members of the group may edit the photos that they have access to in that album. If you want to grant a group access to all albums you can do this all at once and the access level and writable flag will apply to every album. This is handy if you want to let someone see everything but don't want them to change anything. Note that when you create new albums you will have to grant permissions to any non-Admins before they will be able to see those album. ## Comments ## If enabled in the configuration and allowed under the user's profile, a user can leave comments with photos. Limited markup is possible, the following markup is supported: [u]underline[/u], [i]italics[/i] and [b]bold[/b], the various possible smileys are displayed with the comment input form: ![Comments](img/ZophComment.png) ## Scenarios ## ### Access levels ### Say you create an account for your parents but you don't want them to see the photos in the "New Years Party" album. Simply don't grant them permission to that album. However, say the photos in this album are mostly harmless, except for a few that you would like to keep hidden. In this case, change the level of those photos to 6 (or higher) and grant the user permission to the album with an access level of 5 (or less). ### Watermarks ### You have taken a couple of brilliant landscape images. You really want to show them to one of your regular customers, but you're affraid they will simply take your image and publish it, without paying for it. You put the photos in an album "Landscape" and set their level to 3. After that, you give your customers account access level 5 and watermark level 2. They will now be able to watch the photo, but when they look at the fullsize image, a large copyright message will be superimposed over the photo. In this way, they will be able to judge the quality of the image, but it will be unusable to use in their productions. After they have payed for the photo, you can put the image in an album that does allow them to see the photo without a watermark, change the level of the photo or change the watermark level. (keep in mind that the latter two will also influence other accounts or photos, respectively). ### Defining a Default User ### A default user can be defined. This user is automatically logged in when a person first opens Zoph in their browser. You can use this feature to create a guest account with limited permissions. To define a default user, create a user, grant that user whatever permissions you want and set the user's preferences. Whichever user is defined as the default user is unable to modify their preferences while they are the default. Then, in the configuration screen, under `Interface settings` select the `Default user`. Admin users can not be default users and you should not grant any write permissions to the guest user. That's it. Now when you hit /zoph/zoph.php for the first time you'll be logged in as the guest user (if you are already logged in as someone you'll first have to log out). You can also log out when you are the guest user to be able to log back in as someone else. What if a guest hits logout and wants to get back in but doesn't know the guest account info? Hitting submit on the logon page without specifying a user name or password will log you in as the default user. ## Pages, Pagesets and ZophCode ## By default, each album, category, place and person has a page that will show the basic data about this object. If you want, you can customize it using the ZophPages feature. ### ZophCode ### A ZophPage is written in ZophCode. ZophCode is very similar to bbCode or html and consist of tags in square brackets. Currently supported tags are: ZophCode Tag | Meaning | Example ----------------------------|---------------------------|------------------------------------------ [b]...[/b] | Bold | You can make text [b]bold[/b]. [i]...[/i] | Italics | To [i]emphasize[/i] a word... [u]...[/u] | Underline | [u]Underline[/u] a word. [h1]...[/h1] | Level 1 (chapter) header | [h1]My holiday[/h1] [h2]...[/h2] | Level 2 (paragraph) header| [h2]Second day[/h2] [h3]...[/h3] | Level 3 (sub-prgr) header | [h3]Afternoon[/h3] [color=]...[/color] | Text colour. | Words in [color=blue]blue[/color] and [color=#ff0000]red[/color]. [font=]...[/font] | Text font | Mixing [font=times]fonts[/font] can make your [font=courier]page[/font] look professional (or messy). [br] | Line break | Best regards,[br]Jeroen [background=]...[/background] | Background colour| [background=blue]Blue background[/background] [photo=]...[/photo] | Link to a photo | [photo=123]See this photo![/photo] [album=]...[/album] | Link to an album | [album=123]See this album![/album] [person=]...[/person] | Link to a person | [person=123]See this person![/person] [cat=]...[/cat] | Link to a category | [cat=123]See this category![/cat] [place=]...[/place] | Link to a place | [place=123]See this place![/place] [link=]...[/link] | Link to a webpage | [link=http://www.zoph.org]Zoph[/link] [thumb=] | Thumbnail of a photo | [thumb=123] [mid=] | Mid-size image of a photo | [mid=123] It is possible to nest tags. For example: ```` [h1]Holiday in [b]Spain[b][/h1] [i]more [u]emphasis[/u] by [b]combining[/b] tags[/i]. Click on this thumbnail to see the photo: [photo=123][thumb=123][/photo] Just make sure you open and close the tags in the right order: [b]This [i]will[/b] not[/i] work! ```` Finally, you can use smileys. See [Comments](#comments) for an overview. ## Pages ## You can create a new page by going to the admin page and clicking on `Pages`. Just type your text and tags and save. If you would like to spread the content over multiple pages, just save this one and create another page. ## PageSets ## The next step is to combine 1 or more pages into a pageset. Keep in mind that if you have only one page, you still have to put it into a pageset. Create a pageset from the admin page and add the pages you would like to have in this pageset to it. You can also define the order in which the pages appear in the pageset by clicking on `move up` and `move down`. You also have to decide what you would like to do with the original page Zoph automatically creates for each album, category, place and person. You can choose to never display it, to display it on the first page, the last page or on each page. You can also choose whether you would like to see you own page first or the auto-generated. Keep in mind that if you choose to never display the original page, you could restrict navigation for your users. For example, if you have an album with a few sub-albums, your users will be unable to navigate to these subalbums unless you provide links in your page to these subalbums (and add new links every time you have added a new sub-album). ### Assigning a PageSet to an object ### The final step in enabling the ZophPages feature, is assigning the pageset to and album, category, place or person. You can do that by editing the specific object and choose the pageset. ## Preferences ## A user can customize Zoph using the preferences page. Below are descriptions of a few of the prefs. ### Breadcrumbs ### Between the tabs and the title bar you'll see a list of breadcrumbs by default. On the prefs page You can specify the number of breadcrumbs to show or you can choose to disable the breadcrumbs completely. One note about breadcrumbs: clicking on the small x to the right of the breadcrumbs will clear the list of crumbs. ### Choosing a Language ### The default value is 'Browser Default'. This means that Zoph will try to present itself in the language you can specify in your browser's settings. If no translations are present for any of the languages in your browser's list, English will be displayed by default. If you specify a language on the prefs page instead of 'Browser Default' your setting will override whatever you have your browser set to. The language files are stored in php/lang. Creating a new translation involves creating a file in this directory with the language's two letter code in lowercase. Use one of the existing (non English) language files as an example. ### Other Prefs ### Here are descriptions of a few of the other prefs: **days past for recent photos links:** The home page has links to view photos taken or modified in the past X days, this pref sets the number of days. **display camera info:** If set to No, when viewing the details of a photo, fields like camera make and model, focal length, exposure, etc. are not displayed. **automatically edit photos:** If set to Yes, when an admin or a user with write permission views the details of a photo, they are immediately taken to the edit screen. This is handy for editing one photo after another. **color scheme:** the color scheme to use. Admins can click on "color scheme" to add, edit and delete color schemes. zoph-v0.9.19/docs/XMP.md000066400000000000000000000053221415176210700147040ustar00rootroot00000000000000# XMP Support # As of Zoph v0.9.15, Zoph has basic XMP support. This will be expanded over the next releases. If you have suggestions for improvements, please [let me know](#13). Also, if you have examples of images you added XMP to that are not handled by Zoph as you expect, feel free to send them to me (preferably via [Issue#13](#13). ## What is XMP ## **XMP** stands for *eXtensible Metadata Platform* and is an ISO standard to record Metadata for various file types, either embedded in the file or added to a separate file. For more information, see [Wikipedia](https://en.wikipedia.org/wiki/Extensible_Metadata_Platform) or [Adobe's site](https://www.adobe.com/products/xmp.html). ## What can be done with XMP ## XMP is meant to share metadata between applications. So that, if you have assigned categories or people to a photo in one program, you can easily import that information in another program. ## What XMP support does Zoph currently have? ## As of v0.9.15, Zoph has very basic XMP support. It will now recognize ["Dublin Core"](https://dublincore.org/) Subjects embedded in JPG files, and use them as **Categories** during import via the web interface. If you use for example [Geeqie](http://geeqie.org/)[^1] to assign **Keywords** to a photo, these will be recorded as "Dublin Core Subjects" (dc:subject) and will be recognized by Zoph. If Zoph finds *dc:subject* tags in a file you are importing via the web interface, it will display a small tag under each photo. As of v0.9.16, Zoph will also process XMP ratings. ![XMP tags on web import](img/xmpImport.png) The tag will either be **grey** or **red**. A **red** tag indicates that Zoph does not recognize this *subject* and cannot import it. A **grey** tag indicates that there is a **category** in Zoph's database that *exactly* matches the *subject* name and Zoph will assign this *category* to this photo during import. As of v0.9.16, Zoph will also process XMP tags when importing via the CLI, provided you specify `--XMP`. Unknown subjects are silently ignored. ## What XMP support will Zoph have in the future ## First and foremost, please let me know what **you** could use to improve your workflow with Zoph! Furthermore, I am planning to add at least **Album**, **Person** and **Location** support to Zoph. Also, I expect that not all application will use the same XMP tags as Geeqie does, so I will try to add support for other applications in the future. Finally, I am planning to make Zoph *write* XMP tags as well, so you can use the information you added in Zoph to feed other applications. [^1]: If you have used other applications, succesfully or unsuccesfully, please let me know so I can update the documentation or improve Zoph with support for more applications. zoph-v0.9.19/docs/img/000077500000000000000000000000001415176210700144705ustar00rootroot00000000000000zoph-v0.9.19/docs/img/Config_import.png000066400000000000000000006337011415176210700200070ustar00rootroot00000000000000PNG  IHDRrO=mh pHYs``f3tIME (TiTXtCommentCreated with GIMPd.e IDATxwxlOB& HJ." R$AA@JMA"(`A: **j衤?I@~^񊾟ɲ3sΙ337gΜ>|`۩\2)))B!_;ի #wEQB!:Dv \Dο+=YjH!/@bp HƝ!B!_ U!Bq@N!B9!B!B!@N!B,Cr(k159B!@9̑Ogs=x|/|b)B ~Іix\|}>9Xwoɛ+~:A1ZÉ}k9uWNB5Y2m;vpt\l/p`mf#*B JtGPߍ9uh3[;0*G>ŏ_-Ùx Si4p 92~o!Aȧ1U7(0ZJ*B?eg9[^.Rj.@Lfl>$f=Wy -/ D5Yi= z-y'"B!1Çv;O?oZ} wzooaJǨQ5 )2p1&TxLލ(@:~<UL ŀj(Fno|̙,|+=B!NYFWVY2ev3-ы+=%\rm ?54Mj: ʬ*6MG2ZWQBK +W. 0Lfv;6 χn[{-Al6BBBPUU_bag٘Lqqq|>_AXXoHpܶl6bkZ嘖f`|iV+!!!AcWhтNoA`$$$DXXXV0EPU#͎ 2b5MSt/tˡB!a(bZȟ?Ǐl67obcc2e2Zl6ʱc?p- ֭<#(<@^h[W_}UUt" dxW *cRR5k>ɕ+3ghW*W\H˜L?c]r%M4W⋍Y[>L]~%Ki*]vjʳ>:>7(V!L&G`Æ MS qٺu+FE.&%%ATڵ jrQ:Rh._,WBq?r4իGpR^=ϟdf\ˌ}prEV4h0 ||:f.]rsn7>g>ɜ9s qбcGH||6lf n;um۶ѷo?OZjԩ,\ׯ lܸ1Lv?4vM3qqΞ=i>ŬoNHJJСC,\ӴK|>Fx7itIΝILLb5QooLLLO>9SN1}4._B۶m7{&99g2glbccOG0|0Μ9t2~͛Ӵi<>$"""hذ)͛B!@AHMMbŊԯ߀*WLjj ͚5d2ALL Ǐ̠A|ݛ={P3$''v{vHb>|W_} ǀrʤO2e䠴vJ"Ex2d]OnM~zвÇ1z\.Çζm([,SO=ŨQov1ݻ77Fe˧lٲnj׮MRR211V), x͚5ɛoeӦ(UNjժǕ+TVgyԠ4F#;w~Sp:DEEqJ(Ibb"ŋgiF8!~ T4,..MӸqFW\AUUbbbȓ'oi0d˨ŋ-ۼy3˖-#*j'O6-E1?C HlUۤ$L&III99ӧɟ?wf֬YsigϞh4rAd&11DZZm… FN8A"Ȋ`2HNNd2- χh̙zZ $W\\~e˖cˠB!oټy3?_{B~]Fbt%[0lp >ڵk1zhf|P7oꫯh1z5j0zhϟmyBپ}'Ndԩg4jԐ_n֗&5jTgرLС`4IHH૯vУGwxEf9u4 6CÆ _>&4ԩ-]4t~{&111ԯ_g qݜ?^hDժU)P ?;c=#INNf̘18v4MCQ=V}(N'.\aF<,Xkby(RŊ#Oܤ$3sL._BzTE%99ɓ'c2yDZfUT!>>ᶏHg&ܓv;f]E0!G*yӦM|:,Y /0j(:tXC+B!̝;ݣՊ+Pf{~JJ /^x\N:LRXD 4xR5j<ƍINNf͚|ͷA-B!?9!B[B!G9!B B!rB!B9!B B!rB!B9!B B!rB!B9!B!B!rB!B9!B!B!rB!B9!B!B!@N!B9!B!B!@N!B9!B!iTUm^&I_,h$$$OO!!Νɓ'=rѵkr(9r={v?L۷oG?-?!BH 0|bncXVziZP0fq8 ˼ngYA j NHNNjp> L&BCClc~B!@(F*WlP_YxGHIIaѢEhAi*۷b`4Yv-VIi\MS fҡC{EAQ֯yb0ذa<}TL&R%11?BT"""04ƍmLSLd2yz jEUԮ] UU;M4MA5M:Id BH wy38y3˗/3z([Ŋٶm7ndٲe9snݺ2e>t+GfΝL p֮]q毁R=z4k֌]v6Ο1bRWvm_?cH|MvogQ_[rǏ3m4&OQ^˗pEh4ZTB#)r{($]B zpSresJJN=pW~ԭ[\.IIA4t>t۷.VpyCK.\.~G pB"""sܷ/п}̆ JFF7n]x8xpv-2ͣnw-[,6}?2h4d_˗/ @xu6Z!?jhJkPU5F MPD ߺ;wn, 5G`ٲesvV??>-q~'rYhBOtf0kЦ/9H q8矯ի}_zu6oޒ.}|r*7o5r  `1>r[/3 7۽n'%\b!)) MӰZ$&&*6MlZzf?嗢(/KJJjf񐚚W_}ŪU+YrVHFF.K/==ľ}{r>p`[+BZZ*Htfkͻ[&jjj {թS!+zT>VkVft?g`lݺI#w,YMQ[lѧzWoMݻwDϜ9b&44Ça;P:uzj4MeڴiL?\ M֭x"+D޼y9yW^elذQ+99/^4>fΜ[իڵlݺU&11իW7ߒ‰зo}: eժUiӆ3g΢( yw J*ѭ[7bccsLo߾.\EUU~ۇ柑᫯v˻uh$!)<:'OhѢs Dp>Μ9CRRcǎL|7]^B!c2EB!}FB!I 'B!B!@N!BH 'B!B!@N!BH 'B!B!@N!B dU HMMCQNMMUɗ/?eʔvK ?#Xd1ժUnI!?3g60v8FT@NEGG~G̙3DqBdbӦMH&L(ߚ3,[nݺt:yIMM%..N*DH ''@tt4?Torp8Hȉ7׃"o7!fEB9!B!NMӂX,U?:l2?j?elߛ͖?%l[yMsd2Kȉʼn'8qUUi޼w sx1Aϝ;Ct~z._СC~w`ul!<͟r3o߾&-[rѣGlGDta <Ay_QU֭['n{"=2̈́ 4(r89 ,c-٬xnZf9ǛvBB'OFc25M[yOEQ0A-lf?55_~9dp¼,}{)t`0hBv]-#PZmA`fNuj24M?Ο|)N34-܁2f-wNyڵeUU@m IDATUO'kY)i/ҷz}~f.V FcйvټyNβ'Urcҥ:Z93gNu9Mt9q  ]fZ\Y˜r*W䗴ȉ:/ЧOot͛QĉeK0UU4h gΜaddd+0`@8{,Æeȑ#HJJʖڵ  &&H˖-8q7o.f޽{q%¥K4h pu8{,VZgۉE +WBӦMZx<&ODDݬ7PEQ8z=zt_9sEQhӦ59̟?s2d0:g֬Yz#^w^OA|>f̘N3gN7#G󏎎FQڷСCt16{nL0A6o&+W0p:uʕ+Q/ꫯСC{~m}ѣٳ}z9s&:wz<ڵkk.L8^z1ӧ7.\ѥKgӳg;=H9[sN 8{V=[ڶm_бc$3fLӚ1c;FMǎقȈϊlR:ur QUt݈9O?H^=;v =zt N']t[~8:udi|>TUeytОSf>}z Ν_ef;Fb0X`2eBi02e2]tf}=}4;wb„(^/v2bL< MӘ?L`w^6lКA||<ÆFdds*K,&"'N`Zi۶ Gf„Aר qF#3fLgذי5k6qqqfVXs=`0k.}̙Pxqk8q" 6d͚5z+ݕ+Wy3իЪUk>bb"k֬aڴiѓ{Mp8ݻŋܹ(\0ӧAbԩSݻfBnfϞÇZz ,Hrr27o!--+WR`A,Yʍ7ضmkPXb7;3<Ә;wGd2w ̲e+8##A+papnZ*VkxB|'A\..]JTTC eΜYFl6;11XlyEn˗1q$oIe^^z3r,Y8fqʔ)k(Y6mv~Y6o~DDD2fh Jxx;ϟ/d,YVCUU.](|:u)*j}CQE[8m6,Xsz-EuA`:w6 ÇYl9`߾AlvJn}1={:甠hXXd)V~ŋp%N'[n%_^ k֬ *cDD$7nDQnǢU|ɓ>XCjXzWf͙cxn#G{Ҳe K<-z !oz*Ud}ٳM6Ҳe -ZΝ_yժU9p`? BBnvEZسg7L˖-X`v[؋+BHH(.^ǎpݴkמ( nP2228tkrh>RJ>($%%7o^222عs'kצiӦXŋ/_~V+clق9sfw\s&##C~I L%l6dtfФIS4x{0k,,?J]`vMx<$''FV~/ѫW_0p\F\.V||kb)R(W^%,,9emiUׯCqڵ~l}>ƽř3ٷoǿĉn]ೢ(9) 6qޢDF ^CϨQirѣG>m61cޤTRiii5j4x<5@JXXXav;ׯ_@EFF%Kd۶mTVB#x^l6W`ɒ8pUVj1lpʕ+KO+F o(X qqq,Z%Jgn*U򷾽hJ.KܹX,8N^~#GaF%uᩧ݌8qDTݸq޽{Ѿ})SL5kx ގiӦ-XCqܲe3 6c:D0ct5kG%#eؽ{֭[͛7)]x+V,ƍzKhٲ%ǿE˖8~gf z ᆪt^ؽ{1kj޼y 6njorY_n&( kRre6mfZ?:whXbΝj׮MUp"nJ(vv(W}x\rɓbTP瞫, y^WO̙4n"%K[=DG_z->~ߪDvѣGU6ʕGQnܸA RթSWfDŊGIIIT^HJHOOҥ˴n݆xch/A&Mnw]OoHg=TZw$_7|Czz:Ç}l*UO4_nƪUo~ Hv²eB+C:u=c7)Z!ʔ)xᅆTZ&k$mVN&ocA&MH>})XTۙ;w.qkԨƍ{~/-x'<fƌH'A(U nl6f6|@d'4#gӺ}V^Ȼ`6[Hk`eZc>e+_۶mӶ꼛Ng={ (z޽{ݱ+A]IMMA܊+~s~rƍYVZni4Yr*rzT^uJ*ōv;x2_}4-ZAXNSHʢEuv;;҇. L3uGNYK}<>`N_4կZ9Nchi@{;9M"ve0M}(O`@|)L&WxWyiݴ]wSwwSfɜm꨻-s֩2fUjt\6-jŊ펠X/ݮ&I/ž=s$p-:u/( ;w lBH '~#+_}~еk6mڔ/tĉ._kƈӯ__N:Ņ IƄШQC=4?֒%QUX!ׯ/7o߿>W6Q###X`>СChܹs2Mѧ:~xPߚ7o~c=tkwƔ)S0Rqy^ye(:wRҳgw^}zElll|WXFǎח}ٔ)Sҥ3ѷZѝ(zEbb"Fo>\|ID]0lkՓ+Wx֭kϋ-SH^}_ӧwPuԑo掭wޙy.[;uywֿz*ÇCL6-NlR:wСz*'O_كcǎУGw>Ldd{?^/پ}_N񐑑A׮]'Nйs'}ʮx)))ח'֭+o~gtɓz ПhtĉGNߣGw9( 4сm[ch֬iе韶+#G2iDÍ7PU9sف_~6mZ]4ONn]_ѣ;M`4Yn-]vf;wN~A !=@ll,&;f͚ԩS;$%%Vک2e֭HNNFQ ܼyyk׮ֆRJi]ׯ1ٍlnc06m Cs'/_FѢEXx /_/@Q dd3x˼ys)^8III|ɧꭀ+V xR^g}X&ШQc}[ofqL,--9srL&wfР,]\՟F fС,X0_Ea֭[q:XI&Ӽys;woݽ{`ӏU0-&Zf޼قb+޽>ֺuȝ;7 ,? jq0`3^{VZM( V7xv̞=;[YNsz9v+VGٳٳ:uӧOgΜ[rWܶ.># K.uz ׯvknZ>O+К~h4Ssן={vc0xw0a"3g… pѿ}*Oٺ?ۆ iӦ vӉje%z}7]Yfcǎ̟ .p~/^µkHOOjG?cQUYf1x`&t}c~`UYT6mڜ- aРA :EQرKYh {rvxט?> .&99-[>A4 _k U!0nܸOKeZy衇8y$iɓGd60@JJʭY<4iִHAx&Mb4q:A7k2o5?O< j;|0mۆviݺ e͓'aaa/^D&85:.ڶ С`B ߚ~+}K/кukϸqcGR@ ,,"E5[6md׮\.BCCFnݺҵkנ7ZluǡsߧOթSgۧO9tҢEK;[o^/ d!=z ( F={йs'\.^-ZVjժPy<f3^]v^JKK˖uʮiݺ NUUiѢ%(;v1U222(\!!!rY椦s .Oxx;n7]t͛RJHZZ~\.}Q^yeq+Gƍ3g.yeΜA}q}kn޼(\Oݬ%F4@#k׮c5\JPMls.ddd 07`ʬYլY!3f3)4i"FJdd$.K7YN;N-ZHj9rdP@x xy2ܹl?'NHϞHMMeĉ~V@n~])^BXX;?ctN'mۆ曓Ss[ok|9P֜r(\0_|5kgbZ]bڹSfsn]nMoӯ_2Uߎfƍ Y*wCɫ!Z@Ne+CJOx"~iSY|9W֢'[rIdi[lmgrGboȵhђɓ'{n,YBӦM҆ Xd %J["l٬Ҳe+&OĞ=Yp!/o=$$D/_ƺuki.7vѨBlܸ˗QP!@cCK IDAT?T8#G/>g7Xn-͇~|כȑos755]_vWرiڷoϔ)ٻw/˗/C1EQp:ݻyǥM֜>>-[=IH, YYTZBvvUVs6sTm[)))x{{ӤIΞ=Kٲey׈bE=Y%$^zc2l)]4e˖Kԯ߀^=VkժgϞ6J%"b!͛/*gϞT\xg}6&VԬY^(^ܓׯӥK233STPxzEZmjԨIzj,B+5mW5ktx/--M~mr*T ! aayVZiРgΜ|rkBoqErss߿?J `֬jʡ/<==\_~Z>v177Wz-Μ9 /xJNZmjղYs9sRJbUǑI߾h4ܾF͚5 * IB;i+HDZZ5jԤnz={5j~5jT*ޯC*-U|]W.uj9t | ?ZZXVZlI||ٓXSkjU*A휮{kHtf˕+Oړ* R>, TGnU\nZСCy&}4֟СCivۮ9$I}ve?R$.]:ل?+- aX,֬YaX,usL z.lٲt@PzWtY ,1d`n߾PR*rB?Z@ @P(hs>roȐFsi4oS=Uա:5 ={IzV^MTzżyx-x܃lC~[/ OaecU0嗻wBSH,Z^`Vpwk֬#Wr n+XQQ3 ( 6m\.n,xV˷~P_QYZ|**TDP<.`=dCUW6m8qSt,'']jɺup=Z(uĬc„ԩSGN@\?=qQak휛+6l B C`߾}m=E0d PTv`/ d͚J[QX1:thϹs bodj|};p9XdQ:vݺ3x \RϗXzɐ!z*Æ} 8\};^$f5`@ْ*!!͛BcG?.]Dpp b!:zA Пׯ\X&M$$$ÇɃ̙@TTT>+o޽ӧOw$Ѧt dѢHvfdd`0С, ~{={6]pl "''={0iݎ.~:~rh.]:Х?{w^o'$$}y|49c45n5ct/^gՓӧO;T*XH``WfϞdյV]5jԔsSm۶A1޽qqqh߾$[={ԩSNYL6>}ʟŋ f`Xttϼys-Ǐ~%Ø6mC=6 i?\\\xD8@9_!99]vIeظq#iii)Su{_WWJwvm? g2$66VGX,[nqAY.gЭ[wϷ WWWnYfSbEfLl6c.޽ˎrHܻw5kV`B>S̙Ve) 0E{7Iฺ{Ytw:V^C׮]Yl$fjy,XȚ5h4WL:Jdj*L*D G0I#22~!ZJvv6[lEoae:#*͛?f[˱cǎBVH3aÆӾ}ɑ0>f( 6l؀V%:z999o^ϔ)ׯ?f 6:D۲e %K:؂ .r&MO|@֭6ll'i$%KQD n*_XV;*'.x/׻wd$((#Gbe^zph>q>l2Lt:, |s?3<狼^fwbEVV|v++[فZeOqq9YpPP0ǎE$F#UT CF<(o' X*(Z+LFJ%)))$%%a0شi#AAA+ٳaVTKj"OOOݝd<<}]v,^۷tҩ?gXONǎ6nlٲV]V]O/DMhZʹi3ʦ| |Hf37#{J^z\v)jԸq<_:jժNVZT*Mld22{rss>|nܺuO?4b0zh^ƇVC:upww]v\v#GUQ(4l؈VZر1xyy@||͛୷lMbF8=6mذE7}>bG$''#I 6-6ll楗^"$$x^{5{}ARIݺuAu=y /Qnnn7J !!!޽sϡP(W\#HMMu*UDxx7^ƨQW$QVRݻOFh׮=SNݻ+4l-Z)թӦMW}a6Q*iӶPqTvmyWz5j4W^%44ʕj2idܸs'ٳP(xٳ' QF J,IFqqqAPШQc{\Ν;y뭷Kunذ!VBF0]b0~D3>sWN8g6X"ʕjCժհZL25 /H:uUgΜqKjb6iԨN,>ƶm3fL'((e˖RuC g]Bٲey-h۶ z $:zYlfȏӧ7ϟ3gŋX,\` bNǝ>}^zҷo_._vk{9z%ի'69X[7g&(f_TĬ%44~/gZdrub o>6ǑCիx"Ǐ";v,/^… ۇ^z_CZZwqƢT*Yp!AA̛7Wa U6((ѣ?G\r˗/3`@?z!y)J/^LPP 3gw.9w,}OG;E5 tލ{T*ȬY3 ѣ;?`޽0u-۷52ydyB`sJ1RX<B ʍ a@HZq:6A̝;Z-[٣3V/hZ._Jl6FAQ5kbh۶-W+ FkחNv?:V ӦMaРDDDFaݺ4hڵ1:'Gi_@׮y]"-X*S l߾,֬YMɒ%^Nb-3? 8h"j57n˫k׮xqh4VZMPPX,]Mff&v픷F,Ys4i"svg...dggyrssY~..٘mwj=R%Ilh cĈZ۰adeem6, Wѳg/"""``%޽VL&˗O>DD,l+X,QT\[$d<=KNIIT\y2eTf Խa*[+ѣ۬BBB=RۇDDDȢȑ6\ݻnUn="##X`:2w6nH͚5ٳ\yf~jժ%=WH6 IDATt$Iڵk/^XC0VdbՍ듙B@Pлw/BCmL&<_ˊ+8p H2e,Qj%+++bѣN$ѷoɖ׵kшFjҮ];8qÇhre.{fꫯeɒ:tP9!&+`0šCٳ~jMn61N_f???-[Jvv6+Vd͚դ!]ƺu1kɦ`kw(ljo;ҪUk<… Xx EfRik,jEFFl%I y^Pjj %J )))/j*[!5j:.F333Yl);wã8mzcXh4:]nJJJ KyWiNjAњLٲee '̢EѶm&LY]#)) *jeXXHXX8m~qGM6Yf:t+VS!ֵ!33%JpO7^2C&bҥs!I$''Bbb"JKKC70aD*W, . XQQE{6kӾFuuۯu~""^o/w|Hj֬絫B &, >c|JժUʢL2\z;wpUTBNN6*UbhQiAʗ/O||<ժU *8DIcժUTPa(8Hڵ7hps=DzeHZBѣREʔ)#{@vؑ??>|s Pvoܸ1TZQp>J/wҼud)iӖkW3b993۷AJJ*ʕ#77F?ڷÇ]6ZvܽADD$nIfXz#GjFV\+(wkѢ%6m*w`5 2SѣGٷk?';;%J8ߩ?ӦM׏ 8H>{21*YpM>;W{333Oe?kLܩZB)Jrrrv3{Xӱl2J*ڵk,TP'LbXx1ʕc͚̟L0N9uG<<>uuСCiӆJKj՚g`40`_NER)]4.mxem~aaܾ}5jpAj׮ l6%K\r? / O$$\K.df *Vȅ ӧyS%JxOΝˋƍp nܸAPPpʕ+Gj)_<%J䥗jQ|yZnͅ ̤T*j׮$IL>֭[;]R%ڵC$/γ>ٳgxwQVgyK.ҭ[w򓔔$H:^jՊ˗/qnCQ^x3g~jժ{XVZlŋ(Y$ԬY!zoJ5̙hٲuqg-[$..t^'55իRׯڔ/RxTVk׮Qv{/56UVӓի믿Rreڴi#GӰaC\]]ŗ#;;[Xt=<Ȣ+==/GIpO.=I&?)I}o4w~h,;wI ?EIFXt ~a#ȪVtڞJ- bH<==СjZ9-ßA$yJz셣$IjF#jRرSq={@ x~`.<=?CBBc|J||<9'''CIII@ B #ݑ=eg'`MOYpWQre>lf*@nT=Qa9ಧw_WvT($)@ B  Ν;ɓp<&M… ;>dk!;Fɓ'ڵk6 Lrr`8?axzca\ra>dA 8;wIlXd1;9X̐!,C.\@HH0|2l6$CĬ %%m3>3[/@ <7B)焳V(1lpˆX(G ˗/RJ,]/pA`͚YBڵkx*,X{r4oʔ)4o&V$#.0c 5j<&B2d(˖Es1Z-)))ܸqkcÇǨT*ڶ={`X8x _~B֭[ܹ ڵ1 zkqs@9F͚/Ju_}I:ud2w}Gɒ%y3g6m~~1tTO➀YYYܽAr0\Y B]8v'N@=uwwh42rHnܸA-cV+͚5cޯsE O:Eƍ9z(e:tP@ BNc͟[d2P(0Ly drWW7n޼ILZ^yezآhnܾm*&1/iR(L4ZEhh(YYXVt:f߿W$pB\]]X,=kFݺu9q8+W&;;;+uֳnzL*n @ !'x$$$pq>|ƍڵe˖QFM9bm6m*ZwyW~μys1LXV:te)Ʋym'IIIܹUVjfX0L|h4RRRHKM``n%I9Հ@v6y7YbX,5ж":R @ _B mۖWЯ_^|*+Wj30h L& 6d2ѬY3}ݼE #(]˗u$$$0rHʖ-RaÆiӖׯ1|pWZzW. 5V5k&wܡJ*N, <{2Paayޔ:C.^@ڵvN /ШQ#rq&*U ӱcGΟoUVh4Z^z^z )J(߉zyEWa\|۷ӯ_nb„r5N:Ÿq0LB 'EIFXt TRwrݻ7/^D2~x!@T#b Ju- իנzU O=""'@J @ BNcZyg9 @ĠRw^V~r'D``K,FP>@_q'O~bxCO.@PNׯ/ڵXbؓ  ʉ'r%iӦ!'x1oW{~@DPФI># J ikDz  !O͏@ B @ @ !'@ @ @9@ r@ B @ @ !'@ BN @9@ r@ !@ @ !'@ BN @ @ r;F#_Ra0Z}̟ATYZ}@ !qssC o[h:ONcGTwLLS=$qyMؖG͞={@ !G yV_#o:Np,t:z~WT*z=zIFhԨ!ojQ#g:Vk(a3 r ^ZTB@ N˫+9J={P;z˗RZulٲIHNNJu|NEEEP(غu+-Z)o\^ۋ,6oތN:/΁,^#** J^GP*̛7^:M_[tzڵkq= @&ׯ_ yY:thFKHHcG?F͛0`@N8O?LR%Z 8`<<ѹ?&,S]=zb曃d._| UV%11 .0xv͇\;Itss1Gv.sU.]PT* iD"O%'[O7L4k֌z);'N`Ȑ!f1o|hР'O?y$>999?=; јKFF}r}d2O9r$& . гr #Fh4:?jh-G- ǎAtשNh@ ΃SڎRjj ZEVVV!ؾ}+oZ|@ԠRQ*%6}IIJ*4"\!лwoONR%ڵbu_~ɒ%K5Zl l^u %ڵJ[eԨOw d֬ZQF9~Q/]?Q~}j5>`ر]k׮_Qvm>-j[jQtQ @$c2 =hxxd9sWWW$I7dtZ/_^F( $ I,NRR5k@ѭ[7T*5[nE2h \\ i?$IBVQ'gΜ|r:^yF# {RTI =۶mE$RSSU%z!!!F̹8 z!@\\FmڑrxKΝسg7;wEJ*pm޽?/NV?pq`߾}GV&o3L̟UVQKŋ֫t$&&ʼnr)d*@'\CFR1tbbbZڵ+ZVsDVZd*in'VBAVw%TF~[m۾' ղ$XW_?x@ !d`Ziڴ)ԫW_JZZjj@۶miԨζ믿fŊ1iJaK, $$$o|$Ib<ޖÄ ۷!Q$IU| ZlV{9r  I&L] !UF˖ܹ3{Cr3t:zY4mڔc/W_}Evv6M6LJ2eмWӦM[.͛7'44}!Iga„+WG*U7|S^!ZT)999|4m*YYY|W.]ŋL qqqܹsS1yvòe_\p@ <]@ e]""'@E9@ !@ B @ @ BN @ @ @9@ !@ B @ T $E||<999H@CTQgy,:D GPxZ:)?l븹'P(ă'r'K.|y4 .DPbXD,Z;1cbZEXORɊ HB <999t؉۷o&:D  Ξ= @Đ٢3B lfG"8$IUB @ ͪTa4_"]ضGr?ZPκL}TE6%! IaaHD@@׿T#88DϞ=0L?t4r={"((P~\ʐ!ٳYYY$$$`4ׯ/E}rss0?ۇw5vܦA,h!͡P9su IDATuJ(l]JJ OJBPPTrrL>á"h$9Vk_r)T*˗gΜ8^-o/~Rɜ9u|:}i;$pW*`Zٶ yA0 r]Gh {(9ov¢^뷣jIHH`5_J`pjô 'NۭP(Ey7Xb%`0׺pf]d)Ŋ瘿mZd1ITRu@ BN$x HϞٷo$a69s&!lذaRT|._ѣȑ3t222cĈXV" eܸN$IԩSكa>$11RIǎ$$$Šh4ߏ7n0bpnܸ$^zCu N9rV^ Poݺ۷CcX={6!!YRܹӛsͼ?G%,FIy&}4`,Y"k;v #..:,\`/_^@}A\vMڸqݻ3bpRRR5hЀ'g͚5|I4h@hh&,z!G\I{n̛7aҿV޽{[]zݻ1w\V+=zt'==L>d$aa\iRdʕ3oXNG.ZDϞ=8uGz= 4Crr2JիW̜9X,zvŒѣ;qq@ QT*2e2C|Ȝ9_juYgã?ٚN\DQRiH%z'tLP!BAzI@ P"Ho^͎lz|'nf9sf;~HIYKAA>bVˢEi۶- ??3goazr9VVVܾ}[nnzL/**")) >Ã(T*vvvݻ$~'9+h޼9QQ2,)))!!!y%޽{,[KVV+&MR\\Ν(++cӦXWr!:VTb 2m4""Q*r -[Nnn.OR*N:)C,++ϏQ,[/ӲeK֭[ڬ4  %00(v7$'118Z-+V`֬|Ǣahz͟?=Y2wPIMឬ^4:Ə@l2cmϞ8:6bݺ4hP{,w+++tbҤI,[ޱceeZBvYe|%)i,Y{hO`T3 FՍŋcd,]'btl9:?߂鴃XYYnzZl_|Rѣ$&Ǘ;wyY[[SYY^g׮|GkNr9!11IU}Fc`͛7HJZJb888ܹHJJ"//GGG.]FÆ :ĵk? +++6mϒ%K$IU4g޼99%~Z'sIN^3999dggWVV^GGG@[n988P\\œ9s(-->0ɧf͚cggG]GTVypFjqssl̲21}ii)7^ztؑB1jFcb,\Nvv6Ç{jFf鱖#>>ތFAT";T*jU0{uذ#Z ѣlݺ嗇MjQ~ZlIee8QRRbfX[[@VwۗE>zNۇ˗/aT(4iҔ͟_uav܁`v͡COvygiڴټchر#$&&sQ<==h4#v꫔e5mڌf͚jL_5xe0۶mСtz=5bʔ\rgg:[׋ƍ4nܘ  h4`ussJeId_WW7QT&?WuVza2 ++ ahZ@&TҥK_>@YYI]fȐhZ<=ʪJIѣֶ$IAKRRRh OVVVhZt?!--(SB@=\Du,Yٳg8p`?ΝdPjqdcggG~~ի\Ng 6ʊ|ׯÇ?^ۓ^GRQPP`VL&#<~,,, g59p:ts2wZ =Q鰱XZZm9xO+믿_~Irr23fbkk[ BӦMk\ 2/T5Ґgß$IGNүVӦMw7n…,_s=NcIݻzVK~ڏӧsiӦ{nA7o<IYY9 g^~}߿Gzz7oEU'111|$&&ү_Sru1}zx!ii4o\\ȧ#GD&ѿ""o?13Fغu+͵IەX[[W_T*9Seeee4jK6h`MǙ?.֭a@XPYYɾ}{پ=1}mQj5 k.va6.,l6lAV?L&[rRSS31𜝝MxxIIf}Dr/51h͎_,prDddgΜ!::f:ŅHΜ9c2Q1KKKvŞ=5ٙٳg…,_BՅ;wP(iٲ.}Wk/ K,Wm{f%f}43Ӱo#\Wbrlmmjf}_.o_Z&55}QVV ,ܯ_?V7 $IC 腨Tj?΋/ FChLJJۋ̘1rz{Gnn..oI~͚5m۶;wKKKZn-z-ZnS ĉ\:v dFT*"""q̘^G3yd+ƎK hذ!:u̙Өjڶm'y&̜9 VKnt@nϙ pzcqYr9*}ԩqܺuh4 @>u4߿ZӓdzQ*@@3w}z&N@DDIW.˓@IMFdd1s8˗5># MJ'EҟNqqqIs$9dW_6<\_FS‚腒g $I%rXP4.x=zQTTTu$=jsLG_ӧ~ڶmK۶mŅ'znFSI``k\Q[,:k`<$I9r0]rl5Ria 9O޽{L<=x1cFK'wrQ|9qĆܶmc^QT*6lك c&ß e˖9 ??_߿ϤIt1H$r~ _2n35pt6TYJ3U.|!fee%e^}F|yc4^zqhٞk?Ԇت適ֹ5T*RSW3|]uݲe+ mP=MMד‚#G 7 LIAڦDCmG>=W[U?&c`r=¯pM;6669su6fM 7>3I$CN_ވ;}4#G`ĉ<|RɠAN=zOlݺ\εkט0a<ٴix6bƎ۷Mt,s87oFT"ꫯ%117n0q*:r9nb„L81cFz=6˓ljNGTT$#Gs]߿τ 1cFSVVƬY7n, iR’ӧq=QG'((H_[oqz}U d2GW^fQO4ވN:tqjѣb5mڌ&M>)--}$y^_V%((XOQ^=PPOiiIDDwM:WȇFC ӧ&MF'aVsvvA֊K#txyys1#֭ۘ\])**$**Rl/BAII ZAP(رdggºuk9v:VZ3ud.]77wiQ 9Xiӆg  "m|MP(?ݻwgСL:fJ':HcH'm\΂ypss#44Tߘŭ}/`cc#b8d[GcDVPRp"Ο?OZrrrND{)J.j4hА&I֭aMF\\.'**oHZZǎcT*{V5_d2lm7ߢ .P~}h>NsoqEEEC뙔UT(5 ??;;:Q`5^]d&m([\\L&M޾ֲ#dܽ{W  Z֖#GbiiI~a{.GaukNZzǴiSeƌiܹ r=غuX^EE8w*++%++ZDLA00E DxxOfݺRIQQ!gϞT% : %>,VVVuV8P+K?:ٳgYt dܹsr9YrrΝ;+4 nThY@߼yi8?r)7[|N޽=VY=zwK.ٳDEEjXXX.\?Q..,ZgL/޽JEg͌#oȑ#,^?e0{d27-[dӦdffӎٳDD"yGGGRSqJKKkkkn޼A^^>DЬYS>|34b?N'**,bc%n\v KK+7o䐓$r$4x> k(nTVV2u4~'ʉ'ߠKK+O׮]jVO] IDAT}ի7.JH]FNiӦ-:6ol:u7nܠ^=; BNqjRմi$] ݻ7_}u'B{裾|L66mڢRh۶i۶mq_mk׮h3֖W_1~ڵk-ƍ'77f͚B **s5bbjҥ8(k4W_|~?mڴA.Ӿ}Z)S>ˋL39u&LCT*&Oɓ'hР>Ç.DFF_?HLbz=;wӃ֢E v튅tڕ-[̕ 3/VU[p`c}ܹPe$7IIS>| 3gΠVϦ]kP W4v{4? LСCK=*ҭY]Ç{삉_#//8|Vzyy>u1)Z QucŖb99 |cBJ$I 9Iܹͅ 1T5e ieee򩉋 !UT>9>ɈnD$ՆUOSm,_Ndq1T] 7aF&LH/|mmW} FcaaQ5ՑW5ۮkCpm۶Ngķx Oj+9Qhu U=6ldu[M-YXlOc@ޚU>IرSĒx,5B`ŊuzשmKKKٳgX=ذguX\=j$I!'/)JŔ)S7o.2L<R~}>lmmd8q/ᤦ<=矛A 11/͛KEE92/0  믿#F7ׯj{d<=qF wa qAe0OL< =fh&O!=:)S ۊ ۂy!!!2e ܹs !!ww7aTII ܿ1BlKV֢P((..fԨ`>cԨ`]fr>>Oa^ \h\ `Q//O~rr +=5 ~~ <_]r/VX.=ׯ_'$dFz޽̎}27omll,rd<=W u jgDtGnn>,[chaH-ke\.gڵxz'66V܀P :xFyfX#xh쥤9˗!X qg֬,["ՍիW%*O>ww7̙CxxJM69ŋmvuFa ?[[[\.ݻ7|d2&OĩS"88H8pbYرc„qUVZMٳg7rԭxyyԩ#))k)++%##cՔikk_")iUd`+W#Q(̘;ʕ+Eq3ӧqq+`8̙믿C9sf3kl*++a[P($&&q5l݇H&k,8q"N5lݺM4(222O( vEƍHIY5 $$ ƅNQ|M(4h0`ϞرS4kdզ?D||%%%ٳAdy̟?Ziժ$V{~ 6pG !!͛7!ʒu4iҘ]vUb8qnnn2"9s,qq+|2Z/`Z-j5*- hZ>\.#%e--Ze,^z|ɧ >3ꊛK.ݞ̙XbpMm*LppXg ܹsD[)P^=GRر=-[Jbbر*t$^̞=cŇcY”)SHHt888H7nI!'IRu)//'??֭[cccw߿oba%%UHu899R,G.úukquu!77,nvee%hbĉT* Dqq1/TVVruڶmGVV7nՅ,34h Mvvv(*^~epor+Je ׯ;vu^L&c޽曢1 ToiMIVVV4k֌VZSQQ! ^,2tPZx>Kv܁ СCmێWP(͡oߏMիUQz/00VG( @cM.>#֮]˪UŹlǏgPt: ~C8p.رCIJ4iBfUtssuyD:>|ÇBj6JVKÆ yW4hЀ^jtؑ `ggF!33CDIbooOee% PVVfa\ȑ#MƇ PQQ#GՅM6rJ%4k֌={2} LR;@F@NN(--q&hZ 9}~uN$IKVJz")V]N4l$&& mllXd)gΜf߾}={O>f͚1w<dT*5Zz!ׯ_Fee%Jlll:^HFVDd"\n{2F7{muw'N2klFV {(2+/89Q*O9NVd24h EEEƛ|HzL~͍>x3fT#XZZr7t "#h֬8ϯXÓZ חF_`lR,+Κy=OgDd岪T#ypnBy|@˧Iq-daa3رSq)2N> /`a˫!rctڵHUIGN|ܹCMvGa &-T*%j}2|L>'NH>^h4ZJ͚5Aܾ};vo^<br<=~U{rypvv'o޼Ç2А`ɒ%?(WCĖٳߟ rq̜JNN FbrqA/M2 u bΜxqiRصk'EӡV[п,Y̹s爈P(2[;pK9{L<ĺ6r|]FTT: Nl2X޷^DдiSmBzz&~8qW^l$I 9IE<쳜={Fu2yNcԨVuUK.hZzE>}̠WywMkԨdffҮ];J"""ׯsM.\DEE;w1H۴i?R |dff|J :w"?'O2v8ڴiJQ_*6mڠTֽomEի?aeem1[n^Onn.;v|AXXSИ1^(u*ꪕW SN:_cK.If]9ICgȑΎڧP(h۶2zΟ?ܹ80x$^?r9'N2{E$. D7M^<c\ kq!Fy[vyAknt 7q`_`vƲQh:ITGmW*Uu݈>OcǣW$&^2LW^丟hnΒ$!͑d2֬Y x"oAtrVJzBc則P(((`i&߾}I&سOURRB@% Dt۷q^mC__ߝ?6LMMUmaa+cǎslrrrǏG^^ږ_|ʒ$I$IB\9fDLUhjtlllD/S:I&!8Љ:N_d DFFѾ}֯͟SVV&71֧y,˫ T8yܥ%:uRIFX|#WGs6WǃՖGv4^֖UVa3yjۋm۶>|r֯_ŋs&M>+bDY)--jZfΜرc())֭[;AXz>>̝;r㭎~˙[nUQT90v<0`d2}-#?~C&X%psssZ/رc{Z0nܸQ +&$d!!(..ǛR3f4V%Ԗ'k֬Ϯ___BCg3o&z''*bܻw??_6lXϥK&)Lw0ݻ7+W +W xѣCD<ۋ9 Ng6?#>>^L0cd$&&) d+W…իoR˗/`|1/1MDD.\ی; ^*q~~1zU*Æy`|Fr<=VEݻw7n,AAr93STT1@>rDqQz=߿\ݻprVޅL6w}dr6\rQJKKXb9saذDFF^m۶PKKptlKZSVVFll,g˻* [[[^²e?~l@̙3Zb,YBxxSNy[nҒKcIH1`FܕwY}Ra׮4nܘTJҨo!9y NNؾ=C-z'z^DBذs/_Avv6UON^M||"!&f'O&$d4drf5]֤mbD!6v9s&AڵBVݻz#1L04Ȭ .< ML"(۸q˱a-T*"aY.C.ckkKII jwҾ}{>lGaV+xMvXj$I!'/lllĉo%//J=:u &LFNNC EFvqźy!mڴӧyW-G& /PT˼;;;˙;w|'5 27n:uH0>rdZC=̤AЛ iذ!i߾}nrqq'~6RRR"UGc̘1hYYxy926nzj4*ΡCT*T*3c`'j_AB#"(#&W4j55g"Q}%Gf#̸hv]+%PTVVr1|}h4ȑ#ЬY3yY2lp\<˗`ڵf\t:WCBF^%00 9[K~Nو2r3f4#FnnV@ii)i[ksrrpssG:P IDAT+++K; [l/=\O0h`lB\J>>t:<=@TRRRBv_>гgOΝ;ŋѣVVVz&M7bhѢdIjU#$PReXXXRXXPӲeKBCgbqKKK 6lHaap'OSNaT^^X]Xx gΜa}:uhZѨWuF&2j # gAҰIGA TvP,[VTT4/^;v'=QT*рmeyg2IuDʊ|pfq2 dK̘124i‰?iӦ6lhlP~~dZ$"3y8;:qƉKJJhذ!ΝUVjfϞC۶m T*UV`|Lh FeOY*((\.G3\+}~LH( FY~#2"OSVVI$o 9sӨQ#~'v޽{6CDhzΞ=Crr2> ޽{?_WIN^-zsͺwQ{Dns8ZڂfLّÇHMJӦMh4d2bbb8{,qqq 0Q^^͛76664nW^TpssZFXX.g8|pyW>҃6h Nɓ' 5kd!gyw;/…,ZW߿={vamm-rTzyeۯq'---^A[qF=`HŋDD?yez=_cǎlʫZ3gO>aٲ;wŋc?999,[ z饗xI?w`y&RV^ͅ  '''*++iܸ18t(*BI޽{RYYIIN^EVV&nn~ҷW$I2$=J :A}EHh+W~֭[DG/BhuFѻwozIvv6GyUԺs!iܸ1?M4歷B&{:^jNU*ihٲ'00c2hѢ`jā]rA@ ԩS"8x5ޞN:RU(J"#駟b܎;ZǎqTߦP(ebbh֭ &;;3gF߀8~< &Ҷm[ 3bwG^`j&LHvv;δ0"ٳLJgyLFddwޥHAYc03zxL2nݺBF1 J.]pss#'8OˍΝUCjk׾VAc>S/z#())sDE-DPЮ]{fΜEVV&NNL0VCbС#Z7xc ,2Ξ=CDD*Je_Bvv>#ru l/Ye6iР;w_wh4>3gNSz5X,>)B!D!!BC`8B!$B!@NU)BIB!+4+q\4a4̴R cl6ӡC̙o/`0pdggST)ixh& |Bi׮=(B$޽{9q8 B93/n]n"9!_ԭ[={' "$_ʕ_C'A{B!$B! !B B!$B! !B B!$B! !B B!rB! !B B!rB! !B4((={t@_F#jBQB ğ6[h!T^]D233ILL䫯i4BH 'lL&C%99YCRR%L&B!MӰZbUU1Lba߾}ԩ=pss͕ݻw +۷cΜ(BҥpuuߢRTRӯ_?ܝJ(sXV7n{2n6'NW^:u?nLF,AoIIիIDGGB:iF-\2f֭[a0v:gڴ[ne['bXo_>yyyhFիa4yWKf>oAnn.Q\9BBB4 ggg^ymƘ1c￧A~}' B4J(?Ovň#رcdeeɧ^_ J>}: 6DUUnD!ǰEz,Z5 |GX~iӦoy7ػwB xYh[~Ajjj˔*U W׷ns:t``:uҥKCSyyyk+ Ԭƍn:<=K0q$|}}پ}>>%qqqZ@nn.'eV鬒׬YS={n:TUv >~@ʕ`̙B\\gyѣGӻwo¨X"L>]ܽ{w0ԨQӳgO @ `ԩH\\&իIHHf͒o| i,\M8t*B{}&&姟 ]PkPr~șLڿYf &^xQ}vwx3aB>l6{n=kWT!?g0vzdg&''R-~aloBTU~|ƥs>|Tʖ-ٳgy_Q 8t.]Zn ~G^~ezAɒ% H 4w۶5jмys{}F#Kf-@Jvk Vo?W_}+PC> p]غ[QU{c[7Q+qڕ۳g'8BqfȌiX,f~Tҁjf͚Ԭ O=4k?B4j֬I*UiҤ  bɒ,_~|r&ҰaCz- LٲeSժ=bFSo&-͙3%K?o޼?qo3|}ܬYhذ!%JҥK)w}X}̙3.6lX˻6FghԨ1{|QcjxFG!AӬX"^)O⋤"--Mߘ^^LKKӼ ?~J:u8~8uaƍ?~z5kҿZj'Nf͚lذSNѧOl3p@ڴiC&M2e T^s! gyʕ+3tPڶmKÆ >}:?3UTaݺu?Py͛M4i^^^Ӈ/*UԩB`29pajժSo=/J^^CYތ3HII~<._bb"AAA+tԉ'PvmN:ĉ9|]%3gPV-̘1cԨQ3g0ydK?#=fĈL2jժHtt4[ɓ'y4hÆ cT\޽{ڇ ,,\.^jEZB{˯*%9+*#'ΝoBTcǎ /Hc!xxx-!$7Ōʡ x("WrB!B9d0 =g?cي`0~s&`0<}NkÞ֦-eۇ/~ BH 'bԽ{0Wֿ>}:'N|4kWW?ӢEs\\\|֭[N^=kLB ?y$AA̟? q_С}ȝ;w $>>?|ԨZw9|};;>88Pߌ׮]bM&:)h߾](  !3c?̵kW)m02w_b!>~ 6-e`p`{}ubX,lڴ }ߝ!-d21m4'ǍVrz9ɱ*8]imm:uj Fחع( k0ͅN;i;!a=˝ZqƐ!9BVV͚5ۥ\n}XbaŊN\(^6~"wWsev,iV ׯ3%@N<"V{PU;\AG\ZF##F ȑQUKk.5%JвW\! ۷sX.H˖>`0hժ%jՒ$ٶ ~XvMkW߿ݻ{n ;[Ktҙ] eEvS`Z6m~~Xv FVZB899޼yLHIIo>DDӧOo^j7MӘ1c~~Xzi7' >-0Lt܉~8J@? /x"̝(*M֭+~~ݰX,u ))>}zӻmСC `ӦMw=n1o\V+sfĈ,X0?n,X0_a` 1]vaΜل ++hzɍ70 nJo=ÈLPP f AAb41[({:vhu´inW͛7Đ!ucg0.^X-B9|0AAL6MPUիW׍3g&''3pBCC_x1$&+4\\\hݺpM Ohhϟ/Q!$$MӨW8pM8z(juϝ;`&Of֭>,X_~g." IDATꊦi{."#Xv NbIl߾'''_,; ;Ⱥu8v&M&!!GG' "*j|.X,֬c% <%Kɓ;v!!=.( V4fΜő#G=]Gzz:qq,|gg'LVmҥ+e(š5keY?~={v?;>jĔ)lذʔ)1̛7<1b$c 6n܀l>]r999\}IIIl߾PM%J` _i3C?O2fhEaĈi38y=r~~~?~ٳgѾ}4M#>>+W0kl\–-vڝmh7oΠAiӦ-5bI{Ea„pBBBhܸ]8f̘ɱc?s UQ7o|!s'1%Y֭[6m::td6;v ~~~ןzhʹsf߾$3wh`W MPwSưg|||٭jeذ8uOݻ l6LBB}gK,IP'o@ϊ~HXV*WBJv? tBnn˓1raԘ۟u(͛7GUUxj-[77B@ZL&4Mcǎ|7tО͛7ј_eРcZaϞ=tLHHhv/x, 222x)SRJK?nBΤD'77w@R Yȍ1A-6l~ҽ|<<}: 6,vT3g><--qFʖ-kwX&MٳgQFhhlr'͛3~8={peeey&G0hBvԎ&<6nW_}Evm'OܹsDDDo˺uqf=Z\rEom|ڵk(?իWSz5 ҥ.M6%"bDDDФI}_={əvuקR *QbEnL&;;d`}vdeeS\yx , |E1[rmЌѣGqf͚ID, JbŊ:th֬SDsYL.2j0pgf3KfM۷ gB9G4ʕ+c6y饗g'(ba!:tHfؿ?k֬W^F|||X~+W}٩l63p@~`-݇ɥKu+GbZW_}?lڴ.]SOƛoI|fBBB`0ӰlϰaU :tfիG5b˖x>s*TPhz{{7WPEQP"2aBgϞo!*jbRJv(tÆ c n݆՟ @Ngټy{"8pH4M_deeqFzihƊ+zWXI]n]7oΆ iIݺu4 lo~vʶ=>^4&L3g|=X>}~;+ k~|2gҥ f3C~HOO#44 ر|2NHddT X a^^~i|}}ٸqѣǐ|ݻw3z\2ٴi#~/kn:V+F;vȨ?I!j/HJʭJǃdYOXdݍu&MOIc=ʒ% p tL߾HOOgѢOKq]Q%dƌ$=rB'彤!vȑ laǎ8::2aBt+I 'n3%zX~~~UUby;/J!hp{M5Qv!?̙3?O~/_fذOBUi۶vjeܸ1ұc~+:{^222I4l;Fth-tI<ȉG2>Tԯٗ:*VQwI*~J_\mvs\PUuYV`[WnOfggΌ3Yr5YYYvsr^sssc֬,KV϶>z@[;Kǝ% f,1vX wfaC~yzt0Wdb3th޼i.XΝm=W\A``F$''tKu%44rƲe999ѝɓ'ѫWO\ϯ˗/p2bpڵ^nٱdXj%.\ 00@Fȑ#֭+s`v ];Yxq_~AA|gzǶLlmӭ[Wu늢(,[ ?n̛7@wǎѻw/ ȑ#(¼y |QB9$''3o\& 6m`4^z%KpnLf޼̟?O OL֭[qqqa͚ոɂ prr"==)N@?G*,e+˙>}GZ&##ɓ'3l0||Z2v={;wai޽ '''2%?Az(ǿ]A/̬Y3IOO租~&6v.>111ӬY3-ZČӧ]%%{jJǎ۷ON)Y>))) ŋ:5F_Fff&e˖ӳuֽgmHy(#zжd6mL^^ora^z4,;Ȅb+;v$$K.3J旖F޽ZIڵkώ;رc;gرj2\2U>ڽ{AAX,HHH`0`2`N lݻ ֭fY@",'iiiLC1xkuFNFF:ݺ?tA~})U4ݺ}6KǎIHm oXv ˖- }};ݮࡢivLXX{VBJZw>Y,䔭9EDtd||ZRγ2Z\&L\]]K]PbE>|^RY_zٸn+֢4~8=z^>˶Ɛz̞=[2INNOFM0ddd0q$=J|&vI`` _u|G[@~)Sq{?jE[3Ume-SU(;F|fmF>}k0vODGGOՕz'Nѕ+W(Y$ϟg-v8y$񛉏gС;믿AÆ j&DPlY%TUe4oޜZ(QBBHFN1x Ο?ϩS?~\f(S 3fl6 YV7nBTT$O*U/^dǎ޽N| ʪPk׮a׮8UU4igΜaYzЖ5z*_} E^^iiiTZ'N@JJ $''o^7We M&ʕ'""R/8b ڶm+† xѴi3 lj΢sNL&ʔ)OуʰP^~j֬˗|2͚:#;7/sU>/d֭xxx`X䓦?sEƍ1vDUʖ-j%۶'55ʕ+tMHNNj2ctFCl윻vӱc~Ve}vK\t[g8vӦM%&fJf1w&"##8w\eB9rqq>gMԫeʔۛSnT䭟d+Ub;/ ggg?>o~8JDD],ʀIL#cȐ|wdΝ;1OᆪTb0(W\˗/G%_>6m$44777rss~*5$z/*UjѰaC5h''gBBBٰa=tUU*we.^eKYt)W_QFŭZjj y9ёk׮/0fX4M_䣏>bݺ8ڵ@Z0݇M6 jժ[YU J*q5.]ҥҲeK֭lٲtԹPau놷w]Y T^֭۰az nۄa]sD`<̙3f͚S^4]`B= fWޙ; N/^,h Q[v-͛7䛓vtRY;kV/_-DxI.Y,&Ofɒ9=>>><\t#xB忇t.]Rh(,\%\WWW}ӪUBۥ(C+/dB` ,3[FN1b?F,tupFEQhӦuK.Oky=lyyy=ev}1z( X,DGGG|fdh|0!\V,Z03|}y5|S?}wL4Q/shӦ׮]#00۷q [(z-?VZ``Q|aEaڵ,?LϞ 4TnݺEHHwF#999 3A~-eZ&00'O*ڵ EQh߾Ѻu+]h֬^e`֬YfjTU%55P>}zuIKKE_Vqrrc$Ⱥuq?.]lu߲%@Krr2;r1Yh>Y3=ׯ2c z #77'ҧOoRSSض ۷7nbMi{]v3H~~8vz*~~HNN&((Ю|_7un]ڵ [o_ߎ;͛ѝK/d+W׍skժ%&.]:1ܼyn:DN&44~.Sؽ{0sA4Bz*ݻ3mBAYS'_&NG\v '''7o&9!$\rnhڴ)8::db\6m:{С\3?tsK/ŋ駟/B\\9ʕ+ӪU+"""*kٳL&E!>~3mڴ͝/Jddqqq|L8;v̙3ܹ+SĐz͢Eq5*r[dу#GPL5e˖1s KZZ5Fdbժ0LvPT)233ngͲڵϘ1~={Y>} 2% +++WՕ+V0iR4 $$$3`4ٲ%GGbc璕UTWnn.'N`Y|'DEEW߯7|h$>> LaΜE#-[a8p:uw!:z2V#G0cLZhAddΝ;3gbcн{bD.zvj߾LÑ#Gv2%i\~۷7 .B4vK IDAT/^R2lΛ7 ':z bL6'N`6ٳg7iřBvqqKuؑ[ׯߟҚ5Mʕm=Uxxx-}R4zf2jFL]K f!1cFӒ>}z3a mlM)t5-|ׯC0igsߖhBVwwwpssw{w1gxy/o͔|ڻ`GζpYnǎ\NB2r1tE>ĪUxꩧٳgX`>o6W\!88J*Mɒ$%%J.Mɒ%杔ķ~˚5R]777ƍKӦ+IMMZj?~GGGRRRHOO'..QF1eJt7sumƾ}{퀓˗/}ϳyfHN_|}ر=&?I&q1LFUU*Tիؿ~7 >|ISq&Lᣏ>z}d09{kڴCĉL492}+1n}||6mc*ތk6QFZ7Ó&MLN˖ܹsDGO㘼rvf͚3|g?~33yh$!!ݻwcX w-hԩ?6'%]yάJB z?~B0| _CB kמիWS|97o>\rhs&hL&?b͚TZ֭2_^GppJ҃S[b6:uШQc֯_# UcaEQu{uoS?;w%1l9/ زq^^vd<|p֭[G6m^:͛7g ///4Mnݺ]WgGكj|n[lFڵkӺuk֮]Cɒ%ҥkg@aێ_Şl&//'O0aB^ `Jذa=M4/K\ˬhlkMVοR&ݭCD75*A(>}ٳgѱ/jzh6\vp+/_.[_ 䊯>ѻw/"""|ȑ#)**B"299 pqq桇ܹsHw}wg<==$ #I [y,ڵ{gbcch4V_r%..,YgFVE$&LsNqqqm'Ν;#<3={sK$/_3{'O,8\t)>Xҿ,Eqq1 S@rJ$\\1b<,eee|~9r{z)^}\y<\p={v3m4t:٬( oًVZa6+NoqIL̢E `9ѣ,_SrJǾ}P___z쁽}… \ZBpp0}!'g!NfmZoA *]WٳYpb ? 'III<3Ȳ, x@ 6Ups ڷo'lի1x>/۷oydeeӳgO/_r }!+Wԩ vw-[xm2aB&STʕ+&!!]uV+OH6m0Lԯ_\/^j窏f[lW'EQBQ9"q@pܩS'8rby@~y8r~3|zzj޸._.d FBCC8pah:Xg=5M]+WV|-ھ{J7@@PAa4Vk<K_rwa4%ڴiC^=>>^;= }Yڹt;ׯU6lXKq&Zg1Eh4g̰ 9gg7o٬`05j4z Zjŗ_~e3gRXX@b DDDR3-ZTH+**wx뭷dg{0ÆE>Z3eT/^Kx衇x:4ӧѨQ#222hԨ͛7>_M G- :} 4i8yEEW8']+QFj V-kxꩧy^tދn-G n1MYH_~Uzm۶ W^'n:(VeYE2.^HVpq#adga/yŋTTT[lѣ e@ NgǷu_`cV333)**wO899ѲeKNjW^C 6Lt?hΜ9ŋ1͈E@psE{_DǎrߢO>$G%Zc44mo@ UjӴis@@Pf2>h}e EO1!'Tzz+>H ڴyRr@pL&m>MI5j9;׳OŅ|6l(_$I 8Ȳ,UZ-nnsf\``9mj@ji׮R A(3<ѣߣhDFii2= a @JK@ 3"@ 0@ @ CN X P1\rO?YOӶ!'$Ç3ax6mN'@ {b6z*wu'CN@r>gϞ%--tt:;+1kV&I 6\ шwh4rrV?xoOee%>>9s+WCŃxꩧDc1JYYh 0lF8$I a @ !';Y<emiW[[Quܨe˿z|ޮuVˀÇܹshZy''a6ٵkןVO?D||.Tŋ[aa!ÆEfc V/]tߊuVJKK u $$7o4 h4( Wfo*c@?ᶻ#"¹vM3`@C kdgϏ)S&coo^G^$mϟcϞ6޼-ۮaHTwIeZlIBB"$aoo$6F$IjٮWy*WmSYi`SƸXnlْ6u% fmj;7 ޡV.o]y6muz,Il~x0wœ9sh4NtpJKKNMgH?@`O$$Lo .T=rރ쟭[ daXkQPPPnDDSYY̙3իW8pڿCF2s +#x1qF l67?}2Dqq1#G`Сg`oF#AAʇ~@AAAdggMMMa_Νz/_LTPfΜAHH0HĪU‚ smْKHH0\VbS"Ɣ)S###Դ0th$Ǐ' EQxwdƌ鄄u9uj:ÆEP0#q'4k ,kصkiZ1,]Ff_LVիL1t֝ ƣ뉏ǗX~QS/_ :ϫWR^^FVl5kPXx3f~vie̞='''O2P*++K6mD /(YyE>,SencҤ)޽F˔)uf/PkY]v1cL^u[2yK$Ib˖\~S z^{5vJtp}]:uĤIi~+6cǎGL6EQTܹ^@Z$&b cԨQ;f r.^Ϝ94i2{eL6`01c:;s8rp5&3ax֭ڵk?ttZ6l؀NcժUL;nj3_ |F4țzϖr fϞ^7zoV%!aW^ 1+CNph2e2qF`d2ѿ(++Cg=IQ^^٬PQQ+Ey睸Ӷm[~g4 >ێT>Lhhzpvvٙ˗/|r<=oޥc)etFiSjtܙoqf3qq曄1akz)--ɩ1[Y]ѽkYŋc̚5ɪe6Ξs2_u3g6t҅C(:˨h7n<͛7$$j:;11~8իYb9c2 IDATgUkz#F^ EY1MʫL<3gΐMNon())ݝg7_nZZldbaݻ'xIЬY3;Fƍqttŋ4hWM\|cݺu4k֔ zI\\,'N`Ҥ4.]zCCg׉͉'ŋ箻iӦ8::azz= RPP@ll3brGRQQaeڷ(tԙ.]^VzhܪAwܹǏd2ѩS'ڵkGvv{/z8777;v}dggѺukz+^^Y,xzBeӨQ#~ZΚڎԴm)|ݷL FAA> '00ت-4hȴi4$'pa֭[˄ 6Sɖc-u3TMG>|OY~=0qb| T ~w}t:Ca0>ܹDDDx='r1N>E˖-6B-Z InnzԼ-mTf>̾}prr җ_~899Ѷm[|t:o&/æG2hFY2IJJm8p#G^<ĉVarj&NL$ M%ǑIQQxGN nGF#,_ ;;;&NL4+**:t(+W^x[qO4FnӲ,ۼT۶?5?3<3|~kgK;{}'~(zM cǎٳ_,F_>8:JSЧV~/駟|.((`Ĉ?W\%4exx C% ?!?KHNmz^v 9rj Ԕ˺^:5%%鈏aFC\\,UU3L\FCJnն2cuۑe fu_+WT\Blۘ7)%u# (EQeI6)UZmfZ*U[j~3 ++[W\e|Ȩ T[rGn,Ϻ~+rm5)++l@ 6g„cgnʔŢ( b49qIIIII&&fjTf233 gMȲӧ7nCFNԩSҧ{رc'jy^899QXX# a6ywڥzϟtiZ""ѣGV-5Bt*((`ذa?ÇAO>J0yx 2,={6aaV .]DTPFj+WJr,Z?U֫pF…9xzzPRRBppׯܹsGU+W=Te>C K,ߏ˗75kVĉ^ViӦ5T ]3QQC1"bf̘柚ѣGL&dggQx0ã1}6 ~ (Jŋ5RoY;{,(Bqqq[qܸuya2__KwUFATTTr].?!̛7O^5?j5ߐp"t:y+MTP~gB9rAdeޭKH@PP֭)GDD8'OPj{zzU9._lc;888AFLBCCj@ r,\t ??~A֭Y0Yx"G$%U\HO%//!I2WHLL"*jW\aŊjCjՊ4אe<&nZqpp@eb4țd6lXo)yyU z֬Y8qݻw3u45jDbDMNFHNNVovU@#aaa<1c:$3lp^($vHKDFL*ׄ =:biР$'l_vRX<<3t:(۷o#55Mݗ@f, ٹs'|̘1Ν_b֬Y6}c48p ]v%=≠q Ӌ8{1;,dBQΝ;o={_&:z8>:ɓP;w2mtx &US}7obҤ)|W4i҄R˗1x1+5k6$CXX8ь(k`ڴ|f83g.YY4i2 euM6_$3s $;;y0 ,ZX-Fa̘1xxx̸qUF^O60y>Byy9 %Oᄉ Yb9fͮ!eiYz*n_MϞe)Bzу޽߶I'!'hѢvvv<34oNϷiӦm_.^=;wd`$Ib&h&M*SITڰ>YҘf<==TTTXs~6oބVGR^=}1LӢEKZnλ8/&00k?r=,eөSfn+!IA[k*eQ,x7'OI&64h*Bo6lU ۷gΜݻ_rN<ΨQ#IHHD"IG"**J֞H7o hI&ݛ@ջY3@oU_Vr=ҢE T5A{ /˯Whm7k֜_|: -Zcǎ7L'<ĪU-{~YY󝠚\uVJT$wjuTVV|}?f޼y̜9SR>}ordgg ̙St:oZm)whfSVVMٙL#˲̴iUY/|s۫NSVh4X埞>zy1id;֬Y͆ 2^5ۛ ?L̮]?ƶUoQx 7n'*@u;J޽iҤb5Eڵݺug͚5,ZP$8~8Ml6(Mjp~ ?dh*ax>ć~a)//We͛=C^oFYb9|rdYյZU5,$´LDnn./_V;kOӡ ,^Xj$'km!99J裏j<̛7-Zӎ[f" UeZ䡞y^~e͛Kƍׯ?vvvkxj~*YTINNQ=;ݻF/+˯شOM90K5?KDΝY`>^^^WM=<|5kV3~1ÇGsG~XnmU"7oɩݻdG%;;['ĉI{{{q5BBYv .\ ((ZCRȒ%jGfĈ_ǛstScl6lf /+VLh7n,K.JzoHkС Ν(,,J+&f4/^ի=aÆ]T%lRQQĉTTT{.BCC6,|V 6l@QQQAhhb4ILL ((׫}vZX`Pp /;v QQC~KHɓ')--%..Cr% ĉeY&<$&&Xn-vvv|_|au%n\FF&<,YYYTVV[CKƍv|ͷ~Wyy9k֬O_|]۴W_}̙KLVUr`cZnͥKT+W5uk*եjLKde#55\V\ɔ)ڵK1c:S9v;vll6СQDD,^̛&2sS[hڴ)%%%n Cnf3fߓʻwc7 -- I#=}*o 4lؐRU$ի4i҄B1"1cmӧ\y̜/GGG6nȤI1hu2ӧ`}\v f̘iS tԙg7_SQQ޽{8u$hZ6l2ի'UhZ.]]wIll KBx z˗9Kf7Enn.$jݶm[%(//aÆ!\-ZĔ);W\?04mڔ~ . 2ctYVw5mI ..^U;TEEE(BXX(\v [Y֔gxXXqEEEdeͳ:oСCuׯOy(**BӑǶmT[lRӧeee֘1c0 AurHqmݺjqyճa_!Z͛s{0WnXlF~V}?Y1L 2D[DQؼy3fFCtpFLrފP]>ĉ4nܘ-[l&88͛79E~<\\P= YS%V)?Çygԩɘ*\m 2"#booOVּ}M,RpFȡl޼UVc0Ts=Gvvw{`QQaۆڵcڵAgƍͮ]/K"kVV@r["ѕN||0qD0L?~*'NV!&f4'OVyN8ر>}RmKQ `ƍ6?W\3hh޼9>}ZMSVVFyy9͛7ի7\ Ս+V,'$$Ij?bՠԩSZ9spӧepCFrg$13gNN#??Ç1zhxZ qqgYL\999jHDYYj뱔75۶m%$$#G$88?&h[r|}}8M=V2hZc0HKKߏyBXX(cǎ?D$JKK!*N:ʁYEkʔȲ̺u PoFxxcSRR^Ӄ?pΝ;5k '*~%Y\})aǩc1c≌… W ŋ{!:z8aaajՊ se{{;5Ř|LfdYIt>=z`D5$I|J1cL<_mU2ܬ (**;ӠANSիW_DAA_53gfm??*]yy9 :7Ř1hZضm+3gfXȊU鮈z~85͛73sf7#FGll ZKJJ*6m3wŋU%?^ɠAެ[/0mtΝ^ɚFmML& 9 t }ӦMW_UD^͛IMM?F6lڴ4ك,lذ+W4i2H^gjP# 66IHKKK1c&S۸q#Sڵj,FTPt{sSӧ{1jیO׮HOʞ= ~Ă9L&a֭>|ifpI6mT#$$T=h4@NBӧ̜ő#1 hZVZŤIyLΜ9sdѢ* FY#23gY(ԫW"""ٻw*qiY~}HMMwSc񹸸pݭdĉTVV{6m?\ڵkpvvk׮6I888tRRRNMG3F՞X !-[ޞgmG~~:oP׎DEE%fFJY4oޜf͚1w\5,ر rsseVYyOf U7_ dEݝX C LV3%%%L6^矿΀,QF4iҔƍ9V( i,S^=|||jNӦhҤ)׮]6x<=QVVFEE,3lXjh4ڵk4țpa5 o4WϞ=9< 4$/o (B@@ 3fL'ްߍF#V`;1)Fc>>h45ݎ;G0sf1x*ahTzQ=,Ύ^~Cv=zTk>dS{s(^x{kJm!<<EQ !&f4ݺud2fӇ󗗗Çh4( fsᕜD׮݈:e<;FY5V`0аaC 6mԳ븹4ݻ3th`0TO"Iקm۶;E "##xw?֭aL&?{Eq;[ EPFE)@)ED IH 4z (UZhRJB :c$ \y$ٝrfv3gə駟ѵk7z=6MkGk2HIIq B ry-M۷Sh~'BUU7n>ZKJJp-[6VXityV 4SN1{,w%JVF 6͛7F5/M52 bfkNdRx{{aȝ;7qqq;v͛IBH`zs`>|[o1s Zh։p޼c2X$iwKl۶K pZ曌?"EPVmq:4fO5k&III16Alrԯ߀YfjJBB}Ј'ܹ hH֮]ɓ v#幦ܹsxF"#X'=mw`0k3W"22 JaOFNlܾ}[kv6n흡bŊL0/l@%0TRiӦѱc Öz ?,_r叵SL&00OOOt:6kߣdRL>޽PU__? ݻX$NDܫW;v_ѠA,Y(kמӧqR֗^TT4{f8 NmXhYGm]tJbE&L?_KVZL0jժS\93ZNnر.]ns̚53gNӹsv;%J` 8N ÇY`}0Lf:ӯKbhР'Onj{̙3Ǐѵk7TUjr-6n@tt zbŊQ~}&N|ල- =zraun>}sгg/\@zkٳÂUgO*ʊ˩].\`ܸO\l6Ӵi Gy".}]i&CtN7d2eqs)]8HܯVQJ?< /@Ӧu2;vQ B<2V{SyV@fnQi]@a۹p}6l(!ڐ_2yd۰zuP_/^$,7=z`Ĉd˖Fyu uuXhnWm`zk]ʄ ܹ Z.]{ݷ2!Bx$]Fll !!!TTc4DDcɒ%n.\k׮`ܺu N뉌bExzzh;Cq_o߾ 5Wrr2~Ŋ[n݆B 1y$?/ueF~RRn1l֯_Ϯ]XtБ7n0o Aˋq2lpwkW^^^?K.ѻwol66s}J95kɓ16ocRH>r]:a.aϳcvΟ?Gxx8U'Фw*TNGl>wJ˖6mZ;WN';vdҤ/ߋɓ'ѢEsN'RJ2mT P\6gժDFFaٸu&'NJ!C%Ct=][BB3fLK[NG͘6m}/kM (Z(ƍ?bŊrP /!Ct %x:٠q: :2nX*U{';E!c%7;'s=GVN' nzZh/ !xܤFNT!DUU˹s0O ^Ojj͑SչoH R3'x)ٳgINN&GR &mqo?ӭ[wZhAΜ9^B'3ڵkٲe CSpн{wN83IJJF>O"NG2e4i2C DH+LϞ0OT!//RB!B!9!B!AN!BB! 'B!$ !BHB!B!9!B!AN!BB! 'B!$ !BHB!B!9!B!AN!`x&ӫ=}(AIl!B YY`|~z^nݺE;M߾}h=Q(B rl&W\1zhƏ=( :EQ?>N:=( 4ϟ'Ohn;/_Mkһ{^e6mVB!'Fzر={kAgϞҤIƏ(,X0t K~͛0uTEaԯ_EQXl~~fi^|ooo =5kϗ5k… S˗/S\Y|ꫯ0n:| 덟/5jTGQm^oQK.pY//B!$=fϞ/ٲW_ҥ SL&!"LjUiӦ5R~͛ƍYf VuGҼy3TU^hbFE%Җja.>}4m'M6~/?fĈ9r+Ӯ][Ξ=_͛ܙ'O~/ٺue˖*GR* IDATȑ#'. GeV+#B<2;7>Y-hٌl+l63qj׮zХKg5BŊ4i2_~Wphv,T?@2o[,ש}̟?I&ҩS'-Zĉؽ{7qqٳ'ϟcĺwv _x̘1U۾2hUUf( W&3\O&} ̞=5<vUwWXm`6l@``Sr]rJdZ>䣏;Z!3h4zuؽ{/PL7JIIa֬Y:u1cFcXn8|0aaa|'8i\|$KƎ6ϫWpR":AA|Wxyy={'O$OX,lv/@Ӧ7VW^ 0)a&G .D"өSZ-d[tsNl6B+!3_Y-;;v:.it;F`Ўߴ&tt+ 'Prr27n/vh4b0Fx{{k_p???5 UU:u ڵe (™3g̸q۷:CWkvڷoG~l6Μ9C4n ٲeaxyy*&Lc\pAu8pڵeŊNNӭtҼy3<<2sLڵkڵktڅ+Wлw(L'ʕ+Wh7oڵeʕڵktY6m*!3vn޼Iݘ8q!nvܹw柶)))tҙpƍlyfZ@[p!m۶eҥ\zݻ1sL&MS#BΝXzW^cZзZDӶm0w ݻwcΜn5qqqmL\\6ukԩ#QQ8,OUU L8vYhqܣG7ΝCpp:qU8Nڶ ŋtUUIMM%<<^zqMرׯϰnmڴnkfΝcŊkݻwѥKgzArr-ZDǎz|( !$ȉ( F"EܸqNI; L& *ĵkט;{RRR?>)))DGǰtREQt5@tڅ-ZK/1edؽ{7#GD7_~ׯ3g\nݺlFQ,NСC "/^|2) `ƌܸq_|6]JJ !!4hА%\Hl~i;w`Ȑ̙r5l#(WC!gΜ0cLRSSYzo0t0 em4PB:u)]4ݺu嫯x9B I~_~%}YbgΜ&6v[lf۶ZPڵ+[b0u4V+?IIW>|8Nd۶m8CPP0J+fƌXb3g$**M6߿*UfȐz=OpZhAR`M$$# lٲɊ+>"66ǩh$((-Z/[8w,ÆҥD6m@߾} lN% wZYgbĈQt:zA-ر!J߾}\cbbbug?v؎ng;vCqu~i 0h`j׮̀غGtt Æ &!9NǼy䓪ܼyO?eY3 :7вe+ M|ƍț7/޼+Wd޽gUvaΝnK_.1d˖Mj;e޼1Mt偏A68,oqj7Ǎ6>gbѢEL2Ȩ,\"";wnu޽| Ay/m׶!FN]|qO~8pfӧϐ;wn֭[۵KzSN*1Æ ʕL2?˴ZmdϞҥdIZH wjw֪U[[3g0nXN'[_7lNpwLZKbb"gd̘ڱcv٣Msϱt鏜8qUUUF#7nԩSlIttk֬RJKZjѿ?.^ȼy3|0-V.ÇYN %99!CbI!AN<ӱvZիVcЈGOܺuEQ|] #Fd2QVm (i; ,Nj&t:E[Qt$$kuhBEaР,]{t:QU~J*^ot4oނ#Gpڷ@DDLSu(W|ɝдiZWQQlڴÇkv֝ÇmVm楸TU(n3Nkn:u6mem\sͷt|g :T -[n3|0\ Ej1tPz qB(*7eʔ!9Z5d4W.g}&$t89r9sfkm7o$**>}>qnԩ#}ҥK?蘿4|ҪUV?rsNDEE?ҎuoݺExxz$}+3x ڴ "o޼R3n8x\~  <vId0`Ԩ0hj֬yϛ0w3? -pU@e_l9ʖ-_0B@~47ⱐ6rݑB! ' 'OΟ?/!xzNn:OBH{9kLN'5sBCBBW^%GR&mqiVU`ݺuٓVZ3gN)!tzj6lXOlGzwĿ $$G2ctmn!xt:ʕ+ĉqW5BHOԷ G Cē'//RB!B!9!B!AN!BB! 'B!$ !BHB!B!9!B!AN!BB! 'B!$ !BHB!OJjj*NS B'^l6KA!AN<EaŊ"fjeڴiz)(!$ȉghm`.]$!ĿŒs&L$ ] |B rO$EQo'qZ,yw/<8N۷px<37;ʕ4ooohǾ V޽{zjm[*^xB^vMqㆴg]Z5 fN'C ᥗ^qo> ﮩr䩪ʭ[};ݎfsKCXUUi\OOOrJ*7]0MB!`\#ܹ$&ڻw6jժرctt:zߏ"E^ѣx{{W_5dn|( k׮!W/_7n- ?ԩCx睷y& ZFE&w\-[ׯhBEa͚5ԩSGߞ={yԭ[!-'$$A|/}v-mڴ ?zA9͛73FUURRR;^_^{֭[2zW6Z,^~ZF+ BQ|Mvܩk1i׮V#UuV#7o@q޼yˁB4kѣpnjCڄr}^z%:DlS-[}Lv>}Oo6|Cێ~1`6nH2eسgZb۶mn\z3fde$+AAAL8͛7S^=VX@_d[υ ٵkv^vtvEr~/󙫑$'_`|y]<<<or#GQLRRn3kl-Z /H[D-`e˖K,ᥗ^h42h ={PX1[z=6lԩSdϞk8pB ѤIEaĈgJJ UVEQ*Vݞy9 >ݻgB >>>p)ymf͚( Cpbu:kx{{cǎL3gN (Dnb4)\p5}rrm5]GVns1yLw8)R6nu*~~:t@Q2,nShQf3ׯ4Մ.]Z qeϞݻwc_r>}:˗իWg%K닏 ,p';Mx V\b2ɓ'3g. sBFn߾Ez ⋆n?o.^` 00g?ĉTRUV~ ϧr29u yY.q:t{˗石k.:D@@z=v%KPJ~t̔˙[{fTթ}N|ow C|Mt:q8<[Ls}șiEjժ Ă?PZ5UJh߾=*UbY4oޜ-[~!>~Uυ IDATN.\Q@6o‚9t ~ԭaps+u6,cϞ=Q1g:N4fΜ9ŋ 07/_>r[6Ȗ-//ڥO?EQhڴ)IIi5nݚŋ'Om>/^9rpZbŊQ|y|MN3qD6lؠMWP!jժŋ/ȪU s[׳g矣( Y^ z*9st{̙3QrLkKBCCٿ?t-gϞߟ>Ñk׮+W.ׯO>UimF]mֽ{ 5?{2ed*tɖ--Zsȓ'[;в{9&NNĉ)Xe#FPX1N>a<EQ>|8zիWڵk)T}_pB{=nիWo0͙UUu޽{sixL9s&ʕcݻbŊ1k,*VȌ3cjժ,_c .Oڥ˱cLj.Ÿ{fɒ% w6mk}6l@ Uk֬znb ht-[_!!]vQ\9vʲeHLL˿x"o nݺE*U0`@(P ol6,Y|@߾}?~_B~\J*رcvxݺu5lKڝþ``Q F$ z6nL(\r?[m33n8~^5w׺u9r`ҤIGzӧq|evi)?@f(_.AZ#OVUB<e˾e%ȫ|h!U^<-b2XpCs=u =Ear !Z&ɭ AN!0ׯFq *ijDȔٰa[ns{lRHBH2ȑ(Z}{B<{Ѫ( ͓ 'eRre~:(ētwV!9ۥ6N!7B!$ !BI. ;Ky!D :.ӻz 'ZΝ#44//,ǙB'ndɒi$!$ȉŋ cH!ē}b36l(#F mv&jF5k&) ' \=[TZ._L^=رcdϞVE;w.:ud(ٳgwެXkBJJ ]tf?Ν;?ieF`w>}N:?i6o<:uȆ PEQXb9]tf1tG_WҫWO lڴi'Ʈ]h'hN:ѣIIILHH/$[hNbb"ݺuh4rMzӿmz A޽vFQ6 ÃVZژfnݺExx˴f1dH,;v ..Tu;4CȰX,tQ;w.{0yd זsvB߾}O]}6:uԖ۷ojt̟?ݻqmfNUU:thw;"##۷odIN(6nCK.ѫWjl63glBBB9s&Vw0^GU.:BBBYx1,_T"#ضm+'11wëرc3lǕ+WhԨŊc񤦦F֭T2!/_ݻwc0r ^Ojj*/&PPt'hjwPZh'T%44$C ``ѢĒBHH/_ѫWO !ԩS>}2g,L&ӝP0a"#G_~ l6zM ={p ‡~0Z`yemrʼtލ>ŋ3tl6-bH}]-9r0۷UUѣ;͛i@z螡fӦ>|~kfU:f͚Ѵi31ܹsǖ-[رcJikFSbEzFJ(S QQ@ڱX`![zEUUlۈb߾}lذ^WFh֬9F!CjeH۶oߎ6m =}DFFq~)m?Ν;~Tl`0`Z9rF歷_d޽޽(_ڵk3ԢJ@@#^~fΜlfɄgѯ_ĝms&"͛e!!!UV`0 Eݺӹs'ò B rp8( \rY ǏkډnX~=;vc$&&j'@P"E!E@"y(J@Q MZ"*! "ҳe!c$>|wwڝIwqw'//>}Fnn]0bx ??^ٵk'Æpi6l@ dd\b<#ӧ1JVIO@NNUV'ŋT\gg233Yp!zfݺu:t eUV<<<(ӧOɓ, 'O ""Qdgg鈎bXVmJ||,Z]L&<<йsg-R^pINQOOO^|%.\h$22͛7e8+V2lpF#ÇΣN#..-,)55?~׼EͭT6qA8::j|J|}a4NUUY&;?Oa5j/C&M[Fjj>0V\}Sۿf68qu__e;ưa>㏤FUU2e*U"<|d)&GEzEBBBYr'N`0GjjӫWo HJJf|v>}b4PBƃ !CfF#L8pEyބd0qQNΝ;iذ! ?#Fjf3&L =Nf2޳X,]vjGٶm+))Sx{R7qEaիӦMcԯ_hrrrOn Ǻ#TE+11r: ,`׮L0Q[LرɉmWg9&ODNNlٲxb7ooo>}ƌsmvAn`aܸ}Yӗ\kzgyrz^:-88R, Æ g!4n8JXX8vKLL̝;UXWW^ͦBյVpYPP@ :t[O>k_oJǎ$$ēOg@Fn$C|ݷxxc6Z*\p'''WVF t3?ʕ+_FHffiii=z@DH /f8&%e IIEO[,f޽;ve&C`u+΅`MvrWFMvv6cǎΝ;9y*5^"))ƌf#;;O/ޗ1cƐѣ4ݫe;u̘1!99oo2;vw߫5iҔqй$$ēɲeK?#F# ˋ֭]#GSfMz-f3Ν;HFF_}cVg~W>3.\Nl6ӡóL: N)&MDnn.Itٌ;7n|XfB9q8::!!ݻqvv"**9s>gEDGǐ;KBB&^z:uI͚5y饗P Zɀh4jKD6xF.]_>QQxzzҭR ]t!&&,z~1-80L&c3gv"&!HVyꩢe-[>MmJe\\\qpp`888Ǽyٶmq>_t̪vqq ,Y?( #99`6j( #F '88@HHHdٲ̞)#GF jc n صg(uΊ_74mڔxxuTUEѢE {Qy͞8::( I^O>R]rM6U$$CvHJJd!>\_AڴiS7oFbb=zI <ֲy6}XzII(B-xgU<ӥ}>c, xK{IOliӦtܙ.^Ȁ1?cǰkN6PU 1b8M4K… 8.(ZDFF3o\222y7(((ۻKSǎRRe0x*' A$%%ҨѿiӦ-Xv:C\\\cٙf͚yK6Hn]3gUԈ6mʔ)[?e_ԩ3-Z`ڴK7:tcƌ|HHgAZWw22.IFNqnڵ4h ٿ<7B$-t: π:l6IIrrOE_.QfVj1nXBB9|ٳӧOێSUU&LOhhu*iΝ72_YYY5t5 ͆ }JC-f(ԱZׯTgB9q+%z=#F /5mNc֬ 2RSH>}uPF o1^xtbcnh&u7,/ꖴ?z1oDF:g{ZֿT}MNٽC!$r>>| vO8qÇs $9dW^w8wܕ2^Y=W^ӓ<EgwC92SNϩS̙̙3F3i$=Kll  ˌNAAF%3l/WJY&M^' TWVVabsk>EQa+0>FlFQ崙Zje7`@?.\\""Fڕ2$&&ہ9ؕ*.U6|BQ3fMQ3bp>l6aeb eŊ xg͚IXX(999 :Dފl:S.˖-^[z5ÆڽVСC5jӦM`͚5 櫵 G@?F%k@ ^j \ԵvZ e: '|Rf; >j ˗EAA̟?O)aΜ9?kL-jύ72l/ .,o… ?j222?#FaÆ23mT||>dU ѣl۶ѣPv->x͚5/_O? ''H BO1`7^oN:de]f֬OK=%[\aٓ<"#Q&A( WSp]vvgϞԨQiӦRPP@xx>>t !888«Fhh3gΰPJGADD$Ax{?Gdd+Wd2q%6oLbb?G#11I+e6 aȐ\ɣL$'յ"qW\^}U4i„ aŊ$&&ѤI-X,^WgϟǨQ)(( ((;odz.DDD1̨Qٻwyyyt:Bڵ+t*eǎ$&&Q\M- /dbZ+S$и$$$K|D:2fXIj !;ԪUC?dݺO!CqttweӦox"u˫Ze-+*^BRRyfm<\;R +W}T\+sx"YYtDDdĉb0hԨӦMٳ$$$jeUtZK,Y4hЀaYQUժ䣏>P۫<+Wmgp<΄7رc]kjYZ}i3NGll FjxG䓏9q֕OʕXx]HժUܹ R]QF|:~ ^w򮧒2T ~c0_~RJ*l6+~w >o4:5*UZjIdg˯姟~bժڄ 6ࡇ́1 8EQAAԩSŋiǮG~9VXS6̷r]EBj׮M۶mp<5JMZuFp~Gquu[ϰaiРK,UX!nyjUܹs$$ӥKu{oMV2۾U˺,MG&(h+Պ'JZ,Yƍ_ۈNúB*iK^ee e&W>bۛUUqrr⣏Ƴ}6NMWklJ.QzT*p,k%'(({{Sj5LSբgy*T@||B{Э/4i"}ĉ|r^l6pvvnvj7TjcT 폪DDDvU61{88 ޏjok,^_KɌ77go̙bѺ&MDff&3f|Bqwwѣ=z7PF 믿f]Z^Yft 3sZj]jMյ"gΜ9rD+%yY*7LdeeseF#h֬EŁŋٿ?GeȐשKYwlVD6m2%u\c4e޽?~\+ݻr`ǎC Fz OnX8x ;vd2]@LL4YYY||Ovf/sNkJ*^cǎiǯe%Kb]˗YKL ?߅8._ŋ4xZn݆$$(;obZZ<||2K.a„l6L&۶m7pMY;F֭B~~/_jΝ;Z6S$''_*UB}#ӦM׺'ODjj*;w.:?~8-[˫YYYdeIY.!$]ZVɉ[>}Ew#..7|ڵLDD$3f|?>YrgϞb`ٴ2^%3 6޽{s%F cǎ4inTj2VOMVTdX (h"&&1o\"#'4470z(|||zl6̛7t͒%T/ܕh.\@߾pqqI&^)ϗ_.Rz+Ү{jNNN8::juD LFG>W^뉏O`ٲ̜9X,Cغ0` P.]ʷnK.,XEuF-[g $$$O?M˖-**!֫Wohj2lF]vz* Ҏ;شkm>ɓK!'ORO3ENN|עE ڷoOttYYYO{H'99];{%00Fмys:vHtt4?FBB"6l`ǎe+9FZϦMXl)ݺ… lĒʪU,?N.]ܹs 4ԵySXX@ǎѡC{*WLnn. ĉΫV얏/[la,ZLk"-"%qRK*x캽k׮ş@߾}!//Dw*)%f%d▹y$ ݻ7ƍh4yOqB+$BF?q_ƏӸB܅d(ב#1[tgΜ!>>gؽ{D!ȉ{rS<.fjժٳgYfB! qY5*EQHMMeŊ=Eɓ'x"!$%Q{qqˬ7|Օﭕ*9//$ƺu~+ȅrYbb86% ߮B!keggŐ!Ch֬9l4۶mh4rIs0dffrr&ժU#**aÆ92J.㕩)%..C?l6kzt:At<,]䪠(ZjMpeU^f$!B9qIOOW ڴiɓ'򪎪tRw7|Áx8s 1iӦqey䮞7335jЮ];:%l6F``ҥvZ6Th-dm888{!wyjUܠ?o)ʛod={ 2edN:c8q5kއ]i,(*1T֜ueM(ؕRchongggxiii\1c\DBq'.9±caz&ԩS4X"G^do:tÇa1q%s ~W6oތ䈪xxTa E)*өS'qtt$;;Pl.>lLbWwTrB!$w-E)zjŅ##>}:?tp, 4o͛?@iժ5V'''=S,H ʝ0d2ٟC~~>6 VZ1X68x'ڷoϲeˮkѺu:nnnl6RRRpss#??6m`ٸpf̜9ĿT\q]nV\K2rIso7,##ٳ?%<<\8!wyAXV&OB^ޭ% +#9AkB!@NATUӓӧHc!NGAAFt< ]jҽ{f䎪*rB:ϟCC=NUUZjڵk eAxx>!ĝfڵk5j]-f!$w%ȑ>^NB!(j?Q֮]GO ֬YO4`Ŋ'( {*_)SRxwܛƍbŊѰaCOfXrO>MaQ6p 'O+^Ŋ9TUKA!=̙l6uNOãr``ѢETV m[믇5S6m:v_,Ђu ?S2x ͆fo6Àܹ "22O iӦ-V{:ԕi^.! Hve^{Gd(ܹjռX̷t2FXx1nn:} HKGzT"#GZmx{{e:u 70d`ϟ_6z!… wE?ܐ6mZ3~Doxm6:JY(Bc-qr}faZ%:EAQUܹ>|yyyV;FLL,Ffm ԩS-[b290w<:t@&@pp05j 5u ĉenˋkױp.h߷ ٽ{<z1~>fZB!Ľ(h+x);vPi֬dee:5k֤7!!zC\<ڵeZg|}}t(M֭[>|ÆEⓓҥ-ZD@?K,[(ڲžz#|g׼|}}^[d ,Zz}?NhhSLֺ E!((Ɛ! !//YfbZ>a74d21p>B L׳hї4lؐe˖9|>L&V+S'Rts\GEÿ8;:: i6l}%5jMwZQH~~#MFUU8޽{d]ĉ1b8fbcrń`6^~~>̛GP8l/Cff&AA(–-)^I&4ndjԨA e7nH߾0Lӗkh9Ҿ}yAa0hݺ5/#>_oOL&Ǻuk\.44/^zD߾ѣ'OJ*(BƍyGX\\\̶YVbcc9sUlF ';;#9w# dΜ &77n~:H233叨ȉEQ8tz!*Wŋ*׿|2))SgE9s0͛?e5N8N.]oo*UbƌO8uk׮%66xI&r}%Ndb.3dPqttow 00%KSBRW^y9s>1=<<(,,,q,E$hm۶%<>}11E… IDEElFF󫯾ot׮Sxzzʼn'HN} (;R,5u5ҭJu˗Fյm0_~9VZEcXxwxZߡó:`:w~'|R TUPUiӦcuHiժ5 BС-Zkw!rqqt̟?_J7,XpOQ6lAqvvWZzu/<<|Dի /nbd2Qn]5kJHHˋUVjd`dZQc0~xl6:UV닣##FzׯOƏ:? Ξ)k&$$DDD(J8LvSO=uCۻkFǜf7nc`ܸt:~e֭KZZ6 999]t…t 4o 4U 7]\%&qT+V ll6[9ASKE/z^OştQQx{{+]YzufX劻e?h,e>>j]11 8jԨɹsY_//k.)fަW Ξ=KZ8uQf͚ԩSOOOǕR^vxLTsTus5#B!9( {]v=ѣ0nG>}www[٣u:Ν;fm۶L0(~;/\A*k\fgϞLd2ut)cǎeݱX 9r_| Pppp ;; *pE2228qk֤j믜={#e^"eeg._ÿ{.]Zt:N>|.]Pb%t:YYYvAN~tFN^`ԨdmLcYמj䧟~fp*#F aÇi߾ß>F?j:t))((`ʔl6ӧO#((TVDUU @XX(/ճߢvQ1DG0k,֭[O\\w}8ر# 4 $$i֬rWC 2@Y2NG||(C/X&N:1o\ D鈋/^y(F-hڴa… سg֕[־*B=`ϞevVT/+`||>CTUA4hЀ 6|eIx!J>mzrקQFQn=qJ}('ݺu#00W׊m ˗/s]NMMW^)&66kװazkǗ4bZqs׺fV+QX/B{Iq/;vPYfddd7{h[̙3OѣG6m*IIɥ .0uj AAOQ߿I.Wqܹs{ mL&Ktt f+H8ߎ?!!QzuEw22.9qotӦM%,,qCY2mUQ׿ޓ>mT|}}m;` $$ q|a&wwwbc}J*PUɓH6oҢE{{ߺ&MҤIS9Q~;ĉo6o !B9q թ  m|XjU!BZ#ZN:z*pA7\Fȑ#,_&K.iO*ܹsٳg7ݺuy'8q8 .$++jժJ߾}HIzO>>ן,&O"AB!]~i׮UV͍5jԠ^zm 'Oё 3xePZ5\]]oB!$Rqi-(puY*ETqt//B ?ɓ'9}47nf͚ЦM[&N@~~~et^B!@Nb6Zb\UٽP̝;{l/^Uve٢TPA[!D=V,]ZB"%ݬDd_vuN! _VvmPB!@N$&BH ' Nӳ gʓBQPP r.fZy̜9C&B52228{,&_UqҺuRSBܑl6kXr9ɣ @Nؿ?ĥKQw$NGO2UUrҨQ#!B;ˋ4B!rB!B9!B!B!rB!B9!B!B!rB!B9!B!B!@N!B9!B!B!@N!B9!B!B!@N!BH 'B!B!@\\\pww_{Og[e>|m'''ׯoj}WҎ<֭gEoi2pww[W7@!UբfڴϜ9Nbڽ{S`36[nJN fE~B;]ӵZz5d6uԩVggg=ի缹レb4Y`n̞=FŅtZz-Å QTzg^^ҥnݚ-ùsɡsNw_M|}}1|@n…m/$'Ҷؼ"#q/t#VKhӦ5-jժ̘1EQݻMEQؾ+=͚J*ѡCRABq;w,[l.utj ՋQ̜9__RACUUm,36 D˖_~aƌ[ndddh'8p4LZת耪{+d2y晶߿i>f͚Tm]eJvǢݻ*zeO=׶+?/lي2kWӺu;Bq0\k%;wnjjv [#WV ԰L2?5z`zc5JPEj޼9>>>l=Ux`2_?8b۶mxxiРZ-2ZEb}-[OnݺuR^=-4ĉ7ii{ͥzjOYb_jwՆ`DURvcB{ff7l6VӁ\nc<,YH6o̖-?hesssٸkCdd$a_' vY&vfŊ 8;vh]O^Djˋ={f۹t)oCG||[lB29 )))nDllޖ-?Gѣǰc:tϣ>dж3ԭ/ Lxx^^^Z>fll6k !{eʎ;T(vl֬dee:5kfӑg, %a4:on4xwO|KBFƥ;og(?RE-B[F[=S+#')̧ou*B)JQX7C ]' x{{ӯ_?v튪\q[?|GժKWPOIBqOZdddrı2Z-((-rwLV9} m;4horsszGյ2r2Bs\Or2ok΃+=FQvM׮/3|p͛7ӵ˨(lْÇЯ_?^|E9›o$99]V\ʕ+ˉB!0Lxyy۔@uIڵ{nݺYU{ٷo?999(=Ctt4Bq^|H۶mɢhذ+Vd T^sIC !w0ݡj֬+Oػw/f9aB~ cã*>|-T DPDZPPE) Q_{OB@i"҅*Mi}{̒ E?ѹ+)3ssvwy7ԩ;KZLaH@ cD.@  k.@ OBN @9@ r@ !@ @ !'@ BN @9@ r@ 0$Ija@p_#2fYB Cff&aL&dYoǦжm[>pl!'gEPP QQQD$a@p"2'0w"Ĝ#/GVoݟ5==$!'g۷:c & www1B BNףj1?YQTbί@9A @ @ @9߇gm+))Szj5jo899U7n0yrT;GG?օ$$WX若{~ &JKtWj$mG8r=Ž.(͟?V11єgn"##HOOW7$''1aB"6em۶fZ&-m1dee*ۋR7l枧$8fqAII QQy;v 77צKŋm(..V;w !wi^ nJh4;Nw3Nl&99*/2FY1/,uwB$GB ƍ{W`0qnIXh=\}OƠAX,_~̮]BzXx*H^z%KXXҥK|z`v톯'NDi&:u,\|r^LL4?3gN#2?3;wՕ@^ut: .ƾ0|f͚Iz(**R}"˰fjbbbe8$I"00.]՝^!=ii3 ,X0_y6ll&>>AG=8pǎvɓc݊޽.AADҥ f>޽jjٶkz|I0H,ˊ C /Bo~.^С*L&T*VlFRPP >(X,0jHPTh4pqq{[&YTFݻj޽bu:$uƌK5lݺ:uPNQ:kW_ƍxzz9"EEE7;k&I۶mcĈ4hЀ9B3fQV-eͭ, /g888pOtԩBgYF z)ڷL%4baڴԩS;wܬl;LJ_|T*3gdɒ%Tɲ/իWgho6n u֥[7/^^͍{mےex7lUرcY~=*22OTvppAx7qz}ӦM租s @???UL>'Np)s͚5dffOVV֭U=ٽawλ? IDATUh$ WWJ_BdZ,˴l$g۶[Gш2hg2?>Q.02 M7ӈV-YjRRyԩS7ҽ{JɲFb)d2Vٴi^^+= ͛6$N 9$#GӱcGMδiIKlڵkԨQ{pQŻRbоK̚5ȢExXճkrJfMf,˴oߞ o<'Oҿ;h4Z(.. 4ٳgjNʕ+SVG]jʐSvv6̞=Dvv6+DEEU0HĔ)S0̤^[υ 8r":$IƍȲL/3uj*f3fйsgJKKmvtEQQz, ^bɒv*'`X2%FDVo$Ikxyugڴh4nҥPɓ'9}ݡ^{MN3}44 |%K>Uwff&K|g-D[yIΞ=d+?s^Z͋3V۝,^ł'O`0]#Ordg!NGGG STTTL$"##8pM6EբV9{ M6Ծ& WWW9˗yMo{^58{ FlܔҒd@ ~0T*>s̀IO@>SΞ=Kaa!OPPWWy^lYfOOO<<< 7z/h4λx{b&N@`` 6EYjUzdfU,**}z7kti:۶}MllfY788ٳg}7tЁ+ё@#h+888˼ys1c>>vD ˋ-ZRiZc3g6ׯ'>>W^y~?:tڵAA;vq|vVѭlѢAAxyuhi߾==8AAa4h4T*:wL˖-k >$0%K>en۶-Z"00@u:O/|@xxYxl{YY&!!ȲL\\<,Yek Bb b{ /@۶mܹs 220~|C|TAVo6ol;;}t9r#F gye,[9sfs?Z,bb1b8#F \t: ύU[,Yr%iiiTrsm5{HH0J[穧ִkִb*Yf+ԯVYpa[ìY3m+99&׭ڵp^ִ^DFF{g7C2Gp-[ƈ#8:JJJ׏UVq)z=!!׏p4 F+>'00cUh$,,})$'==#Gj*t:[nCb  Y.ă g7۝:u v1Lӷo?\\\ZRދYxFf a{槟ѣfhР\zV^=Ԑlh4{ 0ЮȲUTӼy ؼyj(4 aa7aѫ[+9vرlRnYe@^{u4h`3gX4_7oALL4;[G*..VZˢE +z!AAر*O>q|x≖DFF IAAt؁{W̟~:Ν; RRuYEk6mx≖LV%551cSΣ 8ڴiC>ILL,w+hdߎuppf@x1<gΜʕ+ڵKM5jrر^Y΅ v{CBU*3fd21wxyyƕ+WrW5k9eqs;q+'OSL6̬YرCtJZZlܸ5k֠رcvΝ;t,2]vc888WYrzfڮ޽EE %W=(,Z@pVdb֬}JHiVU (--%&&JūJjd, iɓ'9s_"##x6z*_ϔ)8qA-ՋFcXk>j՚7x%w>^͋h4[/^dnJ`_Yl;S"2'УGȉ'0a0ѣ3fLl6:1*!'BXX8'PTTH~~>f>Μ9 %I鴭xnMLPP II8vMʞ^Z=ˆVO``oM6ɉpNzsBw &իSRRBJJ* wbt:IMMeϞ=ܨlMճgO<=G.]yg+W_aÇGqq1oVKll<'ۈy!ddd7۔eѠtڭ{4mqڵ>|e8t ~w|61bAAԯ_cŋXf5]tatj5DDROϖ4#GaXxy≖XA|O۵kw!!3;wCTTAA,_"=3e.b᭷zZl/&>>Mu!C>Bmh4b2h׮kJhh4O``7cN71`Xgu~Nuִo"}WH/nRt:T~}_YSC8u$g62oR2d0|;]*gݞCZ """9USlDhU_ u]2]b*mU1u֥EPV-+xi`W>vwwwyiY&11<@B !9={AϞo2f3-^ѣǫy@ [! eXR &!r'ׅ1?sf{ŋUXQ'+\r5k cшW9,ӹsg6oLbxF%̖-_I @9Bbb"`ҤDRs!-m 8.!'xyg!}Q!'?@ @ B @ @ !'@ @ @9@ r@ B @ @ !'@ BN @9@ r@ !@ mh PTȲ,! ,0@9I9rz^D EIÃ;˭@9Vː!9y$nnn _ht"''W_}gOӸqa,1GN`B! Y lf͚$ILfa( ᑻG899䤣FʶJKK$oOII)IvE~})**|ۊ+$IF ?wܞX:s,:oن^=0 ~@ !V1o&99_i&Mokx?eٲeTb۶W.h޼wfժhќ#GpER_ڶm`,'6U 6{O>B'JYa@ !ay<҈#G*"ǏgѢŌ977>>2due̘1z<<<ѪU+}w>?T@9} 999䐝Uq;wwy5kV=nD@RRh4"I /@-:uwK|sM1} /˗/ӻ[7E},ɉ'xgyW?iذ!iii\t5k-366'''N>ͺu눈 ""B?i$qLΝ;ٹs'`쑗GjlUl6l2}Y6mʩS8p?#[Vupp@q5kVի]aÇС;vڱh"C%23ovJ22V[!.[v{/?r9t..6lVt^.vʶc~g={3gk$ZjCNNZfMf3X,vMFFV`Ϟ=iӦBYjZjѱcGV^MFhժN 6ڵkNj/XSNѮ];Om۶|4mڔ:pEӴiS֭[gcG^xn+|T*j֬I˖-HOOVaXxiذ!6maÆ̙3B9xzzҢE +4iO< CYf::v :TVN նK.~ 4Ȧ#Gkl+))~,^ (/)>(ڵ,^c*ٳgٲe رCJVHKKN:=ڦd/_^7ΞWtذa,]TX *g7xGyhJc}:/^VZԮ][b'O;|6_/}sBBNNԭI.]/())atڕ 2at HzuܸqM61d`<<<*s;sd,s%nB#Unbz"''VXwzW!=z(w|- IDAT <<¦^Z7|l0` 99hZ?3ZyǕH?0Ofܹ壏>"$$ʂyPED\{`ժU]ٳqrr";fm 4ŋȲ?΄ ŅׯW8>00TIДz֨QDw:tvvl6#2NNN8q®|ZR233Yx1j#<… hذ!$S_~7n+/ 4 44,֭kLdffR^=lE4nXh *QF 2&MӫW/v)lct/CڻQ巗![쳽2~o6mЬY3[c_7|Srf瞫RqqqO?͚5kҥjj*$IĉJ IdXۧR(--bFƍvr\kjݻ777Znmp<#Gɵkא$b.];cǎMjjb__ۭʇ IMMeذaK+˘V+ẁ,1Y(X߿_iӆ< g1niӦ6._~ T:̙3=M6 Ge lbYYY_gмysa,Aj ۷o塇+.^DN7=̙31GN wlٲ¿Zjaa|@p1Mi2uU$}=qBN `]WR~@ k__Zr@ tJJ~ SYX)rhZa@/q:RS'_q,sp>rF#&My"@ `2* #I4jHJ ,˼Voud2 e1* quuFo@ BN @W"Vz F# قd!'CDD8Ji@ +$ GGGڷoϠAi-!+Ghh(>X[I-,c2HIIf|B sQT,]N>}]C(mǦ8鋋0ۏ !w^:u$!1fj֬IQQ0@9?I(--AC ceJ"FB bN@ !@ B h4͟?/2o<.]ļysjprrׯ_'!!;wU=$qIl5)~ IHPP :w%I'N'V<՟h4?w0**JeGEEؘ*QJ=qqlذj}e1#u:AA"F@gV nGNN())Q: {=]l+(((RGR{ ׿:233YraX,ﺝyyy}=⡡a uI@9qssWWW_HOb&1gΜ 'j5~~f8q8999DZ~E|]X>d9O8;vM6ˁ*4 ~ 0iDfϞVU%1q^Jĉa)JL6Ft))̜9V[Aĥ$SZZܹΦCۺu+1ݻWٶwƏOO?)S$(..fҤʶ˗3eJ*j~:FBB<ӧOSF!$$B%Ibưm6z='Ɗ+ؾ};1|wv)gnƍ$hXr%S"I&X֮`_,[, 111̞=Kӑ$pūc,X,RRȰ&%M **<ٲeKwjHLO||oǎư}7sΦMINNd2kLbx*}\]] 00 .({5&&B5fL#%%E8| fΝL,lܸ$F#ZtjJ&LHh4*q֬$''+קjYx ߿F,\IPT*ײn:Yߺu &$*/4@9Z lTo'[߄^s~Yt)Ozg899aaÆpBΝ;^',,^ޢ@V뭜;wQFvmgϞ##(..&22|xhܸ jX4c)baԨԨQ!C> ;;_|>cF#AAڵ%44ZUưaqsscذ7=qwqQF櫯6sI Xf #GzL4I9^eƍٙQFeëdƌ4;w.FӓI899)^BC$|iܸ Ç` Z-,MϞ=ٳ'?1z7~cǎ2`4nܘ:uΓO>2,<{ٶkFڵk9|,G=xX,X,>|ٳC~``;LZzʼdPXX2#6mX'j֬|h?p7n`Ԩl޼)BщN:7qDƍ*CbA`XNeWͱcGٻw/~~FBBBQTо}{zz, L61cƠjT^-ZEzuyocۤI7o ji۶{E```nz5>[ЬY3;`@REqq1jœO>> O<ђ.]p5X,|PmuqqA$%hy衇Ε+W(**BR'Ƞ@BJU!$IdddôiS1 [N۶}͋/_M6999ӨQ#^}5\gggfr9~W$IAy"2'ҢE F888CU(((`ڴgDмysKqq1M6w_(/۷mvJGvmxxxիoWÆ nnnZ"MJQQk֬VuD : 7jHh䩧OdggO;rʕ '|}r54r/^$Izի֭[eʔiJe4n܄+Wf֭[G`UVk\+Z,:vD˖-qwwW^AswM:, ux2|pe=^)FV: 2Z("VARR"#le/z]wRxT +_)nJKK|'''&MJbϞ݌@˖O2hРJː߮Z/ڵ{w/,Yr6,^gХKfs9f^2,{bVYS~ uL&"͝k{{.Z&99UjWUsf6QTzF+& ??^^y>Ò$Uj7In~ro;;;ӻwtڵL:6/BC a zc%\|+N[$B#'ݝ/P\\Lqq1))w`b 3g Szu._̕+W8pȲLYxիWg˖n;ՕWrU}zW\Uȑ\r*JB ƒtkiFdق`͍@93#Gx۬#ތ޺RdYFsg^^iݺ5NNNĥKCezG`0гg @#INNNƍd2y4Q\\LbxdYVm:pf̘) 6PA ~xשSG"''>}scp +X,DGGxeYFDÆ GNAп,ve fϞʕ+x饗X4fݳݻӨx{ʕ+]/ Y)?}ժF||<,#<˜1|k֬fyJ3 c~j~(  w 4iBZpwz'''<<3fǎ̘1nݺcaaAZZP]z܏zfΜŞ=L6N:cmm̀p̺o^3vNu7-kNcجǿr+S^ʜS;M8:jL2oo7&SK唯J}A9mA^|ns;smM}Nmmw… >|Ŋ{+VFN׬^ ionYwU֜m33M1?Z?RoK'uՒm:mMʝe.?r/ COC_uNW+Z縿lBJb„"$[n[iF|}}6mM˗/h2VP>֭EV8`&s~rbڴ̘13gݻwpg̘ ǬY3b9s+ ̚53chE3gRoƍ&%''lRs<癤Շ۰p+j5'Ok>˖-}__7_VnFr qqqduga02e*Z,پ}FBBׯ;H?~ZV.HKK޽t:eqر3f,_JEtt">?@e8p0)))סxƏرѣ˖-_3ӨQvR2ec3Ipx<>#G1v8Ϛ5܄ljO͛7X"z=|s(!};fj a9cXd 6lxǛ={?~ (@}x>jV! J~xBdh,j˟??lٲ ah"XZMSEAz u5\%\gΜʕ+ٲeb)M_:Sܹs̙3'N1_ɒ%3#=ϵkGqpp`Ȑ!+WgC͛77GfLD gCs+N-,,2bb?U*رc<Ν;SQppprqFt:>>޼z ؿ?D||xyGףhLD@ BNHbb"3gΠW\/^_ f(wXj%$ݻ֭;6lPV1b$ L|ر0oƏ//b0W;v,>:uzwoǛGkIe:w pqiNڵ3Vb=/+++jԨF[z$,\t銧gz/w#66/$};;;RSS;wN0_XZZ2u4z >> ˗/$I#j[\&s'7\pFZVĔqhMV5}xOfRfaaaAŊXd1*3ffj͛K߾0 ?իWy&6m͛$ٳzJ;JrrRt)))ʰ6ܲ޽gFe5kFժU9rwQC 66Woի`tyM6cĈYwQ$$X\]fv,25k~̢EAaf}6OF׮]`1l۶+ӢE l٬ڶm6݀cǰhQ}u6al Jll,+WV<|0_}p+]q^(ČJo)H9&2g_V^pe+aR0_9A||<'O}f{PM 喗muiorm/\@:uY1ߤ ymfv/Vm`0nZ4 e(ҒiӦ1}!fD.AK &!4$ɭ2D׻H㏄iceeFQ|~$ gץZjfu|,F֬Y 1B QZnm(R`;;;Q ?~B6yф@ BNbt`*]w ]I A,vd .̫W2sSHKKp@9?^OΝ?III<{{{Q5U?,4oނP||:tEA3cعs{Tr[Л .0o\S?IRQn]krO~4h@T@ W B O A @9@ r@ !@ @ !'@ BN @9@ r@ !@ B @ !'@ BN @ @  ANؠVeQ࿎LJJ z^T@9ߟK.1d`De$[ҥ9zx!';ciiɀ駟(\@@a0K֟q=}QY!o℈1!Gl.]$It:QQb_ѣGP<(]Iϟb(X@hm\]OE/"99HΜ9_ojj ^B%$@ B ΈODV3x`Zj,>Q̙3ǏSbEBCCiҤ;u+~Uc\LHHj}͛7UF֭[[3gFՕ]WRN:ٶmE}@74 ލ?Æ S~KHH 22d^ gΝ+V^Eժhٲ%'NZj4k֔B |r曕,Y6mZcee۷o#00>}z+i ĉ:,X2ePlY:wl~:-[TLxx8:.ǹwG$Oٳg)S2ePZ5VXm{ѤIV-[/8W~ƍB L?lᅬlʗ/ORRbA4`*UdbEl1ڵ-99J*uV*W @hh(uԡEp@v!Qڵ+kסv:ԪU;wJ "#ԩ;w7tԕǏѼ bßNM6={I9sV.֭cȐ!|iS  ,-N 8913BIhZ֮]ǧ~JѢNL830IIh\{y-''7nݻIJJbŊopqqɑy9͛УGug^L49GyjӧOO裏QvvvhZp}YJ,X ,ȀxbJKKcذa:t1WK9Grr2ݺu{r<]E/K?`J靕^϶|cYE`Pg?wEF(]4/zۧX ’%KΏ3ILLƍk//<[ݻGTT5ɓ'=zUARRk֬YfPL0aٴ>}ۍR8wǏW,{lٲ<cv9+AAAT\>|H+XnG@@'NTJR,haccCJJ ~~~+V… wߙ7--{c9r27P`ccÈ#b#h~9rd[y f2;v@e?~Lɒ%3gUʕ+3h +& @0f?B ڵQF)߇JM,0,3r<7k,O|Sqy:TRPH>3Ο?OBBEaƍHDzؿ?JYn7F$cGU>E\ߎiFvZ|}}$<5 &`ooOŊٸq($7,,,gݺuHUM6&M2_ePT$&&5Ŋ֭[~FAlCCCh4TP[-׈#X`=22۷o#IϞ=3YyYz:t( 6D$N8ATT>o>T*+WϏq)&nZAW yo3FcIGrWd%/G/_{x4nܘ[Ç 0@/'dF4 'N\r٬o$|iG*~D x {wY=""eBr9,ӽ{wZhAZZ@G$9mŜ @F@ BN @W"Vz KKKWP b~@98f̘NLL Z?IRaggGf֭R BN!661cF3n'%K& BeRRREһw!B T*n—_~ERCO bxx}U ܹsh޼ @ASp^K ߍ$I`ee%*C keZ%FB bNԁ@ B @ V իTf;NǑ#Gضm[Ә2e2666y?<<oΟ?Zb)2uMm"IӧO{ajI{.湎Zo,,,Oyson[^?gҥoΝ2<|2Zm[YYa@,Zx]$۷o{.Ӆسg8q8/ΫWjΝCvSZ?TW^Ajj[ 877< 3Y9r[;"coQQyFÇ39DgDDDVy:Lұc7NM򎌌dA <0~gBmܸtwVKFGGa0)!';a0W>.]DRq>sr e{BB&5Jő#G^/MM2ZMpp0ڵkT,m111aiiiFRR={V9AA0mTNŋXz$ICBB"QQQX[[3mT$I)?%,_P/^D8y$ .իJ/_חm۶_$||IJJ"(((c޽Kh$[nÊ+PT$%%X0kL,,, эe9sXr%{̙3,\ .mzEضm+$jfժU sXpw.~~,[LIG$Ν~ p9AxbgbӦMlٲ??_ qł~֭[Kjj*DGGǑ#pB?;wnڵkLJ[y)z!׮]SeܹzT*=zT9ի,\Ǐ}h4XիW!˲ȢEA$%%lR^|I@?h!y2խ[I8wjVǁktރe˖cRRRĭ[ yOq4oނOرȲ˗/iڴ)Zڵk>}>}rnܸN# ={耗|j5za\1 O8q>}rq_,ˌIzz7y$uNhA~'\\\kђuڵ 4X9իG57nbM3qa~1L9ٹJMrU$Is\ fxzCբhw}ҥ\rIFo߁(ϟKΝ fѓCBIMM%** Y$00]r!RRR>{O>cϟҒΝPLY&O%@Ŋx"$}6v܁$I\#E:h{>|xܹƍҫWoN:ř3ߛ0 ܇V$v܉ 3fL/RJ̙3JEtt4/KC RRRPT߿x֯_&8::b0ptt"**ŋG5i׮O>CWލ1'ԨQ?om8`00tP퉍EV{.NW^aB/Kԋ` ܼy>GGG㉈Pʙ5BZZU9t$a0 gggz=cƌ\r|ùLa^EN x ,Ӡ'Y?oի2F֭=JDDW^QQÆصk'Ŋ392Z~)QQc u_y7駟rmggǫWth4f a÷$&&:,c^b0hԨ1˗/E$V\IÆ sOe+˗8}4Xm}iv`PbVo~R:XIA^rAQ|j5IIof.lڴErڏ;vdZMJJ IIIl޼ g+Ks6mHѢEk=zDl@h!rD+m-E_~ȑÇyqCAAAjiժ boyEx \r/666G_<=zȓ'M</FRhQm۶Z"پ}+]t`0V"\2C^7͚y&s6{83M6c˖8::r=ۇ~~|ICOxlitރ5kVӺuڶm… /rN7oA$.]JƍY|g:t4 111 2?J% B ޚ$e\tt4`0`gϞhZFKsjժE|3f,sѣGDGG#2ݺuCR1thӦY+Qf90eTt:IIIf0~sMe(/99I"8I__ҫWoRSSڵ 4Ptg.,|,XCsQre\t"#G駥1}TtyRDFSΌ7d,ˌI`` [nO>a۶NbE6l(_uy;ܶ{779sg>>ߜyLy;ל&m'oYzxOƏMĽIjsk[oik]7-۶/sm':B <$KYt2!?1GNs֭[ʕߠR**Cm%dzdwvn;w͞= /xy' Gm]bΜ,22˗GZZ6mÑ#z=y`2ky,\@ŋLґe sN+|֯_o2 h8v~~]6OAT*I֭a4 sY?N>e i‚ٳg7,,L<!j{{{:ű/رc{\p\oKĬY3iѢ% 6{yIIJeKḹ g!iaaaAehZ={t/^\9#2qqq >ܸqC@׮k׮Lw}_/:Hrr2AA_+++j5ӦM%::eBd }u9^Hwŋ9sݻX((fi-Y5kd %.11%1얷w6+$I̜9I8x666lٲ9hүYjIk֬& z=t&+$qppJ*!Iut.Ϟ=|)RH$I ԭ[/#|_ 0u9w.~w 0x'''֭˅ !^|Iҥ)V"^.^@z?)))z*顴 }u7߬`ٲeZ2={g璸RD jժ?|BY(b.QK+x{/ʶG_m̙e]gV\WWWʔ)ùsg`Μ۷>>hiRRRӧ/~~7@ ߆VڵktЁzsa޽C=ٴi#O>ѣGl޼~b0Yj%$q!_矷7[کL8Mbo_3gV`۶-WgmmMuVdTX/_iFGJJ ًB ѹsZ-{ҢEK*UȴiSM:B ʈ#HKKΝ;+Bqzɞ=&KeNjj*nnIII1:<=Ѵi3WI]p̜9ʕW_`W6YX8VZl={6 x1gllmmt(QϟgD]i׮=`0艎 KKKk64,k>fT׮ӧի7G[hZ޽ ۳`A ѣGܹ3SNQ>pm۶x|Ih߾w#fDDDPhQ^zEPP }Ce^zŠA[ ~z-:u =zرtĺeiiZY$'1ɓ')Ξܹ́!՛ ,, Fԩ3JTPo޼޽{ڵӦM5GZZУGONsq/ U˲ ~-$VyȲLtt B 8;;SL %..o݀TRo$$$JxxxPlY-ݨHJpf͏i߾=O<g.\ؤر#v$66 p]j5C  jr\"QZfϞ`N>MttPVc0pr*5j)))Ǧڰa0T4# l\IIHľ}{9~8XXXcGɗ/?6|kgz*VhՕ Vl֘ݻwyT?3zKKK%}"f󲰰`A$%%ѷo_*U 2d6lX$ޅW!22XŇߞ={7n%z&6nݺIeKdUYty $2BMk׮>ln~3m.AܹlΝj6lX^g]t:6lH||I{ၳ9E-%-MժU=z3Vn=_VG|J@ "V ro ~ }hoIJkaaAtt4șAԮ]˗dtZ#:X[[gK`+GIeiWhQ>}½{Wo-ZTo2y4? dР&arLvKze[[l1'OyF}-FՒp\}5Tq.<3YM}3 ZfI8pmGGG&M̄ ]Zjq~+QkL4OqԮ]HB0`@`* wT\ ?r"[CL!n|au¸pbL`W 9'|ž=/^gpeqrrg^EΝHNNV,XDϞ=Z 'Nl6ӢE TUOt+W\|5k2x@W^!%%G @PLY}u ʕ+7}cҤ L8v򸌩i-Z?fyԢEKoݺHM^.cʢTV+d;|Mvgo΍7^߲|ros`` """ *~5jߨQcO t쳭3nϞ=mF6lȓO>I.9rZd2QpauJr^{nt܅bbb![s4 !xlB!CvB!@N!BH 'B!$B!@N!BH 'B!$B!@N!BH 'B!$B! !BH 'B!$B! !BH 'B!$B! !B B!${ѱT^s`ٰl*<}z3<֮]GͰ :udΝ5⫯"11'O{ѹsg}Q{]^z%BCC\9r_fQdIC&Mh׮Ço>X>;>sڶmCPP 5k֒OB!f2ix$QU˥OX,DFFPf-Vʘ1c0`K.%007j/\ds}C2_ Bq7f.]@|?Q\EN]]CTZZ|2cǎSyrtލ~b6}ݷT\ʕ+KX6^(üƍcvȑ#^~e=Bbh۶-k֬ɲ~ ~`٤N&իWSLZn *PUUpiBBBxͶXⶃ@ 8N'GcС]OB(M#GK wj֬ڵkIKsQڷo֭[7nÜu֓ދ1K,4e^gyAZRU'y Zӛ:ja:9B<|,:%g;Sjժ:uEQ|cnbt9)/uknwܳU!Rn^)SA垝`wSx 1<`tO?mB|uZ ݋ݻwSbr{#QQQ|P}]}Q?سg7 ;v~wy9s0x`K֭8qҥ_$ XN>QQF3bp ";U!I O?@ҥj֬I*U۷Tjv*WLӦM(_y.]JGݾ+z=F2eHHH௿HJJrF'O2nX~G*BH pI1]4E!--clѯ[, na0ݮOϕ+H_ӧZ-Pnly!6l/̙3e !=< $$$f{/bСC1iҤAOhѢeקN/VpllҤ1SLfrw-;T!@QZuɓ's}9r }mz䑆:11IO VDקk׮4o3c /w9T\^{%B(] Cbb^ܹ8v6jժMDDP!>&)nCƴWSe F#COi~5^תb2Z.+^qF#˅hkXd2cB!$E8oTYsfNƲJu4]Yr̽fA.!B<ҪB!rB!B9!B!B!rB!B9!B!B!rB!B9!B? h4bX0$Bܟܩ Ҥ1rᑔѣxvCDq_2 SN7oᑮP 鯿Oѓ"E`2a!?iFBBv֭۠i4@N<^cbѨQc+_BU@@#G~L^Ұa#Qă{&@n۶mԩSG !'O1r( )))٤1 M0r@N<  ! !B ?EQHJJbL6;@ N>1  VqcoEQ?~VqۥzWv Or6m]+W4L- |YfrI}DEQϼ2eJr sa͚wog̘l63ax.{]xx8III^;~8yʸqcQE_`Z;R ĭݻw3t ~MsΝ#&fY]Aӧ3gniڽ̙3w\J۾f{$,loZlyW7hb$$$R~֬YͧNߏq2۹gƌM xꩧѣ;7nܸc7y…^)™3gI=o???=x3 l}BH '))),\ɓ+OӦM7nÇ̞Β%KZF>d L6Ç'͛73m4XxQQQlٲc0իWYp~r͢Ezɮ\ 99ӧq!^cl;̙38rORSS>}:NKKci@{QQ0wf pȟ̘1 `4Qŋa6=Y^'qF^,(,_ӧO( k׮eΜ(\EQbX,u8qݿxߥK`24i"&Yfr\.3fLg׮]3fLgǎ'ϟO'̙36m*1m4TUeŊ6Zƛ/DDf˖|g˗Ypܹsz*_|\g͚ɜ9Q>EҪU+}Yz}ǃ^+²e˘4SJzeѽ{ۛYBCg=ӧO#113guFBCgy&ޮ _˴iSپ};0m4Nĉ42mTmۆ(f&Nʕ+Y<01c:dbEd21o\֮]Q={0c Dŋ={푑\vYF2Ξ=1(``ƌ~E;?3kUOȉ+WPP! jAF*U ΠAyWy<3Dll, תfcM+5d;w޽{R *T?$o޼ԬY9sZ4l؈GS˕ȗIFѾ}{v;FiӦQvm"##ZbgΤƞe)T^0f̘AZ?>VEQԯ_ s)\.';%Ja\~ӧӸqTԩSn=EQ=z =M͚5=zGfHH}-dN#GCP@*Uzt:p})kaݺuzEaǎvL&ǎ#)) Gɒ%9sM4nO㭷B4HIIᩧbҤIrUH 'Df.KL&͚5}С=&'..ƍsaΟ?brᇽx$&&b2Xf W&W\_'x%Kb t\pUUQ|aɛ7/f~a#qqq EBO*ѳgO{yRRRG*]tTRлwo>} {QXq:wW_ҥкuΝ;Gpp0Q$%%t vOeѢv=͆l&--UVѪUknŋ(X NF2exV={44Mc˖ڵKosMׯ?=Nwoitڍ\.}hѢ\.=ҥ+EWެX%. @jթSIIҵk7 (HQ z:/_ٳgQUMӨV:79\DBOŋ у2EQ^nwp5ev׭P{ѢŨJZݣNzXbX,QvFѢE_|Fd~e7oF4ާ/\H" 8˗Sn]6lڵkS˗/'W\^+bŊ<裀{ [.իW' ZM)R{ׯw^~E!99nݺQX1qp"\xkҲlڴTU%(( r*K^Z5x5geV"K\pFrr2˖-G4N:ŪU+q\<裼F+*IK$))VZ'iłd2*H=Jjp\^':UU >=-ۓj@@WlwQ\\yr~U O ҷoʔ) /…>ԫWo;ʜ9sx'OԴܹsi׮cƌFQ>u4 .ޞ]v  Z&$y}]s۔?ˣ2N{ud޼$$$зo?ѣ;%Jॗ^VlYo5jG2d;1s0x)֭[G=ٳ'ŊbŊˮ A|`>h lGQN'-[%4= tr `0p8#))- 88XMK>y$ثѽ_{``;EQp8w,ԩSHMM壏1h@.]o7eܟA̅ Kܹ)[<8L&'h4;wnΝ;G`` |rOAjFJPpa:ȡCNv{Q3L&RSSAF``qqq+W./^_z@RȾ}{9w\PpF#z5󥦦@Nپ}I/c: LIMM  ,h4re`*3LRRz/\O͚5gF,YLBBǾ25M4{9M\z3gҢEKE!--{;3-wv6̘15k*& N|^T\ٳoP*WitVիWOw[+\^`O'VkԨ:K:m6 (PUVz8g%((D^l&((P^SBH 'kѦM4hHXX(_bbbhӦ-=z$88.]#0a<͚5n(( $%%|r{\x5OR=z2r֬Yŋ/Ț5yGtƍ%(({0z(ƎMz$TUQtoӇPq~:nܸ1LJJ_>11ƍөSG^xE^~eTU#%%UorEܹoWK. 2')֨HvB۶ozfJzҥK( /Xҥ'f˧NbĈԯ˥_rf~?m[4mڔN:BJ0ӗI&ƠAQUc d:(B>}?~ =n5sN?Bݺp\ 8HG*UԩS~#W`6m @%z*ŋf+VΝ;i&Zjulur\ sZ<Ç㸹 M0{O?{ xGNLve~/?Ԍ3ó;K2}w};zd.3j2|w>_g9wyv2O3 .]bٲezдrJ-bpE.]KXd>7l ::???VZŢE}UX`{* . <O<ٳ+(̜9hL&ǑȌR71+֭$&&_,ˬo_dd X6s RSӘ=;\n:T.550}ڦM3' ?^^I 3gN~s``ɒń꽀9^s>39svaX8yDzBH 'TUv:x$ j1bGlذZjz=o۶oxU7o f͚EFS1c:Z+4t:jcvlWcQ\.E%%%P\.dɒΝC{iM4jаa# }( a\.wMUUΝEUU=zsX'cƌ' ))Yf ҇.)PCOƐ7ԬYqz]^~=}gΜ>F,YDԩ˷~#!@N/胤 j%_|XV @yhР!g}'L24kHҥiժ5Υ=z57oAll,@~y dѢs@n޽+Vܫf3<޽z)O}RtRSS1|WןlLQ{P@ʖ-ǛoIll,)))Rk4l؈+W`6={ꩧ0(PH`lٲ7StβV+*T୷s ʗ/gΜ߯]u#!!HPP;w':zW/](Y${~SdI{9v%(!U0iڴ 8NƎLj(R(mڴh4cùGpv:=ǥr^&=v}#.g PU}qȟ>\XQ='`Yr,[}?\܏nIIIӛ2es%s1]W <-'o懪j̘1Gr3tGXh˘bH4/z@PP.>x6dLѕQ˽ce?S/ѣ'Ǐcƞe) M֘&4 ΝwA^_恆BH '1ALy/ FEǏx|1$$b/~8RR?ɓQ\.YfѼys @=4ŋ{ lY^gw ^lYBBJxӝS|'ziT^pzITT|dR}Nӣ|%tX,\~Wp5l6ݸqMӰILL:Nf3>NKKz/DD̦FߟݻyCɒ%9}Ee zx9s/gBqwȥUM0rt:o̙3|٧\rʟ??rѽ{7jժEjjGJ._UU%((?e?=m2s zoqiUUU4iʦM|ֹQFtE?иqc̝;7zjLI& ݺu孷{4ʸ~)AaJJ V>ѣG3o<^j3c6gy]РA}wq'}DEECp\m& ̣S4o+@{V|$s *w7OmF|BHRt=r+^7h4DSdeNS1]Qܯ3IS=eG2v8eq7` w>?j4Mݕ1-Qv雲jRe +k_)r#G`̘Ok̘|0BkK<)GN6˥݁@ת=Qg/uO*z^Y5ݓVO>Yr+VTc٥SUeV饲KNJ ʂj1dP!sȉBpp0͛}UUرS9)EY,:tO}߫UZ {Lߺu+ YW_}h}vޤw^ڵ5k;#TZ%Ӳ5 ʕe=MZYc08v6Mt!d7Vj[Xr>mɒ%9rK.~L&2qq FC1իܹM<Ӷm[CΝYv-6lر{p!d:־~:kܹui)))r !eoʕ+9cپ} "...GW;wRfM6n[oߟcǎѲeKlrѷo4iLDDǏӬY3V^5MC4k^JiѢm۶%O<^7hkY:(П&M3m4xwTAAAhhҤ1SLj'@!@ڳg7}pPD :s=Ϝ9|]f_Y`>mڴԩSTXM}OTZ`/3_bu:8/ߥKzA|ufr,_b߾}z{n-`_ȗ/K,d21kLe+֭[3ޟ37L&};3gǎQ36̚5ʕ+ӨQcNJ׮]8u$:z۶mgnUΉ'8|>O?m(  6{rj6!AvuQc0x>ܳ> ƍTN#G0|-rp\Y7o^nܸә>ѣIIIraiݺ5k֬fذh8d`` .3˲ݗVISUqv*Tcǎ1}t}]4h=lrקuЁe 8Pǎ6xtRsv*ŊPe(JzoaGB0\pM[LÆ]O?4'NDQ!--xj֬r%rE\L2qc)[,Ι3o.oʕ+k[ӧ 6tm Ow%(X,tw)a:uj׮iΟ=z%Kߟݻ ѣG}.sرltG_תUcǎ;v,m1˗y1TU_>'x8qu[Vkw勈B9@4BBB(Zk&>>^?;,ϗ_~Xi̝;g.JbB im۶С|yY:/_$( 7ofX,f}ڱcLj`L&ׯ_l63ed, +W2{l~72].flN/\QQ v;>|X_ݻ={>-99fj-;eRSS?۶mcѢEX,{.]d"--yiӏ,^H_k.fϞ͡C2̉"&&a01c:&oȌIMM%22{}vΝ(DFFx]FްEQ0M|fg׮_|sK.eɼRyT{XvPhQow̙Ş={8ٳ=ed4Y(ګСC̞=ݻw1NG˗ǰpBKļysٹsyoc֭~tb0mTl٬mΝ7iӦ( SbZ6m*6l`ժ^gcʚ¶m[3'Jo!r2L:P&N@8 s=bfرX,z@ʖ-GXX(6  ߲|B6ՌFF#bÆ 48˖- vƍ)[ cǎիL:U_#>>Yf^J>1TXHd2*GEQj՗ i'Nbŗba߾}|Tŋ8{,.~' >:a^Aɓ'nw?~$&&Đ!1 ݻ+rDDDp TU.\+r]rEȑ#L8+`F#;+W~I 2dWoXؾ'~a# 4hije,Y[i& ihFRhժ=;njCJM8y˗/J*]54p{qRRRޮSNtT8|0ի`0PZ5f3ÇD 1;f߽kǏݻ*&jժGQBV^o`00x }4Ŋ~zƍ+T>d .]׫WHNNgeMl۶s)\.'N""b6+ʪU+ٳgcᏛmb?`0pIl6*Sy .\@*U11i$Ξ=KjYk_ǤIQ9] !TU^z/^ȑp:\t ž=9wVyQɓ'S~}, ZdBCKo>ʔ)64oނpBj4iժ۴lْaÆs(={{ܹsNǺu)DEESN&O/4 4i҄ҥK؂(6l@ѐ̫nݺ9ҫ~ݺuO>?S9sҩS't'NvڪXJHHlٲ۷ҥKaGN֭.11e˱o>J*ErrժUC$V,$%^Gz{~4 UV9|+F`_Fr(S,;wDq^:Lpp0/^Ν,N8u3j4N0{,Ə@`` w`ܹ( >(+V@ӱv|!K%?~"?p E$)dS#iР!Z+hڵkС#]twfӳgOOQZh޽{S.ׯ(tb!33sΡq84oނtzbbbh޼ϴ9>Ym#&&NNJ1>9fxaFx߳g'OGۨBJ*,[ 7Yh;w񈾺_n(;vVdees kp%n޼^"''Ǐ{d2/p)>|s@nneΡ%K>m۶&NGժؽ{Lo>FA.ݰfEa  q~DDsVZNcDDD/CQa '99qc <=zbXȶ@ BN;r* 6mHpp0C ei[>z%~~~ 繢ۊʞ=ڵ׵:5?իWa6gҥ>|;w[neȲL͚56l(%Zv&LȺuxケX,ҥKWM{6L<39NGnq8‹,])S&SNz6*3e˖o$ڵks j׮F:v<Ð$}bZ`14jX(.:uϐ!޽;ŊcY_&Mi~kذx$y G=\AeՂn6V?wߧlh4mrs)*ʶk:Yt:mBtdee1{,f͚$Iyt8j={]uU?^L׮ݨ]6k׮!/L'k-˲"xE^ta\}~} wsgpG$>h>A&rG!I ,PK֮]ŏq-ھv?XsV^%& r!//_(<^"bpyO:XckܱcGNC,Νh? )m_I(kpmݺ͛ r Ѱug[NiZdOܹSMbE fAllj劺z*X~= |7|Wkػw/qqQ0gIKKhbbO?pOIJJndj{;Fzz:[z;wd29-zZ1dESZv-Ngϲm6v؎fcU78Z-[6|r\][~F||.EXŋa6YbmWjj*119vXi=JLrծ-11+W믿evޅ(,_ Fþ}ćQ /KewޙNc޽|A"""Xl."#RJeL<~-*T޽շ*gF#ёԨQo&:իW裏b2oUjԩShZϮ]W/9s"R*Nh4~Ǯ];hիr VTʗ/ǵkYjZ^j_~A4Ǫ..ۭ'NnC8v4 .lf=z(_~9͛7W-zͻᆱvܙSNa0ѪU+z=ZQQ6mʚ5Ȳ̱c %33ŋ!2/RbE]s?2w5jv?~$""r Μ6駟شi#͛` 9s>}2o{zݻwD$ڴi +!A h߾>gϞh4y/^PlY>|Gt*VH۶ѯ_?Μ9C2e$ _~ƍùz*Gp\`@eFM*UeRJѸqcBBBۧ~Jdd$ 4P.sJm۶}}u!..jժ@ !'|26'StV M2d_|9ʗ/Ϲs狔~޽WK׳ug 4^xɄNce|>8sS,X"kØ1c<^2d0k֬h4r8u$vMt^bŊ+Hݙ7=^{5fx@^xyyшFÌ51cFT:ǏӇbŊKV3g|wpQn~~~DGGѶm[ ĬYHII桇L4jm޼9cǎ^z3&_~OzPۄVaܹ4k֌y-^]NlHO?fϞٳoC @o!++}vx6d߾]!e=sڮ]hڴ)s0ujd;L^Ê;wf߾eԨQ̟?Ǐx5bbbT.^D^^v=_l=.$IL&F#6В8j^oPE۶(믄':idY`0`)Y_Qʕ+O߿DZ!#6=0{ //O@ 5tz.]:O׮=|݄7WÿVȵoߞzIZ~'O믿\=gϞlygm۶h Pݻ>W_m#''Gor*BCC3rϝ;ˤIY~*=fRJf۶۷Zf㩧zwzͣjj[_OȲ_'::ZߵkW<I&\x]ңGwzA&M\KZ9rVZmWX֮]K7=7o_|_|w}ٳgؽ{FQjU*W~9svm,v3zӧ7$iԨo1!C3u ><~Ǵhтb0ٱc:d.$]X6dmv~vv6-Z4aÆT%J`,Z#FFIHHL(``aueopعq^?cmZO@ U(X؟1A)55U $<<;ۏez=:YZ(NC{p8ԡ\t:~~~z)cV_`q8o:V+F_\=W\i5__``zN@@m0jXVun㏷iӦ5ηj~IOA yg{l)!3W]ɓ(DO@ 5Kf>Ҵi[KZuaټ:n:lۑ$ ÁdtΟg2 ޭ^~.]Ǝǘ1 "66Nc$[^o`⸱<ճ;_~D _ȲcRNt?&Rڵ+7n#I}[Td"5lv*Tnl?fொ$9yrs/v *(0ܺu<WA *dY 23oSPIsBNRxvߟuD' _n~F\t+&I 6%Ȅa>Zwpk֬]Yv>Lwer-_NGLLL#!! .j,_뚧`2 oݺuYjlRv@ 77gxÇ__w;ҥдiSǟ*h44i^^zk߷e'z|qwEرc}?.4l:YVQƿ[޲,^zL~(5kb |ȑ44NngݺuiذM6#55FӰa#F#iiiB B ?j,Y1/X,$%%rI\$ nX~j~ IDATg4 ׯΝL>lY.[zE$I^g%h4t"8~o%..ӧOyl69uK:#jrmu8$''qQXl:xΟ?SG!66~ĺu)|Wlܸ^$Iӏ$$ijs 6C[͟jO<>d?l \rū-hX"FM{q555$˗IHH`ӦMh4r Z-:m۾"6v-G_#}6sϝ;Kbb_}%z^=wӦMlܸQطoݻ9w| ֥x'aFk׮bOZa~9rVKrr2NDՒ4qq=MذaZ8V+^ ???V\Ch|>Tذa|slߖ aunJzz:pf:I[T3f,5":%L)VZѰaCv;',,LG!11Q֭[IIIw!Dtȑ#̛իW?tl6QQ-[oZ-/v̢E N:PFMv;Q+WʬYoh D8r |C=?>?\?ȋ/bJ,YQQrP=|9k&&fׯ_qS)Y$?PrZaZ-G\2}4ޛGժXlc~`˖Ԯ]իWrUl6~-~\x+beҥԪU/`GvNի ==]mOTT$ŋ#(8Qu\2|ur`˖TRE.gggpB^z)R#wo&ժUc͚5^;vgϢjժ,]3fPF ֮]رc̘1j!11<$IKShΥn:4lYiܸz4 Ǐd%;;>x? ׯ_'55Hkpp0pjٽ{K&-- @tt4+Wj0k^Ѱ]vҧOUYV^y"Moҥ(¼yj̜vF!k 1,2ÇY ֭NŊi߾ l6p8Xcڶ} NfC$j֬hG͛ Ο?Gvv6.]*!2#GG%;;^x֭%77 j$I"33P:wBZh4RRRxJ8qz {M%xN+"( FeVf^|ZhlFՒă>ȉyY.ŋc}K/#2fWrUTg}ƓO>&((Hc:t`a9~/_b1T5@54t01 L8Fϛt߿?M4aHh4dggѫWoz艢(jDsԯ߀#27(nөSFٳgtܹs>}һwvUrM233)[zXV֭[O)Y׏=g]@jҭ[wʹsg1 \v;?VKvv2 A8; 9t:l6+$( 6M})BRq8섄`6p8ȲVEQ4 VCDD4pt8P~@!,,! R%IC\\,}-jY,eYdeeѰaCk0yF:T>UՊ(y;++ $iԼONiӦ*5eMV(LɡC,X۷o:oD BBB Uرc\!jժ3ݖ-ꫯh֬WUW~K6mۯJiӦDFNGлwo$I~4hP:55kV/K|ݷ1Ctya%4W+Wp: ~d( gǝ%K9eEQp8> 6K/ :3=edJ,Iƾխ[yN7 >ʹHfSׯO@@BDDNpr~W9p`?KfsshѢ%%p999:uJϵg^yU6lFl6#HŹuUTjժ-JPh$vЬYZL`` 7n ''3g~/f=z&HOQN@@dfܹsjfބ<3ׯW" ea1SNz=YYYdeҥBԽ$HryױZw@~7ҦMbbbX 9Fb(Ν;>po+n߾(:v|Ip8zLIN>em۶ѿ&NĉǑ$ Nٲ]ω*Tয়~BQdY&//K.QRB\,u%--wrJz=11i߾*]۷X,>qI e^իWs'ӧ  ʕ+:uCtjZ233yi\z=zuV~:VEQ $ @oe4v;րcǎp@ "rxL0ŋN5:5ݎ,<Ì1Ν;ӰaC"**f͚(1SCQF mf"tg_`K pArssFnn.0r(^z)+RfM>|+]t"<߿?-dA1aD҈#>eիǝ;HDnnǿ]999( ׏ŋ1x ֭ĉ"//O} L&eQBZ9 6W^yĉ^es###=ݫ.wŋ1bSF`GzJ.СwAбcGƏʕ3fo&ԫW7ЫWof3/ڵL<Y3f,o!!!q}FaÆRT)_B$Ǝǜ9<3>EWݺTV9O=dqslWu8)x;& EQp3"u$I:^k>p\Ék۳,g֭[l۶aÆ믿p"V+Z9tkB^zd9qje*VEp85Wеh_p Z 7.[Xyk~MnvUou:NS]ɲj=du̽>(\W~|{{U_W+_m(Zzݷ}Qw>vΙOPK.%33CDMnnx=}|=,-.KkWYŊ#++#88qƻ^s˳=+Cн۲,{){Һ^o9>׾_ae_0ܾ*j;uM"q?^Xy$QX1Fg^"nj׽Uͽ,~t./v 6~+j5j cj)W EQxgׯ?5%ECK.QZ433xN5k$I] Wq wuHp!26~D\JJ@ BNõavLZ_ri9<=zCxQA%IС"هQEGv] 4h5N6XReH&Mt cǎ~QvB f<4 sE5r&2W^3$$ijuVuҴt}͛nf~>p"ׯrZOsY_|yuiEaŊ5M^^֥Nw-NaӦƍ?/QߒF.Z-_~%q8qҭul۶M\y۷M~$~8~B ټy37n/P_Z-6mO?U#VfI lٲ}y8':td,Iׯω'N/..]v~:( k֬V˹pႇڵke!{D42,^/$11/*8eڵ, 6o7?ݻwˡC{qU|>RR=6mȁ󙔔d"))Q8}4q=zħ6 l߾Ξ=$Iƍ8zf[ohh4̛ZpFS_$/WfɒU۰#GѢ;wP\9RSG߳qy:|׮]jjrIl6+|sҥKSr:?/>_CEvv6UTe򥪟~… mdg琑qƫFɓlڴ5jfj._\ Bb13}4J,Ν;շRZ5|s&pQE^0hܸ\*Ǐ%ޜ9 .^H͚UD>vddd2u$iq};vzʕmx"K|LՉdܺuk0wL&ǏpPvhҤ GAefxYV(]S@ BNp\vJ*ѡCbɏlB>}z-[6ӤIl6-[c(V8J&=] n5)2O ""C'};NǥKʕ'NUVR* ,CK8Eb=ؼy3sNڷFL֭K!**f͚1jh֭[Wh/N֭)[ٴi#n(}>,˼tY]bDbŊѮ VC53gxaA:N333tb~t,Ay۠Au{ 79ZdK^xիu_> 6 6v-CEӑ͛V/a3ck 䬇v.(pbwosnn:O544ˆ >ZȨQIKKcU\pw}O/eYfʔ:tuUsoDDNW'$$sΑˉ'Ge6l̏?ifdq#]FFʕcʔp"C?#/_zK3T!hCӑODDdddPJUyA}zViعs'*͛P$I[n̞=xnOllS157eu:=S:u*M6h4siР'Nȏ*>-r{L"x"رc4mLHDrX, 6+pyZ-GwfyfܹZ(B۶:t^ :t+WE1`M{W_}0۪9r$ѬXZv)Bfܺuʕ+uWΰaCT۷D`Ĉ4mڌ|ݺud2h7n=+V0`@Zl_od< vq{2ed*Uz+"7jh͛[oIXX6m@eI&1o{,Z0>JnɂnݺĐ!-ȞJ׮c$$s_>F<4 L>]m[0a"/bq)S???F#=zt/?2¢_ν,-t:/???6@2-PtE\erY|)f h IDATQkۗe/&_uw/ϵfvr eS7wOS_.K/_, sb۪`?+h#PFu{}bv/;,wwςEVAa~~ak[QsNQj ~X]"r/" %Iݬ|.*`>0CD< ݬV=}=]AeU1Ok%k[{a[z%QX?XQÎpT{"N V@ ]->@ B F"N!((;wm@ o[c㣨Ǐfk6PBU 6( xQ"H"R(HBG( E .%=6#M6(^~xΜ9sf63gy ܴ ΅20,,LCait؁ ;`T!ni?3ӧOKBH 'XF(>3H9!HQ T< 5lؐ&MȃBۚEB9q=&B!n}һ]!B9!B!B!@N!B9!B!B!@N!B9!B!B!@N!BH 'B!B!@N!BH 'B!B!@N!BH 'B!$B!@ZlܸN:1x N'i&ZlIBOHHo·~iժ Ǐj^WyuViuJdd$YYYWˠAh߾јf,]taԩl6!m}Mj8q>u믿ٴܲ(Ib,0xqRRR{iݺ3g~ޘ1c_>?͛7g֬Y:uJϼL&VX-~URK.sic&MēO>/E4wѣxWسgw}[%J,En]9r$jRrvcUhp~*-Uꫯ2j~2eJӽ{$CoB!p dhv]iMrr֭ETV^ðaYh1i9s6m>!<<7ׯ__^{u> ۷w޹z;v5kx:lUU4i"&'O0uԀeӧO6SbE4M#(?HZZ/^:tgѥK|y/6mxCFQ;–-HL/Bq'`ք|Lߔm޲}^6oLJPU'\|)z /Y3g6}%|Y<> n[LGV2c Zj\EQ4bŊSsԨQe,Xdgz?ŋQdIԩ/w1,Y˜1_Qlf@xb8W{C&%%RJQR|ri-a29~(y{Hiƍyx2| 񐝝oNۭxp:%;ɥK)S g8*WzN9KǶPL<|B5f {G*|׮TV#`rFL<Ԕ[ظv(J}rf ݿӡd`W~2tceff+\}eggg^ׯSw2n엇Jv0MD/[dⓏKwUBq4UvSif7BUU K2y?p7dB!nUtu `r`gnʔ)@M1 p)Y4B$'sݎrO+REr*ө6JI!]Oq|7SOUە@Nܐ|~&VB܍BCèT?zm@N քB M.Bq{@N!B9!B!B!@N!B9!BqKGh4bZO+MzBH ' .cDzw/de٥A%Le_/"J ĝpбc4h6MEq[z={o_̅re0ZJڵQy4M[BZɒ%:t8۷f Fwu\@oir Bqg4Kc ĝKQ(T4 L&ȉ!C! !B ĭq \p˗{kv6 ̚5reϚ5 /^%##+Wк0gf p8Xb_EQ7o.&}N… x<ntѢ ׽Ͱt^jSLL .0L̝;ƚX,޽(N>hr 3gN2ʒ?BH 'JazFP>LΝ͛;P?{,3f|Ov={vpeK~\.vA4ʗ/So;NF=s˟?5C~۷oZ&/WÍZEQ0$&neL&FNBf""JpƎ DeKB{.lC,rFeffØ1_SV-7n /Ȇ 0 |ݻw*QQKQEQf4nʕzf`0kY5͛"*j)II'N@Q ̛7LLL4'OD48|d2ůN^J?=:: M8s洞ۼy~br˂ `%$%%t֮]/(Xp~A^h!&(۫/k4f37obɒݛŋ0,ZSNh"Y|Fbccٽ{K,0f6db~嗀… ػVX&bbb8p^RRK._s[d1V [j%qqqDDGGvfr^f6n(}_Ϝ9,[ &&G͡CZdb<[TRl6cٲ}V+֭ͷ>{бc'{9z4v|\ Ms>^/xxש[.gΜfԨQ)S={v3gl`cӦ^^`soe #ݸr aaa|XVʖ-bTRx^wb%9 C  ݻGr%7/_DVV`޼y$''3dJ.:u 0bp)JϞ=XraÆQhQf_ Z\9/^` 33V+eb{H'==$fΜDFvh4bgҷo  :0&LvnbF[5K3+ BbŘ8q"fD Hř4i"ΝCUUuʕd [&W)[,݃(+w^;v*[$#IL2M޽n={v}|\RRӦM%"$ƍhJbb"Ql {j&^rm+9GniYYYx*.]d2] z u[bZv:n>3jv4mڌ^xm?ezyۀ&MpeF#GSZ5Uh駟ΓomHJJiР{4k<ùsеk7j֬IN={zWS tԙi.wm@ӦMs4n܄P=Uzu>|-ڶmKRR`i}5 `fw4k֌PXi-Z~iӆ 4Mc޼tБ5kҭ[$gϢzꨪʳh4BMh==v-N+W.S̟ _:⋵(\0^ ж⋴n|UV s=hnm8r$ ŢKO|}; ˰ BdfQRR1-.Ea]Qr:uȃ>Hɒ%e% r+_NLHHjժxX,=Ӱa#<_ .dӦRj\ TUEQ4MpХg/_^بbM1lG@hhX/4M3w\vȑ5j$ᇭL\P臆^WEpur2^eAff^vrsů((JNŧ(J|233)Q"%p8H}mAFFFڵkZJJk0mذŋQRkp222(QD,U}W۶[2egΜaԨ~^]'n)(QB?Or]*T?W_E4 SLˬ BHFN,J\ f3gϞo'ʕ+)S\|*O<88ZhNPPAAA9Ĺsgi5k0`@^xEEtel6BBBHKK'SիWѿj׮s?j,33PwɥKѴ@+::GȐ!CЃk]~aoF…1L9sTʔ)pyłnB TTItMQf͚lf̙ԪUm\;\pɄާ?s`0Mzz:.] ؇駫1{wW0v2m6pA3gl63c yjp$''ҽ{O}z׆W\!55%`(..nݺ{򕟻]|ʝ{|U{ٳ?>O?] MP͆i&~i8|mk/ǫ+WSXq HHH<{ @1O)𡑂.7GV2e^:Νͷoq$%b_~ٓL`NnnIY!$w5_fرn݊3fСCG(]44oa ^oшҟPΝ1F5={0f̘AnF̢y0ԯ_M#` MO0a_~9o߫TB˖-X̼(G}[3QBEm:8ꫯb0(|aK,Kmx^-J˖-0 z[眃v<m|رж'X,#uԙsҲe _b0fШQC| GGFx7,DΝ;5SjURSn];lw3oނ 3W37߭7ٌ`Пf9Lbb2`p`0X,_h<VU߮o}M(ܟf\.Y,Ev*AAAz u=um[pO{-{ݮݧL:Mo-PAoZA~C=2. Պ Zv;˖IMM[||㥙L&F_r N';w믿l6᫧V]nGbѢE4oޜ,tQF Ҿ};Ə̻_}^Pf7oi_{gzٲe<<ēZΝ?ѡCW:t&wriivv6EN~g >O?mGR䏽L<#' 8i .|y[i_h|oܟ3._U .oY_߶3׻{-kS_)o~o':66N@jj*M6!88MQ_@@R &Mtl`BHFN9q{*(kwekkenH蟮wL}n?w2_FNȉkM={' s̾) Z?m(̝;7mYYYߔzdgg3 YrpOEpt:;[h៞aF%K~{^\.sι3n r2eN ℸkZ׆ 9KBBB8x }(xM'0L,Y*_=7o_OLhTҤ3vW/.vqC e˖Zn?]_!$s9StE3fԮ]&MP ~StEGG~zL&q"**4l8q^0'_vMTTgΜF4ϟgv;F V^ʕ+ٳg7stl2@3zXo:+EQصk~:EQcڵ֚$::G?))o (} 7rY~11x^bcWٶmV̗!۷o…İzji֯_ϲet119r$`i&rzEGGeKqqqf0~ŋv֧8x QQK9|5,[áC9SLҥKٷoܙ3gXl?^gM~3Z!@Ntמ FdR޽s`0c:v7l6zLh42p@9r$Wߊd2sE wT۷hB-IMM!33( 6lH3QQK)^8ƍʕ+8֭[bdRlْĉ(S v]~ҭ[W@!1q~ y,\ bPii '0|0BB 3nܸ|Sx-ZHaX,FQtivIe/NNBRa?u/_ѣL>]x=7dj4W W`fРAeܸnۇǏQX1LhDUUHMM%)0'OB4"#t:ٷouݮ͛0?L<EQ8v'OxL0SN\wРe:t%J0mTI/Axx8Ǐg/ Ӝ={ &!-KZ*h.M(Wӧ{9NnX,]U'hѢlDo`Zbr ,+h">ᄄhbׯٳҥ+ׯcҤtܙʕ+sE*TҥKW>aaaԭ}s%3QR~}<m~(,^H+FY, E}}C@tFtt_0jzU^/+Wԧ*QeʔчxgHHLժU 88_TFז穴knjoîNƾABf ^rr ooߞ.]vC͗Ye|-v;}* 4@4~)ނ=|ӭ[$zsХKWʕ+}̙:thڶ0}}Ç&&MJvvtlo6ѥg@!Lѕlq۽{7iTTY+""/+\iPd.ŋc (jX(] Ѡ{x<dffRX1^/ŋ#;;%JpY`(--r۶ق ol2bŊ .+TH+V M(^xe͔.5˩oΠךZy̛7;v0jhFТEK|() ~( 뿍՚3o)SiԨ!#M z6)S~X/{l޼ѣGOGk@nʕ,_ʕ+=w}s/+_}ロ#d9S[s%p84lL~ g^mn:,Y7it7o.u;hl2'B9q[kɓ'?oѣGˮn/^DZ鷞|I*Uf4j>/Zg0 +%͛@UUTʩSiڴ)[n)m\tLRRۢ5j`Μ9hт9feeccXX|Y`5~ɓ'QpaF#Ν%88e˒췿6̂4K@t2w<󔔔|^`Ҥz|2~uGƮ/b?{RW/ z`hwN>3L>-&ͤZiذo?$bcWУG _~޶GZ5k2iҤ0s(C!n5GNHUUz#G^[ڷHxx]aaE8u) JQ233yiѢ9eʔf͚Ȯ|yOʗo^ǎe*U_hFq%|A4MYf:u>hƦMZi}J,M\&Mн{7/_FZZ<<[Hup8*LF=^/vmFirX,/?*T:G}SxiX,dddн{TVyȂ[No :|ZV)BfM0r5 6l(<+V,&ѪUkôiSKz;!uκ 6pм?_j՚A1`@?}Q=w]lق &Э[7m^q,K2 ]2a2]AAA&? tsOuro/z7y===rN{ 12|y^6w}^MZ7kF;Ajrέߜu 7hS:,@S[Wp=NhP@۽ɀN&Stzz_,hܯsg|Gk޵;QYry=X-Z(]O\]|ȮgJ?=uXׁ39fmW8!ĭHn !BH 'nW?2ھB!$k3\o0!(ӃP !kŋd*"!#sJc;< o:foRpai!m/L2;O* !sOoԩ#(Bے(<#o߁+J'$wO0׼ysZn->!nk^(@NܱTUBۈnB!@N!BH 'B!$B!@N!BH 'B!$B!@N!BH 'B!$B! !BH 'B!$B! !BH 'B!$B! !B B!$)^/&o>$$ljbZԩ˖-[~LLm}f3:udYXvB!mt+UŋTX رci߾=c|ͬY31L~ЬY31իM(_~JF ?%[|B-3t6#ssy ׷Zt܉9Bbb"c˖L2fW 6,4Mc׮]tڕsѻwoEQر#zyޓYf(d2m6C\\,AAAX,:u… Y`3~S'Np%~WuFJJ {̙3۷޽{sEL&:ub̙L8ѣGtu&vI߾}Yl6M_~ʔ <sQN:ɴi BH YVy뭷(Qff[,fΜʕ+w`믿7^H"ԫWK*_~e߇|t6mɓ'x|2 <ơC4i<آE _%$$f͚gaX5k&}ʉǙ;w5j>Jɒ >zULF""J0jH:t+̥KhѢkQΝU_|z56Zjhݺf>6687~uֱj*3TO(An] )D۶mZ*{Xكz={b6Fٲey뭷nX8q/e$ *c=B!YYY<_OTRPqlwn x([,{B…q8_eRR .|An76[ *vD=tȅedddsa45jǏEz9@FF:!!!\.J*?xX,Eփ|SPMٲeRSS rQL<O+ۍh*55pz)dfmVT&h!w ѣx饺~hƆ kxwT M=С=)Y$sΡTR?m۶ѫ@NnVm/=yILLPB{TT S9{5kǏjժԩ}1p< n7*;w$((I&qߴ_X,L4%JԫWV+c|Eҥ@WgUU4Ew].YYYNd3W\oB`f &+++_E4L&s* i~! dM̝;5kif8q"'No׵W}G|9ɂ )^8VsxG;v:tM^Uޑ#GU.7FUU5j橧H"~qL:x- bcp:]zГ;K;Y2s*UB [3g.L0G@pK.z6sffr<|lٲ=?㥪TBqѴߓ$yܹ͠SMUV%55&a0p:\.4M#44/( rp8#cZX,\.(( … 6h%;;[%yqzI&NDÆ b`Zx<8nÁd"884=˽?7^\.m_"../H }"Ex@NfşqEʖ- xVn[-a6[ػw}BAN6SgM٦LѕLJ~%99Mv{*0'8</?|۷gjJ;:.M ܚ5W܂BQJ,IZZ4@NܹE!++i !C4L&@ȉ!C! !B 0Xf Oև#QU_۷^jj* wT[l߾ub4R9de/]tef8-[s]u-hUV^+ojَ걱+nx[qq);kKo ޽{Yj/^`3m4YlYWZrv;?=;vl6cvZNB9qrK.aܸl6Ο?ԩSضKOO㧟v1퐚’%1--k`4q W_Oڵt:Fvp57Oǎ`z|7/F~a*c!FL̙[gMȝ8qnݺιsgi߾7[$rI 'rPjUۋرc5j'$lfxN>o]Ú5k0ʺukIHHb ԩS+oϞ=~Yµk*BTR +Ws 4M#::o1112cvZ6oތbnn:, ^ׯ` 1q ?~<_]Y~=w˂+W( $>v\2j}ˬ[M6k?(3qjJN< dǑ#G`0jժ|` CsV^Ŷm?6lXd&%%iǑx>G@ׯg޽ϛXu\\0իpႾ/9vYj=|0Vd2bŊ|۰ZX{&n]VD- 6mdÆ ]v( ;wnݺ 8cǎ_WZ?ƍСèWէy,^T֬Y{58V^́n2Yv ?X,$$lfݺu2ȉ[|Fk#0 942ٸرc_ٕ!Cc4l(tӛÇ1lVZɞ=9v`0( C AQ૯Fc69pӦMd22bp._Dpp*ݸr2G_~~JEQ4M()PEFٿcl6Y7˦Mq݌1O?̄ 7_{aϞL2$viS~۷qYzEQ8q&d21fW;wMX~=W\!,,̯~ΝeI&Mʹsѣ;[4ɓ' 4M?|O߾} ƣ*.\`q(B~}e  ;t'HKKG2Vq&zt:cf3]tAUU=T4L 2We ɓ۳=R ?TdIN8NVVVky^.]X2^nӳgO7~x .lDXV^֘1_se;w;-%%իG͚59~8ɄӨnڷojE44iBe(V8K,aF 65_ԩT\׫NHHa1ʳ>ݻy~#$99cǎzln_n7:l6iڵ#88+V`4;w,Ye2{l߾ݎdIx^ڷol3qfKݺuqhƧ#$$ثRRR8vLVZټy3/B_QJ(A:%l[ IDATu яYaF8͗_$;;޽>UUر0{LtBrCw3ѣ룪*ULWEՙ9;zIvv6M6EQ:ux]uzz: w%==.]Qx1֯ ̙Cǎx?zk!**^zIr6l(d .|5e92s/kA4,YÇ gY2ڵ{!117FA畒LBl.\՟YknWV<łUr$J </UZ|-TI `XHB*T% DDϹLq& >k8ٳ|}9s)n NQfTUe >,g=72Ou]0b}}}YM7D~:Qx嗱Tg%3fLfR=lݺ'N*U{tF܌b,ӯ_?nb~Zv 78ӧկFUan_ϖ-%v6o~[o=h禛n;:9A!'|۴#jkO0j(4MHIܹ~s\~nTUeE[>}𳟍gѢ,]G\\|H4S?>

BZZ={dXpO=3g2֗_~93z\wu2xN"-&& ~΢E Yn-YY3|hPֹt|pOҷo_)4~v˗QP~?mmm7aIOOnW0稪fERRK,'3sFXFun6f ^`t/==KxG߿?lx<-ZQrX,~NZZ*?<<1zίz_QGV뤤P__Ozz^{7NѣG3gE!99aÂ/##+QX.z3gOr̺ Da:RR1h EiAXFIKfbtFmYanԫi<#31={ {=W(N'mmmXVS X,l67ۈn5DacڶPV+v##iOkiXgTTn۬xG@n׌㖖zY6l,wCevaNAwFFvb#gKin ΍7Hin a0"]]2^)rݎb >CulZl*~|8N5:t׿ ŪU\sJ\.%֯_OC×A8;4|_MEzhNwU64qsw,s:-/rssK ] s돫 ՘R<3]eC=[к#\B0RC?M~t+Rs/z3g0`-zcu>DƩ>t5g?4N?A9D @m^5lN>}ظqSb"..իׄ `cϠbQ:EA-dpo:iASST;Hb("w;vl7E {k;w~m{, ^n[D "6vI677wpUoDXuEQ!**|e_+Aˮ=ߺAD X}p8|gTU < ee^#B墢b---TV_#Gv:ػwoXTTTPSScX,|Gy뭷ط7|3@ ]LQv13eϞ=xZ[[X,ai|'N8oR^^R>޽QUu7|UU9}4ޣ~+~)--KEE{CDkFuuy_W_݉fbdGm?3bq׮](±c(++hc& "ΰzݿfbccMҖKtt4KDAAVr6l@TFvAkk+vx_.vQXXh %KHin` ETv~N[WU>ɓX,~ع3Q__ݻM(t]b@*|R(TUh3˗aZM-PX*/8㥴w}O>+ӻwonb{D/C:kQU4(y6mڈ-̙Ӽ[Ѭ`5>'|+WXVjjjx٧QիW&=*++}|٧lذE~s  BN'sIj;Çq5ט&555 2O?Ӊa+O 48q"^{m{t¶mX$bĉ|_NrTf )..fĈ[4Cm[;!!ɓHOѣf5M#5>hmms!l6%%%L6|$&3Ν˄ t+btt:2XKJJcI'?bo1n8y@0ìYdeͤ=z}40d`ܺ裏 7О$tr=KuueTf3_ --ǣi ,`ܸ;^|vFWjۘ~5meddlr;ؽ{7;vlmPi1ffJ[[@АE@6+ ZZڊjn:_~Yjs-#KAp!ĉ?~8~? ࡇ~A\\X=z4ws_uFm۶1e3&2NũSLQ0h`2e 끳@7?9s>O>|nLHHZ',(JP|;N7CٲlR!A-˅(xӧ:ÇB]]3fСC|e= WAǑUᬢ;d޼ylذ˸3fi`… X4޽{XQ)f)4ֆ餤Lȑde`ĈzĉXp&t]'###R^^xsX౧X;v,uuu\s5ۏSN1f̿)#GNtt4.W[X*4vxŘ˴4d}0uT<Դoٙ0@0BtxٳsYff)Bϝƒ%~Q^^tr*))),] /---2v:. SՑ')) Qnرi8s4iiuk C {] b;lv;~ߌjE2Eb]FDxz;Y{NeYYiڄhfCUU\.ByvCm:P555fMXWRYA0+9\h] .eĢK8'uv+RФB-"Y{erJc;vEqH\lvU +"ԿM`ەTwb 8"[W^Y6L΋/D u) "0 :z A111.RK EQXD 0+l K|+psIbc{@42*BI&1o|bcc;ѣ %S]]3tEXtCommentCreated with GIMPW IDATxwxUllz%N轣.U(*4AAPPAB" W齥llHAEy^\nvΜ93;3M +pd j!B{5 TB!I B!"i!BH B!@$B!H!BB!"!B DB!B!Yiq;_4Dd߬4B !KQKtŞVgf=ξ+qfdh_giaB[ =DwkN<˖ojR6~eH=,a9 8S>kVd%FѦm"zc2hղ~ƳuvN]K#<mk˧X~':¤y!?[4CKoڵ޽YVV8 !BϾdI)@gtysÒ?_>6~O. Dk7dͦشl:QKJӨS|XO֩e%#zplyxDT 'pj&u{U؜Toё&UYn$&&2hР8u.S{R9X4lҥ(E]hQtX9|~ރIF9y$SLuӡCL&˖-C>}8HxthׂՋq:&:;PH@. =DwMg38ܜ,%zBZѩy!6kE_L|L*'7mDC/1]PɠMX|!W/aÊ/q7)SUt4 N/R8nF~^ܫȋMݚE r˱R)vN}dfϋkѢjt*TT5jШQҪ1,vMaK.e3,̚l3ԩSd%y(4lؘPhѢUTaҥՓfTCʕeLgi7#+3G6P!"yeƷ$d33xrܱ6 W,fKf Хf=#Ɏ9_4<,";1qr/ hծ>w6GкѨ0jT3ѠA51/7+5Z4s;)<8zDi啧A8dC("53'OcԿIʕ .SX0M6L ==MZg{hbGf.)7FQv4oޜ@xtr!g{wkǶǟ5l6+&FA6T!@$:0{F` [؟dXT)gaZ1t=q8ękז ZKDQԠИyl[Wjɫ)x`V(Z3r4j]( jZD5 #9G&޿?Qy :届yiJu JJWt3]M]' VBŎ !)ÉZ>n)g T,A;xwSPq59 Ϣ^@ PX|)^-IS+E]}T>:6iР:^6T!@$z_['=O?SFTÇ.U z)j]0*{π:?ڇ7n@Ś0fޡt$]dڴ8L )_ń-HL?UU:רB L]W_׃r> r+]R;?ý g +˼y٨"CCߣo\Hrt>|9zcZ3n]ݝJdcB;Vyj]nv~>ЕAOٌlg"+kTg~ESO1+j<Ø^&ۀf#-V kk >0aOuIam,KF5ٽ~ʼ;qOwɅ_Os e ہWiӹWR 48Z3yӟJBqǓe,`t^yw$e=r"]: ]uKrկA*/{% "'l-.*xSxA\-|g0ԭ&sgS)E`tmʶUR6$A`¯^ !?l L?vo_`yoxӖImEoXFmݲf%VO>Eo򯗓׷dbcc+1c4-|k4{`w"$^MX,[ @l3qN ChxFNvV+U@)Q2e"VxWT )^ŨAPmβڱXl?#C a̝;wZm۶zɸO}yGo{?7nWz qغu+:t}_}?믳lٲ+_ؠ%Z֧9l0~2xAAy۷r{b?3iݹ@|J6B;_jw~O ` 8B#uIOOpAnZǭ;oVGk¡ٱk/NU`ÍWCoͪk gԾ˶a֘1cس |19Vh ޴nݺ3n8o}ߣGqMV4,'93ߧWyGۏ冟׮]#**(J?ό:;{慄d fߞ`„ DEExĉ挍eԩE={x#oN[x3[Q^z1e{=2e-OF96_AΔI([HHH`ҤI%ޛٔE.;--w}szz:o"G9ˈN+32ʕ+%l =֝hiWgaرL>(ƎjLƌS3fbܴ{uc8~Uw^J MNy t['`Ǻ}DEEdv46 ve:aaWYˇ?wĝ/Wl'G]wFzehѴ1GuX-6ba^ރZCjM 4 hF[Ǧ\}g31jy=wS3k'Y Mi zT/_άYW^%B^!Xb1`&A>uVb)/eƏMetrEfX`2|ݻT`9}4a1??gϺk{{:O'=  F;K,aܹzx[n\= mF>dl6s̙9s? |'<6S U/|3{2*ˣ9c >G &ύ#[ _nF3|1?==QQQlݺoJ94< ˹s+1NŠmiuovUstƍJHլlݺ%oPNyޞ6~p},[D\jɓ%ʞ?eV|-~a>_-Ɉ#o%f/SLq{oaemnL_=r@$}_(~OҼ듥_x^믋^=}x7xW,_ݽKGw)_{$o`yݽ<`jNyut{?rͨ>}==Ь{sq?^tXt<7OwGaƵزҤA]>F5wStQoO[{Y{bz5SڝX‚,xT@}{eyWkB'nHa͚Tmp8 z_8x賬]= )D\#?1[=7v/?e`~5Y^4=Y}2YIIDx9y8̛Wzt{Y)î4/MNLpNG>gBnJO!y꽰k5ּgϞeѢEEk׎M*gkJnw &H?W&^}S'Oo |5&]nЩKSTZEaorIZ͢yiywkA;j '9g-Ϧؽ?n 6a%<$ oWW*!܁xQQBt.Enq} C&o Z.!Eki *_i;!E NGO 0/l~x(ewӗvQQ 7tBВ۽yGV y/-Mv5j߁2e(ʹVz徇:|LϖXl3V-.߭Vd}9K,3AF2Sn%B|'JE&55@o#Z@oaeHK^HplO'3TAAn&WΝmÌ>Tn=egj.*0T3WW^K;x=K }Lnn[ZwSB\,+-ݠ%%1=OD~n'$!&GꞝDF"!Jjٲ%M~×O@^HtB ;{^n4Ǎg| 2/MvFBZNEQE#HHƩXѣG[w)񾧟/^~kE[\9< AvP%j FU9GZAXr\]>>OH ^bIFBg+z_ A7߆# DB!k^m"!Bܑv{~soEQn@$B;o66]w=HB!HVTkdZRFBqGX7 J0nA t=$ !d6KR)Uk'*ՍI B!,l.u( Z]OʡBfrL!wܼRH( AjZF) CVp8\e&B;TVvvQR Qhh4jtgKNN !U2xzjt6ZM␫̄Bq'$s7h\=BZ:M.~ՙS !d[V/fރyo4K IDAT̒W)ŽC:ւNnpQ)ٓp:@ qW\?Exx P|iJFAՠj1m+4+88NH ➔wo]棗 өf9̩\_I!b-^=8=֏.-k tӗ"!SӃU5DڍWßՋYlשdf~vx^Dso?߃5r&.j y[gΝGХS|0T|NEZ>u+Qd} q*qEJF:Zaي:{j5Ò2➓v G2g*67rNlalKx'Gg|$ٰi;JP3ذW|L:]be8|l6Ga6~9rа@lڲlWV/--s1gO28~5;/S9@VwGpj*ZVM!FVŪbٰ8_u4kٺ0]{餑5GҸa}vmժr,>rj3աƋ<ޮnvYBrr%7/tї.=\ÎIʷ\xhT48f`N= 4E`*"!>}+oEMŁHQxyeN4oyhU= )fr2A5'1zZ&F/ucܜ Iʖ%d q3K2h48'Xk:,+6+]w"iN!-&BV,l6 ׮^ȱ5k\Njp *ZkPpOP\upJ]sDA!̨x8K^jQ~ݔ-eXN@VǙL&@YO3>W_. 01PP`\bqX\!jj9KN'"!1傽h]/ẎGظn-W5͸q2VJ/ ͯr|jB*զVEs*Tgʆx $ޤP~?^VőL|=Oui}VGڷ͌ .f-|ڽiEEBSl6;6 ,tC$=ƻly^6:X&}4T%#~&G1>6c>8V5}'UW68c`燺vmLnR͜KZF&H@…QaZlsla CE( Aw%$‘-E97u 6{Ӊ5#8X z*R4\PDVc0(DvNzu|܌FjuQk|FCzN'VKEƠףV)m= NT vEBӡ~MRQ2z F&xzz剏^^xyz`9d&ĽHQ1;ht0!u::]n֍)No@/9zT-&p,t½'nwv{C W !ucqr5zn6 5$ !DEDy$'U !E C7c1,I B!@$B!H!BB!"!BeBCN=g4BH ➎D̘g_#M!wա4⎷|λ.9H{L~N6$-+ Xzk.\N^8͵gRiqd'ṈCXn#9m󧈾|N6!bز8{=cp:z~{/{ɶ9fp^Nu;oXA}5Z-yuΞd5}d.&̄n''\a%DV(˕$i~}ܹv,g\8( 57`ZYiҊ+(v%s>NVTՀ%s>Br217Iğ;ġ8u֜ kU)T* w1!^dD94YRa=9M+ddn?ks8!+9{R&4pQZZ (0+WCDžûIO+Qnl|Ȋ=B~^.'ؿg7Z28ٻ0F7w|QŜ yج֢g.&*CV މ7o.˩4edB⮡vS˕8ބOGVIbspU`i88m(Ti+W݈QӞMI:ۏZ^i妳XA ?؞02.)5<.><u=hT"~FTBR J?1b5<Љ*Aƥkf1c`H>w?.H!B[|Nۇƿzw_*5qVu)apt:%D\p'΢iEQ[-+yW'4.]aRkQuzpns:q:AՠrAp:QpMpb۱%'ӈQEXNN'Xj&~n(9=)Ie.%̄PTF~*ʘ*EK9oPFe2pB !.9| ի},1ʅs06wDDpjMC+РYRUFKFZM[P6o#Gvo!9)jqnҝ!L~Sf-d߶o*^(X-XL8l ,RQT86,Ntzj8YhzZkY __YX,8Vk (AQ #u\?jT*ZNA`4nrOO| /OO<=08q>P qJGVc( ?nZ=FmqODgĨ/ΈFR0\Axz*o*STƒ5[q=:rcHqCfB @ZuxO?i !+!sa=RB!$ q_V-B?mK q*ȸʗӧs8N'f~ȸ+H( =!妳f6K)Q_Ψt$h|27&~k%96֭[l 9gP8g/Ɏ=[oHβN./+-ߟȌp^5uGS~NoZםo\qp|k²k|5nIOOGʹ|4Y쫟g`š zc/dz;+\kH 망\1Tf@&?YGoYON ._ߓWuKleC-t!ڒtz;/\O fl]%"kǜv%[Sy ͜ N; db·&:7U̴/ aNӃAM|3jytXٶs5#qhY~,L̹͚18,X3OvjAS~j4W0#hظ1֬4T V0{1{c{U&~c}Z9ћ;ʜW_s2&%'mRM+pR29;vpF UY{"\}5k擙%!=|H*\ݹPu%[snNѝL~= z>ASutMnt7;/nP*Ә4b9IXμ)~Q#k>5@{˺o6oƥ{ ">ޟ<{c^/kH<Bz1wS>?#2 @]C$c?i|'|*4C ِOklPP5Bt^58fg3`0uvp{^E` <g̦j˦ mTh#|g䧃dfsn:ҏ%ִCKe 7>$yw" |HtRxN\J-2c(|DvI^{g4cf߆UR%J"!ĝ'' _-2UТA5M½zH` <5/M؜G82}f_P -6,LDL4]cU#uߠ?^!_N.`p=05V[q'R0Z9 ejAβӡ ZYٙ,}!#3 ш^4i@YO#Fp 98yJʴɹ] >tA)Rʉz^}-NE릍t'˓Zr`~Z f#iWY=ۛ z{~SHUk)׸ M7%Xb2ӂ& GV4?nObu&4+<5g9"]R3~ԣԭWiF.$Xah\*>]~SyL<Ә\ǻ`?ɂgxǨ b 3c˭h޲!"iy_#/VG|9/ZEoǣ;H{c'hӹm:bEA{O.bto/t9q ^]v{qS2_6{;P|Bt[ lEeN9*2Z̝6{1b{ʜ>{9eF-,cЦs/ޝ2u9Cda/8f[vbˎ]ڑÇǖc!2tĈCu/]&+;Ď$3zț~ #aKaF [v"lf!22̈Cv)(,32] xe.>!LˆCxgٵ2q1윜vt*}5m:b\a2\ϿM^^&+̕%Lr]ᘙU\暫'i^2W~̘56{1z̢2עcKgdq>KW)HK*^aԴ21L tSg*w2~@JjZq>UfRaԢ2qe>16{1OKOH,,36{4W2IL~g$&%I(,3㙴܋) !LbR2|m:be$%n23fX"!=DBzC$=DB!"!B{!s,?ى,2EGQ[*-mJK[KQRjw٫j !!DBȐ!{}OIbG#uy|{?>s_}Pw70D@tnvJJe Px"!KF= $ޞ4SuBbE.64Y@ C$՜PZPw?3'EȮsPąKL]V? R<&K H уAWyAS5RU}6hGJ|IX+҉r:o<%-#SDE[ascӆJ8?LJDg͆-_ך&i_);MIq1N;y c1ʴi|o$("\K0KWKm )Ή&tBH@PчH x ȼKAabr+{WO3o͹q'g:UC! 6l?hݱ5xu"h陙d$\b͛XЬ?n513Sn^tm4i W5ɑ;id͢_v܌+ kC-Hg݁Zw]zZzmbp-A8ۖi=zeM r٥=bb2> IDAT1dse/.EGs9);/rZ *kgʺ9 :8s[y2H]xr0j7\Ϻǹekrr%)%<|=9v vԩ_4YDD^@o@6o'ݓYP@su5?MsEX H <+Lq=MQIRZ1hϧ1ݡSs8&.#ڽ[(,*BPTTV%///i{ғos*,uj hCmea9:6nv$?ȳX"3c_ >(N\慶n} ۂ"%}C^lWn5;#K@.'Xw kމȣD&PEտ܉c߱u]|!VKQq)'5WCS?{Rڋ\>YKԕ$i,J LiO1(@0D}A xkDz-b-6tnU5l<#Ф} =wYFt6޴q..5ٽ{ru"n<#k`#cK-i$;'O;7^K-IuR<+6!56-Z[F:'}z 7&iIq]g?ZlNXz=ׅZT?cG:rH-ht'>^ƣ .əҌmG.еkgztZU̜/چW1} 2$*$H ܋O|<63:YbSJNt@.W S+d>n`t:A_L.gXd=S'湡_gs..'um>[| jpC,_TBRM?,- ֲyYٿ7R9{>gwz9[P# GV(f^ '39yy쉸@H&J$xxq6,ҘqZn[?BjS]JBD"7Hΐ0\2#GO5Pס1QHfr>Js@4o>}f>]{4}ӻ@UNѼ3Kt7y&Zˍ/KW9~1aoBz_=y1ܽsj~1-xǮ(6Fv)EVRn u:*,.:j[!7w HTJ8s yI WA*+w J tM )ڌͣz8.`JEf<pg}rʼn%;]5h2BP(X+I?ΐ7l1;w$l˽,¥+}}WsMq#WQ(!*F`m6'HAgԨ( b^Va ;r.}2mcS][_'Y@,.۟S菙ZD"ZM^bFބK//Ok\߹<1z]WgcW,[K!!dj:?eʜ4mڌ}Ĩ" [=Wٻmݓrżw>\G\U@fb46b'_q6K>|1i/IAEZw2>2 TB.GTVP0kk+lmkkl0lyf/Z+A*6^yy6)]y5\G-7A5F"#a۞#8ۉM5$~̧iټ -7yljT@ @IiH@ N@  I @"@ !@  @ H @"@ !@ ^ČGR‚,U`B$@ A$:<ۚFU%0D!UIӠz`( Q`×Qt!D&XZXڛ!D !%F @ xH D" @ A$@ @ D@ B @ A$@ @ D@ B @ A$@ @ D@ B @ A$@PH$>/ۮx")zN^GRP\\LAa!撝KVV6w!;;ܼ<),(FF5e00 "ǀ %;/B̊t:#_PagU̜t4ZcA.Q *pvqJPz`þ/( ׾(aeɠD[G&V1] Ś HQ(T(e2J4%h%H2T 2404!eZUPY[=\A$<]DOŏT*E*5H$d}RR= $ )TXWȕX[ؠѕ Ȱ8K3s2s"H8"M H,,kDKF iX[ؠWT`*Ktz:QVfJD"A&/TL&\+z+NLCg hJkKP!Z^XX+ R#)@+{e"@ B)mL\&C.!Qt $QwH"#EL&{l"L\.G*"%2̔fX[b+* aʤpLLn,GzAb)r2zeB.7^CueҲNRI$%H xP%W,ɑˍ\.G!c1N&-T-Dž E!=A2 BL&C*}жLeRCo0z1Ie+̥8BaDɤ$tHuGoDfD E%OLj \BQ*hgHj2 @Ԉ{EeMc2&Pȑ+( Z$Jizz\&<. 5 챜C+vMt:2r#hydq2 hI$5r LZ.-J+s*RR@PđL^&P3DU+ZL$ړRiB*Fc^/JkCuzI /@H常{L!aԬa=i9JJluQUۇ6:/7E!%\NV'nvܾHBb-Z6BQ:<8/7/]6*Av~ 8[A_Ls8Z:=˱qi C) 52s Q[W6J*)\]j@$'ܤM$7W-Ҡ2N@]lU2ͫW SM|Q$?q g{krE7VTߺ̵iM;~Nz{RTMP̩AM( -_̔#'?D1{73tw\AyL;vCpVͻ#۫ݰfzRT~^3m$XJÒ;~},i>eϻq' [G:0lQJ޻ڽ>RNQF_]غ̍ft' {[/ bۃ1yfpVfeM$-ھ&LZWH?wF 524zI䅶>_[gX4Vˢ߮a#>Qӗ[P ֬}}o\oDfM9~ qFAu4.\w_&TBߝWYrڱXoˢxؿ~5B> K'qiN;X.4$4,N /W.CSR?s6ۚ#vm\C8rY;)V]Yj>]̚"@ 'l ًhצ[Yúzfk8wʎiK2v^ޖ l==aw'LlWc7~1/6m7kROdoe ) 5]LdF|~Ug|s'+OG0+_Cϲw8=I˶Fڕ 4@ ȳ'TGyf~=74A.B& DD9MJ 5>FiZnnWt 8m S;>1f~'İvB~x5KfЬ~-SܮNt֘V#{^#eh tNF'5 R h 7f>@V~杙?ܜM~1qRHK9PYVހ5+~ɜ86XI` ]jJ!z@ j22{.bШ, ܌=SJeJC+j6 :0.d$_Uh߃lݴQ5Hgc#AkدI֝ەVZ9ѮQ; c;kѫQR j<,]y̜l Z`/rjRA d,CfjGp r[>9C{6@.5'CJPK%J~~,Ԭ/T)ޏWΒrJ)mTΰXv.Zb&?uP2d>vlތZ߲#Vܩc|7fj pvgI^3Lx-Vخ&YϬUKX1á {|Z>7~BӡA-&SjjzԦl`گ+Ȕf4hg8{y퓗J)z@  jH~p!Z-l OZj2Qx{i՜ B*eR)ϔmˤlՄ/i(zj7lI@{tm|%iaS>kٸ=BtT<Ӿ->vHԞX8ane8]xZG9{ЫOwK4qMq3#wjQP(*'ڗ_L 0wr Wc_[{wӟu:b c>IF Mlք BܤY&]y$jZ4i3qju+PXӦ][#@W%wp GVdb@ "qu6ğ=D>Kwdyx]aǣɯ 6C#c'{-33Nu??wkB&ҊV]0jB @ x O ŘWڵ}}o"@ )R@  @ H @"@ !@  @ H /!9{ [kZ@  7yn'fM@ψT@PVTd4[w5}5Wm5GsȧVk{ΞȆ;LN MNZ=Zse~[{hݢIi_Rd eۂDVYOǎ̝ 4g)@=g'jEwhfBvt)$f譴4.z!VV7ojZyAs"=\_ߙKMjcOQq1!":)S:b$R"g<~ ]{E.P0o%5e쳬u6_RSRa (Zy Ez]gL=U` IDATaiifdJlww@"غs'A ~C3(<'/k$n9K\/}|LNE8?nUs^rs' j% ܠ& d7{V+;i_n۳_NOz ū iᒄu^1iw_,O"qZd,-Qa߀{bog"~a-npx*.w/3 /Ƶ#rY>H; k8vd"-0- > sMrƿ}>;t~<>-΄RD=E \ϦO"BocH ; 9,ǧq>pb\]Բd'Sx3 /s*z֖u8zkl̪Rq;SxI7g!erx7Q7-~=nCǰۊ=6x;䇱rS$bbN`^GB񢖻CDFQfb/Go^o! 85+B! '95/{hmOf3&*i4JFMjA xSRt9@LIDb^WSB:kqh lcFbԧu;omia׿nSd)7<߅ZоK**R\H'ጿvZ԰M8[ȵ&58Dp#cfmCԎmIɨrNFuMyԲ96~W'H =q4EL P.c&Wȃ?XruSg#Ps=8r0윢*n'p=)Fb^x8XTPZB[Dl̻Y&3orAdCUjӷ?`PZ~싩PߘG0Ja8&}M:`҂?uFF KQ>]9-&%N%S>_cʚ~1)w=w {Xό&2Z7AH8,:I& *+=#._pz\ѹG7CŌMWˏu#A`|̚c ~\L_Láq:Pn^>B,zWec[wl!Ý~# g9\FNoxB"~_Sz.iW3fě v)ͦn``(_T&~]~l$o|>{)v߈r;t;\ q "#,IjS)d*M~=J}JkGW > 1)߀ў/~ ŪM8̻YL5+fUe;|988phDžK瑯# `DfO}^ێC_BUʱ&M J 9i񘤿apUܖO! `I + ߷fU|HUo_ȣ>~G'*;6jLkڝ)?$5&=ݿ=n.`X|ٽ(?p##%i0"?4*`_~<:ղxJ{M}$Z|)jk">, 3C"Q3䕜Jya$aj.wkL]{3,jh!˻tN4mږA=Aנ 4 (kyhDB ilTxy寃* = WR˫z (z~2{b8q7\.tzu\sy{~6- _ڵ3u.%:d'jvvW_3a(ڵi @Zz&gy9*'!dVUaO Gݩ]|}LL 435^Ȥ ;xө;| 7ך80!*PX[\qt^"Zn$O%+=BmL#IJ8P剛sSgo{ QU}J؜y<ɷ]Sŋya"*,=IlزQc*/_ONTM\y"l|ZV 㢢"$H,qݵo/^]ؼb1G.Ǔp&*~+o~* 8:2,޸~h٢s_/c/B!RgIf|.5!5b:}% .p7[ݚV3/Ӷ/'@8sd^SghR $ܴk ~\=ʺab_zTRr -q8x@z&ĕQmop\ ju(//9/o|r=:ҸeslRKx‚z=‚mVգMyl^"gGNůѓ·4iҤNJ6xyW toR1#OA{B9ӢYc:[vz.cM/_vXLpj̸q~(B|xn%i.umC9eR%/4lkDݽ<228vxl-=xcm;檪ͣ C~6@ZC3EO dԯ [w5?]NnUx99ph‚M{Qf>}'܌%|=MBu1! !'[[@cWXˏ!Vmye36Nu'ZXg #ޜ E_{h})Wnc䁹YIһ[l=ǀ79~꛲XA3e/_~aeJ3:~[ZOh$zHӱPyf6N>u<lܦ fhtNf|U楗ި6QplܛixU1|ˏ]Jbs{RpK2 VOMoOѼ5:}KJ >xj2ȭͪԞuiIxE!+i7@, Q}&h<5V᭞XӡC ûGr{~Нz  I1~8G#h"؟ sZKOQ^9 =͞]2%|MSf}y4MK_p8=:&|jgωgc^ J叴 ߇jgӌo"e͓< InCyc>}X'2<ڿc /Ӓ-}B5v6Y q +Up"%w;jD܅K}|w`6Yj6$H]hٴoݏ?,Y N'7%A1V;{.]k7{jlkУKڶn&c#A9cbM;kMMgjh@5UaGN=\LT{6-pwYmK;XӳkjmO2}oݢ}v'?8\Iy^MYmڱ99 j~Wk{o'V>GFղ"[=ivC.3oOlTA$@_DRT@ v @W<r}g:hE?5gCS:ͅUk{B#ع|A#vi7 /0Β#ުf뎽ﯼ؛MHVqrF;;|n칋lغ_43MW]=hwҌv--,?zd¥w;ѺEjmep;)Rɔ/>IDo$bz:v`f9wOt>g28;9T((,bǾDD3sFgˠK!1l6Fo}!t4ac(bpdkyAs".'a֔UH?]ɔ)P_])3ni?="ɪMif2w^ FAYV]ƈps>KJ4>xgN3~|{o60lݟzdT:p;)_6n9sY95ۙɗ#gRͥأj{(Y[o6KA6C_ERVRۦߩ!c(^lҨϰP>).BTGffW\s1g8~]4KtBOzm{iVoxu!-\΋"&u[ɠp%$6H tB:-Qa߀{bog"~a-npx*.<|qP]I.C=H̊ZNvȤK; k8vd"-0- > sMrƿ/^˵ٸyC#H7`-l} WQ9SÆ;MYǔ 7nv&Ҥ6(\O}lZ&m̜aW>)f(~Z/ eQ*aptrB)Jqp)aXH'A(CiOzIǝT-ET~a2qquBi#IN&Y5`%N&1^>-.S'gogM ptH^rk ?113aaqmz< s̩^'.* *VVU?c͑0|}=Cх걵Lpơо387/B! '95$Tԅ4>CChK٬ڀ3Щk2GBBy &9C<~ɓ6G- &꼁m_0yd~=.44eƿu w{#+2͛PkcjB.{`dV.ۈŇݾӧvZ԰M8[kMh kpF?':$ {O_M,*dIZL]oiY/ݔG-;h᧏>0lvF&ɛ߻/"$!Ma),[R'O7&|BB#=o&䜃<< hqs:C}Rj5 }1rfru'OжM()COY:8qh2 U&2^9y/YW =Q_{獋Ɉ3Ŧx\<˝;ӺfqG+~bW^D)"*rpIɩ 'ς+d)7ڍ!'!FjAٺrZcկ*wn 2~2K4s>ӧ7Ӊ&~ >Y0s|( Fay6 -OG1w %Z=zM>G.dlܺӏaϧDQFNJY9Pq?5- z=ܮhӋWFN'&>d>6W̧X~=6%[|0g%ZB6,fp~߾M3]ĉMp 2fr<~~ >+WW ;u<V|`oJoWjDmQu NJzR4ļ$pTm ɩw*Mf{wݱ5kL7I} ƌm EةǾ uy4:x(6H~AݱīŬOѾY5 jSag焹==3ƦͧCk G@dЈU?4;"ADGӡQ[;0dP ҂~_y`$H,Ro{c87t­+jfMRRހVzie[1oݞܼ|X$8Q{gĆ0VY@m:p||:??׿[x 4Gfӊ_70|D0KرŁ `" S=ΔYY7\-kKXՇty~oOEeÖ?"#Pl\waQ,^JQA޻QM%%{;("("%1&ϳ3s9g"U({&;4e!>Ʈ}Rޟd8薐]{w,rVhT5ۢ7d_2J\AtڒW> )).&^˶fT7[&<~uqFbiZwjDNm,<~",mܹff1qB coS\h囱]( 4=t9UkZ Q0|]۫ۿ|-;K}j.' ?hIJv{$^DB:6JB9LMDN>;ze vĈqѮ%WuyoY_MРlL]hBm΅W}k~ǣ -}0MȦw,tȈ4ڴrz: v0zR#ƒWWM``l^ =:0a)+B~v~w&[ᷓgFUGb.\@Ks5wfHwX5SjHxdrEQ([4S- v4?à8WU3}j,72**Nb&soY ¥H)ccF`[J_kn. [~ހw BtV ! :z!`96bHM^ċ|J@Te* Ca^î>L  IDAT~ qHM}zZa .ڽ;bJ ldZwx:U?EE%7gڡ<%Š aʁ<~[$M7֦@j, tQaoeXUjX(ɉ., I$YP@xD,fX3R#ҙF=lCwy1x9"uM((~\iFv_i5x }&LkˣQ~ 7uo9W֑C>wDKOa]9w ä Fy%LJpׂu'j zזR0~0Iy!"9nt^8PN-PTj^, L0~;g14FBMR^H4>3!7?Bpo Q E+_AUNDFA!1OiPڦ {UKUNDZN.奥DgElc/ߢ'@A9H$ x3D*2M.VD废3gEGJry2zֈzZv994ĹTh ]|=GkGȪqϠcg*y@Rǰ,kYm{} g5kǏa]Uܱ9cN#Fu(+ae/r }óp_N%rUkYӬqݩж2ھ- Ļ칒q9/?_:z Мq?'vYs(Ǖ+?D?QRujZW\ Z7Q.R%3.|/q UuQPzv5GU3D陜pkMIJ!2Jah)i%&bfR3u<uQZ|Xz:198;mgRDE.JjCYeg0Gݤ9z>>ݚqػ+._7Uy%]}ԫX_ R9JGÞ6 di޴1G&v)!*TUt&%2 /C ˡ :b,]}dV qioQh=x\u @ye =^s:x$eD'C~"SEh]w1o}= CK&P+]}G%6U@5qYFpŇvȋ+^qeWF(ɋVlZ5W%Ep=-eM(s;0SI(K}2/9փado+s݌Bnu*79Mx'3 8{އA}ӤqZqZe6(۩nPpf2',Oۥ36Vueә@YY4T|b:;@:.\!=]vCAAzj  @Dڽ =H8cu\V~Uг60:J*ݘ2=^ҽ9?dӒU/P&i>wB9~bh,CބTbdG>^4kH9ۼiҀ]u55\B_#veGFϻ*Kfhygڤ~Ԝ]ݠP~!9,8Ӷ=1jԲA޴oou͠$2u%N;1l\ erUBliGQPDxA^TaXT%G0SfVxĆ?(䴠K{=M55t |"c yȤDyPX" J05˿)Ѧn}454kg^rJ&SFAq1{C騥&r*=wc{\| G)(aݸ}:\ ya_*f_͆b6/sJ_&$f!\O$D"&14̬8x}b<=-+rҡܧ=-a2>*Rs`[EP^N".j ?_?q&3+M>(%%rG)f^P҅FfD^PśW=xCFkP5M?îA=Le|᝞Q89[tGv(Q慈IȤK׺G|b^7E#sNOVfVp @K(,ee5[hw}d%$b˒PMMP"U_1+/3nSZKDRTyVYGb%xϊ_r/Lp+(Pf֒[1gsyy99+h톚?=*Qѱ| ɥװb_Ν9zԇaPb(A![5 kjrO^I|_o||INMom=L.!;6`]10Va.qEiԐj?3K]ε| J$J~+ YaB#FZfqlv_ Xv$V%M^ 5VN \^@TTiyɴcgr?%xoYACqNoא;#M\i| %ek73~S( f/VK~}ijwQZf)/}pƿBr3RBlj=bj-w">EunĽߓYT쏐P^^^`{A?mg&P;WY\u`g+.Xe_k=x>2%S;ҳFxU:Lώ7hAs .-vnOph8+^|AHP8]7fv_/JWl6Dy$ًjeYftTs9{y[I*Et>Oc}M:xhծ+sOzѤ [pp^ -w,:<@0:+oQv'$C>cOtPٓ#'feZ}A4檣5;˓c+˘Yx-9rpĤ6yyyek91%t4<'>&.>Ͽ&ʲy,ai(?M 0H=޷ ڿePRBXL:<+4Xl9gdd:bb?xmSaZƵH(ɅVE=PZ:dxik!#,B(NnN0WԮ); [RX[e1Ry|hvT5o @DdعIZΘ,Ʉjco$5^Ę_L5;>Lahkk򄿭#PGQprPǨ :4':wh]ѻs|#ٷ[ϲkqph& ;PvJJ>@U9k"?/Gkwtu)Lrs)++qmCQ Ɔ~]ң{IIYﯠ.!"e@ Rw,Mԗ*\"eSV_FԘŻᯍFAl>=зG1k1ʣM+5̙!OKǴjWS&`RWA$RfuOKR,K)N׽GpukdƲiaZ_f>5 뢤U?zcZZ8a׮ih%TD"^P^V!A^q\yyˡŗ3sq,ZE}{?1ma8‹..(*-'7'yBߊrl=u(G.#r/ڃ9t sNK jb1MK;lPk+;ӄ uոި>p ;ff0ח(,/Fu6iA҅4&7sNbE䕕 {G*JM9y|$eHUUQdeÞu?1v?)gkYȩݘ_w΃ZwY5#ƒG0{i<K9 O2HI5a y/G ƣ::\UlQ_N?>*JsN#73c$)_㷳@+e-2ڪ;9 SWԤz訫RGDuoGpc.2GOO,Myz9/3:8ֆYin#&dCRuj}2316@_pJ(뉘ViFp?WJhEAjefנ͚:аA}kwU1 _*q&;3$]Ljz2324j*JXs/NNMHFP{ZCC"BX5^>!2P-/%!0hJyč0")Ƞ#QPCIjll[sѦ %OY/_U"(/u@KK]ױ?(ǽW(i7n ȏ GPO173QˬeXRY$-{u|V$0ѓ /⃻ђ,ڴ#7nA6 !+GXlr`uc{0eF.i1p'"6V'mefld@J`aajlt:qWx?,غsc55iPV4UPCWGR!֎4vry|?Y˹J1. ؾi.M͞C'8y]nk4]f$} /Ok۷ޞ={#WF2q ?MZϭ\v6V~ 3Nc٢2KEY#)ڵJlz mĭl9z)9kM~t& fƲeυ+>h΢3v/Ӥr,ssTr&N7 :ڶj!uTVO: (Yg Hi5AiB }XtL{bi5g(uKyة`.>pvl|v Q#p `.,]흯}!ܕny`2ѥm+R5G\13DdCOۅ s'cD_p,̃+!: Myv035C=N_{FpcvKuO DՉE6>j}Ѡ6葍طI01ꅾLҭN#uԳ+T2UyX<ve">+ קZ+d:lVt=ĩE9WĵAƌ1w2١{ mCn0m&VJPG/%+ZLNlQHѡ uuTnЩGTT4vrRڍ;42ˠ[s5 A> |Yy6ff|my ڶj^4 zr K)Ӯ1ir<ӵTUTdݙr'ПC҉{eCTbŰ`RSͰo;u+pkKLxi|"ūq ٠".t@Ѩ) s+ W|n獼iz{ٕʤjV ըBL)S?%s|ns>ip%y̗ڜJA}p9NKnX!:uڊ&b yѓPjb骞tu=àiQ8޸ ,E磨奤2gdcs-EiLeON [vD;%̟=MDFŲo4xi)ɓ6- !jQ>oRf1T.@OW.1OR߲EӿMI#?ݽLy@P~ WgIg8s JU$&rLdUӥi7ܯ`| @A$  @C0 @O ^ͥkFLߌKQReffjG4;8{*!'ӜH^4I>_M+|r!zѸ2]lb-)::Z̞LTՕfM4u"Mܙ4(yյ-[4iN!9E@SQQE~%X,A޴oou͠$2u%N;1l\ erUBliGQPDxdkz7d1>EE|{A,Z5'P³{?5 s~( ĤvyFcgm֬k!u{qp>=Ʋ_\\>2wn[t4,_us`e/Y@r39F[U֬s#N >_)--5_na6$Ci";u}{̤Hy}GO`n!Ϛu-(7X%c)// ?|n՗-13UxkM\ӳ߈A= 3;0܁I<yŸfq\OuwSNJN%($ =x"goB1Vs/+s8ۆzIVXf`'2^쥞6qMQ>Ki5FSCTuBԱWh &l.KNmSx4sԯ 9sh?SB4ɉGy|E ssԍ*~ \%BA<Γ叩E>=R~vQ G~;U{<ϓP*$9%eTpu􍋘0^5PpQTϣs?Sm;Ơ"970Ȟ8ZoDDFqz6Z%痀X}=GDC/++ !> y8K]_G())8 Vo蝞8Q89hڶ[+.AہYvR?5?8ݨ@?MAre<`:rF's\ADFRA1/Wۤ $% ~1%99 +dT"BNUN^#I/-e(y(kDv 2t.A_z =ŒU|>%6a+YЪ l֫t~ݼ9C+8Xr1*D P]3ݮ5~Xa01qzF5_E8ܦM(Xϸ~~ C/|]f^rɩhb+q(}֕c mۃk96f!q ~_Wp =w4xR5W++26wuv (貵?)dʨ\::SذPye_kFæh*fp; $<#.%vAee(+d>{'=#ZYYu,_ks@f|_ Hu= itw(jf$" и:j}p=?WDӵkI96gD[[_.$C\[JʳViZ@ĕC 6-Yq~[Oۀ+leŤItgIO`Ϳ˯@яD=xƍ[Dx£hnuVRk-$6-O~#Y]:iٓ[>ni3u<bǕ(|+A86u<0'7w.ѷO ?V(@#PׁV6/xjU]c4&? z:7s89HvXk-3A$+Fg?Ǽypu gDEEq.:~&Ж1#`heFӞM]n ɱo~.c+gF :|^'tAz9(*cޒ{X"UŲh1yҒ"lm>~pk2Hto v1{&}l,<^JX̝[O+-^G'f:%%%,YGQ(( MQzLdBʙfb^]BIjU1ӿX؜S?Y(.e3i(iP$oY_MR8Y–M׶ mZc̆4T&N>W+t3cjc//4fVN>/<ʐ vIOsO,Գy$]]f,˒(*bȳt'@7Q jsv]sk"nKgU#Iv:$mZvx9"3}Vv&QQjŲ$ /@"-T|E Ց f~odIuշԏ&KV{/ "I(NTk%nb-g Oڻ%D)*&/En>e%E `زsj.2~ET( |5Z(:P-K],5 V\jX~58FP nP}Qټe\^H#*?:K [Th펁IIj;7 WDJ"L~^zbbӑto"/n =p t%4sٳg|2 -."RV]RYDj(H?\?*~ obBx*JHʤ+= l3e%"((.#?"9eZxYY=1GacCq(Ӱ԰~q$stUUӗ<175A6TUUe(\Y<`ڄ`ӲBI Ӕ׳FDu8zz;;U].3f>Rxʫ<J2HI5aMYy/ %̌lxs,ZّF,Ѩ!zűZ ؜ v#轻T(Gn.UKJ@RF[y*C(,Z2o^^E07P ᏫBBQR V5exr@_T0QE%ETɌ˭TP{iCxz]Vw=(u~ .3sScn Ep ۂ\t13iIZ/bw8Q[.3X2ucrPôq9q'7n`|=1 #K()V]fgy07L0c& mɩ#G~LSޚcld]f&F$ؼ$/#:kGD&hcnVefa(-y4kPc=kKTUUhKQ*f)<F}֚v#ۣ"I:e /6/gQZ7+>澄1bE efld@J`@?y@֝yG+7㝤;Qs.hh@ `ʧOid/u3h۪l零zIЖƩ &u`@P^XtL{ɳ>4` u? V/Mc뮢K{f 'w Vo/N 8}nuÑ+VVV9:G+7mA!8z<@M, VMW> ۝&e* @1Nuㅂ3sO9>a6gR-Tp脛qڰeA3HQ<-odOdT,F )Kf92@b㞾~į yС9\-MW].`m! @A$ExDNN,? +;P ]Zkam;QBrwH}>yNtl[Z_DR ڸjm߉;4eH\.]˔q7'yh<'j$v ;j(y!iԱ% m4sV=F {{JI/hu;r&Xn9͂ 5(`` QbKϩUoQ"e{ݦz`,M}T77yl;Js^>$sNt9}g*$E=}*^ =݂%Pz6n EyllPPQ@,z ٤.vҤIYeݿOq D+-E+*/Ǫu[p艼J2Zj… a…ہD51GC5U0cnW3UVBYϊ()̨<$'B_QH+-K}^%'QȈ0zDEcsC͑T gTUUe(\@V6ܪ+)iY#JMˬN 1`@{ʒqEC~jWv쿉3r"V޹e,ƒ-{;rrrN.3';^eIzz'&&JC X[Y8efnj6X[֭yy9\ZϾD$R;lC\r6f ac<'rlh\W|~7+)(OOXhl~fp6Wܣ-fdW: @ d1mJYlnp9FM4fL݀CĶ]يbPG3[h @5~n&Q!=ǴLބz3x@oHIi)uvB @kߎU0^%oPB@]|!']Ew:sĥH v1$ B((}e|,P"I~GdgEjUkYYf?_/ 7[^kr>ޔ9O!zϳ\oc٩@ xuo/D) 6D 1BM5ӡkɕ7nUmVR9xO45TNŚ+cV@4H'h@Ҫ22t&锽Q_ӄԦI[>\Km],n8Dީ;Q&m|F)EM#zОԾyGLSZfu0$^]jAsUvN^5Rn.W9q'kbբa+>qP9緇ur,mt:Uџ9f׊QYGᨏ/gVy'V3[nh>P_:w:I=>_?B )i={:iϦj3DPxuX{9w/w-~;}3'I#mH8h+]+3;Sڔ{̬ vXd'oGA龫vVhcrqS[VWISo[Q)93emGU/0/H-V#{O2Hѻ߾]_A>~W P~zt?;߾(Ӏb2(mܻN{CkwRDŖܪ>cm>Ti=xڶBs~EwО#o{,Z}ZW,q.JQ_wGw?UfvBv$5 l8W.Pe,:mEk,muTQtg(Izu=cu@}&?zkqv셏[D'-]>sP)]?]R]mRqW[甆يӶ] [+!9A6U+zdt^ZrN[6*ZruBms>~Z6hA-tA}ٲak:q@VU-:9꫖ [i=ʵ**+i[ۧ2-,{9K-j%cYu(K;K:Uџs̖v)K[u%YG}?J2rpSǫxKnػ,eo/WuI{6^<%g>u$٤аPtaǫ˽T)i}d,V^[KrrKwnG]qoy**˺g%~./G˸\*03;!\Kn?gY%In^1ыmbέ+зթNYyz}K_רklZjg?{u_:9+/w/fXnl$fHrrYe,U:m3Gۺ*rҖ-K[uK8.ﳎQ7_VlYz~ye<ݔrqy)r;Ks*w>pPFVzY׊+qW[慠>ʲ.Y~Iwq_e92.w50:Ru1(;7[fr ~QC{AVN\Yˎ{:Gwh]2lÝ8=+]&IYE_\]ZvgdkɪE2j,TcLԇ?]엨dJJ23UmUe/|gŭe,U:m3Gۺ*rҖ-K[ue9.0 .&oeec-<|<-wO7egӧ?$-3Mn^JL-t{t2~e%~.'|9ZFE isdBvhTp:t`9t`0*e0-['PQcںzj[&YVUlrQ}ʽ 6Mb*#;Ѣ ش^3 FnV7|jfͳT}w@ZtdV+d[9Z^Y_j{L&Bv(=-vWgyh9Uџ=f/-K[ue9yU\rsrengޮ{km]C\Umr7V￘) zU%wetc!eY~Iwq_oۥ}UTTtl_W׀Ct1}Yw5V)1%AD}Wl1p,::ۮK ghp1svn\U_qf9|Ԛ+5Go@Iߔ*&J_Gش K,M W Tsѕ)\[*`u{li+)&cKj3mB2-g6N[|ooEj(=2eD*Pϕ?jz黕t@{c sCޘ3Ci)I>i̛WݧӫU`ܱCzݯ4i#un=൧ ={^Taڧ/mV1Z>^y<8^֟+_բZeˮ8;na{^n֫,eG_\\ovlvl^Ce2եg_mGu_^])8A~ m\n}nŖd/4oZJ>Ԕ$e6hfd2Wٲ% RMIʻ##-M~uӽ}.Iʔ/{1rssd\x u,~S(j2u O[X.%W6݊h4*;߇Wk4h 7L:Azڽ}|}uN׆55} ']یbԣ6= 8L}$L|V|HOVsw{%7GG+AVo\|Vf~s=R(l٧c1,(k]J>(IJ:{߆5j?ss.~vMSU'^IQ[wV#=1{5ẗ́@ß=X-Z'6M6qX煐d0\Ŝ,Y K٫ȥK9p:/Yy1#X,un,'Em=~JMNhߎMPrdPriȸWG%,BR=,u)7w2V[VNVE#%P+o\X7G{9}$)~{HWۅIvo[]{qP5`5h"E]]`^XkԨ.. >='+a=Qpێ2 ruuSNWg!,bݱG0B>~ue4_W}U]eRr􊰇PTXphA..UXayYVd\rv1_E:g2U^Mu:G76v(0A^^6am;2$:!>X]Š/<[r5h\}UTm 񵿾qEɥ./VժҢyҲ?сd׶{;aIIr 3RSs쯽_ggf:XWURޛbVdTd=Qve0 Z-=rPZ_Optݚ当3-%д|9ͮ,usuw9)ӢB\\\*ʲ^ӗ'Y#&h3գkФPHK_~`e8дc1{'Tݣe]aCV)F)K:}]Qu9*O.]ߊǯ17MMw=#&qVO3jퟂWLfڄvl~կ[^.mG$lw RiƵڟF{=Q6U|N&I޾^r^N_-ļ+|k w/jY['o e 6U k~-4K:]e:voߠS'dٴsZܼzP_!)23N.ojYGL'rԣX$io1o #7j)N.8z!h4i%ۢu{~mԸYOVz]֎q}h}/#OۅѢeu@PCu @1j`ՠqs޾QIg[[(T[o;tpv9L{^j۩jW\=p,\<~X&YuWz]铒r3tl;78eeelr_lzdv)gꎄ>16^kƒ 2}2F B ZV\[oP 6+)`=P1p)`VQz mVbF0@       @@@bvF쉷jjU@b4M|ԶU{\?Ka5bURrNU|r"ڸ9w`΅@r=g45FOa/cΝq5Z'%5Xnj!y>g#'ɬA մU;2ksY  o&&f*=5EmTfG=Y,޼V'VVfl62w4F{r.8ٮdYVfSVfc]rYڻcSSmٻ}CS(KjUNNcXrkET~Xq^Xу?s!eIG j(IJKIRܑ_EaQ}/EqGA(A]okXt-^m<{dқIO+K@kԼ&6OK>3gO,yɬFZ٧9usXҡ};rsr.|˼ЦF͂+}h-ɬӲʵ_8 ɖf -CMCipO{zURʼ |ujGz,Wh0kZa$d\2򅨼`qq$RtÎLYRt ]OW(eef/:t]L3j0SʲN烱\u _ڳCi)o2yE~>#=Uqqtț[FҊtn{kݡ<}#_ηQSuu:.unsڰjsgN4(#eıCiu.ӿnuOp_:uhá_e9(Ţ#'YXWCGdge*7'p {xH9'[׿5hZs ڴfRxee+!>N qy~juL|$i/0rsnLO7iGeљX1(:Uĺzz(-%Iu6zp@v-vTVm+$ $o5 QH^֮K|d0v]z8\@]\ڣ4mW`5ov Q߈AjӶ$)++KO>5[]RE IDATӳ%Il6V^'|F:8iɗT~}6 N,33S_|^QFFϿO%$$*!!Qx%5>D?{?׷$op{?߿<*77W6MoAO==&ŢitMi.q.w?WCT`` F\AAz`JJ $4d@sLuCzuywM[nCհaPPCLe+M>ģMg[cL%>Ax6Qt$McLjǪenuiY0p!P9c`1#W #6~0 tH!3w @@@@TK -=NUDttS-T~Ӿ(ZryzIR:w=7()[z\ebd_²sJ$gI=ݽ +t>TV7OHK |n^Ŗ 0=Bzl2GH/I#;$uY̎%{a˟NS{H)ג]kyQhTf&L)Zk6]WbQTX>m%q3 ^KUYߗ4]V?wDl޷~@E0^&5 #WVp@%phڵ-PInУ ]r[p @@@@@@       @@ @c28wƩefɹ3Nn)!qS%C ݓK@=Y~.NFsu7{LيQU=(&M,wMKӷ sp P[       @@N o)JMKbeh2Sm}_z?Þj,ŪT@EqsZ o%Z3{ʹhVk>^jǜ;TkLMKgNJj8j#B0ȟ뒑S'x:c @mc:@)`+ #N{}J> ѨFj֪St,fRd2( `4]0=GR6S`;_gO+;+Kn j-Zd4ٶm;=UJRbPOk @޼NV,=T;+-I,<~H6ԼuBu۹^GzjmۨО}y\Eo^4g2w4FIgԾk/MB-*)j8:AwZuj|6Au{ߡ n>=ԉ"[7L>kN=h!{ j* $)-%IqGYo@ƺ05kӡH_؀ @y4n"٬A7iVfYuշO.F[h2a|!)۳N(k2ըY+3h2Ë ]d0<!IϓlԑW)_Iʴ̸W=œWe |@ d05e4f+ ż.L7q]  dJ3.mwn^_ JK+r%@g{,\8v>ͷN݋Ɲ/{4>#%ITnN6)^mZdvQfoiGjNV jAZ}ڦu+=t\7hSwjFQ;:>._i;O)Ig5{Gv9d@6>> C.=9(I2u 믕KY-d$%>YBQl۰F/vqqխ׿%dQ5jR7V|QPKjFQ/ (Wv?4Zn񫣿Mb$㷟$MUo$W7Dҽ@-eG5hW>{4: y]K3g Q+p*cڌS@9?N+/@ кCyp @@@@@%/R! 0lPxjgAGU@tP:BcWƬa]]X]TwTv].;U\)?~AuMt&>NIC5fLR]Ξќ&kD@k㢱f*3# Q8*7h/ߧ$i?[L+"I:g:F|.W-W[kV$iK*=:ubRvV$i'ꍏ$=-k/ϟRVf6.`Uԛk}&IR֨?oX!^?mW!yaw_*w[F_woOK}-IڳsNn;g$i?7 ~ܢ5^†/%%EF C3L裏J@f߯o;Sw_~\?r$4T ̧ߐ$W29U㧁L$e5o^qzc &-ߞe[ٰhҤI7oƒZL&ϟ뮻Nt P8*iJ=yD$-g5nи$)-%P9&!b[$27ΛVo,7yyӯ|#p%Ս7(٬S/(*-X@ƍ#Մ@Tfߧp˽zmO##/ܜ_P8}2Q G#!?u=O*0ga|!p„ ?L&SQY .رc W#%ICFߠarb 7x8 NWD}!IR{Kƌ1zmml#clMl^LMfiZ`AEiԨQ񡣀j)`T.=it _Wu25mYgD&Yq_?O>x"NWAk͞ysGLdEQFi…[d4O&N@Tg^]>FC>~uI{_$}?ߪUNrqqU6zԡK}#Ͻ'O>]^amՏ5{X&9a"kĈZh/^חaYpsJS5T#NV[GwSnWMc[,zէ4>1kX=vxş8&IN^zu]RM5NZB_{<[d"OGW5^Ǐ(P"}k"lWivy`FT6.ۻxn23i|4~x-X@&>E-ҨQCG@8)?zrr~suqY-$iouX-ѩthroZm۰^f?Ԇ?WVctu}YMz%Kg܃hw)l,\ҿq?;PD5j.\(,WWW}>|8 |?eX4;3o$I[ Do~5s$I[$<6f/boU{2{_jhч/n:NQBh?|Na釨! ӹ￟-TIw뷝zw^Rmڕ?n}ٚBI?/a#FhѢEZx,___:p\vl^+IʯN"/tb ,׻/?k~OnѸ.GIڹe$iS˿N$oA!>.r$9o+^hݪ -{'O/y&K۶mzt8na|{AaÆh42RI|-iJ=yD$-SIEN?x7lj֨i">sZԸiprI껠^&ݶgX3o;gg壧_H8pb>J:S'TA"˼6>$Մ[U 騿m^b>~J>NG[ɸ@{4P9x&^bԴE/TQ];6ڕ?iZWt F@б[oIҧ E.ZM8 {$i5V.]lg=;NvZ49HʥlФ=\4Ňt6AOg)I^vNTx1g$)|Ш;z -MuSxA'c莙5e泒dv5?&ɤ%Խ7 (0-<ؠ`CP[Pw )wyW.NV 7)K_U("jPotҚ P[:NG+';K1vꉻն kpOy4e$_ŒRdbѢ_5 }㟒﻾QtFIEN?xT??NOti ,gm6:*c4dXp\]W\eB^uK59籗}׫eP}lgo7db;跑.@mW~cIҘ^ -LFIK>y[B~t28OꦴrJw7l񮆌} IDATN()g>ģM皲@X,׭n3M>aF0@ j }3[q՝ôoזB嶮_`Cs9T~$%kbG0EfTEg@ms:5mz ]nSOg@m☺\ayA7MyĺR鉻qp[޾?|=U˾Oߵ5JwJۺiՍӒƒ 74ix$iղo~m\jۆ5Ŷ'<ؠ=idz\V/Ә 1R\!IZTNˎJOKQ̾~` hNݡ4v+w^~Lެۓ4£w6}Qczf /[G/St7ۻ?{ZeehQ60P(2rYP)Q!!l3l̆㱇>׃eܧhr(`kd?@W5k`:@BfjޮzH7(}0;I҈>/YTA3y~WAKC&:/7~q^N%M3kkq2r3?ceg(I-,zec[r3h*\FFuyWԺ=*V>G})zྜྷzzm6om.V3ln|ƯmPw]VtC~k,[m]8_/MZ]lL-@N 4jڷ΋е,,-TųT$<>[kԅ4NSR*~K27s}` ۸du;{v5)}F/y[f}Ikwǩif, Ò>Qg_M@PUte_jR&%j80q#)IYߨ"6h5ɯJըy{x[KemP֯ArtrV܅d??آOSTF??@jYjRIjұs{j+Wհj1}1eFN&[ hF RZ=՗Gɷ13?qȪ3SZ?q(R @(b@@@7,A  >>r@y@[maر-T=_jLJ3(_n3nD.%\d ;3 N#O=_TcO;UsРcg۶Wun|+[˯?L3ixJAoөw]4ﭧ}3;>m55S5occxc 33N+!>ִ,l!Qr)Հ{vJ6%I׮]Վa?} IҗGkqv.)lϺnx}Jӣ]M2=m"]~Mcg (0D?ccU_!yZ/2}3{FhVGӄϖKvm3d$)5_%Iڿ+RRSGRx7}JLm-pP]Ra߷EwE͞=[fff2 177󕐐@V$IFϔsr.]VFL]inώpI;&ȱ|(H~uT"MfMH\Ε>83>^I6rKxsQ+((H}\SeggyS31ђ ne]+JbݜIq:mEElАw^Ÿ yҧ4.5=w$pa Iԩ,,,ԭ[7%''1-X@[& 33N%KKN8bZvx㒥\n$88IwmMq՞HfZL5|h'IjۦyKJ*f/3KJT$SNc~_} BVVVZxEOV?~-V\Vslsr7ݳkg) Kr).@SI TQ Yޱuś%I_+I5dJ.%W䜓5kEiɒ%jܸ 0t5"[`;%;u@ I|8e9UpDQCٯuGյ׈,kD; W9::2@>)`  `" xracȃ@ f\X5P@ @@@@@.@\v`' < Uz3@ { #)eoՇ7SJJ"2@7g@"BWQu}M|\LccIJ8wZj0 ?~̴gGvGmNT4o K5hNIW鄁(66vUQ ?b ;tTlӺ3W(gꪲgMUd?ӿ [$IZBIRY{Ԩ+I7}\ֆUKfPPx#۹MVԠitӂ"@ ը+2唒"z} k3ȣw$ִ(%{8H^qzX}8to913kx|]KC YU4WG9]$aq#i#0v6JC?l8>2Y3դD͚8XNPo;%;uOy9#ӏ|{$I#fm_SfP;Ъ^- =,+k uy984=Ϭׯʴ͠$k2Pgm;͠Ϫs]RīXqAꥃv{\-e%U^# K+k6Uv7>6Z.+I:!sh5h./&-.6&ZvW ?I5[ yE RMYt*YZdnWZiw魎5rBEBƩD)%`FS9~>[^nm\c ]>{#Pռ]g}EH-YZߤg4 xEaW뇯s (*{hu5)QWjٸs~Rӏ,OoBQe4^WeW%[}jԼj<筥_Ȳ6(CW s 9:9+2Y៿ҟl' )*b ~cl,5fJ9x=jX2B#|~4_CJre[Z{uԘ8dՇdiei8p)\ E 1@  `@\v`' B!Sů7git oc[>J~@[}x# EeB'冹P>AP(@ Ppo\ךWox*"tUL6^z;imttN=yD*xȷ^iZMaV0 ܤZujڧZerh-5NWF.)lӿs۹MCujܫ{?1xz5֮7[C]5o3]槅Ҳ=.?mi?>Vڴfyk P@,3Qkj00C/KI>]Q"%X$H7s&h3Ц?{F-;C׽!͙4ToྜྷZZW.kê%r3((_H?A_/$I+UEԫB[և(9>k2U[ԣ{iƸrp1{x*u04r:4v7$3ss-zyװ׃17ugEVskܼօ|ؘh >Swj?(" By ))|@*~qq|   ;e*2?AG6@vl ՗GBM˼ L`ܫ͡@ _n @DQi4䕴;~ |@yfPejZY=:H3ixJAo'Lo}55S5occ%l TUWcnz  pwP|lllI>Z fgUѨq [|O߷Օ˗t)!^Xu7-z t5)Q۷SycIwr>Xu1B=_IÂ;| (x!MЀfI~Y2Ov毈$))#)w=Lm-pP]Ra߷5_D% hUQ <ڀEpCv\;cO*ZQ?~=( ^|-XŊB׊Z7AV65q"IJr83>^I6rKL$:EN%JɹtY;dۤS&MZ^S1p|A-;)bUڼ]{wn( J}$/ jSUws'W.5=w$ܜ5ZWմIw-n|6QevIII2tjԬ5h谑JJJ2mgV3vO{hO#U)I:q_ս|Fot}[dٮhԤSU<>}ҥKW^ՠCUz-sqޢ{}պUL׵nB[^SCm?z0ߵ\PEmGb{&[[WY_o$I:s驓Ǐ<]?|P;ɿiZ7e'~jmN>fH>g;nio$.ma,5n,۝ڻwVTecc>bZ?u \P膵erEl`ЄcUntխ `(xk,Jmqs+kp@@,zaI_QWV-1K6S17$>qkjzIf맸 }& 灼ѣEڷ/Ӻ_Virv.R5b*:dJ,izWllldoo~Uؖ,XG1De˺x=$WLf}:]ի?-I^it,--;HIII`0̜DT9ot[Irp*9u\]+K=u\-딓w֚8{+)V2/VoS]MJԪH=xMjnj7?+`#Gʭb.HIJL'5it3k:jQ1|*T( IDAT֬YkI-5aHc?\}?h֯A5b Sح|ܭWASFJ i/vɵ}{JrV#jT.ewo~D7_Wr|߾U zjz&}zٚzVS7PMKiD[V:qྵM{vYRjV"BWC> pۺ93>PRX>#RB>FdY)` 9ݑpg@)` ~ޭ7 06cx+@7     %|B> #Ǫ  /7ȫ@y@ ?ARECv5l oekx*"tUڈU5UlJj ["CC5kK5_5 z==^Ȇ=vڧZiώp};i`֩5Wt^=TsQݾnEW._KgKgO>o>@OB"OkjdQ_oZ͜2k@^dQN6̟1F <֗FxN oL9.e钤;#LwU4,|/H4դMk˹tY=3uݦ9xfϞ-333 lkJHH` 2e1=.[1IRB|iY|\LƜq$iYy׊[겻e@Qeoo }g23sss͛7O۷1n|?U$IgO~^icc**ZN%JeچSҊ9wV'O$I'$,}.;}2uI֭,X֭[@ G+I]8I?mZQ$iJrYV-AAZIgs-Ij}.;}j׮͛'ssZpZjE"f/=LnRw}"h3뙦pmYkԲN9ref9ڴi# uYF1K}jڴ(!bԯ~,-~*{4QGg'V\ʹYkTUߨlm1;՜%[\\y BVVVZx@> j{nZMZgYDjxng{TNNNj֬-Z$kkk5h@ @f3PB  `i¸O. 00|}t)" y0|@ 9V h0@rY |}X53|Z;yd p'[@@^nyX{;K wtE͞=[fff2 177󕐐yqW/7dd^AAA277W=eKkNvvv\ 1f:u/BfffY  v 3l oekx*"tU׮&i҈j򴣚*qԥt5G׀-N~46ymu+SjPV}^m3'eh+yejZY=:h9d!Nv=}"W%"tUj[UlJj ["@kN͓yZpZjE"C^#RS8wJ9@w;_v3śkͪV+]_ZtmyE#|#I:u⨺t5N%Kk~H\ʹf#)Vcz@Fъ:~[lfndQu|4_j }Yf~!Wem[Ftm}h?e3UpEI\\~uY---ߪiӦOWv xw=BӉ_뒏kY0X PHi~V^ l4Y3] 늂85pm\iZ?%ǪB"O+$j{+6&Z_N.=N{m%igl47VEE+$?vBPߛ̒$d$)5_%Iڿ+29e4^ؙ?hӁDLF͝|H@-\Pŋ!{ iNQ IN? c g:niaSJrY<`0hg]V1jr}$?Ա˔SȶSڻ5 fwfJ)y=x[LQ^˗}ZoO>qilDgey3(oZ 4Cu03\\)qZIPWF$b!.-Tp9#W޵b?1Cvxq)[2S&IQ4u1BK|\^@z3@>)?LϞJ߅SɌoYoSҒ'M3'wf'IrVF)z|Ŝ;{Ki&.ۦ]1c\?]83'5i;w> o g'zFھڰjiݭHᡫirmZ\Y^X9o>mVJ*>6FgNOw:A˫)2l~=SKysq / _*7wOYZZMWUv>1\m-f'߀d uLR)򲴲6//>]X,+VGЬ~c ً7KR,FaU(0Tq m' *q  rI aڲ>DgN*%%E\ʫIrEb jw4{PPNު @>E @@@@@@@˂!rׁuTů7o, I ?o[} x((d+8tB/"t<[F4u+L6^*/7ln=͠f@>o6 ZG׵I:rp&IjbJW._$]|IWʶf?W._R˵.]P7[;iXp> 7s&h3Ц?{F-;Ydmc+&x岶nEu}ۯmlkW4w'=u<~TUx Y~B]+ǯg2öa[FxAWIN +$_gs^Ac\m+*|ZvxSŊXq{y [|\LccΙSIElZgZvvt9TnU޵yR.n@m`P;*>6FTT4mO' դD(K+^fVB|ll[Y? 1BǣF]IҼ飕x6Z"/7|I槅Ɓ)))23Ϧ`9?G=Y,-RU̒ߍ ?L/A~$S=Q%m_.'0 >B vMZ˻Inw4>a<J~_o\m_n<8g(l]~a"6kͫ˷<*ӺU oҴ֭H>9~5UJޕ,ժ^M\NWmӯV_55҉{fPejZY=:[~{]ڟSU:C3Ϭ-I:zhtmƞvAoѿNW?WNMvԨA`(Q;#7i`VJNNi@VܦZh.I:rp>]Q՟$}ޫڶW6ΞҒ>QR $iώpBF$ڵQ3>짏H(wP(bY=k?{fUq8un__ eak֞pK9t=:$V,4vT"jᜉJNN_+~V+U꾙3QFu6H˔l4jɦ W5rBIKo3{FhVGӄϖKvm˰zh `VOB"OkjdQ_/)#)w<;%ǪB"O+$j{+6&Z_N>Z6커.$IoY "۷+Rި*\F%˨)vUR꬜oIKcOV$ |-s3s5;oݳ#\Π r,,6 ?v]Ȱ_ m*[r.]V%ݑ%I{wFhN\n ~&+ƶ^_tb/Fh)Rl2eؘsǣ.Zg%BTs)V'tGRؘhIRכakEIR-1KGM%qu/$cegOPrVEENIQءk7wO}z,ݪ Qmo]tQG2;8Iw}kN%s4N%KKN8bZvx㒥\2ԛY֗$MW 2ެأF]IҼ飕x6Z"/7+ ]L^n4a9ydϜT܅1$.L$3s}F.dFz$ISF39wVSGQޫQ  Pb:z`m]Qs3] Rd-z~=Ӵ@HRF-fFy9ݶ hzJAش:C;m^~_ßi`KK+u~gyi :{Z)'&5qOxpmYkԲN9ref(B"ʣF}<7D+yJ+yhœjY?ɪeii%r?fZv2 ڽS\\%WQџj?&|\nT)>wJ9@w;ˏW>obxگ'*=o Ӊ_뒏kY0P.ASPN_1@@@@@@@       @@@@@@@       @CÇ3 PT0 PTT뭈 3@@@@@@@ (,J|bǎ_(_3B 8U*t~}1@   >/V3@ja}(¦/ @Q6om3+blmUN.i5:|P׍t~Md$`0uU)!1A{JIIɴ@\3NS*TFvթ'R.8"mV=tˉo$*TG;F*b_yRRR@Dv٥tU_.3 Κ?Wѱ9d:qwIRZ<Sq:dF+S 9@17E\.21"?tJO       @@Yغ䣌@Q ]{` N@@@CAIIENDB`zoph-v0.9.19/docs/img/ZophImport002.png000066400000000000000000001344631415176210700175460ustar00rootroot00000000000000PNG  IHDR"p5sRGBbKGDC pHYs  tIME  9- IDATxwxTe;R NB/ T,(+X]^ݥElX*J$ י! d&=..f23gb7q p۳ `! `9Ղ2=T7PO;i$5P!wxUfee;ƕw{+Z*nC @0">5jz)NζVv[.yCeKQYZEǴ3˵ T"lAuj_OúBk Uyz쩲k܀e̷KnK~]˟w˽i.#Ǟr@ n/ήlkĸ$PɶY{{{E=B8C.abA&+-{ZU=V[Je_1ᡣԿ`թQ_'G)#3CRi$ج'^[u3N{皏wK; Qz:q>JƬاjO-.n?@A.ۺ-bsGuW{դn3p\e-T<.-Cդn3]Q|R\vvt.#4PhZSbJ>e/Krqr_zܭ^m*Ә{>`uh\<[ԨJHӨa@<2 kwAf3<-muYԳ(l~ۚVKZf-R=u1fclj^xZ&Z8Xj5qkxjs*rQTN.N _i:~R-:7y:w-}{~xI5J㟫FRMt\{.6kXX_kU}q6[R}ɻز z*>m~qglmpR ǿ_!]F_Ѧ{?Wĉ'tيZg졚>oOڸO|={ؖjG-\\^ 783xRoxuiqO:_LS^m翛_U mStϟ5[5=1o2ٖjںN^8E?/PMZE>ȪZ'wdM_}UX}_,ϔDPA[Gdo t!F?Tn[Fe)yޣ;~ynqWd4uk7)-]= :uPN.-b233tR-mv|JC8y6 hh4H}"ReffhۡM+l]E=gKI:|*BjY·imliۡu],>k6cK[-k9YK5T [ήjݵΟûIPfFfqDŽQ(rK;]628*"vغk XK5iSiC|\eCa]rߢJz/EY~awA_EeiyA(:*-=U.HJd$e7Mn^6,|M^5-;RR1S.N.Vv}vS#BGh4u_Rԋ'Krsq75֮lY-8s=s.2 kwA,e< ZgҮgqYkǵƵ /rnjnqܐ] ׫g8nJOybUL&SZE;V e08sn%,-ϖKRf]e7y:~U(ͳۗ,벨gQټۚqmiq-Ö}Ro7*#=C{Pnͯ7X{PfF"V^t;~Z7j'{;[P~Wk˱*6[Z[_]Xzo['>SRmC?cauz3OiEϠ.#Tׯ_oۖ8ꝿhP8jGf=})Zh֛1%4m:} 8DE&XmؿFBkDQMO'CC;+VӰnwC:{]YZ-xMN_?[]<}Y;OKҺ.zuo[+h\[ji\Ke_TW]ƾm5?v<졁%Z i{yD/<^6ow.?MVKふǪliCClY~awA_c-oz(LI \٘8y,Y1~pijOeiY!Ԩ%~YI̳,YYy;mmvX6|f8UY>ThOid /@`pPf]t.4E>SOP?Ts1gˋ>S9q)O;iTKɥ^J[/%v`.Z0@aPo %Pi 쑻3S|!(`Xе^ʻ_ׯ]a3l͞:"!k 䑻|E(T`wMo~RKo~Rsfokֵ ߻M&cwԞm7vRB=>ޜ3EI !XGM'Ӽ=V̧ƚ7z֟+ ,`zscZ՟ 7\.~'sO}LOK):^xgy}8_=z`r:uzDkԮmci'*}{Vǭ}CFoNP}+g֟\G}kڞ2 ~9ؽY!d08mڿsc~H.njQ;n9}0effۻc6!odžiGi74bD-fm\km%iY!=Ё[O.ars^@%1fںv0\P]ήn$''%^WV5dRGq3ڗLdLWӤg^jZk.I>~qr^..>nB*G?Y/߃1 f*W$+-5Es j>nժkޢʽEi$_^>JNLKFF|L%F uIݴ}jeffh΍jݾh7OMO13SIsDw|p74iR~2ӵmJ Ut.aw yh~B'kdž՚|__mYۓEϨOj Ľ/Ǟ+}ἬPuV8qyyky"էMP=8*q>Kzq-MC/ ޶7r`(rupfأSH HFcn]w)5%Y>~5վk ljqyڬ*U988(^#u 'Oo|k2tA ߫甞"g7ժ&:asIZR%&īa_+G#tp&]|QGk׿DjNNJu&*##CNWP6j֦츷  /t ؘZK l]SQa1_;ǩNe_ltyfFN( 5^~5?5%Y,.%>~DQf 2JN.ww$zlL~c*ع<#Ǵ<,/Ruc%+%)]<gua X!609Ca6cf~nqPd2i sw3=-Uk~V̜s?}}KHZǹvrVvpTxm-g(;mkVjr{ۺPؽ0t=8eF|jԒ$9qT`挡z%ڻ}m[/IJMI -gؕnbc.ZQFZ)=-Mֵ)3#]׮đ jVtp.9ycJ;lEu{uO%]ׁu.sX9y4B w޾$ %~6Ɩ~#Tach:y4Bt1OH+jMfũ'PW.$5NBIҡ}ղ]rTav=X5jՕQu=,lQ:t3bq{Wf%YtRãs<M{<|dooPu/&-ۙ9cve~k6i)G'U=0KIN5nI$xWe_)*rRSS&L SgfzN7eUd8fmnyaV1x|Eې|3^1~^5,|ǯ$ү6l{y/AdM*μYW|U+_c)3# ]n6*w]] fqy$ Iin9RN޾7O)leZ*_-`08nI֤$ݹG5dk|1Sg7uyp*c0thQ,MJKsbq:)n؛\cʖ~%YQa OchըUkWPai8vh-:~w^>7%nVIܟݬ{[pQ:i*~GI$oKbjS}XcwN gAx{ࠦ`pPCھ~yx5jZN$߱IFQ-چSqt`\7@4kd2J_`(2jI8[K-μ|_}ȑOZrquWP`j_Oy04ڱavlXu0Ww5oIvܿL:kڜ|6i_])ߥ |W8[K-μt $IW^R?e>[`*})9']n>1 c8ըe↍[נ=z~RW6bR3玲;3W/_Џ_,goWjo_uޗ=T3}R: bYu}ު{4l:~WųJKMjԪfm:~P"[!Ѕ'e08ۯf8S[y0B5sRS(Oo_5lJw)j&Na] ݡ" TV4q t `!QJ7fp (m17|){U8cIE8czU}i:`! `! `! `! `!JCEiHWBbF#kT*u=Ԭf7߰ikkW/_yob =-!rSAc nd~-.ibc.!W.3?饐ރUWO#222n.ڕ[T?TaVnHIJOK-R_K^`+c&;;d$L&ٙn5];J\\ͯF>%I.nYӹ>5% qt,|-+GniSO.nYs|/glkI lCf&%91װ CPiMgthVӧZvfE\NQdW2چ Y7]ܸԱ0W7|Oq6+5u*L]3ygÒsҜG0kܪ\=򝧋[57 *JJ9 (T%\coڻebc.֬碏)33CO0V2|oN**k7PXV3rI |TynuO<,MES5Oܤ(#=y!]g jwX\ڰ |A)ɺr\yD^ szopVth֬2%etuP: t)ed+Y\82-ҧ諛KnY#/_5mѦy8c M[^PsUnS{^`35 \w隷 ^SۆziI:V݆rrq]\UA  988ʿv}h<<,jego/jդM"գ(}*6lJժ{^%t[8yڑ-ԫJ9Z*#'}>5q %  JP,U;x|3 r]X J_?W&PETR2 zܖJ IDAT`! `! `pLޯT ZKtkw5B L.;=b ""bN 갆Py{:Sh4fRd4(`@2@0 L1 H2;cPQڵܹsղe  U-g ϝ;+IJNN֧~\kI4s\gM8A'N0Vv}رԩ+ɨdPfԬY3M6M))2=TǎdTN]]p^&QGQ2?ԧOկ@!!]_^zIm+00H>CJ3MV9nRR>Sm6]hŊ_uTF}U.\~z9sV2OFI}ѦM[d4r*999iժe4ifGFIO=5EO?Է.ծ]|ڷoVU{얳^x%d?l9[V^}իW_M4k왹Ϙ/x_[lf|}ٳgiٲe2;L[ndի5aƒZzL&6oޢ޽{N^xQ11Uv^}uyך3gjժ)ww7=ܳZbg `hd?I>:|N9skibbbT^=z*&&F&I]v޽ݻӮ]{k.2Lz~z 4D=zտwE?@a@uIW\O=n>ʐw%I;F:/ySNA7e|}}e4䤆 ?WppU֭[O?SArtthT6Gd2iuzھ}$FTV" Pq(C-~FwxCh(&&F111={ j[={d2gze 3OȑJOOqIy}s=hedd(22R'?iިQP3Pp(`hٵƒ$-[>>yff͚޽J b/,BCd2*4^~yz߿{1EGR``^5I$wx?nM=T,v'ϰszT6`׵zvfj*XuXCԼ}tѣ)&EҥK_u;*^e>5q !P8c@C; wEB"j*X)*V%N]US0kUBva.zeC @0C @0M*_>iz5SADDDȵQ_ m3 ) ܸߝ*GSn#1C @0ВFMSS@0Ho};"!%l\v@{v.Ә+rqrK5%$j͞U:w$32y*BkR1SdggNͺM`;z(19A[e2Oa {Wsxykպp7llז֪:4 QRJ$ujU[imAZRa[rڰOg@;>K#bI[[}d223vIRhqe>[יC!@Yጡi驹yWRjb)YV3~g[擜L@a rsvbrx#:'aUӡi 4Dy*y'ѨĄ8U*8mYBG_}q*NIҡ;T3^]Aw_kI0m~$ 3.˧${{kU6/{zk[^̐$F9:9Kܸmiy),pbߗKBz `QRbwYS+0hKhsz\ʾ$Ewo\J4*Tս*cxiIF_{tKשG4e@=>&L{Z>Y޾޵ Iڕkʬ˶I q4HKMWKzn~$ؘrU{cp}΋ۏ_MQy k1ڲWl>nҦ(㰖|v#1ns}Иykg̲;FUu Su$|n_IC/c.rPɉzqڃjK޾JV vvvVK31|i̔p% YVŵKɿlzsttR˶!zsSjٮ NW.R! ?ΞR̥\e: r۾K>]KI{^'g+z~3?iҰⲷ:qTAw`_Fة;ZP<}c?<֬]_.@5lڳu:{$u GmD]t^ q, +LVJINҚ$ $Oo?{_uvnXA@w W"ݪ}6z| z\CzEoGu&5K^>5cϙǻpVzZ6ܸ#l_~).cٙ>0k>gе$iߴgZsг=N }L ߣZ[l@*%WVtyy6Í [9?׍s?A ^^f==N>54r$yjĩ55iTjծ9o] fg_@sX3M~~Mݤ>)*z|ԣp q/JnV]`R*V^e>5q R2$q)@)tw.?CU܆ͧoEP\բmLA0PJ>/a{C#!/OBv=^?]?oy?`l'a+? BcԠպMZо(L||; B! {CuA0PZߡ; *CP`$'%ʳz5 x A0C @0C @0C T0n@!TxA!JK!Zj [j5D!J˘ S5_Xº|%V39]c'>C1JK~{h nOXkq@q5>;u#ƪ{%J$=Ԫ]CJJ(*7wl)3RC(@Y;L}Qd ! `! `! `! `! `! `! `! `! `! `! `! `! C @0C @0C @0C @0C @0@hᅵ"̄!C=5;uTTqڌ'4Kz4q~zvHEGEZcfkڗSǩ_jO ^|FV׆ i˹sQ/Oa!ԳFt=WЀ2ph&^Mt`޲qΟ9iq{~ kK?{cCDhw o T^rvy=vO/>qTݫޣG#4iT\47_/ӳhMx6^i[ȃjUOWHy$YrٗzYǯ_)thMD<4UYsyXܵzdTwmYB}~?,Gdtτ)`fN{8ȱ=wwl嘐z%z յsT~j)u5\xV I *G'g SdQ3aD'%%8<==M|BnkY\о4h8IU?:]q8:9 S%I֯4{/ܩ̋j޺]\5zdm>noֳ!ehq2JLSu/[ٳm"mWno`k>U䁝Cwf@\;hgg'{D *m~$ 3.$i׼|jH-gѽPxuYΝ:$)33Cd2,I:w~),`~i%I!=()1^û֬)vo]'?w1q)o{ߡc7.%O*^CWA߹x$f~|lG${{{:qDScg[%%I1ak]{ $]֮>M!!#-5E_-/IegNJ|kbcʑV%;/2 '9 5j'8v!_t-6F[jP\Me֒& 13qM7Θe/Iw_KnΤѡZGn[]:y6n̓ͯO{a=_L޾4]qsn1zf;avYLcŠ d25Kwjȃd4uAؽ9::e9)lENW.Y[89ȏ@==K߯r$)N\O?~׉>[_Vܗq}g_BӲ} 5U|jkdP jF/$M1fMONW]5cb'W>ãj֮y`6H]ٺ}=I r#ԶS ૄknÁݛ%eR++1!Ns|Ϛ$o+w$'iMD IMm=诊:|@;7^b&cZv/g6zĂW\J{N^~ZL>yTgi[SCF\tٟuGuoڳu9 SU~>zsQBu--6 g \Kٟ~kԪn;/O߆uzk?b?hKk3ЬۧF?7mĜ%iٚ?4*TjלV.aN׳/~?SQ&??ߦ6VnWӔ=V^>~o @b7q ρ.Z0GWOhv]ZURG\g8yq) Y;}idžͧoEP\բmLA0PJ>/a{C#!/OBv=^?]?oy?`l'a+? BcԠպMZо( @i5wPx57ERJj\բY1@5})rQ[s罡w}`w%(1НCWjJRRSg!GGCP`$'%ʳz5I7?Ĥ|KJJ7߯`ʍWu%%S< T:ڨ_XխSSA׵y^r""( }z2 n4WJj~^NVZzZh'Q^!C ]bc;;;Stu CO<M%Iٗ?P+@ YQhj򴗴a.8a(  ܎o\%IjP6E!Ŕ/?9f!I& Ci衪QMt]C$I3% CQj@[4_ONNѷWE  g W7w]K$ ~C+==])w^~}u @'7w g mCպ̝SK\TE@CeVN`0U35MV _ox{j҃wS+zoR< 7\V䋥GQTWc볯SN!0bJQ!Qv]{k))1=ԲmxKz Ce!0F!R2 `! `! `! `! `! `! `! `! `! `! `! `! ` @0C @0Z%{ IDATC @0C @0C @0C{]r3_k GSQIJk3.A;I#iu[2~`[j_RbfO~ipG?-xed%On᪇F6$6hlЀ`OMw]e]QQAQ@ѨADk"F/jb4nJL j;(RĂs~sy]?N9993=ðt~xE`hͰ0|-pŧbْElhsgMÈ{`nqWkm;A;a/8c^Vl'v-=71RB.jU8Y=C<̛5~?' _~;^V;xi\x0g~>>nycn<`a'n|$^x_9nC\kN]yM_oc2zuzyxgqN}|bvdL~my6uv;}~wcOqyvȭkx8' 6x޿G)8p1;rEh mBH8W|dXkQ\$V_y_;5j=v[8=vEEsK}6؜!qt|+|56~ |͢6bjl~/YpmWT[K$Z_ynت1oݶn~ܿj6:V V,_^}N?[.(.:o ?:mmn[BBnk. ; ]wWhݤ9W^p"+7o0\r}cEة3~wM Sޚ{^gۯ78vñ`nmbPe3km}+۲sW}Y%M֋VV((\Mα1ޛ/„dڽ/=azs{zҗ 5C+"]o V2!$ϼZýf?ý^Vm3/mwЧKyk׬1 :<9l`V”aGCE8ꤑ9A /}_CVvk[=)oM qe>6+ʖoe"4HbHO DzB)#/S{Xx~'ҭX B$ٽm:㔑 !BJ&_v6!PBH xi̘nbH!YӤ)nj8Q=0$䉿Gr4f: jٜB,]VgƿK;L;8X(|ߗ'Gc>DvmX zEq1ai^;`( &\{9Qx2yyL1VIAIq1G ڲQRi߶5fqM5`V2!$3fL} W^w7>pOFի1e \s߱dr6 B)93ǂ0$䋕h٢`̓ϣbO,**Vb(ne,V2!$/LY}t5]jwxIx۴l=wX`'ИglEntK.8 _-Wc2)3g!B( !d5DvOBA76ުNhܸo5E͉Gp_yq(((7 "{DQ3.3Q+WfC PBHZ ֻcOM~A0dP?6;n-w;v6?]M~6OIg !ƍaUxlV\&`ݳAm∰X5kⱧ& !?ybH\w}>͖o]f欹ճzƮ{ߌO-[4^%X C!!ЦuK80LRo388!”3ИgбC[o/,\/& B( ! :y=mZMŤw>Tolyõ?CQXXG|M_\0}<4j˄BaH!mwhִo7xDl.;퀝zl30rZs((r˄BaH!m:o: MJ܄sN0djAl*/y]{f͆P\<1+W}^|ꯏ&8b@6 B)K.Gi, CBHKo"N80l֡FL< 6g͆;q;Z2&%C1l :oFAH=q/b^{ ( !?^}hӺ%N?,BHEKp.9¨_GɌ}{<,]B!/Y8vߣ7?RB2B]ĝw݄ge,BHisإ7νV!!.;`(ʂ o%B! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PBC.Q외`!8`ث%.:0|5vm*pega؞cpgTVܓU;*py〝?]~X lp qNqNpoI9]!j OVk~?_t n^|*-YĆFH0w4' L~ջ}^Q+m{j=mbC8ѷk9Ǝp4VE-G/qg`ެ8ȾXvL<]8r\tƋOS?qqu;p#¥7oιw:_o#}6{^^c~s|Y\s-__^h9N8"s/.:060B;G8/\3.W6kһy}VL7fx]}>!!dc=bqWb]ѩsrX[ks۽6I@Qbt&ͱxY*˜߯]7: MJb%7; wP >xZ'T}/}=vԓШq1q_}]spŽ;&8ꤑ41!R< :xcPb9ZjG8׻o1]t|jǭ6FQIk~ֻ_]{[ͷغڶ?۳>v0ۮH/Z_ynڿmm;kƭ|kVBvmSҤ84/?Հ,ƕ>f*m7|Um!8\׭1T\}ǣ&v&gF_6r1v-]yg."6'BH{y'51Δ_oplyc=b&@.$E-@y3~U+%г)V+PPXhٺ]9|8f{j !uGO05GУFn{S+XN0̣!"uHG{|]j3kc[sB/G-:woOMex[ـ! 蔑_Ջgz/݊@Fu{e9?_כWÍۧٸw^NJehݦ= }(m<|6<~Kvj:%Mr$8l̝5u. %u׷\u.>`'㬋{q|WSWQT %MJ\7_ye˖Uv8X v,!?avoF9nq8eeB!Ʉ!o%B!L>!_F w !ƋOc`wFBɚ&M}8fyՌ !!$O?bq7!U,BH`2<3%\ai_B0$ ?9S!:k!+Zl9 N;B]DCY0|}7oΉKa̓cXz5 Mbtߡ >;4o^3& 0+3Æʉ+|8MUWc榿c rfN}HaH++ѲE3'GyJ !?**VbneV2!$/L ҧoOtڬowhִ7=FZh;t,0xh̳X^"6; NW U~g, CBkX ccH!$GfM<+Vb*l*ko tr3DQ/^y~1` pడ6[JOgN 瑿8ōrU˧jˤaRZG `w(mڜGaHj.1v܋8ðYv3S/`ڌNNn3{Uww>we.6!lR;tC6 漟"v+/q. ;x1,YB7,^ qGo + \1$di_; # *X(zAiKo{٭ػ!, CBH]wP} IDAT0A! J&B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB!BaH!B( !BHe-B!dj. a!B&Gi8]v,.c!B&GC8]l,g!B&%ѲwE$:,C5vusTT6Fآ!Ҡ(,J WcqZ6Z HF+&BT] !B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!B!B!PB! CB!BaH!B( !B!!B0$B!s 9mk~zIwiyj[uwͺN}k/æЖr|_εߤ-w}Ŀ i 7M\Ov,KMIڧg/QPnSВbP/˰M  ڮe;t.߮sNgLI<" ̓(8ep,_lׯ46@j9.0 .qh} )V`[Wq.f]wEP?< (Y4>ύYuqnu6\>p~ORFC +EX#\5>fZ_>?*1!l!P7?z4Fɼ?#B'PNL5}Z+(\y->2Vph<0kvHb CccCID|A9ޒU+@ۀO`1dzF.~) ߳-gO#E?q1'bTz>A3jBi.ոd0K;xY\; b,W]K5$5犘&_9y(;1DM2y̾J||(GŰFY|17vk׬7Qb9>.vcI]' &cUEyVZ}=Yd!' kgMjǫreP{Nԏ}B鎖'C\‡FjV];mQ廫 M80`?#ܵbr+I.|BWkCۃs)B6~%>H1 bsqΰ-dO2 ]=tP)D$u2EiHTv.]0=C Zdmݾ#3,.3c*E-:o׽`ҺfzvާGښhfv,bn(4m;lm턹~-@y2|ll٥[lq tޮ;֮^ҦRV%Ҥܾ3 R qH+JC_f0.yIٷ:eUۤ4cE=f.21K;1iQe{Y r ܻy!v5*?OI&Ys]{!8'I`KZI=Ҷ(}L;?m-w.[n Eк]j_n(,j66L]&Z:m iXK[\v(,*Bbw "vw_W jEgi><-,cyI&vnkI&0k+ߙe1W!zGqIvm 7Z>J(֬Y{UEy"Ɇۮ^/!4D\S&LC5/2MzIՔGҹm '٬!e?L(mUI&s"u$"¢Cy2Ofr,T[Y.۳>?uIg4F0D V"[O٧h4*exZՅQW7p'Jl,aM_F$f(qiޑШ%?%ZSKH[qdYwdQXֆrRz-\eFtC#lV3t\UF@ʴkXm|ɢ7^vڪKN`DW:Z׹7k w"L[(me"phնgOw^R2g]ָ56+UZi`5m@7ʕ('PjSX*)e;dCXM7˜1 _my a$M> Dq3G_ xiMY  RE#fkI+Qf&jE/JhVBcaR0b"Phϻ/M]x=]㮽O>e(mlo[(j=w? ~W3)Թ-yF6zgMGQAA?>(M0fk-74o2¤ F|krEG/NV@1Fo)AH:+H([ܘ,*,%/JcFJPH6G fiu& Z>qcMVR1&XC0HJ233͵'uJb0]#>7˱m!ZǞWk][iQxBQF]w0Ȏ;_hMDrB$kV\ Ik#E0inF3Uf.ow]BkEʱKNmhp&nK=+Agv3 ήg(|VesPZ(:KB6k Mˇi~(h ?$"QEr7׾}1뉳 9ߺ'4Ѐ0Vۇ|m e$f./@eLL^yi'$eIF6F /gOAF,KD>лs5Ti'$Rb!S(M4=lW[<`sZ 25_S) JǁPqמv5rfA5xh/B2@ QSZMN "@ Ȟ^U.(E-I+lCɚJ4 @Y~)NȬ1aS k${,_QOh 5v3>@Ǣtԡ6#Xj7B\yUL&]6B;}iRmgsh}"4.hV[,eJӎ[2JLi> ߹To14 ȋu}담J]L^TaEB4braҌaJHZ \,C[DD(FXnh}obQ#P4YUj#E5e>S0?`mCTȭqm;t '-Ddj'RR5 Ʃ>Islhưlf+V5~I `ֳRaxf6B C#I+ }8>[9!eợ"CL+<tY#6 ha=Kk"zsl2 ]I׬Ć' u JΑ _]ҹH딴EK#ش+dRhͧ;d,B>z l ﻬƗN3o {}_261f?^xV&i/15 $}B!!0Ԃ&+4fύA]xe7`>;eUW0K߱|MhFh} mI.bPt 3Y\PsRwIֲ%mCig&Pj0i: gSbO@Pj̚L%m^4,k&}YRkMOH݄xHa$Mh3}׭sՅ&$ ׆\v$@[! W , !KmAOjmM55e倡Zoк 5sؓ*Uĸ|.$xcy aYwx&!Ԙ yIY+^iJq5_07VX¸_[6y1Oq!9"E?@ig䮬|kY] _}v5(F*WffK62c芋|a.JɻP#ImF@&{o IB,ay&`AciY@Lk70PW)C*MzD+@Kόxop2m!HYW&p5ǐVkCmp(`PV;CAܠ`KڐDƃ44$p]iE$P p (#L&BVLx)j(igrirbؠ4w|4\0o|!O5"tb6B !Q2V 0nT[1 I t&Ⱦ\)% 'ί)PqhgO:IqMjC2@gZ h3̴*ZU!M<˦: 5,5#Ѻʥ`B[DB_ Xd7+7L]@:Cz:B|ȖBdDrb%RaY re+=c SS&`ؐhTeڐjq <54׊eǬ {VP'LXd$ZX84Q,iR﹤AscE805Db'ͤ )1FBRoYFE2oYe,M^PYe"X&9Oͬ.ukE[M[i uXv#L̲Ur'dlQW+lY\WXiWZhP_fRl+B!'Kӓl-jpGk*ؔziBmz!Om&\* iuYW#wIZ9M5%~o"~fa7>Iė$i<4"ն}xDkRe\bMB=]c+ ,SVR~$/c tr1*L4R60vI.FqH! *V(p_ЗBM)d4W2Ieq @j ƏK;ih{ N Gm`[d+!6DWɞN*];IHkDp#Fɢ*eM#fR! _;Ls_[bv?yne4Ϥ}lV^BbƏQ&`R/˒,PI - T"FkN*.HX㳤zfQ.QeP!IA9)?iG 2q!Jy+ &+WHJ!@ąfׂ'K+1iъ/mBd$M{$}_3`Jb 5.Kj+Za"ϱ7ϵp? .Hmmɱʶ4mPzo7z9 !B qқ4Y!ZQ98/Bl!Y1+ uʶVȩ+KUJ%@6CYZC`Y$ r%DSZ#Y_?fY5ytIA vPw]UmKʵ4wW n>6 YŤ?Լ][}'ҶoєG/56nh+n$ KHшzzCX+H!w YɾU,4Լ8I S1&*\K+SFG 7yHVP11ZhDS7*,9 ,-'l`q>!2&!gE%H"iw7~$)85mw/|CƺIf|1]Kj*:A2oW ,$=mS8'L\>~fK2x$=F++! @c) 4ͪ+'7ʄ&k)MjeEM̈́*i;@rN1)T$?iW";NӌY;Orka]U ~} !BHbaGJz|@ox%m]ZIGigD>|!eqYyl`%үRsf1F^Ϻ-dAw;dC51#+|yևq/* ۤ6]hPa7Q}1,g_HC*jt@(S3 e&frՑ}kǐO2ǵ9MWQlje"GI21 A0մOɂJ39Ťzѐ| H?$ZWuj|mP 9vXE]ĭAXvӵqY<4 OV6I`UV"CSq*;4fi2BI5> PʊսwBM=#忥ߵI( 8~ :r@0a^}bT Y[3]}V#%SxfFZaw ¾Fm$. Fc,f xzsI6SߵJBQBa+xHR`c`Jo/,Wv5>u&ɴ\#`B,1V0n2+ (~HJo>UuU0$I^q}f%]3ዄ/ gp 2*q(-MBWw.A|ONL${HqN*A3>j n%*#Ri#K0_Xұ}A,.[kA~(!$W{ Ԯ[UzS.>Öl/KwCrVV{oOD/7 }DcY4cոSF}=қѴYb@, LMpoJh+5odk!L|: vWې'_ /U'B4w) S:wZN4[Co]%Q,pH Y4>zɃU$/GYŀ"ni2]'kkPR՚#`6@Mnh=|0X%=nT:*ᇵΚ]xp{λwуw]͊e/>mW,+VWg{صk`M+v\K)i dəFp&0Rݱ4B|V s6(& ړC6&er4 j6>$hs S})q_v1>Qml/ʠҳ_Wy,߸W&6+]mpU>ܻߚ>1r]Ν0WycZs 3÷+IslnIslsXlI&6gg_|^}./b\UzU2+IS5oeڇ>wn#nHqRhBܒdc;qB\6qRhV\z6auJcgWPwFq}&9YǸX)dX!8ve@}HHF87M; iR>-S9NeMCV#Bf?Yvd!X!ozC\qڶ pި[1ɹ;+(ǻ}w]wQ ZYwԹ ;|!V,_ZWƼ.ϭXڳ i&i\ ]Iz] dQI╅5v,3$!qCUbYMm{-X,_%=e˖$f[nIHe- BHG1+(NH:z!8Y\fC-7 [w_ZjQglqY!m')((ufݘm%I\JzNFu[1DClY$+/Ȭ|^'% -;~U*VO4>Dtx% |" i<(MKk]׍Ύ.A4..\`D|܏|6G[S'?SYi BJ,4n\-[[e[҈M4(Hu`=en<GWmS]ۊ&36al8@GiOKkNDd$4hڄJa"hƳ.&NZC6hm4c bQN::1 o%`} _[pyf Bg|[ڗ dnJ\֞潩~Ak}}~ܨQ^+]~/ޛrtΫE6Xtն}-_Ϗ9 {_PW?C3G]&*G@Z/t엮2tސb Bl6.J~$aWq K7 Ϛ3%Tv;$n _;:ƍ&Yvg3]:5÷{͞u. ArǭJ˒ueIlʀd̔teֹ]k˶ue+Zɥl&ȸL0W9Jٓ뷎“~ϛ.v56z;[n;`heqI-[Î;=u3R; 1Kʪv՛.PʞzBemL'iN:^Q(OWMr &[S>I%3+!a q;&0ixטg}Rя ܙ侱%"ǪAfe3Eu nIo<&j=59Kо>Jc*iڊk+U?rκogjͤ6x2rrTk¬\SOP"`6k )ul\6~JVo{=/:|}zjV夻MFSڍӵ:zEcu[7 KvwGJ #NFyPgz+ }U(p36.DB"L$}=O֖) tB%1)A&$o1uB:uzķv@Fº1K3J/ #H$%!ڇC5P+d>}A2V֊81!LRɀ{[5|٥ŏ$L-twh]z"$$c'Eo> I׬gPP| JbPIfhCEEAUrUM80ȟog>5XL{ ĬDuƭBBRƬbF(hV7ؚŸڞxGZ45q>d!V/!Bp |xӃ,72(4Htm#Q|sҺ/1{ci@~{@(Q~w~0?ӱJXP"B w%FuIqǗUÕ>w@9^j4iŐf@V@ޥ E  賞2H" c$P?j<ݲ\J6D!8<&-FGV~uV@֛U*oIAv{O<m"KOsFb?%57 &'sbh<(4:o@] .D)..ljI7v=WjĠXZ)j%#bLim [׵hU hqW_x# Сi9зx J߾5^:w=?$1G ^{Nz&H:Gxj=l_͊$|qŕEĨJXz@ի1і|p]1!i?g'Z8}8E=|,lX4+d%Iop-6Yo;~|< : yfdΩg^6m*MjuJ$ CD#ic .&MV$0(Vl@ H*~F?> _!eekW$qdM}K[v!ކ! ՠ!!B K߅%M؏,ȯUgW^vYX O-%LsY7m wDq:^{-I>/E[bE]f5Zu5&=4K컒}bC'4!~w5x }$5n(:U4V`g=Cۏu,LB~x m@瑖$JmUl@ѬseiZҀ+%8%i;YZLLXM@@iE|>R9&>r X:/AMƷtl_H磉L_\c*eb,q^Cⓨg33a 3wŮDuHИj b1XV:)C` "R 7y&\ʂ4h_j;磡5|p Vu5":2C(؀ΒJVFk_֠,h},~mbg3nn_i1%$'Ҭp=h^(=P[E V{k-lx+Y$E}J PTAAFX2U'*]HU fh[쬰* Fb5smS KUx%ofҊo^F@~?PċNߥI !ZsH)pq!ĮWZM͠ҊtV{ ;tfޒ1 Q Yhv}EHҎB鵷}ҞC(:s YF; dqP-ּh<ѤG9iWCbOIM!aےn%Ѯ'1N4 }1IӒG,ҎOVc'M=1w &wIDAT-7;"D7KB iS޷!{8d{C/'i`-Ll[ҳ{Y, lBs<ZΡYeƒ $icN6D- }E64д=d2PWef5, e38fF/zڟ1 e2_n1$>]֍ {gWUg"n :0iel0&aJBh]$eFHƖbiMKdI餣Xb(&dZ3Jw`;d=w}6ܗs~>'ۣ=EoTjE{DZCCO7]|:؅xܬ:\d$UUee7kK,S.Z˪e9j p9{Z V60UQB400`sz*_@m53Y e?u%e!Q-eS=(<tU-Zs34U M!Y;˱D}2x\%C|C #8 A/W^#NCxA.o-k¥ F9d`EIo .tW:e-DʓTjM,$^ilETOu^;jWr!._1;Őd%7Eq1RMGޤd~D9eW$>+]$FߵRh4EQ|˼)gԳ:ߝ`d[k.yascVy2\$v:B췿12wNV._:T<\ h֏49PaI7ec?%ߏ-ۺGMR^1Eb]Rk ;]E}kvH3+Xk~ ݈%=ivk O!>y)HX!3#>-Q Qtڸ%Pr`T.ʮDƪDƥ0R2OKшyam[!<z7, ) >󃝸a _~8L~ԨP2oz^O=>h.~a#ؾ5|Uk?Yo?c9l^{{|p'dXzCՄ,{O5†Ц&h6"0(fHعNU%+ODZWŖiUT+S՞aKEvM _s3ͩOB`oR7Obۨi pfvM'JסdkZ^*`2P=-Oe9Z6kMɪ]du1Q*QTG{DU Aįoqۏ˚{ oyÅ͕]xEsEBOiU"Qr{?([vOJˌK[<=q_o;M"bןo@lFɓƒueS2a+LU :+[+cUe"t#&\$бBF┪4bw CRbzj!"C2!4b͚5rTe/O, }+W LXLFEmOybvmVE-OUZʯk*ѨPbň\wbh<9R+D=-zǴ3,*d8A)Ɩŧ7dq̽Z&vC[pgn!ћnI=5KVa7 ηsQ ᪏]=qʌ9O? >1/:8x3x0e% ɖ{qbWZ0ZraV _MU׬ J+</da 0h4p59ROhF__BS {Ϭ Њua[dU (6T 'nu"fr! ;u*hp^`0?P'{=O9p7<c!$S3fNy~6v܈-_?;ՍwNƒw8)dʼ#@OF rp'CsL +@OO.rtvvbѢE8F9֭[. ]]]uUAǣaJ,rن±[0mݫVk/ma{uG[x-L>z&3xs3;tp`~uttP>؉e;?t?q=;8o6|uGǰLp9Xqۦçġ N"8dxS/7.vVZuuۻv#7a„8o޼ى+b083f 6n܈Yf'@V s%w?v-Tak_í)1\CTTRW}T-xd9Hm1X=`_4s p̜9)_e Q֑9waHW/xܾ~q䖵Eku?h4מ*_Hu̖R1UץugX::#l #efR2휍|zj!5mq'N(gϖM6͛e̙ӓ735/лS\[VXf:wRޏHw &t۲9ǡ]gEBV߫zs<|s++Y*GI 0K{xGbڱ,~j:0ЬWyi1;hӠdWLՁ u_,Y4ad̹za`#[T4Szې /-8lAg] 2ϒgN޽{qqk;ؑaޱ촗Zŧxl-}OUcUEF8o,S񴞔6q'1^uM΅|2 .e3p`<^n~|TL5}6lk1@0 8H!R[aST݊(ֹԼ EU26c=,8'iҺ-;9FhXdf#G $c_+БaP|ntQajK;;'bxk.,,a!mfl12k% (z3ó',T/M{JfO6kLqޏ'0dp8eٱ,B D } ascaN#vbe -Ц贉9lLU)*Jv]UƵBH]UԠsfL_1~qwVpu!J -#Q@88a< ڪNi<B։:0qRS!2JB*=7ZV:lX :ao?̏'fȉ/ [9Њa{G{G{G{G{2\MEsse$pZ}/u it:auh 8~U`}lWjeFC,yAԁcr? ʼ{r걼;/ε+=eA)yaƿcѳ=TSvVbXf.5u ǃEoT&p DB6 Y,Ji{t.5gZřm&{.I& pՔ싒Z) ӜQqNJX9Kɩ D =i `HE0R-l-$6(ttU!^b;ZD[sHTɬ0 (>ŀqn&@i]yZ0u|gf2kZVGǿiw%+aMZ@'bȂ$`%S:ݵѨJY-ejK[og+`-Vϼ1+aҎh.Y~ 8^h a3]-J*9–dDyDԩdhLgh0hy7L1p³H 9u?qbfHZ챔F<<;;푥&WnHTIy/z#Ahʹ ޔ܈^g|LH`+F[V`@arFY-"ͼ]Pb`g! p$ /+,iTL摉'K-F-װZ  ͫ(C`g*)SQ#́5dӱا㴠UQUF^@ȅ_|Z =;@LT)>%3}9ל fUl c!kAL"YpX S 88c/4; X;×sc5F#a K.[dT 0SXuג:[&M=aUQj`SY)=m8 s\,5Y`mQ}Fֆ(r]V+c/&=n~ǰ=ڣ=Z07hOD{1U9U=Ca3O\xo]o.n5_l.;uuzUs~_8uA+m{j//m4nnU_-rH6*,WŮo]{%kJ𝐩 ,{Z<viRHyIbR u:A9NN0BG2\ր3)FejKGϒ%KS/F W`\u9ʔ8!3Xsd(egRX&|jը"rR:OP֬b$_aI$1(9ZY^5c`ޡ|ot| 6OX@v]hB*eu;ۙ 1,oyh4P&`q,qL\?a(K)![1b̋Z7{ DpFX)o%&:OyˁZ.O%qF`ꩍؠٲvieb(:^ P#l 'D5$ӥЊaT=Ct:iHV,f੐X9J̏UA2!5:F ӂԜyGA6Rӭ Xu\M4! \03r,laW<,9];DLpo"Q7!@\b]1 Fh9\6gwRv\#~XS(XDplИ18b'Y%P1,tӑJ1^5.>cvL U}c:fVNMꘇ% X?b!zY9<2mUYb^m0" y[;DC]h믶nhF @ H@2viumO'59u mh/zE%Z<|#mZsemiOk7tH}Ms}Ֆ뽣{.m+^e%>֦}ОDkިM^}ž'!HR3Nv(/Y̚C+= 6qG۲VIҴtnwf3%uL`_?f]1izu'S#7rWSnդat2R3u;5srIɺ~Mb5GGN+tgԧG~M7J4QU.>*P? >\p@7UԭvGSA+#gjHa:[RҊFt$ktejD*<[ʲm=ܮp}OͺUs].ף\[WUn]9m?%?-/?pTVQݡ=CphMiES2au}1%lckiG><\5g?nx)M Zjef z[iٟQ~ԭOfoA{7Јˆ*)9)/oK]G5ⲡr8Ӓ_MIҊVp/řfެ9PeK{i~ʥk}Zoc2dY7} W5xmCv;5uj;5}ׯO~,ǙhQHMtsث_{RlC=.ԑԽKن匞ث4Y+'_{ןڝ4g'v젉z?jӾ9f^퍴LQ|B~ mسFWL2g}wVˮiMO)j߱ZoSoוR}h/I9J֌nջ2#mC44íџ-f0}M]M?փc6\ַ~UqZ:q@腟-d#{ք9c ؙ}G]9QzRӓ_O, >w=\=OG h鿖{-ޯ՗I#껐uv }Uc1m(ԯC4o.:~5?i X3Yy:xu(Ljsi=z:|oTUSCUlRr%g䵼:Tp@{i.u0<9SΤ&]#y<5:q*A‰'NӀ!ӭ<]e'zmQՀA|<߳:dաrWWFhժh߱cPDh>/36DSH0ܺ6iDSpӆ涋 9W>OFOG4r&'ixBmIM;F{7՝ܻi#ݦQSiӝPJZrD 'siﱝx=ڰgMk ۳Z5]:ink*~[6M[Z⻩W4pK[F,\ u deUkJRݒj%'^P]㵼&HբQwܲK3UcNPNnک{zמx=JNJ o 3o+Y6^TyC)\_&ZHוY4ˋT* g-z7UfK+2ZgͭXH .c6\e'gi;uMTEnR3w:tuiƝ3U.~A?Gw!뤙A UކiK wSǯh X3Ȧw.%9vaOjNRK\"촟uӞ;C}a7 w6\Ml-pBl6FOͼEג&w6&SU%gRU unyѴvYQY$3dMԻ2[_n[?͆66\D]Xeʕ)]^2RvU)9թ*wR3Z 'JMNSL)Ԅx5?hqH!,nirl xgiu>#dV^sd4L0N\vс{_|BO [e3m}ԝ6zk6.>U 5pY^\Um9yPikwG&˲BP?V6] unyѴ_&&i>#u@DhMp,ܺm~66\De+F[?ءQӇ>q!OG7IM_Ba/hwF/0lmf_U?f[kt年_'ܱ~_t?L2`ڍ7J8}L7rzz3zkMNbӛz JNJƽk–{T>^))ֲuu誽Jw~ytL, \fܬseg|?#jӑkK~fjtOk)Z]41unyѴ_\tToWDhMp,ܺmXkjhnpͶ/L=zO4x@PC'Oן]Ac_2F\6DWuܭL>^ޱ2!ᶻpHUcH!,nUK-=īnE'+1ke04^ꝜG{ nVﲋG?/mîT?*!'a2PmOkgj놖h{o(ͮæ88s h.Vjr OOkqcۧ~I]Oaz RoG6 @b{?|_PKٿmlǖ52Jlvlo(oSR5a\*gr&L+&0ݶ*w6M7~~z/Kum _4đ]Kr$95eUڱem3a\5Nܶ![LmZ>ů&TK/ș*IJJJVYiI陝|ٰ=i++9{oU=ɟ32;>P/ D4ou.9nRRIhOvnk~/h͖X)[H 3vU(95MUJed|nFD夦gꉥLi%yev"Wy:gG?}+/m_|PwJ]D3jtmXBO>FOhM c&NGxѻԏq_iYۿp[j?eN4ͨN?ԹWkoՔW饥O&TEy]ZtZ_{~~qߋ>K_%9'gF{Z?c&7チ-vԬO\/Iu=L -b-dǹ}tzB#<k1ٞ.Vbk{_$$f1DK-@II@" /m&n~eK#JF M6cr $H @ H@컰{/>%"*]Zޛ:hv@Α7U*ki/ D3@6$Q w]gw]ܴFvoUYeuє9T~zmڷsܕ.u awktxN?+ݮ>4i'9Z#wm*.*PuUɩSCFMT#m ߣJڻ#McmWvk:{H6C}k-.}\vUi5:wPeΩFIN:ewӠc4l$.Ht犋?۾Vءϊ /Oߩ^7Y;C=5:z`iw){ݕ.K*k?_W߁4p$5U_U WѶ+֎M{m_V쮭'iiΝћ_UV7((?OEy:vx$ Ꮲc-ǣ䱖eYxÇaˬML3UnWxKԻ|A2S߱_ ;3}Yݡ~M"v Nϻu>ie`Y yqu+n}ҵ$đF|b#Y5%mXׯ$++ޢ.]{hc/ :W\Lݡ9WݨFJкoSSgudN 1Ntd.8 S5jTefvlHvl$5GV!#]nnS+#$iܔ98hU39E3AVRV.%IEImµ7>yhLN$u9^{IҞ7hlP@N@]{ШB?{ШW+Öy٬+4`hIIN 3Yͺ"y^][Li(SMh7hiM_߸sck]MI2RvGR3uU*BgK#;C{].]{>/>nk[>9޳OuDV^S- [ac⢂#Mitu s bgٗͺBdLzwHzU{:s$[>9 wlK來(+i;4mIOir6U*vҗxm;4wMh+ԵGdEy֭\Ɩ@GN [=7x;u{dMݻw{wں.kn;;QR뢴+GY5nl]/{7WAϋ %U֭fdٕwp6z;yVÒWjĸ)JRyyز&Fa'E]0eY^IRlMWbkko,e%p|t4 >V} B/$z >KNIq5^{o_7gi5ڹyM2oi399pߞ  njv"> X3i>Z$ih'?NTU <ɗb )lߣnr84[L4m;յG;9WGӭgߐϞ.lv41IR5?"yt^{4n8r|bpI@οfO{oRɹ3r$9ի M>OLnۡC{(_UJ%9SԵG/ 3I} oq}ϿFfW]Oko,eϺ0DvlVqQܕ.euV!4jV Xhk!K<5nhvj@C - $H @ v蒹 `3bc$h=q;=cAo\@T6>{H`.}R#  $H @ H$  $H @ @$  $H Hĝ=+Ы='JUV^!iHOՈֽc%tj[G<ny^]()Ӻe:UC?)NԊ+<b+jgT6}(Ξ?INip 舼 $.-v@s6\x=eނ?n_)I2=]Oje N$vlN8 0 U>n#WERRoHedun OMӅr+ən=ճ 6[ȴŅ*?*Wy,˒#ɩ9{Plugwp\evJ/Ӊ#U^r^ͮn3hLӌy4`@;s*Jey(+ѾUh&yѮ-kUx⨪ܕ,KJ ҞmCbptD*G/M}tm=]u[ߌ3eނ{@Hx= fS~;vsŅ!9Ux=vM@uaݿ3YMMM2ϟiXZMO6%ݢƣ V\ y!Y$ɲ,htvgr$)9%-^UP՘4IJN͗<2d:kkT?WEY:hLNtbpExĂHͲ%>ςxRpI'OhurW*KW8=|=Zhn-CHL>o*|.蘻_ ]s%yi2;uQލ~Vr6"nW>IZB+]:ST3Eu7gvRnߺ;tWqa$iuájIQƔ uO򏩦ZŅ*.9=)mMMPyIҶSv7 3)2 V@Hgpg.Ӵ)5-C}б5yS%44{JHҨI3գw%%0 9SԳ 7%!Tv\6v>et$95leJMϔaJMԐ1Z-iS<(gvaSє2.ZlZ%}X\+ ֔/M.uwoJ]j1ß  $wahA{F @ ֓i:ktHw$~k[D6N IDAT蠎s $  $H @ @$  $H Se\۵kv:QJ1¾2ڵ=pڻ[3{ޓ drs{Юu2z $y 9 H$@H Dl27gX#G$$Ȃ|IGw}zDEÃāH O…_ԑ#GK>;M4YzeyUY׿u 6LÆ ?J,˫3f,N,P^UXxRՁ5cLYWϟ}iʔXfMM~j̘8p^Dܦh$ ;i+*\z?hܸq!o޼U˖[ǎTEZCZJ'N'?yB^kz--_뵴z]~|yxz衇o^ߴy2%Zںud}?MH (>}O2d|zGB>_K΁^u=wY]c_ey5o\[VՊ+/~A+VeyfZ͛7/0YTT3W?zԣGw-[$@hҊG(/ۣ{!w-db'{>U\\,˲4mTmʵeVwWyfUTTh۶m6m,o[ZJW_}f͚+ WTT+R R5qd9s&6Eh{D$H6))ImO~Y999:vO@󔝝-׫$O?Ǝt=Z5x 9y^3Z,K+W~o~[ڰa$k׮zտG-n}aHьվv\z忨o>~^sǾbu]|ܹzg4{,Yٳg駟ܹsڿ7>oTSS{jѢ0(>$$>F H #8p$fiϞ~Y_ڃz5o咤kYj`sgǿ3gȲ9s~kفiWc8p~>/Ks***}_:5;  -OK챸Vl> ;5VXsh:w$[Tq):%BNj׮=N:@56YK_-\8r#pHk$Vֹs6k dk5jvڮ]85j$- t@FBX<6"[w@ H@$ $H @ Ԯf@J!k,A޽[).#1%v#Dc7Єn3.a@$  $H /e-=pH\M+?H #W;WHx=:WzFI)JKNWEeotOۭ[ߖ$æiJKPL;oզ}dYV4WOr -g.K9Yݴr m掿"@ڲZh),]$IMӌsyzJ6]RΎT]SE @:u(޼*:W(IHl0Tі$I#|6f4B3p|r <F [U}VMKI|V宨7MFr\n#A3-rWY]rOS) j)8t$i]M3dlAlpMc6Y)#f 4OȢ%jZceemٿ.*NaSحk@G$F wޭ fHܽ{vZhCj2e]p3Δy EN~?St  $7X6M= WrLyoTA]2e[N:yn4SA.S׹}4m 681xU~'T]nQc=؂F3PR3N}%NaÐJ-˒ajO&3ݙ"IJNI W4&iroݕ!ӗ][*ԩN]o]^[Tns+gv[ڇܾtbyRgNT2؎0 \G hڤ&8ٌ$i'g:U+եFN,jfofEWFshiZXF5|u3kZ*8Ά HL7vdN2J)i骮r݁RRUQRs&F*+ʕ.WP<݁7r%$uɩi Ɩ?yUao6洤ч*#s2;gSS<8r@,8_zac H|=_مsڹ#m-m[]+ݽWSǏ$tV7Cn ZVH\EYN׻z%IY]r $XM0'BmXLV.-kGSnsZWqàGT+US]uV-OEy:ew[ @#% {O?'O49Mf.ۻJΝW~g`3I@;](wKg tfN[7*.g:PMu$i uO򏩦ZŅ*.9=,@+.L'l<1rӒGeFQ}_iIQեM`D:Z} Wjzf 92MR2g0 {Y ?EiY2LSiY>nJO$4S=zWRr Ð39E= qSBoHew˕inw[Ͼ1~Z@GS]LL 3v0M= 3%UifSv70=䦐hmNK*}(gv h8|rTJZz`Dӑ}lD[tr=׊=~>khCnR9m3W -F @ H$(壯)@'aOa}tI .Ao׵6H @ H@"xGSxϻ_oZnSmvYѧ_$ɲ"5S[i3H@⃏r_Zd\V[ ng{!5SDy7cjOe;?}N^WO>$IOzx?oHڞ(}T%57}^]_qGУOYUZ ESԯ"iS6$KKt_qgDA}3s$I6[㇏9~䀦ϿFG^Yy[\o{ڷCe%qo_#< ɲt'O #i{iSmisa 4i %v%z65oS}Qq @C4vL}%I=z3P[yPhm͒d-WAZj6y/hn'b65oS}Q1 hg߽Or{K"Izp/n"8C{euG~~Kw|`Qʐr~w6/뫋?si4c2M#i{iSmyw]kp?yꋎXhqcK<.wq>"xG:ʱYK+ui_kk}AKqR!gZhq0 AOwUVVk.i} m#a"k#DH@$ H @ H@$ $H @ H@]zGSf@r"fzqA\ $|,祥O&\@\qHzυ~ZO]?>Q66M_FR qA\v7SVuuB?H_6~" ml5-hha+r@\qA\$hG 4ie"9$P4 .@\qAvWAZjyG~ϞWN\;yMS^V̈q₸ .k ~/wn7XvѿL" t[PjxD@yx-+0XOҪ}ڸD\ZO^h4wx:Y\Uq)f*p$uIO]7LP̔V_6tTA޴B`PHenRS஬UUʈ;br  . ₸ dHMIҢ;koⲐ䪬%KVϙj?@ C|T]}'7OF [!V͖{-;x+ . $$=5IK Uj=p Ng%%4e4kUUY%0dYv~R;MM2|s5sjjjzB,4 9I*,,Ժ-xdqA\H FsoUM9+wW.{@~ᩂFׂPn^/ק U}{Ϡe . Q$]kij˜ˊ'JE%ԩsq)_^0Im24޷6{₸ . @oOZ_{bYohZ"D} >[sY!N\lf;@6TY\- qA\OnmE%Щe6Q wݷ͌/ky%C\D\qp d$ғHeꑓ6[#,߾?pӜB>7ݙG:+-@\qȶd3 }my+2 Sf2 ` 4ͺS[ \O\q6>@Gw֒!dnh S!# r@09%E\qȶ۠'WBvgo!;sߝ#/& SaF"B%$6n\₸ . @rD0mSRF4odAl4D`4=/NȐ:c;Lx\ IDAT#. ₸ doԴ-0| A&~?O#+ 40d4̺2 C|x,qA\H )k/v I5,l6=dgnL.}0}k뭑Q#0d ₸ .@ a-eMEǭ,wC!&.Ruu*\kFYV`D0Lm&qA\M6f^q6,vfN%˲,d6S7 CYmj F&&$_ ik1 ?(^ !Ӑn|r$sL₸ . @ͶoϘL Iт'=fƥ<İM6: =#tT_owjG# ₸ .@8FǘDܜ -'h?B 9lL[μe^d?ÐZimUI\qDL"I$cm| M%d ӂ-v.7_mVXWYo . ₸@t! m춨j49& 6@w( 6)8ݸ=.l![p9uaJ&₸ . G ]c9x✾2 "lf"PZ=>_syCL6K udYj2v{_T`Dnk+ ₸ .4tAl9dzeLOmU*/)e&)#5I*V1CR[l`2l,WNSCa1?b<xćif38 . $mdq^Z]4g_ 5Ne9t2RTSџڮ];^Yɬ6bY즩“Z~Mȩ|2l6zղ ₸ .@v*=:VXT<%uekI:g$xriZr:l42SQHNe_'9+A*ȋkžc$xK₸ . @ o䪪9Uݙ3 $2A&yT+\+W* ^T N/"xU9 a C adGuW:ksz|>(TWaY{^kŊ1U{[ׂʿĊ4fDP\ա^^NW\78b]~?Z4X 9KRԲʫll(qi֮ӰvoV!kO}4~$]ω eG'_6 y k)v}Q]&&jk)j̈gxFu9o7NOǏ*;O9^kB`o9vkfmq{k4fD:ƌlq6^1#R,p EK1#۪k 5EGjFC]6oMXtƍO.|~f:i3z6ЌUQ]vNZ} ܽTnۏƣ)cuӢd5籛mXmE=kVkCYmBƍ1Y6e|=x&l6Jm-EƲ>=U1r4h1(xja7GS[kQk;5j#@6$Ij/b6v5k-iXkrI'dȍlRQ8l2%IBHQDnH \.}6nI֓D6-^oQ6Т5U( r1chUt14lٖ;+) 8FPS,j j[jͪZjV^5JdPPgG Mm6LRIR #@fmpq8U *K*]e8*;ŅJJ-U'rQ*0̅6ZZѮ5W}juvtܩ$I:K-HB*8d#I>IHIP_쾑JsBd 'A$@6DJ|ǎ:hΝ4■'Cww3O=RO=>'| 濨35meO<:tOeKIyrN{^:h}]uc?5ֱOݷPέ7^mעD:x6s իVꢳ?Cwk&v]>v1Vq k `ּh~uSz䁻?a#ߥ//MI/~8'koԷZ __bRI%>yΥw{oו_³N;sM~ɺW$ng7=[o.:]|ůu?Z_c.~vե7a+mty{^bðZ.]Z:v(s/ZuJ-}ukuIҢ7_Qv1V<; @4uCwtGЮ{8B_܇%I^rv1V<; 4׫IMQWEv :óg?N9]=cA0_h~W?;ktY_?zvGҍ- NW^v94$Eo1s"6vz=:^}./>v1V׳,2v+l#;첗^=p~=zŒv@/C߿9g޺~s˓%:h–[Î:^Ǽm֬^r+ytYäX,~;^;% aŦ{OJ-}' e.2GN=Ɨ?v͙O}Oꔏ+I:rzbb͞a>;t̿iu_GSrY͚]>`h͚W=t7a_뢳Oaž e͹q׽ e]]@OqAf-+o<^U4ȺV#'Iyo}I/=&M^:%IO>~K4ͅuu%Wi5s]IGOw#GW?o }h7t1'@R.;cz٧j?׸z~DBQʕ:]ynn:ѮbTI2bɢ7t}fhtwm e]Բ/1 rM&g\ \M4H.8M`]PM4 8 8  8 8 88 8 8  8 8  8 8 88 8 8  8 8 88 8 88 8 8  8 8 88 8 8  8 8  8 8 88:?t;~g)Qq,߹k. H'N;뢦|{]`]v83o޵Ǧ;n{>QyD=t}tGo9`]`v1x(p\5V?3tV\( O4{Y7%Ҭy8]`v]`80ssä$=Ox$PT`]`v -&OݷݰޠdjŤ}w/[#G`v] X ̗_ /~N8-Zߙu^c .Gϸ zkV|6{VN;N|m׆]pm]͞/md>z~ .,@4HؠHHHHHHHN&hkvϾPPXYgg:;:LjHV. .rZdwBÆ$\\/xYI1jFxӱtv]`v]`bZSz$CX(hCӪUU+z2{oԾz;yJSv . p Q'Ҧc]<2WJ3;q^<I}Ϥc?gu&e7:. .rDZHaui͚տ%I($RX,GH(RGG^I)#J-? EQD`v]`@Ɓs/KZ@J%MEwYﯽ>_5BOrhcv]`v]@f OEՆxy ]P=Fy^. 9𻢷$I$dPX;]#vIe. .a6@$@$@@$@$@$$@$@$@@ߔh_Qmע߹fտ=}CuNuΣtG=׃ܦ?rgmI~4}QgBcqjc{jco[oڮEMtm~W^|kX-J&t]Y5؀V?\޽tv-Q;o.N IDAT]Ԛvw6:h]t?o$=ߪCwıZdv8^fKz#IG]/.f\oQg|6߬YJW}Z~ZM?dbZYԌG| vQk^S>xdz۟_)I'ϹT3^gu׿]d\Թ\:@V:kѵ?$iћki{Mmt 7=_7tGWz>#޹\3O=R[eGcC~=v oi^z+No4ywgz'uy6SM~wug.PW.FyϞjo_b+]l@vQW,_/[.vQk^X5 1JƛiW$IgҴ߮?Gzsp tnX6⊕$v :óg?N9]=c[MWoS ZZ3gI."brIωV`le.ӏigO$i޿т/jFo~QWvbz?|("teN.sy!I%I(awyY_?z {]l }[{Ƌ%:_9]0E]~:i1uYg^9mf*eŅBϧmT'b[o}\zT,0`qkӏW?;sR=m ч.6 aGE _.]5/lٸ%Kƛ$M{>-O懗谣? [n]dz/B?K3)?Wt!S::uym7hg>|rb)]w87DfU{Mتy.׺Sbh?8m#U.5_h=mh}uwK͹Wzx^oCT\}fh׽.syaަ ^R7:m7mwI҉m;gMVЬ[]D=Z5W|Ex> h~iu׭tf;[_K=IS~v=xIғ>|}.-~svssUjmkn_}su_K-{w̸^_=#*W!:o:ѮbzY6$]Ss{ s]Zyj]o=9f_qIm:u=iמhZw9W_R>^_X_>k}%)kX۩ֲֵZ#R5rڏsT뼜6>, v Q'b96"cz2稆m[;2Ay̪9牥M,Yu|(d/l2w*ocIʿy>!ǵss =z>mʻ?u Ğc|G)NL/ڈk:ġj9O#jK_k\܈krY!ܫu (ID#1F̡'DQYbaҽbLx;ME~i[c+Mղ(i J=lUXøF2S?97 {*{cǪ JB6=*my>O>JtC#6޿EŒmG,Yԟ!=(C4'g׬eYU wBBd\kW,wvG6ӣxY(ÞNK=x-.ܗXnֹM{KszɡZ^z,Ju^Wx7rΡ`##ش+Ik_aDסv&h[m(Y@o;\Kd[ Mgm{<{^U>^uسgf7MYm1Ϸvy^kVUYT77ats ^gasa= 52D / $(ֳ(rdB72` .%Uaֱ*ijbH:MڸG(Wַ 2ؕug'E:VUŪHTHV!ºPvO7ͅiOfô~HSBSy,k]e)kyzRZۺȳ< ;$φFE K~Mڐ`nmTρ`$ݬw !a$<4KgDevwQuT$}[0;,wW^]Ú(-/=UxIlQKdt:Fɣ=icϫ&[=Q[+ϼdy8zEmy?9YQ7V>$gUc 0ٖg d4$y7.Ρь<37Ft84O^Upc噾Q5Ƴ,if E͚Û_jhs3  Ϝ_ 7EuRK#XeȲF$ֵ:ߣ4X,>--Zׄ(KS,(16o(%귨!EޤslP ^giꋵ-v[ߴq]ُT CS[4d%DL%$ Ǭ a+Y6c4"̫tdRu1%eR<0%9G"Rg25߸*yZ3!UT*V)CgoUi&DUgQȢf)y5z!%L唖jYRzȩxՓl^QJذ'7 9-=O3Rx@]|=)۳*$eW- ]nW>97ľC`dy_'(!}Gv =JImM$e=dQ\Hon),MbԞU*\i]؟gf%$Xחvyܟg>$:ޤy*޵8y!t.2xD#ܡRUrNo -BX|IѨÓm?.Y6u!njB6䕐7;kuKUg-Apm!7RvG N&o>n^GHDvo3Q3CF%xMhߌT*(֟e{^CIkd- l9Bۨ\.keeZd3mk<$5HK K:o<'^-뙗^ ўM޲GV(/$:,NCZ^e)y"yo+r[!l!KDZ0k zQ,KJX?dNھE֔WimP?#ül ]Z,)ja}FI-dɈgɢJR4ד'hqq5K67}^+d4IkkT! v%%৿pC_|?Rm?n̠zUݲ:$-[»-ElYObB_3zKB< [ZtTOa쳮ѓRw %uĢZxuZ7SfI")-Qen6$Yl-V;ٟ625!Ϯ4e2xҫy_ C,:[ߖ._Icu^Hoov"q@.#tI=y6nF }i72@h<@ީ3?w4kNI}9b{J1zN񳤇Rl 7k Z;oi籨)]JI.!-i5z0yD*@qhk9ƕ7͑ KYFD 胴Ԫ"[6Sy.i276M<㥿[o%8d}k%bh ӠsnW QB9S9wbpJ=蹯rC-)XpZUzZe-1Qw-#=.zCQkq4i򥋣=X8*KtDmvxchަ~L+W,װ#5y۝4jƽўSzQjimӦc'j䩊 j;lO=#G%Ks>^ˇf'*jϻxuݴ-&rE9p#w{&DV˓ӪFdQ|j&RGjU(f5Os$)jo_uNP(D'OW_dɛ |cOEM<5yc+ ^z>nI* Qad-IUOgVӓ:R*4t<`)GߢRYuoQ\9*2>Ҟiiu${g`rؐegI8Ǜe7w2D6Nf&+x3%vhT{gЅɶv3啉ֶa{^y\9vֶa$ Qp͚V% jX^w_?gQ0vӳl0qQ{kTn2$Eͺ6KrdZC$ )#J՘-'Q% l*bR2aHdkU7=(l(Iړ$QE=$sq(JzE zy3ބ!78{ VXgfU,==UݶB]miSl'XayCb9 eFy %e^$I:oe'KSe@O-Fy+W,𑣴zg-=ֶaUrՊ6bV\^=fK 18˼*ų$ȳi'=gM'֌o*%y#Uc,nɾۇg][}Σ7 I2($pZcIg-+-CWkĜz!kzDkbn&N˖$qGI,K_^gƜG&Z!gRf'o9uRc. v =H릊4{4!IDATXWxYoVv+voP’zƳ6ɚ^LV"ϵN QO QhCJ׷]{]9Ͼ~!G9nQS>r_]X*i[v#YG:QdIMAd3ICyTHɾgy*`i+,e<֐9q络cњ:ͥ6kid霼׾dy&Ϊz63:9Я)q}y)ZR% cakVIɼGKqkN߁RE"z$g]J$i=<4h]Y"FWhUx8 } Q,Ʉ =}uA'V2i(7ij E9ؗj' Kȗ1-vnٚYQIˁc֢ 9\!N5]hrM<YRy'SO`}vN~zFV۬6kD网 ~mFZd!YQ)B/.b?GOTMZ$=M* $In]Ò{)g)INѐgi#CDYfQiBJ) 2}Ԣp܋5Iw. M[(tiŬmᙷBJZS޲qYQ'\Um㐍2>ײ>LEBJh.md黬6ɫ-&elfjvrfJ-nF6J8mY=Qz=32WHQb'˫(ڕ\-_tV/zzx(i9Ƭu> yIi ɣܜ̣z:f [UOꣴ+ x~x lH ym 62 c_WldrϦ&lpT5佞e|Fe6a@#Ի*ulk CT&*N/! K>ֽC{ڧ&)V!;΅<ʛ7+A=,ƬZq|> GUKEGmR6yU-k[SwOyEs̪PZz^dW=CvM{T~R/O#e铉ԘDʖ Q>rL2EjF`4>J䭑5%)uMZDvT1YE5ȣ2XK]Ge*siѥUwR[^5b\ic{Y6))e.Di 'g scxiG}LVbkq2Co <Z{&i߳\!tӗuSX]%y;[6SOHK$nQ:jT#rf6R g}x7TX$}5Rɻ79Uj҇y^tc,AʲY1O/ M mMyՈ벶a9Ļ(dA;yh:VsKuӁRdzMKuPːIBep;5GM#4Dia* x?zK;XnZw~}6H҈cI6PR}}$n+Cc1@jYVOTjuUhy^KB]}"[)ؒ"# o)<{RYn[,cעeyBvF1x?֝]ʨXSF4de~l~Ƴ* lk@wףz )8(HY#f dkN  B%h!o%7۴!_#Y擼,糮Wh9F]{_=*RRVS˚[b1414ɫXKi,Γ57":555F0e^ܳӔ>-ui=ryִZ48^4qι޲V%:BdoZfzNe9Ӑrm޻ 'mO{[7Nބ l63)mZ7}y $h̘`|Pߚ9IO2-ǒ;w2FIiPo=,xKY#MOR]k{Z4Uƒ8$}UJiz.)}`VI6gL뗴yNm)9gI2Ueor|o9G#缔Xz`K2O YiQ'<אҖEYw]gIcy'i dD^Y Oq/QRN'dy#ʇH1|_j#e1=xڮ?ʥ񌽁x%$`PrLR>UfvCA΅9QLg*O)(QE2wm׮qS ٓbͻ.Z&IkU$P^5У"EQEDx ɪE־?TULVe=|CӮ5:JQ4! q!WD)l#WXKݳXh\8ǚwsR{𖱵ٴg:o BsKs27Z}ԂYy4x#z= 7j}^FyVY,H#R%ľlf*y*Xu`!V<"n@py&<_㽿%øp7k=sYHkDPq=*7%EE)(޿GUR>ͣچٱ|9\/gz[}ohC~C|gz,yW4|HC/"7ah)Ѻ~mIR.9[7ڨX*#M=;1reb'"6fԑߏdKmM68)e“{SJN^'חyЌeT*+eے sNǷ8'5i}8<<[3ZC4gIWsG"q~K:A풤gs>^]۴I{?Z˖.7Q˗-G/Ͽ޾F/9C& }:Rrw٩^$}{O'-Iz%$3k^ަ%?i|l>}7+߻/4<@ V>wcuIytK2_/]rHV\a:_M?tIҲ%hxriP;~꿏br;/~.~3ܺmÆK0_Z7AIENDB`zoph-v0.9.19/docs/img/ZophImport004.png000066400000000000000000005672011415176210700175500ustar00rootroot00000000000000PNG  IHDRj3sRGBbKGDC pHYs  tIME  ;XY IDATxw|lSq-[{ý 0r$!HHq1r$ 98rPB7lcl ݖm+Wm+-Y~==|GYK.s4$ q#H@G83h=Zi'mY}_Xm縶no{W{mC0G8$ [gMz6;l_lKKci[+W880f ]6_X~s6qDۺ%g| Z<*-t@ȯ#'>-9SW}%{m-Wy:wn>zce.o^O_ ϻTUɱC T&S~%ϓ+\yWԎk|U(ԅS/цԉG;*uj`wx5#6kwSutڙXgql_xml%}:mMV6Y<ŸMdmk!:w/W]08k'wWGNz\/5žzvϿm[S.Oݧ#՞4r{|9>=_(Iz6|fWkބ 5ϰ!.)%:$ɾM6~S=W-tQ1c(վ?n~seM5;'c53 -7ޣkGK_}θ6=ezy{/\oYGQz⟿jXzgzim5v^mpz*j &ˎ2YO[f[rSm%ӯ[=l] ^ds-[~UW.iO/%լ}=(SzO ۝d{_ zӯS8EGm4L3ql_xmt%}:mMV6Y<;l'GsJۏ}Urb:됾u7ѓ?|Nٽ['֤y'^_cܣ.?Js}/'\,&.оzW->%_:Al&TR lWsiLcJ&6Z>,Im_H4;d2`v*۶N *ZgP$;ԣKO}s5g}hd[_kWvEmݿI^ɲmyGhD?.jжdRm#5` jz⟿lAC.ٶWE}JACE:5vP0Hk{?wF} =$m4?3>Ta}}&ٚc61N[M掋1,ќ _ oWcg}x>h$Zu5i8m[{嶵;4q8Y.KcЖ^;cr|)'YL ѶZuek ‘bO;-=WeJ/-~s㻩tJּj8BY,AIR8Ss ulǖ$"&>ߐW}erIYlIܔty9w?95sO9<\c(=W:pK%Fkgp%M r<->9B=#mٲW̹F>.}m}%IuZ.%ֳ/riT1b5ikvU}J@/ǧ`( Zm/מ(UuN8IL+:dL3Sl5YdHH0 #岕'ԩkUu{j?uR/۫P0N-?$IuJپU+nɏ-_9]Ptn+-62>0gF:=^8JijpPnQ=Jv{2 ScMе ?dzhm*.;C$wșLK{ C.ٶ!}euY-.ZaFʱmC]=K4L ;BɛS8uj3p,˥FȾJt/ISG̐eZY8Z{Li͵:[:R3>KD<[zkʦde#c=YϔϾH8omԘ#㯏,q|{Z4J-xٯ\qyxHv%;z?fO19$77zJdmcDKg26qdeΙ`K^*v՞>On_.]sLlxֶuEV{÷ml~nP{;.2ў>S69ͺj`rY.M1]3 s u䋕ѡz%˗>ӱpKvUϥKէKGT}.#^>mChG rМGd鲶M%WϭbOc7_ٯ8=qlZ{mݯ'2ٙ8̺ qD*Oڑɖs$'O߮u/,]_XV/Kkj{yue?ۮ{۟S]{Gu.~:_yi/t֭|#;]vmH=M{є㓝Iޯ⃌}{?лoW~v6Iܪ?6~Rf_(riysa;u/eekҴ*.ڭ/V^_&Mh4/o@ Hn|Li\Ntٵ_մiRϤ󕝓QY%wiڕ7}]UUqRެlISeEy幝Ʋ^+`}5e,?[s*K63'wU:4^%z_ϔMԽG~LⓕMr?-{w,Wlg{lTNnge( (S: Hz𱿵vI8yBvJ]pvDzGtMwO2yl7nj4Sk}UhD}Gc'qgQD||L /ky-ii]JgϿX_zͽHO?PlgLqU-Z拺󷷨o]<3 Կ팴_=KK;\~δny |E7eyc7MbIҜO, _fld2'ݕ{=nj&1KuklyWԒ2vW[tHDhyHs=ց:F:I#hO0 q@ij1G3+8D5h}8$ q#HбUt|*zE-Z - ݱYxQ^J#ı)\ qLI%.hڕڹe*OK6;`l;W){^4c Oo6[=71\.ҔٟPn=m8ڻcvnPeeG>6fl?/ZgTUYCGi MO5nm۹EֽcGJe*(I3[^~Fbx+iJ,ڥʓDz5/_CFӈqSdY8??Ɵ/+ի/^֯^ŗk}J}0:_{)?Fڷsvk7Gu~קUR\ I9g١Gh%W4/?$I'e/K/\U(/i-zI\KcR۷dmMgA?y}Y=zi'?hӺڴne6Zs!#kNkZ1I=nl /Y>)uW(US,9Ñ@c_YӐˮwFӴtѕ7gwD:J.'O:Ϗ)9j ieL5u;Ykf )_~_0y<|t2q\xwmXGxwM@5(ˮlߨ[7X`@oz㦨-n̅(x,˥n=z~Fkb\[S /WaھqJ)vK< 6Fc&M?7)p0,]=Vqu2Y8[lhe$ q#Huػ;# 3 q@R80x1xl?hǼg>f@G8$ qH@G8$ qH@G8g1W{kж[[VZmY4Mu֨~4WǚkWA-.cm:Y^[*qy8Iq{iRt({i{iǹn-*]z8&qfdOi:";%qEԷfK,J{yko:%_UcGJta;o.#q h7T 0 #/Վ0MwoJYٹ0l:ulHXvؑCAy>QCdZVe%*-'UǑUopY֩S[ʼnc*ڵUx[Nw*N;TU~BR^~ ii5qk//fdMT]Y!ǶU]YW_LV>9`Cwkչ+h!ٶAUm[( Hvl ھq+tiۇ1ٶH8"ܳE}D@խgoM{z*w'] TWI 4e΅_ I8CE/.%I2zΛL_vؑ#j܋Խgާ58|lcGJkl-X-x#vπ,K} V~hu]媳я6O,Ry:|`_DNmцm?X{$[L 28h0$Ǒ$9#0⋜cŌFW$Ikĩ1gˎ~0P|e ڼJhAj+>y}ٱ>gtx@&0+! ,KLH, j$3d5EG ԥ{O<3y;Zhn-mCGn)ėee4f6,8Y=%Φvm 3QY9ӗ~")}=u#VK`ڴ?7u^}]P.E>7ZnyI5/wOG¶g++t:u9-IҽG:-i%}H_' (]d q@^>*?qTGlLճ_ʏպ_?\.)L:XǏ(h!-=usGn*jn=zXuV$U^}D*+)VYIqhI2N8)Ip՛ꚗ㦤Ud 35pXd4-etR>&?7r4t"4ӹFNSۣ1Sfwd,0D#'LV^~L˒V~5qF|yՈ ); Tvng 7EhI2ׁ(sW)_OceN+= ۟ц$#HpqW5舟sP3 q@洛K7_5:35|cb IDATrèԁ .U$ q#H@8$ q#^ܸ. ڼy{ ˜1%1؝O ıi7ogڄkn iز%d 1}S8uWgѶ@y#9M$RIq8Hm6XP'vkĉѣGD#p.HgСbIo~[vmz7" 6I~|^-YrOկ~)So~r[__5b1Bw}֬YkN9Ço~*)9,DZs͚5[c_… տM6]یDqk![TQQr84vRI,[]F&L|ݺzh"ٶ*))ՊokŊ:xX?mG .Իﮒm;zWx+ɶJBٶ;.m߾M>֭ ͟}?~z}wr8tf :˗-wu- /{)/> r[ ի8^}Utꫯql\J ,n)xO?=ݻrroS/3@$NJ?TTWEE{}VyWQgy~~~uTXX^XOeeerG3fLׇ~*}zvۭZnjƌrG/b ]|%3g^}x} 5h 8X'OѣGSS?c6]Չe=>>XguC׀j"ɶmy< 8@O<Ə\;Vo5tnٶq׿o֬Y-Iٳ9ݫ}q@:m~?F\z%PYYt./?};wܹs?enرSp憘H|g?{"E"m۶MK_>hА:i93p`h1ٹJ,ѣ=Xgu}kwЂK.drfϞ%DZ5{,}_ύO[oUQ~ ϩTWr[65pv0,]=g!N6x'5fxeO֭[yII5ܠ J>D7oP7 g)]vkeG 088m8=#p24-=c˖ @icƌy3o83^-I$ɜ~Sdz!&>жp8$ q#HG8 Uj'l٢Aa8"qslh;SmhR,q#H@G8vw\-q#-@K) cjgj{Q;Gd)Ǘ@\vhH4mhW$04u F蒯źZXUWhy<|UZy$i5vmOciS= o( 06gSh>>^hJH:ewnPkF5QƗ<)Vfg2OlPϪoœFƌc B`zV~bsrjtjP?'`4]M.˥çI_hS̰ilQtvJǥӔ8N>MhDm{[މ/[mhiWhӘ5[մQEH&@f'b>ouIm[n\w7.O>رFXÎmK q@p:!Șq@J-[4){ -[h%v]8f :_S^h#H@G8$#H@G8$#H@G8$#H@G8$#H@G86ao@9NIs# t8(i7c/o# t8)i7c_rF0 |wScN* 4IQgpm[.@ꛫ5ߡ2s#H@G8 q#H@G8 q#H@G\AJlm=Xʪjٶg4)7[u҈^k]%+vZmdyVo=4w$!KmFС9X jI`tg㴛ı:*?cq# h!:5_?`Q[[[={ꯪԱ#%:vƝ7AиMq\@Jq %ՑiWW)+;WV.ݚ# x.;Rp((ק>3`L˪SX䯪8r{֣ .:uj8qLE_]oKS¡Љ+ [^*Sl:f*V'*?!r)/@CF4ʹb絗SP.-=`/Ƕԣw_0TGعi5wVWk5v$m`Uׂ߭4jxrlPgP0"E"a 5!kw$ֶ1pH[>X`^~ı#3y\nwf2V>|OHl;"CGL쇢[ á݊C8|,:s@z֔)WXI9|`oGvl/D"yh6O8pLRABĄ4z3cqQdJR8lQ 3;xI蔹r hK@GdR%QqdF|QX1ս,I/+'Z(!iLQ_vl:+Oh ^WBՕ TP%+h>UmN:bΰ1l~(?DwmI(LJ5N]84#%$%&Lnd&1L%YMёujuS'Lގ4hnHf+m -w9avm`>>t @8?Uۻ}\CAߵ%^.+;UTFO33r^_V|+P]\vmϴ 3T˗bSe4HԹ%$zӒXe"sX_NiI2Ä CiױvTWUk^S94#Gu&t]= 5Q{sR$uHS7av֣Jbwn]:v2r %.gtRq~E"aeDuTJܻ36sVetmNKbftϚ~MuqSҪK:^VZg?4`@|Ji*2RٹIiZ#4|yM~niEi*s0ˤ.GcV~e0 y}Y3`FNVK %˭>5j?ǫSvng6nJ۷\.4C ɛ-4e\ꚗѓfֹ#zӒXe"Qn~^O#ǫ`eg0zR!#94`,YIwy@c3ڐ',fom(@+s¶v%Ke>f@GaU @#V򽯷x[Mm3L/ʶR)s:S[Lg$v;o>Cr[m)g㍦q?eMmu8N^sc;鿾DWxF-G}*nj }c񃕒N46;cs/~7_ezk:X]MJ2mmϳWh,ӊœt]K+MӦm*r?m\H>rr;K~|R}?ʴ,_5X2k^OZѬFb^_vTln园Om䁯>.۶н_џ)Iz}?h4]|^O%MY]r֊? _L9MӦm*$xO/O S`.y$j2̅廛/voߨ:qtS{{iS}/aDiűHSf .W{iS6K?]ַ|$w3w흍'M)7Pk}]>Xk Ncwޠ?Dwӌv[}jiUo`|yCnzĴ>KaƑ/k7ҊEsejcԺMŢc}6S8[ӟ w<\'Ee偤3F8N6c۽,]qs=?|\5 iu c#b4(3G8$ q#@G8$ q# Φ\deM{`\q@بki@~'ٳ`\ϐW?|o2֓JGRn+X0. q qW5J3h>h;Xڿ$kROJ)oMWmJmZ.ʎf\0. ? u%y-ww4nceVqNdn)opG%I/<+]ь q`\t.BжJ4ey`.y$j|7eL2JCGO e*=|@w~Chq`\0.HZ} {+^np;@_4n2UR=j{}Y%ۮ/SVv.;q`\0.Km{~ z[vY0T_zv6Uf̤dj~"m%hGk`\0. EŒcRL=}kJOHSe~ܪfˤ"m%8!#i)dq`\0.H?|9_|Ǜq`\0.i\ 5ƒ˜tWzm zx+(ed+Pn#UՉ*NV+:)Kx,y<.=.ݖ<\.Seɲr\, FE"ٶdWD D8 ÐazU-3vtwm8w}GS.RRHm1rI~Q#Ų SiIQ[N$h0*'*hؖ18arGhThTvcȔdv mGPT#S4۪ eYYi*'P X-0dBLӐm۲m[Q;h8h4*K\X̏,KGd|Y^\.9Q[n[HH#n)ۉ4My^q:?~1 C{nvvuS~^srTQQp8_Ns%9riJ.KQR$V0T0De #lj[4.V\b#vDHDQ[rL$SYYYZrrCeEd"VRQ(۲rr{yݹ쩪tѡ#*9W9F_UUےL̨L3,ݳUXS*Uv aE)jKPXpP0,UW۪(6$-tRK=:EPZSwҟ@g^ +R_Y*Vya@ʮh4*pd$ӊ*;S󺣀QeyXj)tI%KnLkg(ʒ$#*[Af@a1lIYD" mI!yC {Eߏ?zվ^mIׯpaTzF'oqd:i0|>9vTDeʖ4dYLː7\QsdE2 ɶmڧ 5m8Γ8irɑ)G.Ö%GlvDQǖ6b.RsslOMMَ#ێH$"';'q#C8rLӔ0u}2hX;X8p8$YnY.Cౣ"H,A#[cװc͈[\ (j%S2l[lew%DGԢW^+:|TqB`*dx$%;GhHѰN٪~z 7Vaԣ@w~k[6]+ұua~ԳgO\I&-Tqw,Y1Em)םpJ*c~yL\!iĠ!*zkl;vt&zVDfeLÐjƷDe8Q9lۑaJMٖ#r1NҔ!tdF<_mu5 Gmtb57$rɰ ɩ0 َ#ӌmryb;h\Ԏ}##QlB(QđH#QðHD"2boGHXv$P$FmN)W[ͣX"UH2dFdZREPN[5t(?}*OjUG*T˰\TR\~= s>ƛWy?en[]+O#_~Vt'5lHmz*ںW㇍юDUQg#&{*pV5`gub.pԖvȖR0`a:izg"Y{p PuXٞlUgyAEeJnEr|xrx\GwMh0c)ÈJI 'P%#O].oɥ h/(  CjDKCۣy#Gx`;Ǝ yck,<)B@$ 4z﮽*3rp{UIVt2U|˽{ѐbB'3vXI`+1`\k@$5ޚ~Jh0*% `M 3E\81؅^!PJ*%R"K %%J%v/͢C81Ȅ)и4,s.l: }}{/ ~mx/ieZ˖5:驪,24I^AxyE8n G!Cq $݀-8 |c~POJ)$ 'Ui !' tbs~Sa*B7|<~])\ *BAaBJA)=֚+~*W F"EJ*G,`T8p )""pm>~$~T8,e RD(2MA6u"c4{wXX TIծ%n:8o`]H8'8㩭0vx/@ Xx UK,LxO!pnv3{`^0k3޸v/ Gk?x,s,RPc8,;׹r~S똪4j f2ŁCǟ }P%ovKDᝤ R`ǎG-j~H](RHd)%[i6sz+Wj$!:G$)5ۇ!GX$$Bfö#%>Y A=p|m`" J$I0 .ۂT!h#sh^[9J@tMZ/(r˩2oe$ZKYʄ4M[TN]&UUἡ,TU(%: 2}eZKC>IG *ZD`#H96V r 0QJ e ]*ifZޗ3vO? J6ے *8#0^PIZRa`ᄤI4Ft>nF΅^s4A^V BH!UP)ND ާXol`l26ƴ|I!>g忋ԘqיLyMTXgeMzUukϙ0R"!^Np'}߬㊊FWBDZy=2r^`\XkyPVjpF=O>r78&I4a@PL𾹬A"0;Z`}6ba4NP=?= N9xw~w>>_zo}/sPLY8K9y$wSok|~ O[o`GEljg_rɻXwz?N0+ب94G?gy?9rD'rD( $7,UV]--|mVwVE'lRJZDZ2c0z8i݂G hN: Bvav5#(]:M~׾]_E?'f{Gx/sÍ}q*Z~Ne;sÇ|?&)4n)[ :W^@zewjY)د>Ƈ>aK7g鶞G)FȰy7DI&|WR8kD j0G$YG%i FRH0@9a#xà !REx-:2B"[5 *k1Fxyt#h}I5i絩ZfpA{sZO}8MKkksF@le9%gI=ulof=IIp^cQܝŹ ڨ©G'.z\CШ Hmi`Hf'#ߘHmS-+)-=^1~a]^|𒹽Q]=)d !4i|R Q“aIQN#X`D9g(|3P 9mBd %48C`xG]#ϟw860Ot3lqo$ aB6]6JXՔ]kTF/d('@ %i[99BbM#e+D2< u]^`}KΙ`V@ T*B{0Ūip"x PxAKHOFS)J$husqv6<'<׿|g>[[rr)>_g?b{kՋWܤ /9ַ'xwq3_a8 t,Yr[_}+U9}Ocoyx7 i~lH?%zh#\DOH4_KDmnjY0pfO(wмgԦXAU@cg2eĦ6CȀY2q%l݀D(G ^ƓwsCϷ 4!4H5.h뱌2wtNp“o/(A2D`q ѳh{|Q ʅ NEhm+ UkKI*щX:_$6QĆH[Qŵ idv\("bvHe'U&q X3PeL{F! :|/0 q^b b715@2o7[e#KǤO1{BSQJL6 1cBq5D#6u5l!%SI˞{!`+} |(1>VBU)Y># El RXmx7?=sMaܐNs4G^*'lTe#h{_Ö9R$irt#?s[#Qot\.$F׸蘜~~WCN8B2ÉG :=NN'?#Gc 7w6)k=pMԕ?C˕TCKOy׃g9/pQn9yAuM7?q AǨZ2K<| WMP,t3O|zuk+.󭧾Ae+N'75:t5Ri`JO<8,r'bx^'^#  Dmٹ 9ҙ2'@ p="jWST"ix i .Q'IBftD*\mFiA!$B[KU*D4Yw>q2ڑ{e {k0^/0IB!cZx<2at:m!Q5񘝝FkMesFQ{7ټyDX[S)qt<1ecJhʲ In'H9rc[FͥTiBit*; i6=c*x6!îvB7[9RJ/L9;pP%bSl˚R^ϥeEw+ bWu1m5nj .4k)L~9j[QۺmhRAm ŖTB3$C|}z$ [0&@Zة18j%B~xz$|z2JRNYC{G?:$K%ZVE ),$ΐK.=ZX$0Kzr饂\12%YuRVщ:L5dS餂N [Rvz`! ݤuIJi,I8N;R/U#{lEO8$A@Uq8?6 mw-txʥs*,ÉU]3SfFs?"S`h%Xʨ#a5wL֡!g{ V6)Ï:CK|%sss //#24 GqW.bdY#ށOsI4QI=+ffSK9S+5ΟD V>\Ê Ų(pζHi36;%L_% P:T EhQiz!Г&u@&(qڿ'aywϤDž;H,E] õF h=8k0Q9P2dEad|TGoB@x|u]SU՝%77!?6^4ѱ= %[CYހXe؝ZƮk}aMi>?b5N۹VG1)CK`Х@&b70ƒdڋ"KjJVSzBIҡSVRf*$ù Տ7i+X ld\hr+,oW ";&6-=wȰ`<Ƽ8 yRLbGz,.X H*v{{o"+$qT>zFaE M3Jh?WPg8HhHFhIkNByTkV[7@ f*QnAA/h[Н-+>L]qcC׸ IDATa*:d|mGIc7uX$:Ϩ{]0;)mg=XaLkb Au E%!,ԓ@)T#GE W*;#/Eoa2b촛u~w|7:q`_s_W^>;7^B?< jMsc=q5N$Ir:g>*GÖ5Yrqo'6I:G5\ٓ~g?pd =[&kt?j];N'CF̳S9ʢa_ŠfXZPB1pkXx_'<~_¯!mJq%Y[[qi5FfKk(J&a"Had QJF* ɇiiz<&"vB ƍ VIr#wJJH3B]0ުQT 3p&L<o'$zGR #@ *z;JЩ ƮlFy;?68ĘCd&u^i8Nol%cڱMKYxJrF6Se5X&-c= m‹`w Pd:ID_]qe:szG%^JSYDJJaLM@eN͎𴀡a$DfL@[6ⴡEhqatX7g\J[bX%0 A^:TRݎRG(wyOq|ϱefmlؚ νR5]TLM-n?,~H%HZ08`2h?,aI :&d"DMM u”+5MfΘ4IS DQczIlwO?V9b)ZkʪK+q yuL@o ~2p}{D]Cs^eθ+Y2:JCDL-0o|k"y/?@βz.Ǐ?$_|[3_7n^GÉ3yK\/qx)8x0^TcyeR~a_3/{ŵbKU,8o}'} n&e5yױa0@%Ev]be6\&۝Ej\B%ϻtg*V!uZ1tuxC#^Nף0sӗ_%6*C4 קn2ڊVܨT)WI%/Z (´ABz&K5H9$z,Qi}{J+T \*FjM|S&TJ:fY5#B8  {'&|DdޒHK/ L}'Smq# @u#.U۴T oLIʨ, fovUU9NJ e1d{;[cbc%o‹8s=4>0B$IN;|H5X&M=7_F3K9ǎcWƅ,--Ql[{pB'0DK0N'`P s yG{~GzNՍ+CCK+\q[+#?9xr;by1&e1f0*N~;_#'8SrNzufH;gcc#FEsu%Οm󔃧^!" w)d k+RVyp¸𲤞,u:N+@;E&{d#U>ԕ ](朗af"Kxk%"~6MP&sey{A9Ot0lcC,z'07M:z<4{.\d8a Bz3VoVl ٷxm˳-}GyF,;7H>ɾq?;_֔ʐv"դigR]}WqQ#"5Eys݇bAõ[W2똬ħ;(m4Rqu%V`N6;L-`]H$tMQTn!D EG)L9 $v8Wң zWiP@(O+ $D'$ rjpȊsb6lcɔ:Lu6{kL8N ]ͤf #Vn>OÜ4lK}"˖4Ħˎ?V 3LBSxqGa;<^ц@O_R&ZC6OXbTL9/D)DҌ #FU`eH2R8F omi]d}sf;mi,^xY_G 6 ϳxXI4˯q^YS0fm}k׮r:W*r7t:wWn|CS5W߸yC=&]7\DfQk~n6w-I~Í[/=O1؞[㺌 EƵ3S5cv}s\rfsy%t ֆ/mwؿ'_e{cH>v/]D%Dz 4N~Z9=p{ܸ¥=᭣ Q&Iu]3)+aQRJXTȸi72EQPcuTa]MQ* `5v /p2vДؙ*[ߤNlimRmӀ~je߬#ݪUy\H*(PZ5=W/[Fww~#m3Ȗ&SE\h0j;{6m#?pۣ8bqoW^d[ ةNx!'^|{-3DZ\ͺݦ?:|ǹ}ձq3lmeܾܺuk9Sz%g?͛yΝ;,~.}w<@>z=?VH`kfNck&9=R=Wgs~l"/-.W~k'?Ay4jų}Y[[gvwҗrŋ/=fSG)2 Sua=`-[=<8sAgx䁳(#xqnfU&1ԩv5BWXa1A rv6 LrøT(WCDaхGllK]6FM=oASUcCۀhG0I2GT:EfV(F ddyLey>ckNH. |1xv&xȀo*k/9Â6nQ Kf • T9ˍNιCַ**˨u;䶦 JW@/c e2򡢳)]<;^@nR.,!dNP&*(JRA2N8=Ti l!3کYafJQʡA;J{o>0׳y 64Hxvc=Jȳ77Y\<9آ5qAiI;x?O$(`4l+dמb`#ÌG<)^oJ\W.c0E-2Lvq8N M DcR0J<:M^E9aan|Ï2?;G?O~ ^wx~\hPC*_Wvf'yx,:6Ǘex_}..9֭[,--q 6`\9:)TʰV Xٹw?0 58YC'3/X:=|3\v}c{l_'0fLYЉ63:Q }szd;  !5pi dȮEwTqN P1Di\m@(I{E#;ׄ6Ha6ʩBI$¡pLdcl M-Yغ]nϣm% ZJ3q Zo!8.vƵs9땮FxVQ'9V@ej <ƒ!/2Q2N,YބٹyiՁ6= q"RJ"z ⩌{t4N1x(4G) :WNC0DgacPBخ IWC `K[cܿ@6'59JMȊ/嚌.NxM5%ReBLBV_2feWSmmx)Zƾmĉg䬈3oZk=N3 Zxр̆6'):/^|$ Y!2Vw -ucGS vssۃmFEl~L/lF.F;%f{p23/`+P~L=ש-˭+~>5O=MnܸirԦƓ$iϯ9Y# j5eьWX/28lc@h0.[ \) ;%39d9g}G<]x|wO>[2\w/.ekk<;<ӏrxiF/x">s3Zf,,, >oXZZ57.]˿%~>§@Z6δ2 t3`EO?M.]yO=X8k72qG'Og82ZYoC*aJ}U^X:,=q% S(83ˁ}C9tpDg"1?7É#VCb  c8Yk#ptg}M ΡIPM]#$cv 8OF`N@Q04%-X%2t0c\ q*^@F([FaDP^$ݐ%LUcL \s#H['+x)A몍Zr`];[H>ˤ];.J(bC+{@G9&FiHOCh&4䙥bE؜QD.m"O׷`#ݓF|/:blTO/q90b{t@we #aOx8sa֌quTeIU&e Ռ{[_K IDATgfϜAF:Qt0}f_x5^}U^6ϒ%H-gN':ͻ.`CОU8'Ad-R4X N mO}*[QVZ $R)Y15MPDei@D 􁥒WDƄ>6($8jk 2l~{D%FRXZ V!B[Bxf>0j5‡ -%B)a~&w8=^G-rnZz:RP#|7!Tuc۶!J68D[iw]ԑ~Op)q#Mi0\GKh\ [$Ãp .Έp<:]Qۢ)AhQyLiʏz[߶_(oF4MX*3ߕ\л KYD_Ƒ2yjա ;1pOCDM(ՒiRIMd%0,#ɮW"V1w2ꦢl TUUY89,(b55yŅ !RĢ;mH\cL?poV6ԶHzXUʤ pSJ( [$jS,~\ɍ+y+ }h1^ei%!8~ڍ˨D_Y繋gjJw_c/Ip.,ᬡ7M)vqɍUOmFDڣc5eSNb'z>Iu^'Eh W=qG ǏGӎ<-0$@SYA7II1Tt [?/j#\-`^Æ"^+kP ڕB'Q`J#3k_{N?E<%΃OB`L>u"&j1·UZVqmIgLtD}r/5$!=uQHo kai#-еωr<ڄQZR r >kѷU|J=FNx0 pPj)KӈS7 w+8HZGm[qINLw8}+C^W{>/!q qĥճLl$%vVֈjhlMvS- l Nݜ{Th lAp0}dtNqt.pbѤMrJ?V0N6Rٮ*sҴe A"xyIǤI! 4^}1_c@pI#KA/RcbdL#3NOƇ}"N(xw<`p1<֭[>ue^z;WW_㏿9xx6EQ:"u|8fecϿ*_/m?r|[[s|cDGHrsr'?!/K_E}WoɌsg̩S{tw8va7nllv*Q{VVԧqY'Ul4߮Y`R񣱠Lv DiEcdaAxŻ%yRA5HT+2 S4_k[yh0Br "E G,sHmIG-, t`#G|0:oX*Eh.1LcPqDEK`"d[!ؘ/@y݆^Z)5)%NxV 1I8 aH}KS>-(P"!QD] x0٭MM@ vދt5!l{<~8DDՔuĤc}ؐn;̱3 lRcNg 77 K1E}/"'T],6hdJ5p[JY2"oMɘN' VLM$B<;sg8'$8Ν{\~=~癘aDhSh\p3t|s/r3 G#nlJypHgo$I8y8i0N:u)EJd~_9iVWؾspݫܿq /y^xp^;=;7b؅$'Rr3Yac6P[* ɐ0?NXYYt:=1nxRYU+Z2^Sf2zƅΝ;`i$ijC^hc<s6feeNqayx,YU7^x އTAJw7}yI;['sOp8ui$?aCCw|+)ЭiAoW n3^k:y4֜?|htsO]`r!?"}YW+DhУkp~p ϸyN0sN>tc*8fw5TU(oQ-,x_ů׹u2mqo+0Θ_o{jⵈqmv.U'L(5XdixJ HT@õMbUn-4No6ʉ%5Ta'E [^7 } Q'*@t< (G(Ic11X |vrADc i렱y01ޘ"MFr$h5",EJM؊6bpXAެ-/{8=׍Мf$:{A4T <=$ A/B]WHRJfq*,>%9zE FJ;L p.ܬkjp.[;VZ(|K W60@Gcb8<#\3lSm].o޳.D[F^WE8UY׍P°7k{ĉeg.W>~ (dIZO5J lU 3oYN]eC7Ӕ. ,Εbɭ+re1H_hMS4EE7Q'A$IRVͫb<ͩkKPS &Zј0e̲나BPMcȒܻwh™'"椇~>8>?;q4oxgDH/3F.?x;\8v>͹'ŗ8̈́{?}ȟ|9˜yW%ӻC771z ؚ2 340!ol?Ǎfu:A81 eEKӧ*A^TTUEŨ)5Y9>k%y_W8Fc/~t2/>ߺΓ<͖K59S3t?zK*c7f; X1oz |r/[-$n)"LmW1G)Q<wD#cM{\`rI8N '=QW`󚪶HӀHǙ(x2t;RO)t#]+:}F J1p`řp8 |,z9\ 05^NRmR!1Fn.u}J"2GQ$r4IT&uSSS%U)bbuD4\8\ж+һt(,zEj2JN}+(BŞw`9NayrFr/=K[(ZV1v2!S)ݙ4bge_Xmuǣ>,ePΐ l* #"R`n2r>WcpI>ܤ~3[;X3_ydFb+U Ӛ5"#Va՚Hyy}OQɏ,e*^rSGiTT@4u>xvMS:ڑd}K 5nLy])k%!MRrkIyg=OA݆򬁺1L'#fyMuB㭡TuN]u^8J"bM \j!P"Xy $Y FeNYj dpwi&L$ã#\`=EQ<>|LȄ5q$IGiLoM=)N9'K<{?]NmpiXwrwPݍ"`QDR-ӧO ά`}opo;8FiE˿+__N`C]Md0h,;LQ㫆8i/fm0d< DY'g_v[Y;LJ0ĝOE޽ 7^ǗfObJ%AY"SG rRՐȔ2<"aX 4 b-0\txкqzW:o}bG5>I@ s0"e`6n3Mx&gX]nMJrffs& R4BPyO6 f= .aH1g<ڂ]k=8^!'k`S? !VJxBδ!9bEE=R0@)Qu7otx9I3y]ɔ=n K'[[w_ߤ+`{3"=Y]]GG|4F}w4S(+ZtzL5n?)a4[w0spųgS6Vu,?_oDY~9DjLcq׸C.trV0 E=+^$XGqX|Im`1#pP)Ġ%H[i(ѠCVA \;ql*ʡbbhj#X0+ QJ hEP!i1΀5dn}qkI,Cgt+K!6>چD˿scjiEѮ}XkBm\IPm+DHҔ8jI mrcW;0P*LWX1QcĚcOmxpN%ׯ\ ng-Kܺfc9#Vu^QxOӢl; " FgiٚD_L#?pL~~^zwڎg=8HuP5u=,,Ʈ!cyAܾyK~mbU{tǗbI.G f6%K;0Q:EG4 ,KH? $ ΢*QQD ,f`S$q,&`ZCTeRQ+E eU^?{$ZX9Us$KPBAu3xE= %ǜ3A}, ǟTCD$IQ iT&ACMnL'*,YOU&ܸś1>gE#(vƈN8%eYFmkՙ|Ms\0g:p]1s.ϋe?&|z'/\ J.,ib|=ELU3md$qLuȋ퇌ʜc8w+4/ؽwuJ0Mټ:/X[p [C㪠b˯O΃-b4޽{\qoc:kԣ 3kBk| sӴAև+l}HdDi-:\ѠTDcZ&(u6(# i'Ipw"^#Y̤[֢k )Z87 4#%԰5aDJ G)%JŚg=cxFخ]kփV1>@!ZX(΅is9^sak  DA[l~OvҬnj*RS OMj[WsJ_ro1QE3aBzfۻ247${j!Bч6L2TZp.?]:Q,~ap1%l?]b/DD2l-i9𖺪2lX%=75:1.4t;HV 엎z€*Ft}] D}4jj*` ؖ)coJNRR55G̠cBt:c\J:x:Hmxe 3WMYPpk8~aźu1EY4MudR&jyc}@ۮ{P5*a%#A$b "Ӂ3ѴdXS!:F CWR4Y0gDͫx5^yK84 ш4vVG**J:IJ'XE=қ?1__g.;{Y^~un\NSxWWS'(a}LiE5dRgIC<3Tޢ8[ =;< ^|%Ȓ[;i?q;8“Ob܀SY巿^@UԮk7.cT jxaID&tgST2-yt:qe/;[u\Ŵ_W-um7;հR:B XڠbJ>Biw OCeq}AiUu%Lhکp [fovrr$3=M6@!Kx1Jv(H/>EP)UIx{Dk!uTGn!:`Z@3,u!L n3SU +`u@heV~?C>yeF1qFڑ,ahFP&XIqRpR~x Oz9~ߵOl1TXJƓ MoLG(-v6$QM1Scb]!)n# MiHUd !-/jAp̐Ja bFUnYqb'V&FqLm*(!$ 0g&3*:>J Tg) .$H.z<(=$ICixYͨk< aPY:*#q~?Z?ǜYk8g5IѤqԾv΄sX&dH$7ദܣLuHUqS_lRJIceYOWUa#B|dT5J2k|拯qzyʭ<ڢt}k|𓷸`7^ o'8*w 0 hYэ3qcxe>-/a&"wǥ$+V޻c:Y[@ hkDe`2ٽIkgF8"TxZҐK'5 z#vn(^KӪRTuxGwftrrZla[r1aDƺӭ; GfJb2ωcKQ4Ùgk`Z If88c9Mt:=LS㌧QTMIYґtY8: Ǭa$q{lqB)?iƋpN:q6 Ӽnj$4"F@%BL5$M!`{k8H̷(Ő=~<GcB*($"NZFhLE8tne蘌4#8#z2cᲄrJJfNs#'#Lo^~u<`ǜ:s"lSPyœy=%֎3X_!"`:#;=:qB#fgsb{|䀭m&#N<`0`2p{o7̅ OɥgLc(>tɒ,Ȳ=Dw~U S<WTzưkƟmG[fB,*<5h8`nJyX >HyDXJ!z4(PF0&`l8T"4D8֨^Lc-e]T!58V2 2xh'qR!f5҂7QvuyBE] .LG4t.,% wtXןOT LrPILSLA>>EM+8+8lchdIe(Bh)az/:ܸAbzH?*˜S/v[ېHt:]xc=X!#^`uD ql MA|8Q/c3Iܹse-|)+_»o H穳-7G6_/`y@s-r<@GGsa']ȟqS*Eٓ͡(RK$HF,+F$r88r#pVkv-PũSuo^C.]ٝK~@bp(~l{9yzϧϧǞ\GGԨ0CFKt\"tX aśur0L8\G˷ng_cms1ΐt:퓕)y^s.`GHt>X/ݸ&ÃF!|`0;R]%XTA?7ئ/#o}믹ûg)?ulk4uͼ,]y/ޓ=߿Oݔܼy53},s @+LU8[0;KP9ئIO/U:#Ixbΰ6|[?|{'$ٵ TQɚ]E$ 50/.r^gޛpMAѭ3FfqƸ!%6B q2~Q.:~ K#x|`!LN,I3^hAH"B7T y1hkjrj|ݘn"CGm 铆cE0XLMM^+ J[RW_9oVYFO{.;M2(cS !%#yd(bk$vFqM5LfU\pŤ[glEܚsy/A|oNSkmw2./|C ZnmQ/?S.ݻܸpeIJm<MǬ_} ҷ8YܕbKb`Ĩ6yo A˵cY/xo U#<+kK Fzk*D!!/_eXtRΝ;LS7X,f/_[OZ5>loUU+2;(*BL4yV1[⽠v^07tE$k۞\8b<9gn\a>3-XfK^1xxbeD4qL/r|h>K5˘aDLSb !QPLN>=T6|L=>,ǹ8[!N)00n)%M`X>bj H ]5(h4p[D54ERʲdX|:}:8 r퐁'1ZUbU)2uXoX}v57>WA0<|y x2ۻ֯ ؍4*gc`@ajD:l=oų'{9{hJHNOONHi]xfcmP ؽ Z!oHKhMq =&_hg7GO3Z=$!Y1)M*[J!LM!4F8ʢZ W`x*[rxrH?ޢAi?]91rx]L퇈n2k:[1!.O |p҇j1j%hW(#Ѐ@x[R6HcȮ#Ngkkk6`B13x铽m*@Hծ@4HPTm;pع7JPtG/,f#rWLT`eVV,z:IF YW}65v>8xor`@iMȺj/%} L0\Gm@(-̗ ECTQӗX osr4?3L;1VSOѪV(կ9 ޛ[/Z֣hkAMjt^!:XJx5L(rCWZ aNiFURZYTtiNΠ"ug" 6P'pĀ:LJ0*} QWF4-K;hCai xlSU*#bR=SܰCq}RPzL~yS$XPFe1J1(RnBG<:rYK;> xU,-Е%)x /Hh@Vs M8x~5gsAiJz عqIz[|)P,%YUCzړۚj2-PLE[MTӑﱷ`&|[ !?m:96 ~͐$y&K_~ t;)e0J)<9zʲ 2%EaL ߞr|9,K%δɪnX,ܸ RLK(Y3LνサGw; PlA"Rg$4ґ:Z$oˁQaRTAiAtm@0?~?!I# */*'{qe._IQӊ%E[$a*ey^)q`emɖJ$:<<Ƭ^Kz?≃.Bj-âZ.,Yn#TU*׮]c4\Ι̦c(fGdYFc=24me~S%UU8!0Mp4y@b:s3zXQ!48R!V~P }u Z&ۏ]>_ Ǽ\Pm@˫6vEoZ~t|h^xwȋ Ӧ]&åN?Fɛw9cvNBE<"iN|6AOȦ_(GuV-o'T),Qc`SqSJ!9ЛtA,Xl!aKG^Ac)z:%)[ޡh{DXu#;-Bi͓Q>Uvڮ$UP7 9:}Ct:骆-jWE:`@%I#670/ݮ%0Ɯ+bd2Ӂ*|LQxuad2fo˗qfΥ+H)1I\h$L勷NHOEaJ7WȨC.9<SR9ϖe}Ͷ[k5g˲D$HNg kY6yeqM211{JM`0 2E|:EO[jQ׭Z >??5HGˆڕI:.Yi(fnz;x,m|`KoצB}|{.8cҥKq̇!2,k^Zb1Olɓo=O?an S-5 I'&v#Nш Y{:q嗞YeA?I9?*Jbw,9<k/|PuK29n*E/̧3\cЁrnnW^HE]LS΀-1yNY8Ӵ}]b˒{IˆaTGOp,+[d` ;vYLÆo70pC*,w?xǏi}>Ͽec #SϖE l^e^,S Py@ĨP`@Mir<Yn; g& ?*pŽ&Mxlllo1Q2/)OW-[GuyѸ9V}xߒaBa˻ ijDUcTC  \,ԍU(Z*w5nŅ;K\PuΞ+|Czmȳ S=;"SQ+)YpbW0GD8>KȔc[= $^AhkD(T+d)z֝{8ob2fgUft(ONF\|\`$D*\|lB;K:'l_nu:gNⳟ,3}t(Npa'OYTuM*?k'oa)DQD%"M&gcu LYVD!yq29>&gQ..MΘcfY`zy~^IC1Ź-^u *"8pc?j݈ۦtHvȑh-#X!"A %+ڷօhb"Xc Cj* @Ȏ2SgbVN!n|]Ҁ%br9(26 6 KH+S Lp7> 71o8nc7ԪD0%@5BIE$7jIIC06ᇒp=DGm?c0bTK<tS-5H @C kZp1P vXqrC˧ 5iYAB] ULv$]&$Bb E@`J1t9ØLYD{sh'>;Ӗ֮G}KYa> WO|`Dx!pW)JJ԰@ d{~ vX*O[+0 m;Ҽt@( eT՘^2bww8>"$DA9yQ9¥]ѣGܿO4֛lnX,gYwud)I|QU"sm!ZFe#=P%a$IZ;W~&t@ly۰FdY7tJ$Jz}<ǗKy/3B(D b8_q% 7qΝ>wtpAzoAq +gULJTE c g uV뗷M/ppoCEUcQ#cw蘺.) ",bsc>2=xčgo;!??<n'<::w [<>x;ߦs2?t'OHCO&I'%N#*QXrAQm7Q;U[zJrzti~{ ]&gcm݋988; = '=(m ZߡsXk10kP5ޛP#[c$N|P+EC ̊!_pPُuJ(Ei.:d(E| bm4("* @;XBPR6xNL‰6>`@tv|AXpC%Ӑ`jI5\ ]^ x$"4$J"oR#lN(Iu[s$ڋVX;\-B^6+F'GQn_|U4C'x[0a;jEU{t9>hK# (/Bk(Fu\y1(z6îd* A 8X{c'w(!ql믿Ɵ|_ n~n\D1U|89i;&~`( q3-U1&Z!mMUMZ9[xw(!d%<[0YId %Iv7NB,("v՟4U\<'%kHW!ZVu)'lR|˽гռfz| U/3&OΑ4,c~uj56F<{s;{{|]I:V/|pȣw)6]fK6ɗ/>}rPẌ:]T7/|,ZJ_ ~ώ>oumLg~pRli6C%qℤ񎺪z[rS_Yqیc-tHzEqW$*+S4B-Զd6W]q;|oo~[P[(`:^bS)Bﴭ|M-:@xZ8tSe)5"a@\Im/rA^ś$@0;Ӹ5܄e- ;KNz#ϣuPU+-4h>KBnZ$ X&Fn™ .txֈ-Hp^M7 P7Q U!>tÈWg(Ph1E s5%oaq¢iGɕK7^ a@2å\P,8oH'/TE縘,RLfw$bkk2(fs )M/l u _,z֙<"Ґvh>?D#q=bss^xt%@ܼvg^,rl3mh Wԙ 1EQ->?i ^$qJweM^> ۃ 0?A{ū/ ] O<12C EX" ln4UMy^}~|ȲAW49:8D7?VR y&{]w.ݭMϾoa:>ewwW?圯}(|!{7ꥫCɒI/<`Ōh֣9$t %crak}nozٌ8"H"4A:@*1it6ihEx9fckȯ?+|W*o}oOq@aWvm!žlSXJ$8\a B "j eJHB P(f޴®@+iJ{\pCw5ZdѰ_,ğ ._hf鋚b IDAT4?}$:@t =M946w̄ѫez7~r+C_g]-0e#!>mi3lEiCؗ0,:%/@g4S0ci4-=p bBB!U>qh#G m1KH^!דFzwU0<;3xPxJ}/- D݆RO4 y{/G%`|V`M4 r F Ti兌8P8î# K<@ChY $2RA4eN]@ 1CO-jģT]NuCӗH 0uE]ꆢh\Ck|ݴ6ծ j#d.D@*jfBK5DF l/z%XcHBFg{* }C;.]HQd_W^go2\qt q-?Ε+W!{ŭgW~ϧU pN'iѦb>1_YUQ9{)Ts6U9M8b?+]I()tuDaݽ,X,fEAtM:s泼pvvFq}a7&)[[[|WݽZOL3hģ.\ó#Y9\& ɒ\_WzIJӥ.J677v{-Yۤi3N&:d{xP"oQpc@И0O94+2RH>wIjl}=Jj]͸|Y[amрM;8~p 呵oH{˳sr^gH\$$Y CEK q%o <C`Bǐeɱ(%H-8LU]뭪_Βsg(yS0Q@p ? T:xO"5:Jg96s8+cWU ]8lf%Q[Ac3֯lQRwg?*į^ҝK8p~$$ @T"J(U@h&&[7ag•Or ɕjwc3{|6|>l4+ yN[EJbhD*8rIӢ;Zx9Y&  ,BuB4fhFshSM'prS~e1AiA;7H'ØMh*#0vnyPH{~v Gצd^`rP kQ]ꂀ(R0CԴ1Ga>!%!C9BS>_T DQzdJZbOSt#Y JJi9Ўj8E,Y5 >DQ)WQ`cIˊf%sleq52cT%BLM{*>.i)CXeWُ"da-R! #a+{ۋ ZHLQ",u )vD=l>) QA 88:Qis=(첲Q;wqt2BoU'淾ayZ鲴BJ? >kܸ$ί}~c KK]wbjZGt]m2˨E,ϦF bO/,Ke. Mc{W2ؤ} k<=$"4G:7xr֯pi:^t _vt̃65Յ8'4U2ȝa\qG!u ]Y %gOOOC01'!e4C{x:wr..^d2%{ϲ;o12ՔdHY][٬i5шcNhWTtL^tOYFRCuLzۊҕ=p,IyĹx'جkyx!מNV8)H5T-4@$*fQ%Mg;Ƞs1zȊ9]˯̽Yi1_DR*4;Z1q0P!XH֡;5!#= 00$/8#ME> Q8)CjϠCu5976ǿ k\ܚs"VBC\Đ,-9Փ,$Q!WӠ%<=%SnpW+cO^W8oc`irp2tjEXI(JCv%i OMil|%CceLb@d(DcX)luG:Ԝ.G!@R,{* , 0ZGؑ'(٤Yy&<$"脽1/g$8BO#`|٧,Kv'Sb!yxK˴';4ZM6ܠi6jc6OTBV0p&#dmcG{{Z *蝜ধ1L8::$q%W^ x7 !VjLpwxK:'MS**FuKzb`Yz8pz;d=cƿ =i\~H'oQcMT ӘVai}=KKmsyvO7䏹VcKm6V8G=G'k8.ٽ Dr_.H`ǛRj+C%H/Dy^9퐹C$YZT)P8Q(D"^)T p@mut9KDC\oA eMG5B4'$:RXcL`?<@ Hkx<·\8gB3 @!=6NAc6:|O/ 1q VS#aZZ&fiGiB4`f }҂+A%h?/.s2hYR7وHk hI| zmG(9 bԄiIÍXի@*#|[cLAb6t8+*fOA2Ï$3 UX"$:Si&q $(r̛uxJsIȉMI9)'[̧jMx39F>uq~;wO\i~O='>4t:>*d6g}+dYΣ={'y!ݿͻCk:wcme'mh*l&EcLIYd}NO'V´ cZUԢ)/ 颡Ɉ#pϗJ0]]8 k@.${yȚY p1pkf5B!p F+-VZ,ջ4-lBSP9QtKqMNi6Hp"NI6[Eե]{;s X!I`6#J>EpI| "C)p *IK=`g tU U:JsY,Of2M9 z)*WKعwcf1,#%I$x7p >ܦg@}-nu.ED%s[2D:F9[럢bͯ_O,w#ƨCsY}0`b?{h-1S8l4DQb}B"\3,óʗ +\Y+k5̂.:Xg`R\\j[>Qyv+Wswr +в$Z%M)+fԜzykm-o7X9 kyMԂ$*)8c1^9g#j;W "JW gPHr"DR((0"Kk#B0/<( BIgC߽sT:Q6Qa)=F9\0,Z`|؛N#1lIO`6tҥ|R5aFX ֙&MD,* gٛXHmRMZ`f2\e`C6 }h8UP4!:娔ՙenRbd.!1uɝ8m:*<,T})u6n"aV1$JjO\4;وR"DhG[4ÎYHU=>Ėe *9{/&J(ǚQ(2"GHi3KE3z #Jwфj#Ï^}u%^KillX[[RprrBɐSa}tΟ{it YdB5'={=&Y`0Bܧ(Uş3&6"28"/D:Je>G-LCDR1g8h7[Ya6+sq!0<@Y{vQNR آ1QcRl+ZhSDx+(f}{oso/3sHs~h3?<째w.Rj}=vz=6.×_?Sk)zEAo0w4M8wK+\v&SuZlRk5ݿGQ䴺MWONMgwxU+W`ns8i 3ƓқNion1GOw}{yਇ)F*bsKTbm@T+M17n֓7hVҬ#E?2Oɥrne.4`bL%Xkpur H~הFP䖢$$VQ訷!EcTHpbEOhX)T>4H;3e9ƹF/dfİh#j54&q2ȅ 2X}`B3T7|iOf*5xI%]fI16vL2cUKBru-YS&u W޺cR=(aiv 05Dǚ"MLĈ|Bcp8g['3R9Q*A8b,B؏XI4 Bj)748sV. yHPȜZ\E;+ YCN}D"SjB{i%)QY"gMIM "8 _.^(*2W^e}}dDpofGvY_>o^ PzT'SJ ;G,/-XYb'fv1L O^:B'XR tXJz1 !g:IY%B.p ?}a*TZCNt[4 zc1p#{!R!i|2BVY]~YP}^ﲼģQ(6n^͝|AbL+ћN{x/[899t'N~+_%9k6g3Dkhf>hx8ޝ{Wq29"S)˂t ֲÇ4-j6K)'>!(۷zp츇5*ܰds[ҏ/ /\kWYRjIZ#jԑՄyYП>ͬ&'c [r6_>?}H-PqNޒM8^NiTm^Q !^dA%"HG[Z|a!Wh )$zJ# wATzo :XP/1tYcFZ+~ un=}FKO$]/zYd(:cK _A,4j)0wq;{"oMLoU!ղAd%YcL@'(P =74j n0a֛c&1UiAEA)j 34?ykZdby6=VL硫0ќh-2!J)/hoz;:ll󐍍 Z /^{,/S $IJcp@1'iRڈNgC}g; (E"(gsy0iT~*!ՙe32˲Dbx d/kt0Xa-Ϭ4dyRSZ"1&eo?8~ kVF-v %i\EYD3h&OZE_>.&{C>/Ĺ_Y[$ ;9G,,ng.y|t:gk,uSN<#qw?h##r;ox]"VͥVne৯˭sk\;7y%ڭ.+V-yl^p%j5-5ަ c:ʯ)oKRayyFIeaa.$GdRPORS#fu= IDATԗy7y70$F4Uz3Қ ISF9Q\eM?9 j),B4D#uŝe^qiATQ`(]F .!2 >ô6LY9 Ƽz7lL5-Tʜ24#M[7G3)etZ+ ^)a#H"1ҙ0xfiR+R*5Q`CESGt)TCh[AnU39s2I\efNq}l<>_UHJ+dNNg(?J%++c-=X{񜚞'|&7J/<ŵ2~R=Z".{,pi`U޺[k_15h )D9I7(#Mc4I7q;9=~x)ay}2'YKQJS x:^o2 cED p%yN$^"GhN~B|$^%L1axC)ǙEJ5J0,GNQ37( <99c^gĈ L˗zN N}F1өe9 L}o`9d2A32L:A鈯pLf9E+Oxx.xK1rz##z{-3̳W^yx JhxhZV0ϙ3j:ok\x41eYZZ:l;`Gp|ۏƅ EIicd2" Fs\pgn='Cz_e*ln-__ݜ `<" RcJGY"8L ߳jx #yZLF/]MIiPQ'B($A-Xx?@~U# W2"t%D}p xp6"IeXPPj+3Q`C't)7ټhcXƉ࢖ʖiOA9!"C :!&,MXIzZz j q%ttO DCҔ_h `<,ZiG`NʒtTdNM)Z^zF31Lr/1)*瘮M;ICtZ2!гRְ3FE.%v*'KvMU_o'yxi(e]s#)4<QfGL+s"FsTSv1?7qӉ-դCԘC5% DyK"ܒE<`1.h$[rD/C#O^ja"/ Bk yLQ16"洣.+>b**>QKb(/.%o=}6>8~r\K"~v0x̀<BYt+/g|OF,uh;53kA]j|_oMf˵j2Me=zs/q]@鄽ݣЛSOBguIӔ(J "TTpK [ ɱ0G J!_ךzs!Kk~>! gcOP7,)Bhx YxFRsxg8hvd2BjZqo{apǔ%23|BwwkO+/Wp'C:f#{ 8=Q3&N{G<|,7I*~3 *I^}5n= G^ݻT5dqgV:tI4fx4.j5}%K]޻l5kK4/]cg O~W h[Lf*e̓G;T,0 gzG=e4N?I` uf`q/,B)!{,$\z2QEz.-2|A) "tC* @V͇4[0geSLL27os\~JF3:23"Q&D pS= S2 :ǀ2@0B-n:PS:P:$ "X#1F!DC ٚV Z b-5,|RIlxA≽@ QZ)i*gb UBBӥbhZ! :"5YzsR \eUcr_Z\Dt:$%VZ%;\-(N=$iTr4*%W҆ &fAcwk@6:34E7 |ZRhLHs;Gs3Z8L ZjZ.sFB(Ԁ4cH6D"bL#-fSCi#)\><a:! 2&$4U`҅q-B6>3ˆT>+Ƅ\3A9Ln0"ßE0x!q4Ec/B۝8'in0M89QWx8;䥗^ҥ\z'2'I*S4EKݥh8Ip\?5NOOFSyYa0%y>!Ω,'\3xa:k"@ﳋ~> 3AuN_%&>37Qkӟ Ihp  \pAX3?[?pz:EW 'Lsn\{Ӄvw?;:Өza{"4_cye ( /24tcԺDZQm8 xt8PVO'y{wnSY_׹t2++kkU"vI,Vwp2駟a7obr2y? 8ZkRZ-jQ,<2u ҫ/1X=`N^Q6QkK~%cXET2S&Xe R/RHR2J[4@C/Fʇ)e﨨%RЮ++ĭ%dkZK bK6pt*tBODAƚw\g,C%q嚩EQ4hC夢J_ S&pj Qc x!ƘM5T@M@9dx% qNaK qqS%nq,2RTDh%"ExHSLKI0EȠR::.*"aY9Œp25"]hH5EɖegX(JrcfB^糷6iss[t+ {#d84xno0"JEΉrf0D&EZD%)68&TfcsZ2UYDDVH藖~F!lb3 T-k"GpRK%B*`QnQ__d&z"4 ϐxvNeL=Q6qc2}ea`r' rcGN=x?E{yO-!s<9{~VKy /&_/sޝ8^lllP̢TVEŋ>/{{{|s_e߿OaJ>wܡw|tv&H~BeڔHpM ۹3rq{ϼ ?x Hc1p.8]s!P=:+KSמ{'sι[\k ( t7^fszf8")eҋp8BA{B~rЃ?(M$\eHq  4 We~snVaЏ}+2UYK}L .q BF#V+טh?!;O)9V昚kÍnr/-tap$L?CN|r#f''X]Yլr|ǝ۷ H)\ xdvv4ɹu$FZd:L"W_Zd%ls퀣H M:c`zz H->&fck׮rko8Q[oR;;Z-ūwNHЦ:stfnnON7xчܻwK]*VkαX]^CI^{@j@X%]^4E8BFznכeR*ق4- 2)p(ݢ:l^:rl=_+|2-*%r|U .A LMH*5zgߝ!N!B'%8!yp[LwA vmhiAH_stK83tlPI³75v5}iJ_P 8PX7FQ.0'@d"%E.%j QՔcf uJ(i?6 mPJ"W/" d F7 K#1ann-Ṳ3BɃuVg7o|9nTuy f?ݻlnmf)HkoI8 kLE*#ITB ^x4t;|4 ?HGdVEog4Q׀8l3)..1ZQͿu y7>R@{ӼW_69iRƊg|?]|n9㞵)dY)ctZ!):BqzZez8z#rpbH-6z1#9FԠlT&JJ (&B=QCaոv" r2~H P"5b@aƙyep1p GăԢFPĒS #GONd^{~ [[| Ie :~BVMvwY^^=>x<~qe:ECe siLg4C`3KoAY'V?gq4KDiƧwKka*k/p'/ (c>?8VETʣTtYGIȦ&\T> IDATA~ȹsT*c.33S5F<#ZIpͧ|֚s,QmԩT*<=B(,NeKKKZ~]uK0)2loPhrm J3 T͏љ*g30a2L&O4Vy ŀ``BE`]Thddqy)a.aUc& :ozkOB|@R8`$FV2'Yxf]VX)gPA(PCŦ",1$ZV52M2)YrBa.uNVc+YeHQvG׍C#GVCW)v't:|oGyBh:$M|3 RYXazk{2-;Ih!:\]3Yb;|{3_!Ho$`js]yR:L(H ԘjOxq=;caE`hӇԨ//v%OO&&#S<_pg8#O!A%}̓X]@ūxfрz-Z&/_fQ2)Eĝ>a`4=xģcLS6V=|{_zɀݻK8 )r;'//rKHt= kx"aQWk5.FI&,'iT<4jKF)[oAa{{IgDQD䣛6SQooo AW.57oduu+ywR;?w0XZS-,B9ZK$ȼ\OGRJƣ!g!^2fsMII# $'nc 0iH R8> M~ " 98&BH4;oL P~i+N@6i罯7wY:HkzPGl\9'8}xJEP~PM z&2 %5bq)A (J$0 :a'M*eϰ(/ 59VyJZK QJZk$&P_^°m&l޹|Q[0 O<8fsos%jULMy!rQrCy3+߬0+T,M\;=Ec91攄?'Էnϰ1q[EJ+x?ޞLRd.QFdF]]%61I.mTC..eKlw_c?LBB%j9ؿAO;T<@FJ \Oe:>ш)jin$;yN KxqjP9ڂ*V)/3j%* QqwhNN  yx/ ϳJȳ( ҕq*hcdu ѥAF剴4&~D,9* Y2Cٱܺ cJAD?cB91cϋF K%1#d#,X$ΒBv @AF& Oz 8-ƞzN'Eq9dqA(G8WQ?+ Gh)pxRa5DZIQ<}d XNr9=ITeq*hGsØÓCyFVS?<6cKyDtN'okvv,5)O^[2~T0=s댢rjbĴ^nF KllQV\Mm2Z=ArN9d!{'(60!۟_ewIu+ 0>KEV+j~JgZZ,ZQW<7nvwoZPkS!"&=A *ԭ& HjJV/0ݺ¨gY3 Y>/ /ޞyvs#N+&4 @~6RJ(7ZTeFJ$.R:hy3H?~Jw;[% Kc`$.r9M+xƪ]ze˗=g8,5ӯU1`F5up2>)Ѵ_y.B *AFE<yQV=^ǭBPc(q͗u_ǿ"wRPt\Cc'69yb 븸(r4[{Lpx뭟oѨ8otaoNZ" w|N9V9w|NZ1шFFӥ?2ZYL Kj0m>6kkkdEAnrZ4O+'61O7Q?QߤѨt)]ІV8x`0][\~:6S$)EEXׯ_gccqz*шOj2 XXYB_ۿ;T<@'w ba"N>C:ʑ4Aʗ?mJ`^` lvEQ1*A9) )Zg qѥr3=mn MO |DaJWTn|rezb}ͻwxE=v\\z 3W/>%>a›w0Q (NC^ѳL8О ^gc6"9}(I']F+E)Qeiw1)GyDf5ZBxmrD11gSSXD2 xc 0($SjA!x .yӋt0:/65Q/n</VR2a g䘼l8dIF@q),775(͛l<~J𘙩Yw{~F;?S.\D}ifZn;Eף?!įGAAY+uZKo4gyzjg}XZZΝ;<\Fᨏ㕻FAѠlefɲ}jZ%]>#rpsW(AF α)M/^n*$$Tz0o㺔m.(nd6 @Y9gN4RbY64{- I Qư?Wk8ad7.&SLb@d4Q^ w- :G+NHl{O͒+xL{yX[eCshNsxY?~)ZZifZy_Kލ+?8b5:&y?f!ڶj=֛3sTl?k3sypC2kv,{ےBpWqMl<-9CCkkhMpȫ5(1ARoMEf*x>퓎z|%;\]*c{ !7>n1aADv{aaǙ9K"&$4"9Gd{';_f4JXo ^嵯~dd㗏2Ԩ񬭤t/k yaΘ xFcQ즷PX8X!A &!4(ʴ+BaF(awwǑu^{5>{tq]p8$Ir.1gq7"PW 'q?  ɍA"Hυ-#J e#T11kUܼ2srgT'TÐQ?1arUO{ ^hzi o!#^\'q sݖKk &]ʲdjI𠽎DqRPy8Jh\;-,)Kʉb,~ 3ޜF<%p|7jg\c4SNɨ2t8W*_}LW|q?G$q9~~+!W:+WXG:Jm Jj}1%7T|u&? {LMO0 ٢ޘU$`BuKpIOdvgrz \ш@(<=FFTȵH3\\|QaȅK [n)~͇,Yۼe~#Xܬ㘹y,;EADQiw; A/]wwz_n e.Rm28[bI Bj( "{M/[?#.,&NrWtQJ>#4E  bp2@h!BĢds fp'en.H!邧A2_8NzS`b,)b_$BJR0vH3ðoI8(UA*j"A(E': \< @M񹑓:p@/4M9 $a["|,*Vϟgqs-0`+D6R4+PB3:` YhM9.jn]PwomsPvl}p3o1=[KWy|.\q{YP(X[%0"0 Sg['H8SVۭ zT> 9a°"ؾ{םwpjIl}b\HGk C޼5Vf92]]3|68 Y>o]g_g[hl1<9f;ba$Wk2LFn_}9sg#/f8>Sg._BI=!oY Yj9f_(qE4%/-j$4عq0NLLT|xDec3*#z%iZN]8Pc%Y7qo(9οպHp S)[b(EVu]4rJVVkMR鸓/MS ([tĢcˤ5 EfK1hgk<&HZas,_:w]FaXWh3VvxCrrtH5>| s 8GEVq9t:h)hMlM $UbqyyڝId1J Y__g0F~G$MEt{'LMMPvM:^0bJ@Xi)YsGfh6@*"hwƭt=^r K+ˌF#+' IDATrɐUn~vɹ)Иj?bzak_ɟ~%-T#&.QNaA-%a&<Nj0_ai\g.g&%±cTAai @0.e$O{+Ң\IP,-A}~4q>"tj M0rdžET$}I3yxK[ i^ O; $JKQ@ހmc )RN ) A$" P Ҭ 1*Bh]$c'\\^9jpD/xttH5B5`L{  QJP ixh~7:&<& OAX67 IT6:հAo5WS 0ݧw-l"!PגQ]Ɣb`A3<D;Y*cLX0PT=ARB>\/Iﺤ5s --259[ {\zCnݺŅK+F' E>sdEAmƨ?G_;Ƿmxolll0OO֤yF xi~f|!8GFڊ. H!2ZLaȣjW"=-8NC n)pfB588H \]icRh})Lc|Ft#2F2 Ca 6% j H'tSܥӳ\[Ӽ<_3ݹAaVc 0YL&8ϭiJX 4yCRB c*$J9t{m啵CS;!xk¶DŽ)Z;GsJѪ9dC|#scKrE]ҡ% 0敷/;`$OMa֨WP! E'+wSf.܂ Ǖ0t'Q;>:/F'Z9aEe/A?~a 9݈3ic5vYk1'gAB^dia KdIWBRuVyeip85Qh?֋SfRg=:3:5X4-YQjð:$FP%֕g& IDQN\Zk\\<$:wt#@GܥaFu e-{ !__g̒)kmϨll :ȣT!}p u#zRJ.H ί]V.9' NRjX"Ar&f&QtJstfz"3tNpÇsNil)R]jIR2DQ"Qm6u.NF*-?ݤ55_ ^Dg$Jt#*ٷ{6ίM],wɵ"RmFHcٟ_A_cٖ+l4 rF@ l ^{.w9_<ު$GnQ2+s_9axvp*}troƗz_q~¿b]bsB ?/w/;_ڧ%_Ʒ[Vݦ{ޝS:h FK#,NgcW\"XJ)R YSR{*aRi˄Rbkw6bZcL12憸r&\a]~}tŇ'k a1&\ikoUJJnY O&ʱm#NT8=#xc 9p-l-8~iT- 0c$1C-آz;pRG1^Y:< E1YhcP2jT H2 kJ"Hi X裣/N@M꒾Kz.X]PVs Z3Kv  FY, >=~lbVQ,g>@5&u:윧ޠv*YOw.(aӶ'|Wh6򃦧tW= o8?~BŤƜ&6P|d{>ԧ{T v7{+a]~hsA稩[͛rJ(uCBpl61Lup..0OncroU5w&eϽ"PDY=Gǂk6;`W𫆛;$ݰtGäROHcS(!+g6y툋?P]ehz:5e)]dэheQJq4;XVSUh*kY3\Ou%rwAYC$96Ь= b]:O+ڣܻ%?Z.GxvG^ѻe,EQXl޳Yw4lA?i{^6KK&@"1&%|}d]R `^_ F Q f:ʲd>SU8(|S1f&@|"dZ9MzM߶tMOǧi(I 0_.ګkl.<{w_<{tϧ(yvƋ舳3(vӧ9::"jmt:Ν;u="fZkzu]_hX.L&4M1}绨~ssӋܻC=)Xo{o|'|osք??`-o?_'ovvӊ[7n+XstwNse mo;|b,kY":LRx"hbTPW݁}>r#.:IU`dTFRZ8B"SKFc{hF۞fffPIc@w]yʫ( \Lf$4N#yQ>Y@=ӗTUv8];G;RU"#eHpB* =kQc4\>GA%R]lж>]%}nlYJƠMIM ~EQ>]L(2/+Ԡ5L5uyZf<)0&!Tа;+Β|t6cU$؜Ҝ6I˲DSwe`X,(#?08b bK3341zRTA0D'\/A1$8xqR|f٘p&\+.y}V_V~>V) ]o2V5EQPݜj䄶m)˒lf,KY낢4}HwzT6EI9` \Ck|Z3&2E=?XnZxש)Ϻgܿ*܂%&b'-_s)*N.XCߓ3LF(qE]'nj5(r|Ay5i:^ ć8TU.k-m4~rpH"Dt iڭMjQ l95[靣wło/~N*^u[/>ccs]Gl(oǓӧOY6mm2[--hwaXpƘ1aHЏ1n-޿OY<}Ʉvy{ǽ;wi6-w䣏yS)ofov{ٞbז>9b;Nl[2ܾs ?˿`WܸƄgw}N.9cmǍ}~)ӏ?ї=Mw'?}3sCv8((J'R(GCKD\k;BQ>lIP ;̧7x ʂEϔ;R#Ɠ(5Fо*9Zb7=O]G0JP2DܜC=9{11hk(H’B!#P]RܵIUYWJvo%#M6ܐdJ2M7t-^#=RH^Jr@9稂)+ݹ15 S*| ->:T(EEI8r[Be謃p*F!CVw!9{G$a;LKKa5mг)ߛLF:nQ)aٌ7X55]ױiP.T> |nVsSV:TvՇ t %:ffc>ɑTRL2 Q1I Vy'EXr0 kI>&W)x7n0% [г:^)u]y%1aPl7+w-Z1KQ2%YPϦִhLԶb2[{K놌Ixvwwk׶-΅k8w p0+:Qce(J!8x^5#m+F+_>^ap=}'KJ>DѸ`[(ʲ{u}Cp;=nѯpcv}Ҷ-}ΨBƁ+vx1+Oefa41x*v}[O>o%CỶ'? w=GĿ-mr||5LfwbsOٿqzo>_?q>b77׾MQ(O~9_beV.87ϥ8 W6/(Kqy U99}Fk?'^%h .# %U S%!Wܛxg$Ӷ0р^`TVj 29sM }!D|Eu"0 Ic#KR-S=%p dg5"0F1Dzb+iJz5xS(#u(@ޓ&NC'p.\kװVJPb0m@Ѓ3u] C>8es-~KӷLlAhDcs]TZ"D  *o*ɁQ!_oeȺYjK۴ToI2[;Y72Je{}׍:Ewgsz/!1%q2@{ *I4)Ju)HX-Qr8R)_y07ڨ'?*GKtV3HHC ?2H|&13KZ+ !mJ,˙]'C۶LՆ󓧄 ??ɣ_\.u&> B9FE"bgg3$Eؿq@5 إN051{pp@UU|3UU1f2y!ϞhQIzW`:R_x9doGb UZ WT2rkB#O˗_Jf1K!T`EIHg̉A6rVCC A%Q砗z>׬ItxN@QmA(jhTqX 2uGBI8[eIPd hze#޷xU=:|HX):UBz@UUlG/z]s\Bp.;cTHAB$&Bm-JWya& 4vJ+FQuB]Nl1כ`SF\)BĨ";K擂0r5>!%tHΨ[v)yؔƲ E_&@`j25ot@e=Q-Zi"f[.iˇ&0/c$f &ŤOj隖 <},Q[n2JCYXS$m=}/dYzKQ.jyg ϟ?G)JJP "XK42pƄMCW\j4)F]~v( '޳ݴ/6>\ĵ\ >biZRRTÉh:2 v gʻg|ɧY,י. sm3j3b(% HJv=/L&5łtYX,ƅlDZt20\&S>W}zM0f{M=>nѴ[5L+o{Xo9ǫ5 y|!\sxU gkb;|ߠs zٙ#mdA:y@tQE1 Ѱi\Bc(R *M—SZkd G^y =oۼE&2=3c`%B+iMJ3u.AeA ] 3VkPEFTJ ,%A"+8ZIBĤ<]Z]uw|] Cһ!^'Yoi)ʼ(5aTBj W@#tKbp F#ـ-5>&|h܆U{AG!BY1քNH}@9`0:z?]X :6cY4Ga`n)RBEG\z ;;3"N51PF%Qg$JEˠ@l-*E8]a1rI(BĤF캬(* ˏϞԈˍ/@U4c.?Ш*%#ZͩJ$! Յb6z^5oLU4tVt|R`KVlrRAeD0ƽw9m&\XSr\>(,+22Fv8އ1FRA?h}5Ygݲ/5(Q;r}`tq6^ӣֵq IDAT{}?1r]5 yx}UnIt"&%+n?`s.gǧl/Zb ̗3ۿsϦm܄IR}O* D~~ro.6ܽ[3N8<9~N4pɄt:m#{X.|\.ygko??GOh[sN/8_k|6w8>_|W7;?1-iHT-l[|M;)?K=b2ށv~R~&O,ZkF&#dW&r-dXDBKBYiL *k<6Ai*-57tܟpLo|5޸?|sO'DkJƣP! <8V3MC%#NTE*E(%c42y3 B:WH0{GM7 *&6.$Jl3%i]R[Ci`ZWLFs̰4MöSH5Hiׂ:]- Gۄ.uPѡ5βvlCPFS'Nh&Ьx1+e<*9TM蒉ָEHia9+ٝ,*SfAo>:/!V@I=ƀ0s)B$"NRRĐI Ŝߗ"7UjQTH(%7(UUɒ;EЊ z5/Mz 5iWA\f* b{g?c}By~rlT,`9@!|OOY+]`tBUOmIbs*ku.EQs.N.Z0(lIP-.dM f<֭;8RW}Rԍa뺎#%]+3^[= 4ZFI?d\s\(ˊ_Ns/ }O>{ .>U2Ed %M(k76M࣏>`RjZy_y!7ťݷEdVuEd^99;k_ŦٲX.,{NOO4M3ʲrIYL&Bͽ=vww9??-Ls qppd2j|ܿ?wspFnp(gg|kos}NW``Y؝rr[)N37L]I?O n͛D9xs>!E:yC vU԰h(A8+sjiJ@ T*-rI6JPV83REUqMwٙN_x]Mq+"DPTHv& R.)|*_]kL46(#:3b>K[=ݦaRz*rjRVI,JG%{{h:h\wևljm\QJ[a-$44hA%T]gm/^d:/׬!HK@0 ں4#>P">9|ҒP}řMч^AT {QQ MBV':ףL*"2"[a{G5M-j&Q޳nYm6l.$l׎UIHtÀO1#5 / ЗI^ c:4EU.JbJ,&{ H%1Vx:9H'V$W*;#Z|N8a0Mk9\w',e։Yyl2.zOnX]bu- Q;_TLsB<}d¤Uf!Enk(|ٲ4aЧ+BQ60n1HLW|D/%VCGHxm^5t}4FJe]QUx-,ǾxpmķɯK8:4!ODB@Y@ vLw x銐G =b[_Їk `16g<:zHQ8ٙlXNv&>kXNf"9O1(.ʺB%1 ˲4(BQ@]t5fZGہr+]nA砏tgk& #~g<1nO>:lƅH`mrbJh;hb@Յ6,w}"7o+֖h$NW+胢Vr6H2̨KPL?gsw09@ƯVkAe}b)@VT(*Ѥ(BYrZ ECl7-eii[wn?hZ-j;q=UUϘT5=|CkYKiUx`!ي{%)% TVUE]ל_L;BHbQP1*'˃z5Ƙ[V(c)L9Rke6i [T[7!uK]f+'gz]ۋ3Ɛ=Lu]v1kƥǦ0^4rpGpm6t!듄 "Y]OP!7`()B6JLgp옾7OmDiKC=pA2ʴLfSt+={5%w,oພܺyzwo+xߚѷeefqhd2aPͦ<~7x]hI)~f7>!8eYV NOh6-Xyݤ#HpymmI4 Red`Xie8cb1.@1FbB:k GvcƅJeAM>2O "C̝'נ\N8=kz Uƒ'؈- ERC2 ɠRM̎]=vب0pp=/:D)!&M tOR@feqllJӬ5Lͨ( y`KF"ar^>kyØ: Ȃa2𢪥s+ٝbA RXk$7Iܶ+NJPI]7HAL啌Ș/hə:bm= FCi uibO] "Gta!ܰjM|\$%ƿ𕔁QjɖlRJP [TyZsyh64BKdN;]|`GO)˂M&qm#B߱m6FbttL>2֣F$fK(JQ {Wut8pYQb xt ]nqգ2h{m(u]+fF3Kni۞\cX.v\גZ_Z[h>IQۦ:LIv[)A7j2y dJqd.x-h!//GA@{6EIIQP*ß 1]Gc }60|b/COGgfY$,,q B8>>N8,D)dmbUU:fKa.]'A;;;wrrJۼe唳9''gl-J[X,, . 5 |+?`qښ%!R!iI*haD/!Â(URCi+)%E/^[^ƚDOHCHs j,Õ<;5]VSv)tEY3T'ZQ! ]>ܷ~fΏ5DBIƢ6/F R'4bJ:J6Y:Ln 7Jt=CJ˛_jʗibDu[k|o!8LrmI5bˊ1 ZeX$PX6̆/O4~)u%[<3Nɪ\lJx<^+j 7=n""'.LUTO1 `LFT L鴢,-$BA,yg;ZqQ#U!Rx1JyJcbSD] }ZU$4PUJCUiz"9M_02^>M1;Ih+#.刘_ڂ-Zth K0|"bT1@nZ+p_Kvgk|,Ͱ9H_z>Ӷh)Qb\SV".{*kE:JpӴ l[4MjBkb3F -+Q=\v$3 >*JJk9h+Z@{JqDSU[JfctDahF $UxyEs4l$i<$Mf;ݙg2V EU1O$V)}RB_MkSf4f1Sd"Ksx!1F޽{\\\0l:Y,%^&5UYb 95MCYWeJ!W΃G3~OeiphRjdcr a*tDQ!&)6Wo.wN,f)«ƆV^\0( D\{;M@ oO(#tSy37K:HTȴ3ϡ㆐),f\QIW2'(ل*FhRx5#CrF׭`0+H1d9EMnG Q)LYcdݴ!q2aV [ԕ]W+6[)b5'ϟ 0PWӱhb4nDC=M]3DKs>FmAi zB9)%:Q>).[mT%1gk"g(^>^}bhL2N.H%rIywrf}Kk!b9c6ZڐoUr󝞞 _MlkY 7nBut@N<}(y7klip:'{{V4~uIr{_~>{1fr9{M'?潿o,w♘,v1ck/~ʯ7䣏W`^L 6b"Ir_U$LҒвQa3L_6.`HQuB&džhEQe(Ma-F #!؈=ZyE>{z6pBAPyekB] $-'m.I& ʂ,(/,&q HS!&M:*MYTFG@aT` ؄P]D1NZM$FUe%-I>b4 G>F %C 4rNl༄c2EѹxsI5A$?bR{O4`F6)e) XCCΐ1S6ԐdBEZSĖƂ8<.E(um|ss0PC \uuOCfu cEKEMfxdj>$ \8<ʡ/F=L6=E{YX(K͚"w@'>nݺd6Wn<|xw*~*}=z[CVw!U9i FG(4Ċ]6EdCVYhu+Q73VZh7q0Je(L)-ZrDN !ud0u@H 6itDz-̦̈/W[L*9H[]p\#!X !@QBgL3tтQ*?yb2ܗ0@ti'qƤ0p$-4aNMEw2 E!z#q*lP1rGc Q>_D8:M5.nRJSG-]Q<[1&Z Y#^/f?c&ˊQ .3&7jL 娲6ơȘGA@A]똔m׳#yfێjt:͡12\F:6 >Qo58>>5DY`. f᭷ޢk>SoZ[*Q6 vbwwz6enUt1egm'{ǿ'{{79?;9˝]&Eŧk_2wyޛ[-}BK9Q : cID{C4ðEC3Mpe&cbFUVu]GcCeBZ 9,K(wk;BoaSb8O0@RQꥇ夲B6es☠8qv*PåRDLjòAQd^Rl RNTɇA'ATy1Dnh֑v]`(!0W$ 楁̈aTU]GL $G߮95h C6(ϽF.L㾜'D'1b Dbr]g&EIIqj xh):| r d\7U_LRWZ9r5_p{.֐ޘjJ5\ysPð!0J& IDAT%E%B ki m#𐳳3>}ޛYUu߽sfdXU5dQP "N-8=ԖjDQi~8|-š[Q,*o {ǹƐ9g[ĭ{=s^kQZդGN$eb2^ccc87(T;а/6{Z#rQvM&IN<Ρn! #n/  <%y~^7]LAAD8  "wݠ78̗_y+w?{Ob^|k/}KyO_xsgNwΜ7|эe|g[?y^^uKn?^5?w?"A[>xE}/ʿ? v!vbO-ov.D >rn/7s糟0? }?W}+o#~?=G>wg={;teؿ~󋿒O~q~gW^W7зWv{?{!}ug' ~s|œ.o<3bOn{]lw{]qL"o}a;_坿_X^gA'Iֻw.?~Ӽm ϼ Oox(EfbǮK:7wex!oͿN^o_-}G Idwe_$*__ѢQmm/vԲv_Ⱦ.8 ?}9?# w/rf~mQ^o8p>yseianm7ܵvOtZo(>˾.dvq}ELLM.ny.r{]lw{B%v!p<[M14_S %/{%_0ozu=-v!Ŗۋ],vEg>O~Ŀ7vǚB/U?vo3}'i}33{;{/Wp_]di\fzwÎw#N$Ba?#B1']L}F1x;r؅Ŷۋ])O_7~_zpV9q8xs%Ŀ~ou;{Kُ+9/:g#cç>Ag_\g'u>냜']|ዾw o؅Ŷۋ]bVm?=x!n yӻoWc~/o_-EN̸+OFh./}~ڻ[.vbO-./kSeYrPAAE!1sT. Ɉk<؂؅ v!]OE$9FAA(  pAAD8  "AA  GAAA(  pAAD8  "AA  GAAA   pAAD8  "AA  GAAA   QAAD8  "AA  GAAA   QAA(  "AA  GAAA   QAA(  pAA  GAAA   QAA(  pAAD8  GAAA   QAA(  pAAD8  "AAA   QAA(  pAAD8  "AA  0 x2W} v!]b qK^ ?S ABkLU?Fޯ7wnPlKm˥b؅؅؅؅؅؅؅ǧ5<&wcvNs:]]]]]]"R|W1[^ȇ~n v{>wf0oT.X_}ɍ(\)~y^_ַ ?ynGo>`wobbbbbbO/gNw}%mZۜ\w0]ꛘ=}r......D8 Wʞ~9Ѯ=x/>S;gس=6VZ}s>R}qR^ͼ_ _r*պh 2U8oe~oxxW~!+^w^bo}#2 v!v!v!v!v!vB<3wk_/|돜9gq m ns1\o^]۞+7YBBBBBBÓ(ꛟ6%~_~bbbbbbO$.׾_&]M <A@^tzj5HG+)ΖvjP8idYJp=t|st+ wSٻw/u9LO<oQ@L.cʩwm΢]XNs"sf[n>Mn`jr a0jHP(6Hg{YI&됆g[%cFFFp~t*=<_Օz/?zҋLq9p|LN->W G{~㵩q1S+V8^o#@= bFG?I3<1*,klԭ٠x576][Minw{c|k(1ʣpvLcvoO o`"*IH$rty'SņЎA/W8.,, CTcrrvM٤ 62YQH[0;)(O"/, JX0=0iu6ٟZR4qL$j5jj(O4gs)kGg٤R1A͘G21ŻrJ_iEߣWf.X.Lc;ݿ?iȲnyIQfP~g` AXf5E *\P8(,Ks ^prp99qt0 C)zmVZKdYNl8}8<"ZtzKKVɳz]xY)K[^>hD8^+Nq:(s)VJT k]xn7 ss-%a0:>ER,.31xBQ8N:h7J)g YעXfFy,~آb ߮eR{ʫHIm$5䖃8^N!2$!L*68*=(SZMѕr>eЀ-<֖1q!cS;t:dY;$.kfIՕelؽwCϾݻF15(e:˞ĥ,ϟoIk8|! si֡]%  AnC|b448CǦ e_.R#"G{c3z[~pGo#k]q  kYl4F<`9ݴ -,*u 2P *y24[=.Rd2KaڹXZPexfs˰RpSe<ԺpaIpJzXSA )t]o^yC%$ILEF=Zee^ZB{ffvq}왙V1ze (_9s|{n"[UZ֚(Ta5,Zo4QYYm8}| Ás qm@>.}xA[=z0ܲ¿_X} kYDZX-(XWcn:*i :"GyrzD^ȕ-tWqC]U >$QZh=<32/wa `rdp?g&zXnuP4#Ԫ1*6&՗wbfKV[>kz{eVИg܆27e!?U T?koRY7ml79x'2y&)tS،NI,(Ē)ψBC$\{MZV] HCt:mN8'$FW3ӨLk QU1qPzdiAgA nF`bj;=q46kMBV]2'Iu<_{?*{yx64⼖uOU ޏxfn7 _Fs %4OopF\;{Ԫ1.ׂ˜Z\e&c(.|D}A;/p,'eIj8WP y\F Jr'fsqwu^$X'k|!9O)i:.ζ$ `yE;MKQ0R! c:.oofnvvN=yz+v_ p6+ų:RbC3u}OTx/t7%/~%1 C(+C/wyAcPZSh+'K?Z8jupJǠ鶺L6سwC,[Pqb ceeZFѠh;}'OڦWxzEt0E8sH6vf_+uk<Ku/64hV6|ء>(~R0qMz8'BɂY*u}"G=F[$ɱn839>1*Vv 4FsgȺߏI3ADPSTkcZ'edy,G$29}q:Uꓻx1ccby<ڪ x#c'GO ov͙Y |d:qx%Ѐr[ݛ+i F W[é~ pZkVq$p YWVV(ˋMR]ϜYj1JYC'2Z] feyyc IF$au9nJk6hLMMQz\lP8{Pf*Qe" ^y{~vzZ-6YV kk&"Bܑvqn?3^##roq6(:(ۣQ %ؼM7giy<|A4ҬQu)qM>":ɬW0r={P+x,YޥsD!C B%,.3g8 "8K\9W1Bmܲ9I<}x)RBu~b[mkY.籸~"p Z~|ږW\&&&Jdq*EU5yxt?a9±vѨ]N--̡U-R*ʐT e]:N8dYJ\IBYn\k;4 ALo۳U}9CRhn=|zN|*q=f|QDG利.j7G%'w K..1ڡ*oS;ǪR c !.QL&UV:ZQEr&T*A^(U22Sc m Ϲ#qp 3LA/K?KW5YM-mFk=>[;u^wQqX2(A@Sgi5㙷Ç3{$;Fڤ.EVmwX]nSˮ.zCOSLHb;3DJani.Q&[T >XZ wYW&tΧ?Anc'֜%n X`qy,--qv 陔 .‡ Z+z?&D \gؘD}?i3 ћ~^j+CY^$qhUptdm$hzEH b 5^)̕Dx>.e~:rg/4%,u-oUgrew(pʕJkb@`.yA{|Ce1:ǦmlBTk^ASgnnk3(siJhYfYX8GֲZ[Yall(jiJLC6±%gNd1vͰ{4르Z-pNÎqvdsgV1ɗ~316YFFNqnvn;p, yأ}_(!ce|cZca愵QLT+XXnQh\] C6 G=EߟTxk A@`@dDpTrLT) iEA6\ט~e 0)M(vE=E¡~0nS(Gmj5pT^/#R"Cr@7K?j16sp.n~75,ggi,Zݳ&6z5>pMwpGOakDIjRC h  s"wt] h022IRrҼyk^0!yQuRkVjq@,/S)?a;юLr}q1:6;v25"KY\Z1;㌏>ciLlX^Z( EAV4?InݻZun7AY\\d~ɓ B3{̝=鲘|\eYNZ-چ?nS?Էo?7l5٢Q\AorQ ߴp㍷061 1&$c0{O ghx衇HZr 㥅y0`]ԫ '#-FOKm'嶛̱c9@5qc4+0F ] ;$Q,4WV3^20Eu /nJmlBk(]Y=k-U|sF;JUY˵zs 4t1R+帇h($Ę'(E@0 @-xEZF/e8իEFQ˶zLr'1[gWL,or~hM-)FGǙa83OrMۡ;4~ r ˨ A&1(C/Kiwuӳ"YM<Η 5.( ; QPG LjV@+j<#]'I"nqxQU(lFjV<ǩDkxo9y*UvLgcD"5-e8j/jɝ;h4ݿg>Y,/rI:z (ťy8}טcff3Ug{"Jv+cyI6_V{QM.io`ke45A3_(#8Zbg^mj/]S-:d4Y1ܒڂ,@vt@R;&$DqP^R0CXR|q٣v+.X/T8:e70#qWROYjLi&ḷ'OQ=##a,O<? FI* zj@4Zk'+ JSux>Oxqχi4&0Y$4cU$LVx'g<م%>{Nrsi-]M,6[d2dC*QD^gB;Gwu<,4]]+%Z/3p*FPeH1l߃1tۗ2[z!r~lr ,Iek|7{wr<@+Q6XVB 4d1v&p^]w8k2M9yZdXenI0k'2ڨ;mZpYx"A!^PNamLP1ƒ9O%uNmJ~R hI6e6Mr6'˅a յ`-A͡C;y퇹ng ]ή\$hT*,.3>:jЗz}Z7\￟#=}azE_BمSVKۿ?{nnu&bx e2ί\ࡇgqqSNnw:9++mZ9^YDrg(~cdZ8W_㱵a+*(u*5LBV̷0ڛ_1^[(2CH\#ĉT *:C;Vx9`LH kJSkNK,7LEs v) K;U.TxΖe6h<# 5DB#V$AHkTbGy{vqtM,//k.jZ9mZU&'9y$F,,,$ tGrآ*:dULL4Z8{$˴Z-\]jUp312RZy*Y鲴Do>w:R惇X\X+*wd0\DZg斬(RXuf'%G;-شJ2ID Jx0*EUY!D+?U2cߔ"Z)* R8r}@: s,E 1oGg(*c4H;=6׊]!Ft\Od;Ú2JyQA)B1Tc4yE%NK54aHV4N[ٹZI:9G gH*F9^x$GDZQzW#V˳LBQ6ts܁&]n`"VtPY.BLi]Q&(WѦ 5*tqA:ǩEr19u6=qםûV<z(:fviQKXZ, _W+JuY]fۥZ䉣v/BLh h(*sJrHS&I$55ER-b] RPՂ:Sa@<ͫsf){𐳳s*![1+W;<ϥRf)y1#I⽿C9/0t>펏T`s~><@7aZk|OM\̾_`#t9FZ/27/WMr]2^/2s,㗗NUAV$/l{Lf͗|]~'GGG%Y[[ceel?'REAԊ˿K1K>YL>RJWF/SO1V7ֹvQ-}sc 1!AV֘jLN߬ zf.~\ƍ.mBPY +)]Rʆq\u.iwT(xN'Ejq)^.6f W4k!DXp6n8akc-].v8RRYQ4Imz;~H.qy *IX0O$I dEJ]H4?p ¥gӦnRy.PKYTd`Zv:+}L:crv@^qM̈BsXjyIQTyE~ YNZIblsԋxR~޵{K-R5m٧b#3mP6S"\CU{zo< )x * ЊbE ]be`|{ đp0R}CA}<70dEE?:0[&Gywyqllcϧ<SVsf1N<9'/&h-۬m x6byy~S e󧞋= )|^OHq]4c-A|pa ټ?4I^.7_`MS J_Pe8Ԅ|o=G䣏?六>l&Q1B8X}骙ق0 0={'W_ U)Z#$bak\Y#M&oG:)i1w\`-vt]Me'ڜw9==e>ELJXkY__G"u>ׯ_g<\t $ј݇lmmE!ݾMcp}w {UC>c^}UtUiuiDFS+pzztoǷ?뿼\ea|>}g$( t1CFFyfOx&eԢ$a:ȷ ZajhÂS/, i.+Z/ =+ niۺk X] N1HÇʊh4K̩0 㨑:%UY3O3 Hx pRPUTE+Bj=}`JZT%AHoab{K %}D9'5"h4u]88dFь3JAJؚ&StL@I1]H6k rgNNZ DڬNCL'S9eY6̵>G[?GGGO=ٗ6I8MA+Ev",.aqd61sҼh4Ģx"n粰(< OG``|1u y7me>na M̦szom}ȝ;OrD BXbhFE5t\EE}_y*vI?O~(3*uYC jZO>ਗ਼lt78;>Kܿoo"y"iZl_\'ś.snׯ3ϹwQ*poo>eu ހŽM5Fcx0kTUC;(xֿ6IWdoڵkܺ}>?OZA6cs~I$z}=ի&(|4.s]j;dy7^:q*_sMUsw:fiJxH"R|UddҀ7BQp\#+*%D#WD;|} }v .0<9%mv98GTpu}]ʬDC;1HJݘJI,XɁh~--JYwEW]חqDO8 M?EسǃK:/C_nz3,ecev29=Jځ%5a9Yz?.t:/nLx-xU!q7ÇkH鰲#>q3'-F:G<\ׯ@ǜ7{|h|c[NveNQDQt:e2`}t7IVDe|K||VM]On-Dg!јu[3ʼb}}MQh+Q-QRYIY6DqP+d!R'ЄZhլti"X ``l5}Y؄a}ףj>鴘q9j\Q-Dg-K1!MQie%YYS6iAZ̳ѬqVK^|n-/CT!G><E[׌8;ֿۧ㧚E[r _q!p H83Sb.a)Mϐ<9VhblD8O\H'F(_#Km,n](k<ƊD1'ɳ )46=u;8d:peܺ)Yʷ6n6?x;?9ﲲש秧Lc0duyc+wkׯ/TE_dy!*ո{*#K+ƹ_( #Ks9F'j.\pW]mpƍ>oBi0M)D7`jāC;ty;\vVWX>νnp2jȍ0R9x~*]S%6_"i.74-p\_GTUf45v@j t>GJZ ՐљeUb~һ' [[[lnn79l*uGg-YYhdmhB2QylQV&^doo<ѵ*5>m$C4?X4[[[TF;\0ښ4M{KKKloGׯݠwH LJO_c599nZ H5Z6p߀I`Z&+'S_.)@~i'X@P8ʂpxY7M!uNVHPE:>B.,EE࡫,KqT%XMU:s砋pO.1<=j,9>VyXcurm( LHUyt4&KbNdYBY,KNFeFYE4G,u+K,-* }^}%mok& $:&<<`:?{ (젤%r#q]E(W KQ\$<3XY% -l2|xu]#.63hX]!l)KÒvD_OҖkֻbA}W?;nc׶>31\^4U^ҠPaǍSStZ.h0!Kg^l\`);G$YM6eI3}8^4PS!%i?mx<8/'3f>FzF=JP&gQxzns29h`뀥 r-I+yR2. ɰ1_H)Db:J9cLqYll<"CV)-euEvo\'R+kKYd 6T,C<`VEMƼҋ?ucxnj&SJ]R$0Q脽GmaZ-nJo\^ A1ZyJze V"!2j7)bu>#={O )hMQy4GGt[kii":q .e7Zb6kڏ]ke%H\\''^j JKq8keetRCF\r0X/Yc0rsU7=uѧuI6,jJ[NΩ0t~u^{Ƚ I&'}Zbϭ5|6>_ PxKIx~hBVc 5ўZRGR#¨MkÏ;ܸ_?'ܻw=~λ$vb\~#+ U E%$8N]O9'$tqRa gtTlu#5V[p34i W^^.ɍ sL% `pm%bu U.-V%auϧylP5i1L[-x6y/=:$30(P# IDATOEI0SvW^'f9шԈ08#MS) J)[ qi-*Nӣ\֗ Nv=0D9XIx1G6 8;1%W}l1]Cբ.5[C׀Kk5EQ!Q!¨0 Nv~9=}> Od TH  `& T4̧\ai HX." ,m6^6sg{ s:e&,\  8__gm4_m\<ˁjXP-8XNqp]͵k[p>wxFU}!W]'s.K><ʙ&:Ǒ:Lq\^9He8<:`4k(8< e^J%R) FP|3 j`˚(őN$.K('s#0\aQ PTas1kL ͨcd;WmiB7 X`DMV$N'OYViS-$C[Zb61<ek[s50D}hSfmesMG) R.X+pN|>UiI^~].*.]t-@85}檉B D9=H5,G1emk7 "GLTe|mƯć~§B=˲ hgg 0&GV uU'"&c]&Xw8j4_~3e7x>+VVM|Jp6jE>"9=>mdiޣ 8Ny^c~HS,-uiZO<|^!;;SM^UdX].6+-]1W6[eŝGQÇ;ǫ'|Tu:iy 8jGp= eYkOyDV~D"/bA(o8 Q XFMW_ .//0/DF]-=B֗{zc:&ں BO5Ep*MZHwux6)P$sU : (;`2dS7.+G!#(f>b zeYLgL#&!uдt2et||rJ7VX__ +qDe4yQqt|9xsbt{±YTIy O-kdpZ;Z7/"(j3/JVyMԍUf u.6LZ |4ڋ!Y,T!=l5g߻ͧOy*ހy5EH ʢmŬTMl6ѣj0t];xO>bmmׯ0PNJ8)9qɲ^prz;{FC84RKE!vL픢8??'2VWW (6MEa.EQS9RvMe*9իWɒwu$?_n}H6A 0rHuh"˶M+ZH -$ZE.3=1]\S ,z ?8m]+Wa7;3S~ӟo2LKN&Od)'Cf !xG2-.7駟|]/(O흦gBSs)"Z''|ͷؾvx9NNNt ]zeY{1ɐaf&IϨmJ8v\:/"#M誦E8OO`(ܤ.]NKJR 9'l]a4Mk5X%R7U|nQ30):ytea +a'fXXfkG}7^cu5f6Z 8Mz}v>" cL]Pks9IKEEY xxL$i/3S242c{{)%1`\3lQ{ z7F [eĺ$ Sܻef]66XuHӔdK|cE$ B6~$+ ХPA4tT0LfDpw'׻C' 66ŵKXDXC蔶 xAH?m~MAN8n9<AJIggg5F#yLSl-<#4ƍxt:M?R8 vl6X8ßbamm(p]8n*8;s)辈^ 899accyh0{׮]cwwe( XYYammb65-^.nf#{pp!vwyfkv`4DM +XjFH)$08BsIA,=ϚX*% \z.F ^yg?(sZ|^eYO&⸍֚(lDsrrB֝r i!iQ5Q1kwUUf)Z/5UU-vM5cg6y7qN6Y6n3diA*xtJY̱@PW_f&uXOD6뻤Q4: F 椓!qk|vw~|>gcc|;\Me7c6x0 ж8L1m0aM$;;c+kCU4& vhMȑ8:h&RCGhA Dӈoc:њCkasnQPU5:WȪMYYdVf׿Ccdij6m G)h5Z<`_i\à"RU|U600^`[PI5.q_y~}t+hH":'<,EYRePRַ8={<|x>d|>_'I#fc\EiJX [ {{{ܺu3 MS~7^˗/"sʲỎCemmﰱeY])<ϣnj٧y.w}ABh4mmJt"Y #Pjonnz'O/2>z|p2q2Y7%9p]{]-;W2ъoYsIméOh5z6gx>4>iZ9`gM.X. k]6|AݤO?(tr1HR8H7D#ȋ#0D)Ryt>VR"%NY]0a}=bs};;áћ/%c3W6n^o˜dBL(Մ+;bhZwk3Z/viҥK\t?pAhakǸ}]Œ?{^s[~CJmyzf>_we0 '<|O:C4lAx$qF2;É-!p<{eYD5=,ϰ+_C#-hy~G4FZth!֢nv4,E%%9egkk>9::b4aE_|| R^⭭-ݻMRܹ) Cݹ[hy󍷸x"ggglnl1s}Vk:|&eYIHYrZp:n9>>я~DܸquVakFbgg5^č7H͋b<$Ν;$IBGo3j`IӔ`@s*TJcE>ynϙp~~Ο|-ߡnɳیK ZTKTv#z9&aKt2)`4Ak( .c&łfYJiTgggx`0q<1si4EAg5='D8n(lLUNkbDb>1Nt:oy8=>d|>xxb:l|GZsloos2-i u;{5\Ki"K!c4gLȻU5S(ajY(ar֞ (KM,ɒϕ˜f3jRyXċ}xOD~~1g>fXjh6mu6{{v\|)q^GeI#31E89=emm7xgϞGlmmIա( ݻGc?vLn<4FċFú <9kSYEn|}Z#=ַ.׿zK'y^UBKMH\R\^벽٧k e+J|Y& xNopzL/>c}}u9?/ii>_>~]%d2c05ij9k21چ7 IJ o|NeIժÈ9e2XCQhC'_ `6ҡq8lFAׯbL_~:m)㳑=wy#<>y1?յ7M9"A5}?`׺Wc8"Dk|ѐ9v͠AeeLFH )* pdlO8&!p|9'1!IadGקSV 2kv\^W9>?ys`׀@ IDAT0\TUS<>3_+X4¨fUe݊FjUyB?wx &qBV(Jp3-̔w[ld>o]nܸKv_'''lllj̅ pFQ\5a<=Fc'h9::,հrxxH֭[f`e)Q7鵵5:ir%4ql7Zp+n9;;c:,>7ի|dYFv]=ϣn]t8}Osӗg.(JX,fUnmv1H{-uK>ѓ} 6/8!,5*s NAVG CxE Ha6:a{sg\r#x lC.Hll4=X.gds9e6# pIt0.nhD|真aqtO~xZ}*\8<9`sPc\O6H BJ<鱾Io8mf*,L0h#w\Хh(׳ Ac(˂q?;.7okWi5)KV4b pָ͂KېtU#?}4aQHF#NOO󜵵AӢ.:ٔlrA%[2e6#Z%\ <~ϘLf%ba=/ pmJwJaZ0b@s~wʼn+ 8} 5HıcjU 8I Af-H"ť!qb\sYkI.*o쟤loosu>ݻz=._|&V֚zǏ ]1J`>~۷oS=W~Wx)eYEZ]Rlnnr.lmZal6c:2L>qYW) Qd(2G뒢Șf6+sb:ӞmBקP{,9;r 81%/nuZo{2 Rt0e*S4\Φ3ʲ;{<||gӌb\&nqAr>Y 憲,q]4McL-7@h F,ϙL&!q,` M($ Cʬ#$/Z?);<%jps/یvp|rB)jv׮G1_&ՙ/x3λ]aD7`L _s7MvwqmbEQ a'S EvEI&tvD<9܉K+ |' 9?cc-$ORcƣ V~`F!Y?|Ƴs>w`0 9>>nJD I:F Ti/ (F*CܚV{HJ[gVV;hl nַٺ= $ЮfIQ5HuS!TCYv/u6;,Tml6[H#`y^i,E c#!da&t<i^e`/KHLJDKzȒ%RJT:PcוFS$ 4JWFs!DfGݭV<lllT6n)!*Vf0k.X,btIYDQnYBNqʅ{[]fL ǏbAhtϯlۚYWSyJ+0I - ϳ kGTcjna34pD#<^\dIJB'SBϭYb;wjNS;޽˝;w8>ӧOyaHStGksA@g888;;# C~77\xx\;_^3vh0 X__,Kw{{,x{{n7!Fkrd4cC;u-s\ZY,t:†}`<ShU;[|d2n,Ỷ |4Q,ydFJCdr".X_c4s|y!wy7ph.O3q* QTuH:?ox$[۵G f̰/5fpl_bjvc IeIS+eC\.loo`ኹc gggc_W_h9|F.eYTYlR|TZߗ<|9rA$4]FC6 @ڝSI ĴϠwp6~(jra{ע'L&3"GEE|εK\gQstt' ٌ<+'Mb9"tB6H̲IA1YLH ,_m[sRV χBjuкGp=77߸ΣOv$Yr2:Om2aۭl:B:.~{!sk9+ `,-*3$IV4FC vgYV]fh]& m1|d2CNfٔR;]%?DQTUϷyJhREi$g VqQ,OSŀOhYZw&ilmlX,j@k$hDhc}QOV5;~sf##)`0`ww-b.Ws yx_X )Li -A) *&Lkm#G,I{(tƲ[9F EԐۼ;EqMٜ?fcc}_gssqxϝ;w=._x<?l(b6:G'7..\~zܿ۷ostt믿Nө-IwwdYŋ/jO<[{\x˗/&c:'''F}ygw"?{KWlS?Hu^{j ٲ҂ZȲ^)U0dXJ5v&Gk߿ta4~1oy׿.,*eω$Iu\W;|Q+Vg6U'$ JY0=hotQ]^fպE6Bj2"B>JͭCNr]xpH^̅^@+l6ց~Q.QG\q5/dLP^ViR IzViNSJ, qrh2#d:c2_xh<b0%B*<_)JbN^Č%ҀRVR`C6 h::7n@zÇEi0kX~rPH"(`<\.ܤ( :6GGt:vwyLzUGI2e]hk)&l$Œ"i(򄲴>TN$z*J)~իܾ}f3b00Nks֚]z}AuF|g<C5~/f|߬oBvvvڢﳾNeq޽s?scX.4M~m\Rg=f0\pGOwѣG|Gu<&}~~=OEƧ~H~y_HQ4ufsqt}Ы)s @>Nآ4.y,N8j: hZgcMX0['kXeUlRvD$ < >^.Y\/+(Y, 4vR9ǧ^X.^ jP:C) uimХxoYPe^m<ՕSÓg$)Q/'LOc6x睯óbNph0 qDi eeL-+62y_M,x\^T:[94N+6et9199=l|rPT#=6w-Z>~kp6ҡj!XYeFGxRPBJYe5 v[ M:qv{4_ MrS>=xV7o)wr1T>1'o~d69c)2%Wq^p6p>Deqƺ"-Xsq(mǕDQ||WZ TQujlfbafveDƅlNe$ɒFaL6%})p|VSɌhDYL'c\w(]KN,(4!:XܵyH:T ^zȒyh4HrJYj9FHSR?cz6C677yea:)f/CJ-KU,繤N7וƠ)t9H[hFH2:J#`mrd<=ih6mHɪA 2BbEj%|0 )n]ZkZQ]\n79?wX.5eL $a8Ƶ7p\QբZQ\'l1*e0 !FR]\1[IS ##Y7m;XZvor_E.J\s+]# FDXH1JFz)lt]!*GM|~ky狜q{ӧ~V :;[>"ARfSC_*|ݠ4\Z˖eYRGDQT Gn8^WZDa$؂A58"G'rV8JA08m('?BQ ?O1<>x)S\l6͛<}__ghDcنtrd}}裏Ȳht:% Z#hkq=~|1\r(z*RJk׮ny&өj4͛7tvncβ,L&;rt:loosE>J))vc?Ν;E'OHhw˥4uXL .)/^' `{{gX)e Jy HFU=}~[E >n4 |%لK{H)o k. ~%ZR"Iy  |W[.y^eE|:c)zUTuxGQd.]x8(A.0W:Dzu%qyA89e锓 pv\](4QY0%fȠױ T0%34'"쀍k /@"p eFXVƬrݺ}eZ&8r1E#d'Iglnnrtt 6Hb +tzi(hӌ rj̮JeEQh02sC\ǧԐf 2~6JRaו(ce3vvvt:{w>a634پȾ*ee+ssv(yTNr oJCbt{ eFI hM v_!WE=7Yd FlK`ʘ"qnť6G!t49Hln1:=o^g{sN',)_}ω㘻wo0ƍSx*{Ez$ Cvvvبҷo߮oł^k_?5rq\WY|V#kRY1i4g5Vƍugjmy,K>cϹx"v&UYF맟~Z1^ymxя~h4"sy_^TH|iRJ5ii݆RYsj(i#AZK/AMVfI2K4'I34(C^bFtNg"-ɲ h4#;*=ӊY)XqVSUy IDAT,NEfVugA c"Ő:'Mv rN>WU"Qtt֊Udz,Qp(K*A갈nLϠeifMR,H}H'N5;Ld>f`Nvzit~dgAr1 M C#4 (0\cQ՘OI3j~>JgK"iASQ#PƎ(8ymQX 'l U{N hMXAIFh"]VDi.Pf6{t]>k`0j8gggF#9Zhmh6HV >uAZaUhT0 IeVAPVcUq$LA ݕ0VQ熮~ޮ֞Z͈ dU^{ZB8)XlhY9G_?p C0Zf<­EHY.՝e3$e}n.ٕ0O8<Ca}pW7L3._b NÇ_|>ʕ+ƪVekz9GծL?zfYtzF˗<?7o2۳~)iE$u\*!Izl69==e0h4LSZ{{{}noլbBR:lAsu6՛MZٱ `ssYNOOȟ,˘.ҥK6:9==we}}rQtnN__$ nݪZQ|嗜(̎l((J@H8W%Vmv,ɋR*G t^ -̖3gAo0lW/y}=قP^4/) -s%(48~eU+ǠHeuMV4?k܆1S&S>8VCU-b;r6BPҊ\_Sg1Z% rlIR<_RJ$ IUbDR Cp0s7 BJ'&E<8 i4U'}J$aArf*'s:A# Hp,s1@S"H9R{ Kwcp@Ґ')aAh4q/iBU9˳,!sj$Y)ը9ҹ ! €vI:q=Il%^QRvDf=v7gL&vd\ۘBӘ·Igr%P E2ɓg=w.8Ab s3` $3OXEEmrހN/&3jUbYƬ9|fmn׼j "jA`'uwɶCa5F۟ӶmвבR M[b]! n:DGm#n$طiVJ^-SzS0nuU4qTk8߱]4W^3~_^9<<6>)tQP*AZ쌶Ix@q<ߍmI,oɶF9R|xl׵ vDQDm~e5x^4hy5p+ADZA\j D%ZobC^\XϩxmngwXMsR0px'pk<8|B(8::b4[o$ o>N11)RxlnzBZHӴucK)988hW>$2n߾d2i!''' P%;;;&57'OpttAZU~'''v`{agg^~qtށF6KykQJq~~`0`bdΎ=~_!>Ν;1|LS%ZS+DFPhli]BJ˵[ʘA 8yBT QK\'dn??_[o6 IQ, u%i+s3u\¬4ōmۄaY4/FXU&y.*~VJX.9MXr;(Дu+|OQhc˚Z@!jeZkPucI61۵粴,+hmE9S^*ng0g* Z- bے28"/'h vh\Lе@4 )5)JrEUd.M$ Q+yؾM9hYPk(„QҼ5[%B N_4Z8)%E][2ŦJ 7lە{Eg3d0wzL32LFh`MYKYII}P5`* P2p{qmg˭FMFhV[Xx8b e e^G>7{ vGVNCv"kC?(+ TZi4BS`VVѸӗm f6vli,n"IMh4j\##H)(i =6JWe64ۦƖr k_UU&ieQ5 GE O899W\Pu2*(찳{^8Vݘ&đe2#vJ _q~AzK0;ݽ!E]P~x|8f:B;s~ʫo/se=Uܻw~]1iyEQ_rqFT~xxbvMl˂p&itrɻc!21E] )eDC1X,L39?zRQ>&=N=o1(_N&}>&MS, 땁7 vww)˒'iᐥ,%yn4GRVH7+j2o\f]gu?dY>0OwH2/QH,4\?O "?]jtiHZCQ(R:EH+$K*]EAx7j,J^AXm-\Zt"*B I^+UC- %j& lYPw"a?$_ |(uhCH֥B|UxB0]O) "WӅo. 8FX|p8$sF>uy;}Wijc#{uQQdVoXxGԉI7kxR 7D5/pz Xy9A:!~QɆ$]p v;t}4cwɓ3 e˂pco7v^OȨEN<; z"C[69ob\1bX-SN{ܺug|/鴅-iG}N S7oޤM΋/GGGa>q3Afh4wmSXukxn 3^ph:6..Mӧ-X|ltl·{wz&˲WUah\Ef&d2a:Cn߾t:6."5GGG.WWWܼy(899}?<~)~)np[5/2ڬ<ؽƈcUpaH׍ tؖkS sA d9_n4Z5ViHK2OYys)1y>mSd+2"-,И7V Eƒ(#-4Y/3$I2J#H`u\3)Pa j'q\Z! |ilXjjZQV諶qmd\k|xeYz4kZ>Yg(]:-d4a`|ۦѶӴ%Z)<ϸO)_e h U|/$v]nY.[FS...)[[P%TCUm-4.*CIvq\i٧kȫz>lK:wUmV)UpBZ{vѨ̍%p3UQkȩUIĝ5e8{bAmZ̰^k<ӠP]\ڎM^Tuf~wR{>?EYVfPZCwdMYd̮/yd*@W&3GD;HF민ʍ7>DPq#,{S ݽ?bh&ObI EoX IUt{!aiBERbà{7J*]X&"$8t%%U[bh]&]"Q5k]˲Xw JlL;7umZ/ZUTEFB9Ю`67P$#+SD$o2CUü|6פVOڮۋϱ`T@1JeXhm"i-pU⌳''yJspn 9^c)I >#vv*Iǟ_1wEm oofA_~k>_|=O>{NO=!t%f}=A# >WS+?yXVSH2rt|],֭[ci86|ܼy~OFU&27[verqE[4 t[%k',PdPWuJ.HZk+lA7\^G܁FTItXum,-_rwR5ecUuáleEah(rfym"ieITm &-u>toŎߔ&u+ f[heB[)EIyi CJjNV$ 9m܊y+1%Y5 H2j2린3..PdBE)UUЉ#zāߚ< %/b2kt}Fl-rtV iUQh5Jp\5Ya(;>m3]fg\fA)5IQd0qr !]#IxYrD;fE^/32hp,IwWZ\]]+Ӕ M4Yn}liF(F]cs9IBt۶e'(:/JƉՠ6˹ (s(ʌMWʖj4]/Frb6<vbn21DiԔ֚X${nz6 IDATĝ>ZH&__?ѣjxWyWؤvx5s,ky?xןWs o_v J5p5jW_.Ԫ./Ғ i$IeIݻxGmq]?bnڂk+7.ՊC,kqCG>C>vz nbլn'g)X`I(c6VF(Yک+6GZk,*6RWFSv.D 0ii oo2l@Hqې˚Rk`X56Ã#f僯L&ˇ7W^{teo5 AVH?y^=/z*gyE^<>G< "C GνF7ǟowy۷ostt'|h4"n߾ME, NOOwﶓ3h)hd>inpخ:-jٴnA> eznf41,4\fݽMPJAv]+b:J6;< Cn'J'n5\]]{{[gowzWdhmL*h%}Y|v-IYY'v긝eDAUh*5~ mR&<ocY\Y l5ZW"MrʲƲ|ΤH)}QxnDIM\1HS|ge3$P%V8MYW棪 Q`&"Ez>2q-*\<7¶-ʜWafbKNC7뚝V7& $/r* T =FQw낢\̧Hb0/p&MQt:=t\,jrM9ܼqjj2"ɍ61IXV3,) U%(\~$5g;4Mx`SfYpf%-ۂ_)ziR\ۡ. ƍ17o3YUUX6e(LU 5J J /7L&W\]/PZ{IZhǸ&PiEK|DaZB02۶E&Yjqbww)Ҳk=Ssyq;o2) hUjx允ur}}zFaqxtѱL֜7_'I߿je!INOO-yo~bs˫/O$`0:`Pk){՝\\},>wyhÇ[t:>?!WWW-zc4q>gggܼy-$"˵,6Mآ&n'ͦ< 4m{{{dA<W_}xC<&٠3<ߢ~9u/"yi m,G|̯~+N<888?9Z^]O?>?`ww۱q@4Sm61ق^z;q'oDc}RJʢt=I)Q5fR++4%( V U2ũ*7%\AYߡi1BҲHk|CQ%8@JuJ^{"KA]XHGQ5Gs>cliae)ls^ҮUEYkR%LWKU+_NbIIiӮ3Uk<2+aHem;[ci68Ym^H35D"i oLeDCnszMtfe$uau Z!\lA)aaoocF:6~\!S˫ =zj&L ]F^3vCХ ITA^PU'&%1k`@ӆEii+;ac6Ƴ3fSl&!O-k@WWWZOHKek{WQCR|X^k8smĽ&T$&IXUpzzd2!2NNϙ׆<08 mٜ$Iܸp8'Op}=z>mYaYTa|~0+c1/޼EYg?㗿%{d½{g?ömNO͚wtG Ms &yUI0SDh*Uլol }gYbsm_Pơ]91h>Ui-FщDQ 2t (l p"i4e;K ?"ǖi.H}xqV323+P]5p^⪊(6)/RUAUP5‚4\Q(eZhհlo%L԰FVߘ#ZHnVZ!hy^ l'Ui4sӼFW, l6恚eрa9PߚȚ[=R|/Dklf΁=9 x{8G% Ao-v,E*uUYEԃ1="%ifqq9ݘs7qMjYRCkY,g`STyr6&vĸ "MLSlh8RS`gxh]`~NF'? .UQm4ET jZ+|e8$}<& w \sW(A4-͆/bF'DQHUjB7IcK-Z8ENj>tHorI2zdm@ɩ% B<Ϧ3TqUUAl)ll{oɭ[ۣ0ΰld8Ml>7t tpz^F ||>o0 [}Fn 8W, .RQ?޽.~!&u__0_?p-vwwxL)i|-Ν;زl5䥗^"Ikn޼iB;NQFdX;IyWMmS5gggm=mY@XV6n(vb&IM\.L&uk"d<⥗^b9WW$C>#z>!8;;"Cwxx_;[omYh<舝&;!yV"qo6|>tje&DAu^ϐR rv:f Ya;a]W"ה"GPWq3 0NgtSf+$F,!`8єLptA6{MJ0dh$ZXE]HQQ;3EnfTUAWt:fVd5YqI[h9uMnEP%JjnWq[@EpoI)lm6êk\m:=(۴]|lW[3ިدݵ%  `ϱY.X%vȲgZuWTZwznOU)$wquG+ܽ{dYIYbs,\!H4#e%<#P&Fa>3^?V9'S^?&m֛Ҋ]s{BFBDŽAl%lz8>;;cWs|,gXfk!2#FQ ksFrZ2u})Y.D4G77+`@?a{.{aǏl6c42do'{>B)j8z=]V6*Tt:(=99iRJ>Պ$It:M'ۧ鐦ivYшrߒ$ic pjiqNEkݿwY3?s(lRJ>+07>Qt9Yf GƑJ<9}[ ;{V3&I4 #ץj@Z^̤QmIɒI]4iZ!L.ZBh4lDsx^8~ݐ%E{:~[h-y/ܾݮ (( 3ʲ+ܚ^7ۭ&!ir;w~a%?~ӟk޸Uk]ټ ? );wxCɾ^i/ǿyJcy1A0 zvՊ0QzE.ܲ: Dq48a4n}> -Rj Hq-ѫLy^&raEf:VuA]?ſY(9A.MAY#Ȳt "SrM-,aEzH JeIլ,p46y^P9 J% XmSXzt" K4ouP['V N辶`G<񻬪zj$q68B.ndl8Z4琅x~@ ? yt&NAoA@W Qdɦ,uݠox$g6&UM[oGUۉ1Gmbi056BƤ(k~ڴm%F&<=~wy-0Ąvqg+ên cl>hFĥj#g9WW~2ѣG,kcnEQPT5EQ6TA#[0 aHuJs=6W+&-Sc Ð{_~ wFf3Ȭ:?7xxz͠oS[%r-x}|M ƍ77| ٌ%vvvx 7nh]V#~葁x(ǑTՖ#hl #B^'BǍX7؎ SЧV&UF,<7&4 *KjZHW{YQ*C->9uQF b+-Z&YH: 5<3+%eVT#HR5 SZ㹁6:R:-0/i^,γx+.yC]>5m RJ˛<+[VĤL]%N[dn s5Yםe%O&ʲu o|Dem :A_-)QR^-HMMyNn~ױY,3,vs]:8BSic B nΛo"Tіh^ ܺylٌKЧ׍! Ctti<|Ŷʑj<1te~5l˲fg)tc3X3M؆rYUu)V]WJx}F㝑YC ۔#Y;g:E:;;;\^^X5gggd8rttj/i2Lz|ppPٌ=jNu!9uXYYa2>m]4ʵ8CDQTuqLۭܺ:QsnDyhm1@HY4Ki6< m.fmsU1琦Xu0 kb0ER=3,,04)r<30(.b}+.f,K]!pQB<$$p,Ri^!EQQJH !"M rˤ yHF%(rJAT1])(&3$ J(1/I;RdiQ:ȭ5J5E:KE.Pe*GQ. ]7/S5:Ys(v%YS$ZԀı0]MB'Gr~6n_7v[EdN,Y W".~VA$h!Rh8m֓ %I0 XQ -mi:fm.05o>&"泀xE91OlZMa864vz:4laFYקݰ$A2"˙Oude[]]e1EKhI,糒G E#mc!u677kcv^@_\7_°Rj,c/5Rrܽ{3θ-/ҹBz2 u>.]buuR#1iJ { loo:L+HuNRHeWĮ5>fW*Q'hfYxX[[c8?$IؠY)/l^_z%ΫSNOO?sFs8dm}*{*d 8==|;WWG[,gU3|eI)0]8X%m&<E*(0dT$N$ɒFès,92K~Yv:|L,AusPYs( YPN_ёUUðQ*]8e1_jlV%q:!9::(RTDFvK\4se=8D#,Sâleh7|zԼZ[iJDŽ Iq\xZ±|2Y 3EVHySs0|lc8賱vuA-ݧ3& fI:pEKNmmM aX!YiBxGGYL\kb$'8͆EڄKm2 K獛Wp̧ܢ$3T<` ΞtG6/;lO\k(:]ӀtiX u* \떫zvښ051m8ǔ t(/.. B^Ek)K&$qFf> pdL+m1_FzB qAHd1ӉB56) YL_AKB`F3s'vu>?f{Q8>g=oaC&*Nkvt#LP |eX4,_pڠ:5n3uJvMWb͗;w4Q)ťM2fȽy&g0p-z-<8gooׯ99٧?P y?O?Sη(<筷~ӟZ7nЩ4 P^ӴNKnwww}kw<=:6 $I)U}*OtZ4Qi)e4fEaqX9|d$]8$MR#byZ?$t17lTg+RLRH!(`mu$Pi)ҐH|2b2 5p-\2e ki" @t2HREJP6RHptsP#&IBX:VаQ,X.q@'TwṎ&R}]"taBQde*՝Z+;:0 Ko.Rq8/o.EQ`} tr`6 891$M28E8Jq}XH-?0 .n0,fe2`cma; S(r Uf&Y:mKFIT'J6L=zGU( MY>G *l% 'eΙN/Hi)M̉:9 SDl ȒtDjIS嫮*4' qeƀ !6)J#EB(P%A@>8ؘ彥'ijbY4MithjXBOdV`X$IQ8:pIF %Ik!}|uD~qHf3NMYQH! L&U0]anHIVITJP=^B,`{{k׮b{{ꄝ;WYY2v(Sb ۷odpʫpw>{O{-8WvyxM7۷9>:px͛ vvvj䄻w `@e8d2ᣏ>n'ꇦ**+Be?OOn1~͜jZmڪ:VcxZGVG ~<88`~ׯ3 o nѣG|mVWWz*}6~)q1s5677髯f r]ERJfG?w/u'xQjSlfuuUld0 x}.,}\FV8Bx<ւql[JkԔhJs0tBFضaX%bCw Bk\G̙ADE46i4}i4i`{:Ml+f2X,"0rtǧuyȑĵXB E" Ӹt^vl[ԝ4L&CN42%ȕwc`C8[уcYNmdY -S4FDV'9MS\mԛrx,TL[XVBDBʔs% cO<kݕZvh6.k\kLV>(P(p- 4ewQDdNe, b&uϢ, 2- \%T*&,;_Z eZX  zNvI n^-Z 2x"+HSA)KLQh'dst&2%?֭[zcL')''' =$IuI,tN3R((ӉR;:/#,JJ}V`Z.<,WI0ڨe VPG %MF8e+$RK[dErz6BJQô9H md̦p/"MSgqf 4yW~Xt-<yKs|r*tzAۿk VSf NOyw {a1,Rc?~|%ʕ+LS]!5jyT /=fLß_˯\'<`9>0M0ǬWxrpkWxLi 'Y}l\fq:X˻ٹtFaf>?&'|G\rvx:~Sr؂Ps8 b5Yg]r]\$| Y|iYOB1)0vww/Yck\LI0q ^G#|ǹL& i* i'$HEF ӡPq.Rx-Oy$'^%I&!S't-]ӒdlQ@2v=xHF0`x~#<eԣ΍o6uaX,p]lba%6+iYLʬP\n\eDX ڥE6x]esR z!ΐ-}ͥ4rtvP" 1rYG]ڪa>&͖Oߧi7iP 2 2Te^z $hHDt+hk)Pz9_H(' E0eX,)q E|69K_/O~\G|ߠjsqvN$,:dm}F۞NZ4c:-0D Nt42,"\_\ tJ}>(cu6+++O ֒Q%iM(3B Agd>y)<`HRDQ _u(oy @rttYLF,M- rm4QXgSYE42F|ׯ8OsmLgtXcxey.C<$9x >3,;+^_*8Hݻ|=s+++?g|]Won\/M&ݭ!jkK7L&+ƍNSR4߲,X.l_ޢ?e̻[>,:I楗^ɓ'ư\.y7jCM!~=ip ~m677999lVC?cz-n߾|' pxxhr׾{{؎o{񂕕mzi؎I\.An1vӜ~۶ zmns:^P,NS=r4,0 4jaaZeAhwI4y't[) ,4ܲcRԸgYս9H.il<2a:qJ'ݾeawӌv W*6BQ O3Z&3iIHp ۴p,m,M4aޚ4z aj@3UqL ,iH1 ܅cש>;m4-/0-7Y 2%zt /os6he:3H9==$Y Q% օ=bU l6vt:s۟~)GLgs+(ӄrp7*7z˲¡zFvO=`VV, B .64:-HZE5Ed#-D;d>=$KQSB0 R*uRg"KQ(]^ D |h LA)LJ,͆e{an8ZkƒJf ŪX՟Uauٳg5UH)kYp8[D%a uU7qF޽>eV&^DaeYr||i޼^ja3_o}[hO;q2qppdkkFh4f3|ggg7nH)Zf IDATY__שa&ŋEOVb(O>D\@iت86{%޽uRAx̝;wjÇɲK.n:NG gggXEL}|h4ܝmۿܹs㇊_+i6pȣGxɋ&18>>>ॗo{}mզNUfz,KEzqxW7x|l>!WV 88g0hmWA2E K~n9>n0 _Ný{x&eݤnEox dyбwQSeˈ8Y(<6OiZO]qkkH!ڞ0Qf\;dr|_p\Ϣi3M(bDZ/t1daH9QfqM4 TYFض޴ KQgIi`Jc1LIb(SAV$@Zhz+]f k -LŤ"W aH@H/0"7i4n%.|+SJQ(EkPRx=סix>V`ׯ_g}}8>wgR&qH)R"y*@)KLq-J( (qloF#=BY=2%n$I\t} `^׆)с[o &c6:q߯)kӋECc=pyK.ƴZ- dGɋ2*MndBfggn;$c?OE,r-1;uNG'8g0ԣ|"cL -]HQ(9N rde4RxeN@duuHeI:XCP(c:nҲ{EK4kM"5tdLlG8|Ge EZ:J {\dRfBcYzM K !, ߶5%KQiyYJ!su*M2{z`a* 8n}r)G( IJ8i8QNHAh3OBK,!F]XO;L3)ab[9q+UX[7 ~VK٤Z2-2-*FcZ毥<ÿKs_Ei jiؚX( RJ?J2BmQ:WABC ku+4Z;ܵsC<,;J +)ie/ԅhB-[-FgXok,RE)GG'L&3]~50PP{=^&{Zl2<<#S_W8XZUEaUD>;})Q}~Ք$2fAPN>'!y?dYFIYUc$׏>TS$YĵmhzuȲ;w?%7ʉZ\Aśobwws|8[ʬe?߸"#f3݋ZtZ= S1Vs$嫺Uo. rlt:=z*ٌ`9z=R|g=j>yo.l6c<קz\.t:]SeAF#VjbA :V=4:!J UQ999>Kۦnŗw9\Ga-HC(Ů_<8 x0f{{dJ|R"hIsMP,әJ)zV??W\4-7)1h|˥52x}!apX$vOu;Bj'Pbv8u| %VdYiRqR0j?%U&˒uGL)Q.R.(ȆaEiaXخi[$q"}:QAB%$I&i>M&c86롔[B<#I#$"M(5paٺK xE e!NP!1-\%"YPWQ[IKNE+J/ؤ|uU\پ 0Mx!{=nX[[xիWٹFօb5XYXY: z-oQkRW7ZP9ӫC5e6)mBrT.1Hc^FafE)#/GyAqv 2ifQK k\%J")XԝgGi@ᐝ+$I__ꤖ*4 0Ӝga+m>C>RCӣ(k4_R""z-*Wr'<jfMx0U%UjS^֔ s;&ky\8TbHv9atXYYMVB>>ϢXA`0ʕmvww딴*Z;~Ii1/WnCP2@|ΥMt.h|ꀋ .]Lĵm&':%f2q5>#!o1B/Ѩ0VZ tgmmn[$=zDHQ;;;;l4EpLs׶^cu}MGw"\0{4!Jpzzl>tafYe ^#Or8Ԯfe2b4q8rZ?mݻoo !BhSqh] F*tNDa dYB#Y#B,I\R}'IV/.S̲t:!M81McM(G/=2ZEv9h #Jݬ`H Ti"i8Db HMI´m #Ű ±Z+W T6r2@B R m|lER^2H$ \8::d6F0,yq-FadYFfss %(4A" O,( Oi"Mܐ䦡#Ygu\풉)T!)K QR!%vV#E!MD+xs) Se8MAAV(UhcB3lȕcPn]("R],~B4@(1)ryvg!_O=O>a_cl>p$INo˿;ҧǨB: ~O ۢR]ضdlw^kٮrUm[V@U_|htNǬtiKiYRq\va6av}#궞v8YR:&{zB.uƆwMy~)ckkVVt4=U"uQR4pvEs$8ͦŔ4tGGGɗ9;И K }sE0`۶Y[[ڵkDRG$Ǐ}^̧)[[<|{1Ժǵ5|lŒE$aWWWHǏsXߪ,0  99ё”=mf ~x*ݹ|9PY"۬ ^[-%m"64>,kYF89::)V ֱ\qtz($Vf`Ӕg)YfY,OI6a;f 7`vsI"էkp"l J 7](8Mȕ6H)W<4 J%3_Nq]vcHǢ0IeX$kca@&e@fdRK :0E9vKQeB* RP~dbiT/q<ҨoZ"␔`l)A)Ű j)e2_pxDǢ"SVk\WܼՅ 3Ƿ}NCQR(fɷM4sMӲ _""rvvJ6GB|' _غj|rl6c20 裏vW\ ٜհ+WGZ>;wpU4(+H]b(5zҥZSrÇZ->CnY"lF$lmmo??|?1.WÕ/£||cdB'*kf3e5q$rl9e)v6Ak~SLӢPYx= :qsqqʀ p9;_0\dYʠziN($a$}L#P9Ώiwe_,zz -F-umc) AϹx5&j0{i,9 قd't6ao\HHئd4:|2 `e0O-˗.xH$"Y}G2hIٮCLK։+QgzQ;$ڄstttTڎ,he̘^zxD}>^~yKXv|߯#.? ٘2tԊQPMFFtڜjuJ]L)..θz*LebUyՉn㺔8,e198#M2}v[‰uʳq'4 <0aӓsf)q#Z6\Li(2.F'zڎIԘBL, CB*;XW߶\^ >+# ST^{ Ee9,HJ̦ip]d+I 0œ/JiN^䘎g,!JkHk6 1V- (0JADFdI+,1EȰa SZ̧s8f$T]hfd'FzT-ǫŔM.tY)iT~k Ro}yWuwnuZ-f[%L K ǃ&$$%0`IXHX2 ba /&۲ZRjUn9uZݯ^ս:|]G"fJeTuqIZ%Dl޲X*B!w<33r (M5qIQ[p\l)(U=A ` **JXJM;56P Z-;`d||\2,Դ%NQ.03UAB!ф qH&5Wc}8`)ZL Q2FFR%lՐՍX. Ƿ pҴ~>jЪ099ZȤ.e֯_L6\&BcF& ]-MArsk}֕,Y{#>ytN"Uta)KW.fbNЮ܄؂Յ+kqi:u;IX\JJ"il@VSM&\Ie},?v͙W_~&$ IDAT!Z5NtR5\jd29tuP.Wpb׮]8|r"r2z)8pglhId&t6 *g;1좖ysg j;mTU8F낮\>3KCul8 ׏ JY t |mϹLSò,z{{gwvu &''qA<P@\pB: r}"ejj*p8΁Lы L±T%/$9U@tbbHݱJ)kqۋ3<mӛ@[nq}a߾} 199D"v$<`]Nh6NGO>c711uFAoR.hOߴi6mDz=P,s!N [z! #9dCCC8={U-P?bO.\W!Rɬq`;׼pe# JVk`5J٬E:~r(8A-cc8 2V`kPɽ 2h؃I8605UBd#]$)t=ؐʕ)Ge˧Pu(tJPV cK MѨ t!KA27i(2Z i _H! ͆ǀZ&E\;!Ϡq 2tpZ 4*e2i؅35t 8}ZTj-ԧQPv˵MjF.CJnHY)i$si(grXۏVe rj pSP%V ͠GFGau^l>a+%T5tӨk#o\-hW;>ǭ͑ށBnrYg9=1(Ids@6oވ C˲0>:[5:7udX;)QZ(\,Q)g J[O.\s%z#l7p|qiXG!ꂙf }ʫ' N9^k9T ->u<`nxNI}\kYl3ߵBUj4&`p[q[Vk"*chԴNj5N6v8:FGG5[ӇpԪ^/vsvl8<6lڈ7uH&5Eu ҳg]OGԾswOOtl+J:-Akf؀eH$ՙP~5HgvR6qf;ypd2ryys;3WU/=SXx)8Ζ(Nh,d~V >1ݧ6݇DBkaV-ׯ)tf1@y2駟ƞ={SYtuuNE. = %aڵ?D[ltpOOX<]P S%y `12> 93]83ʕ.i0S/l6U:i1J)(Gt[c ~ceZN8}mr='ҝ%E=,:<͘r-1@5[ hR-Tu$-d†I9HC܀ڰ,]cw-oQ( ;'YH+`tl{=oضFg ɄזDZAQCoo7w^V&ꀣ0ϥz|ʂju'p2c]?q|||Nk|>b |6`}٣cΨF5KOyǯ1+}W FtѭY"6:" hy-'488>ld@9hZOUΦ::c||\t#=GQ4f\3˲~eW}^ x1::jZP(UAs^#9>TJUOg "VAn/ `zZWEM`Joߎ{/|nc۶ms*vWWCWWR`߁^.f6b7U.l[kA^$eSS8z(M-Y;݋j\S-zQV155a 4ޱv)v.Mθ龪 PZqt:"tE_" k2e< -4utuiwUo ̺֔G6 mhuH:֬d6kmմnA%^ 00X@ӞD4& ӃUȦ3&-t II+>$,=WtYF.A6I=Y+@T tZkzW#9-zЕՂߖ nY{6A&+Zpl`s.MpQd3]F>ӆ_đ(ljx9B}lVЁ[kbɄ/B:D>E&sw`UJv!+br^|"(8[͹մ.ก7tk>oс^2Yj6Ѻd0^о$R w屐 IeJ9P; /j^;`;N9\`)Nl"lNYH$PEoQkS -ʱѴ[vaFua;6얂`;znF {rE)S]Yɧ5=8(LLNN᣸qm?γcCXD_ow5{[3_0g}f>>މ}nΧnh/K-kjyW92?V@͝JR4ǗJ$-S4[eT*WBZ B^tk{ě#I̅l&JSfT:ZU;.lՃk$VBN;7=:!ړ;׫8t3C=O{Q[F]]-pow:*О(R rYWݎat0::۷cǎ( nXĦMq]w/E˲pAZxxÆ TgcZ+c4+WL"jr.Tu<,T[-xtMe11Li&XS@>Eֹ~A?Vdk-Awj]6J+H$r7[ 3:OXD2r\d2)6 ;i@)V  _ ByT2z˧5̡^ bÆ~>|VrD tcULNM!J]D+K=]EXE-!?Y9l۶d]^nzk=J!ҹɤe#G:u_WW75ݟP(i(d:/J)kV#+'?9Nc`bމgoF6F SIc~׶iZh4tww~dsi&'`bb `c\U(P1=5Z: yǀQ]j{LN.Cu xq`zJ0N {]3+0}u!C9Vuǜ>Wl_.f'm^>)\8ӰJ^n# loH%3pxz@$`%\ N ȴtI+| 9 &&&PuH. nV#ԂG(j,+ qNe/}岋Ճrp*l۶ اz f6 |Lלn@IAHgZ-@w(gj cv$$@ ѬyV`Wz* |ɪ$\Kt: @KwQ, t NS+V*u}n:8e 1눽!Oyvf3 ω8~.YyrfTk"l1Զ, fc8xff]]]8gkuYط@pL7[ |G\.ѣAnO ~2 =ZUel6p][n(vލ/7-LLLo|#36D"G}ׯEū2Ŗ-[0]<ЃT*(AL&R*lדͰ]PgYb׫jݯJTf=٬o>۷鴮xf nT[OxyV^Je2Nۄ\6t:d"b~-\SH{rTKJ4ۋf- Vٰ$as=^RG*UI yG,@ww\ =Y -zBO==E8,A nA`Ӗ!X JUzVGiz"V J®אI[ȥ3qP.[D\THQHrH&Hwtie6\L&rUh6l{#(Xu\OQhzKyUCj\yF\ǡq(016L)O`43/5w}E)k187oƎ;qFL #_Ȣh tqhV'PlE>;lذ dLSA~ d]pXp$cj8: ,4:ǔ]'0M_׵c\iK[oZƄkg-U]quA8fD|N+&v{.0idI$-v҅i9,P"zR(M#GF=% H%3,MTJWN1v>,[ ?;sV}&X/k2"HY^-/מS\3;Q3Z«5UIYE5'B_W<]R^ G“'ccc84Fpc۶mhZx'122T*b> w1v׾Iz#O.!`EK򽙾 |,Y2@ q@ 8ƅK?WWlM_ف;~aᕗy"wk_kJw^'ݑxK9: =g}~;n ؚ%[~y{?GbKl>7=Smos6S.{voؚ<#~3bKhsͷpO}3w_:>WuR o}pˣ%5Xvоp}W?-po?f;/~a%8[/v! !)C9 ~4c _:/zAtw]l:l襗る/;~W-tgqۿ{>gס±m3Zf}~Kb{nÁ{pÇ߅gG?uoLJ?5l LK7"vq EKKNGyf.V],/LE%zW `!3wa 8T _x",+zk߂Wʯcw!tջ񆷽+JS7~m>^t&uܔRZq p]|8W٧őWk^pp?W?|8E EI\׽b+=2߾0''GH$ũ 9^ϽwKݎm\pNWޟƛ~o&GF /ؾM]?>+$%SQi1XO_w=;w 6d2o9sWd2% ²,lo|A[ēgo Z_ANwovx;fm{qm|W~v`M،'٧Ź^]?ru>"<לcmo /=>p7}ny/z }̷/xkUo֝ʭCZ[x)v!X|Rf;h]{lyu7< ?ݿSc8—O}o:kz1zxxe!J]}@"?e\oj{o'|=oED2_ *]x9E=ϣ|fݸƞYgױb],4>wsO .u_&֬ v# ?k߉#\}8%v݂?{>x J]t+s@ Ǒ)YsvT(].'#qda .b J@ q@ (@ Q @ @ @G@ 8@ 8@ q@ 8 @ q@ (@ Q @ @ @ @ @G@ 8@ q@ 8 @ q@ (@ (@ Q @ @ @G@ 8@ q@ 8 @ 8 @ q@ (@ Q @ @ @G@ 8@ 8@ q@ 8 @ q@ (@ Q @ @ @GK2@ V+Y6c!@_A>βqdJbY@ VVlǞTkb]@ V fѓI.YB!Fo- IDATz; Ubq@ N*$|5zRlvC=@ V DG @ @ @G@ 8@ q@ 8 @ q@ `{P}<jc,}B߅6iw_|KʽXw87/sw y]`qhr]7^uj_^ݐgwkτ'w9 S66auн6O7qŜ=eNJh ٖYhFZWoga>{;ڬc2aQϯ P5($JhSqN\k1)qDq؆mR6CsTpk9Z`r7,s%p;<`Y{C]u^٩$g<ר cM" s@)MC0YEaTv:g 0cT6 00 DaPtiR"mP cbʘ Ly-QlsaE;b`w >g+PƄbǔ(ﮰwaٔ1bpmqǘ1%Ȍe60Ntݎ= DQ?gzqv2A8)N=A& ljlXg0]ӓ'P+gF*jOw5#,1"0 \lJ"MNa&Me)(.%(9 IFq%X?q1+*c:1Q sJe[(L٦>O'؛q)W8LA'8[ )$n/IM;oP^aN1jqȮŵ(YqɹP9_ RSMSMljP/Fqs;u{n-B+O+bk8Pk|cTO1;{ES̥zhʢFexC28ړq$LrLkϹ6d*eM-u@3nz0q2X.qSY?S68.8N:8QM9ʚSr MsiⰛ(~(vqǝQ<6#xYډ =^\JSCaG}~D@hK)NhqL x n >oi^ha*eLz1BZ0cXk@|y|TY+GTb`J{8ٞ8"NFh,r{8ǧCTo1r[o.0j*qs t@ktB?w,aۚN~O\mJ6W3#ƴ)WR'G48T!gDqt~WT=j0{G!G|"B9{ Chj[¹wN! g?Ბ&>g9"qٴۭ;J:{WjǔP1[_UXl;uMl'2lCq㾟Ō(7mىy$w2Pm q`')/B.d*KwFc0#GgqĘcE]r{}a'Ԯ^Q)˥%f,%*g>Y8/JKN+J2Ef0g;\6Kpüv.Hx6'j> se\D"a'TA 1k:碗>JPBߚp(p'JiJ%)Y7olLP7n1 Ɔ `&~ 7drRh2&ܫZF^ba iCirQg1w5j˶s11v>0`!rZ=nY?w݅P\!YQV)z eߞ dS=lƑ#fII0bMQkNrI~f԰ BSj{*`lq mDB{䷄m֑9DBByDY" (zS p]|DbPw8R7`?1l @_j+K8w{/77.j};v ̱31TQmRߥAv D[6 rNN/71˙ȸ!F}Q_= &LEŲ͓zVwQUgl.`k2r.T cysj:X -OEܙvbܖCʼtJrl1| q PIi]|NJt5M4!E3k}Ȍ1NT!vAaƀx 0FG/"nZ0A 0iy8t`oݫ{T#ϸġ}Xw)eY8,B2ؔ)kHa`8EJW0YLv=yeM1ԜFwf=Ĺc1,acl gR LcTi8;%)(3qFq:u*rW$ES@82qaI{1e̥d,hcL$Xwm؂#ת;V%今jY;U'Sia453YYu΀削}Ϧ;oc(Lò, ϛ^Є08s ~>ӈ lաch31`j;eM{5YUG:rM3.ƓZQey39(܉hB-26S^'4cRbtL=}(}:a׋ 0i^H-* *NU\'Q}ۈuBHol6l{ SIMɘ]Df#u , .hٹR9&kqG91iFa)+@ƄzRDc M %Ŝ玳m)׮8Թ58X0oCxj%OdܞzNɍujL2J4݉g3>N6"ĽO2TF+æ'FQO+xoDiO8LRLV\Lj'GQ֙ߛ5G{ƣe'9a,9FTAd\p<8Kbr03Uւra5a8 UXs]s8׺MPk)j)X;u(i"Noysg)9?Մ:+`(rtf9&Am`Tʩo8,YͲY.3- rSy<97 DQҗ=g^;uG 51RDRJez(Q/ƸP0Y֋#]EI?0lT*BYaff)IT.3CM[MRű&lp_nSs$=ZTFُX4WGS8`Yy=`+;\߀ (@٧A{ӱط:q]K)\l:''zRr[+'˞)dCmD Gj@4iGL(Q L1,QDa9-¢)lKXVX3] &AY!_˕#qqW8acIeg4+â<+kS4VR N9eR੧gԢ N +9SpH0{yw0u@)+[]>+i?a/wZGn{"k'rLQ.NۏS> t=Qbc 5^nRƈcwq1*FEˑyٌ ^dRvh_cڂٌ#5 㐗0D&Z=sQ/4\+XJmyD{,G-l hV`RǛ+qb"1Ae3S3 [J?P+Mn:Vv`uLMRn1aʹRBa{|TT3-P2YG<$8P ʀGC(Eo2& HΑ>*̉㴩nn3M4bsK-Y`Wn>"w.A&Jʑ8ĈZ^[wAwgL㐃#q8'&Q?{g~Fy.8r N(l Dԣ+7.lj9rrFq {Y)m"OᰥTIeLM2\p=Pr9{ gsO\a0̩J;熳G;hR8ޠ9{8ǖL ܵA}ree쿔u~|qL(8.Ɓ~Ɋ8# ƸL;&̄9Y*q8٤J΄eYJq_71qqmF=͠:*_.buߦb43:S61.1{NX,TUG-3M@XS0[E4,*@MFtTy(9&ʲRr߸`ө,r;"Ts"jʚ7^[q.1v (\elڂ}rƀhu}p*sPqj_u(!@+24g6M)=$_p%l,"z+w dnb'eZrDE"=s]RuL۱#(ʢP#-*֌AV/R =D;>/3iϑh } 0 0T'wfข.cяPq1}wJQqNcŽV럺&'T[4caAbM^rK Zh0.)wE8u;؂cS}w?uSA=>9G&c DD28u;tQ]4^c{Y=syj*w^#mb%9[ vrwe8yl)QS yTP (LS8-dz(y0^a%)s=W_߷?dw׳.sϻw?v<DDG9:.& Jصk̵fT; ݿi%'0䰿wgyq>5?VsǼg9 Ϻ{괋s l?lIOQ~>9q2%4;l!D؈x&"aN%qEQ[Oesbώ.K(oY}sq>4Nt{ ζ[_]70zwR|_r׬=?pxZp>RT}oT_n8uF&f8Q8 @ -'šP;I.GZXڶ* kC9=H&mFi,=a@}e썔D;sM#N{& Nw'ǭ7;6ZMp٭yo߁s/?ͧ)MixOB^O4 /|$Lu`,N1(yKq0LTaWS.qFc7u/kq S1p,iƹureq4X,? ^ )đBl# ߅lbνQ]˲׾n~k߇'qk߬~aoP=Yw>1m``:ox5j׭?nT- Z R%x39H=Q,a{!M `M9}&{.5WҒVQ[r =)2o }Aaa) +B$W; N9+@PYnwEiI#O܈C_ExϣZ)ޟV_ [yջq `fzR^ +IJi ~v?Gf&?tڦ࿳\i ۜ<ά%IENDB`zoph-v0.9.19/docs/img/ZophImport005.png000066400000000000000000003054421415176210700175460ustar00rootroot00000000000000PNG  IHDRtsRGBbKGDC pHYs  tIME  2; IDATxُ%w9~sj#\u)-Le ? d؀fI@7= `43fIEVʪܷㇸ氊l`AQHd潑q#w9%?Ud;^y{F(D `P\,bc*1X*1TƠK'- zٜ}"aOQx8\rpHUU Co}[F#~ppp#(bcc(X__g8aeqqul6OYYYa>!kkk0(˒dd2AD|goo7o;w?qxs9::u]}k-ANgiֵ8l6NKYz=| C4gqqHq(1,jI"I4] @Uɓ$In$3",*' w(û,-6{8l!>dkk Ueeu^;,K?_.N I^ЈZtǀ0>Tx&ٔzLVhb"'\B - шU`8daik-Q s >y U)FQ)WƔCN,s||1|^ys_x2ncPUŝ@LE<}G? WL&3q'l]]0ƒ1KZl)+KQT ?٥>EqU&19q2ED c6771!1硪O6$ a賱A2>UUa<$s<ǻ_.@U)˒pq4M<, FAYF#6EFtFᕗ_oٌdDgS񌼬JYZ4e· 'V9*JU!KKfqJU)Uk믪P(\>y8k A`i`0CFk(w(˒,zqv$SRZBz P\ʢ",`xymWUZ/Z|aïwT?;'|npQE5s×K[QAb)Q1(:fn4A.WWb12qK퐪l,n988u]VWW,xqhķm|lllB$1 O0ư6 ~x7 3%4:]TyDn9aX**[#O,M1u QDca~8P4BBkoܺF%<%ˊ}I#$#I2F#6UUш*'''q||Lʕ+qLEIBY;wILUUx^xhURaJAq\6I2n3$qFs, y6E p\_de^4Muz4 ^z%Ɠkkk;L&NOO]<>ib #ƧZFqmRLF"4qlJ;mЊ"NI'-hE iyc TAK*6hE w"ДxtN3t'~t:o&ׯl$ k_|d2yFHr}~;ll0}7Wy啗;5%';nD)񨪊lFLg1y^nu Ð$IȲ4-(q}c !"TUU'^,uП$If~f}}Ue2微_?T( }0j8l6iZ> 8UV%?dt>@YCjBZ{{Oۭ^Yg"lU& °zEQaC'X[U#AL a~ >bԀLzG%ZQP{{{,z?=&IDc<$+'1h7#+"EUb+Pk}r* X3hW"fn"rjJ95ld @B"j4Hsv:1qp ^\5\2:8V)Džã=Z̀^>͆O,[:-f)~uBE>YZYnwOǟ|BQfeb4ANY.FU"VXZXd}f&)x@bNv6n^og9g!i2\_H#\DztO OiE>Wr4s A.tƿ͏cz^;xo x;cnݺ Yxmq)k|1h:kjԗ

r|<';,L?KuJsE{RVO,e<8PC%dNKZ G|vKBGOyp41^ p\"zqN88#(=_I>^D{USlFS*> }oc9g|;Oq_}{?g_"ꨈJZZg *`+(bT1jdy1ԍPNGONNd:ģG%gggܼy999l3]^^f4^rE|MY[[d""Lөnoozz+?яHf)V c nΝ;DQǏeoo7x}ߗHDNjh6qkkkI;õkJ8Cףjzm~DD /TU"O<ѭ- U]YYPܻwO:E?/牵Va PVV*6`\\<9}H6fժJ dicKV7o.KQK&I~6V%J\ժk ]][5Ę\])M!ih<9{/66w~ҚdVc L|g0Iz*2uqqQ+i-DPq#_ovof8899Q'59;nb{^K7cg5{{aEU @a+ ǴHI$p1Dɹi$3 q+jF~UXF1FS>}x_o߾mFW^ճCvni6,6jy0ʝ;daa_~p[W]hc\R\5MJ)JyQat)ҹqv40ZUJdYeuR^g*Y) #:gIqD=qD0T,)~-|i @1ݚL&á,GuUUb@#"%S TлHȩBDĸ.UU [I笮,I#h4i-K,#eYJqbR1xe_Ϙ OkຮTUHTUZ];2<c3SKBfhպrrDەfAF.K٢jh8,tdkkKn9G9>=Vkuq uT˹vP1uM%3zY"*Fȗ/"yq%_ޞEx}{zZcA%$Ӱ<ゟvզu H kh 5*Qu18("G+u( nFiR˽@mL&籱[z*n'ztt$.4)84 u]~q?7[[[ċ/JEZ=88,C֥%yK7xx,^.%Emm4,,,H4MzIq4y/5穵hݖSFrzzCyq{iet8::ns5kdqqQS8V,Kt20m|TТt,I5qDX[[SkdYY1(mKlxWNΧvQ Vxj`eE#"n(g)[$AMOs0>;bme'\~'0_p.>)i=E7<պ ԬapTPPKF`#\"QT] 81XYpYh 6L =JemmMo޼)⢈U_,K999ڵksavE~( |ߧnѣn^nO?4o+++Ǐ2Z}yAH0 e.ֹX̅}>#mCYEVEUU"ׯ_gsunK:h4b8yv:|ߗ^++o:sztH^fX*O`t.KxQ?sѲ`:KEye.֖Ex<_dyuiS1t:m"^R,پ4+7[ql IDAT^jե6yFf*fZTeb-hLY|rCxp>8EmEUU:eLS#Yr A0ƸYIq/ʲuݺ |kcUUs!W ci4dyp8T #2I 5 CM.'iL&ic;\[ܿ+{1n7\Wh_N(J2v?dޟOxxc~ ݮ"YLhGoNuWb0)-J_|[[ ɽ@E|}APTF vՅcv:jER8ƣ(JUQҼ8M䢹#ʋ\-m%Zli֌O6mR6/cZ0$ Cͦ/,*H5}Tas FkЗ,z54-EH (q#:G"j\_·ZjOG|_|?Qp_ЕXX%31iuTUJISc#`-kP,U%O7F]*"uɹj9''',UUvA0g!q2nNNcҙeQ0FC7q#uEV:_d?ytJXq[p@#zAu4R1_%5_y+m<𙯈1.w˛W~FP^6, -$ȼ\_l X %H# |)gD$t CDQDf:rm>|>w9::ڵkrn߾! 7 Ð  Frrrƻᄃ;ƛA@q/+9==% C~ӟ\r`py| M F=diiVȲ7|;w0NysYkf1/WOEt#fX]i=Qߧ( |nZ"~),//3 FU$ qa4q],UHӴ%g8fw1~oX|*v$qtv0V>vDx G(%kTrxxZw>IȻ,S\!/+zܖ꿵֒UFA5UAYXʼML>gg'(k=˲y"4ZKW Ɍp_V|\bTUEubBE@~>Z=uq0\nLǥfdy-ze|'"o8~W~m}FQ׮]G?b08𐃃ʲ(䄲,1jǗcd泷d{{[?C^Ƌ}{lnnz=߿G}/NA$g?#2r޽{W\Uk-<~cwߕ+WkװJt:8::"l6.;z-68==%s>^{=w+Wo\??`k}(?s$aii{( f i4K =)L,aשÐ*9Bu 'EnmjvHӜ?!C}[kxT)!G Bϥiݝ;h<\vΤӘfTBߥHEGrvғɄ(Hf)VK/1r򲜝ieaxŒc4 $&I=p:b{Y+[pvv&Yv +rttQJײJOb8ţG Y~H_ '''Kb H+j*x<im]iQq?zן = ߤ\b4A)5ƕ%#B2l&+++En=:kd2QBsxUU$qU)TjAUǫWi_ƭ,ТV,+PUF=z,-r,}&hY4Mf׶69#,98Ri6 88cE!U1FDJa>2F89b.dǩ%qs0ؤe,/MN9 n X|OkRB!j#4Egh|c]^y~K4G;zr:+T*΅Fj<МV#tO>+hR_Q`_) ~/5L[W)<}XEM=JqpD!Mr|7P HheJr Q+E>fٖ@{Jge$)fiͥ%I*숲 i6TU>0N謽qUUUUq||eYJ7nGIb=ԴP^O> BԬKKKqvvf3@kn<^m}7)UUiٔp~fkkǏ9==XX]D]&o_İ$3RʼP0bmXPŒeyy1ƨ ttz-bBLEgZ k ׅ*V(ZD_Ya\ ێ9&:I \_eJuN2qYYx:sh<8"$sT/<_|UJU!sfOٻL&-R q4gVU]*ODTU6,M:M$bZ]Ut5vU#D^Ǖ+5rSQL"bm qDEUganeqO3ji|ΙS?"ϲcPksJе\_e?WON`<ѣs9lPZxa1ueGSB3YާL<<͗\|)/b..iaQIqET..SZ4#Ѳ( |נj)Ԉ-(QZK/,^P +Isrvc==Ͽw_]矗&7n_JKd21wʷm75"Y^^բ(ںjZnnn繩;A BkGc}}ʓSE!="* I)э7'''/gjvE1u0 Hr-Pl5H;Ph.%YN;Iti69矾C<9\?W_B[[48Ţ$+5gqB68_xZ3 K* %P%Ֆ! Ʉ֔e cBJ $I{20s0(w:.0ڢ!x&,K$yZ:x2iQU aV5`X撒8nGP$LcXSbBJ6ƓZLqx$dDZ!֐"j8Ꚓ/rSyS y Sb1.U9wb8||- bg~UMO+e & nB Cf," $B3F %D4kp8; B[Vx )|Ǹ?zrB^w}ٌoܸA<~6661'?i0y:"MSZ,9D%=ݻ\z1ڵkBٟK/^7n`: <ϡ7ppp^Ɩ̡|NZ\\\p4,K4M ~\|9~1c0b̌-֭[0\Ucbt2@8;;Ǐ޽{!c\t u]hE$C(%h]Іf+%LA<H iy SNL!P58ؿ k_ko4zjm0 e#|[h3 tAq" "^,ccL@Q%$2F9#P-[JKp*,Yi8Cպ)^@6]mkj8![t:)h0=FSPeK8v!6\BH 1%X۠,z11A(#2ưV'ONqsf;b^(q fJi\)%K) }&Ab! `lYJc%*;.tHc(!9OSdIJK6]vFߴQp[dU'Ugm!%d|s.\}ۈc$ I!#IȗLY0cȠqtδy?X6nqeY⣏>믿#MS+KDЕW"bc#\/_pd¿mmm!MSbsQx|Mw/0˲\YrA{"Vz B'͜<_`pqqh}+>DDFヒs\|zC(U|Aۡ+#7M?x<xWۿ_㰏qY!cDAyd G}ôiY/eX6??AcUE9CY/PUݠ+K 0E`7htp(g 4M$w2qKкn(KާF%5e*!Ţ]yE49Φ$BD`55 N^CCY8;!Z4Mn(Jz8KpևFdE ˒a%t&:y "o E&FYnp8ָOPܙEO0Q <>(IaQ`HSID5Xp`ف;Z'1/!I5DpA8߲,mFA`~u2ZZ@gıDI!aI2,r6 d Dt3XPIs`נ$ |-ln!N"!1aT5gt:=Z+/ 2JBܘXJŃ5A @BmǛqCPYnXXF T nvFi!> YP;4~Ư &eeâ~w??"N4FEI$ ?)D?C IDAT5ol$ ho#+3! c,2^862\B"IJpΛ _?J CylνC7G/|^~*'YDZ.墁kƁ!gǶQsrf $uMMSueU=i(Bkbk/I;c:??(J)UtbR `lML n,p3H)ĪCDsЦB1AރQc,㸮5ߦi0]M >PYDs OR:;p\QՔm^N锛愬)1û)>%1 #I,`A7hD1CO)U@*cB$ xἃP iyNm(6z|5+-Z;HETr`mi CH6`DLq,ٳ&9;p =  o-DZBmJ,$E $fc(&z:tz)E'66y8<}I4%"0kNRD4TO= D-MRJ1%b 5Y=1 fxdqLI-:v6 eAH,-IBAצALew 5$xbO`OL$6! ϧ , G'0 9 8Q$'ID*R:8%$h11pEZK9v!ł1<0LG# >'(jŵ7H3T N"K95|=LTO?jG'TNWyg7.o>!_l}VIO~Boy+W "7nܠS|ۨ O<~7,CL'''X__yd2t:n}F#lEQ IUG$I(w[l6l6&i\wܡ]e$Ixkk[Nq[oEeY[oEz= 6z=&"sQUҝRJqۥs]׸v8==]Wb4쌷778::t:#vΡ,Kݥ<Ϲ k-FCRX__m< GQ  8>>锇!EQvw0 h}}-kwiX?hD;;;'I<^T=sؼ-QK]"IRRB8K6F|u\|>5\ uXjaٓc"CP{t.bL`IKCuL#ybA{!{)ϻ,uyxos Mf) f/9p9~$gDHL٪aM{>ZK I!:K2RfH;@G?zTlmNޣM@gt~_%x^@'"i #/X /#f|_rN 5L9A72u?AVK y4q]װ{onx dstrr ^}wvg_:~m,QJUUq$N;}lFvh tr\5qZT%Ƨ'o|eI lmZ~OAJ=!Msloo#ILSb<`ooyﱶMiob2nlomS C4M,(HbtGGK\Ummt:Y1F >Wj4X%1ias"#'l G#b {{{8::&fb,kH$A*fǽ63Jt0>Bt03yx >;ƃ'?惃pPʣfC44qt|Q$eqsv[ۗ1+Bcf "bAlQWOi<n0aGR"j^Di,YNAපNsI kiƐ#F20KS]YƓa,I96 X$I0p &/8W\/q>MsPe+4Ybhp*pz*G!p=t:)Oΐ`]08F/KȐc 4$u4]0)'D4.0a4ia_ht] ^\!)eʛƛ} jxb-Z;$$(gHE(AE(*_:$)Ms.lsJ2i5.t%lnoRUռ9Gq;B6"MYwiSQi/X(ڕmTb2K T΃}@sapN& `X Y5 V RJRJ`g0bْl3QR1/iAȲ "2`]sln\|VU̶_֜e$_'m*r)=+.I^O S!\||7~뿃xhTU[nacccE!Zp.5>S\~GAE7(Q|e4 X[[ Dtj%R.WQa2BzOo~EQ9<~u]!M_)1߿k׮a<#3,gNE} >pet:"MS|7۷oÇx0i|E/c6(JWXq HdFQƘ6ͳ1~5Ǡb :. aguBX/pNw$)b97!]p֢%A=o]"[ A0$C"EU6(f5ʠ,kX냁)}A; nCD$b )  $+M։E9cy V7`dž=8Rݯ|ބ MmPefew' H QDG+0p1eTU"(`{$ nL)Ų+,tl$MrdY2qc zg(Yh0ĕ+ }ztM_Ac eU`2>R*䝠Y^HDh,B̐dbAu] YGX@0LۛhNS[TglbgwM`D[sm| "F540UM~@ HH$d623XQXXq2(9eеI n)%`|Nbw{/|;[pxNJ(AT R,lhZ[4Bq!:2dJgkɡ!n,v rQU[2u^ ^:Q͡{hjkC@Ca z:yNѠ$J) V7YTkdIZ d톨,CHywA ­??;w(˻oa81X]obG]c8Ν;_je^/ShRMTAe J8>:GBq~kDt>`dB?}=p^.zs{.isso޼-ܹstJJ)F#n4x:RKg0- u]SYt:8g͛7EM&^,"{m ֻ]z(wDi(sަ(M8"zt?#:::ⳳ3eI9?MBʕ+ƟrUUT57MH\'IB~!._UUa>? gr뺦i DF;4M#!٘JR(%8cj&X ,tl]Ee5媱p^ASxTf3B,tsscZ,TW-%v{c9Zkֲ3Zh c6$A¬U@ <"%A΀V7ܰTi*Ӎ(DAʥ(C@|&؇þ,(`цTd8JB&Zxڟ [/_ʌ/Wn g&G1 6K ٵ$6)Y+TR3HQ*$eB JQ.A1$18tv 68n{=nv.7hkk ^?O>D<9yĺAdƒwu;P7KC '3&gh%mj""wly IDAT8gy}= DDQUUܠf`oREKby'$%. R2HXn/RƳ bf&|RkR kmeF!42 /|(H9&4TDXE̤  rdYu]C74A$DDdpr:GON0- *ePI!I)RnE#RKս(h1 !ow :Imj R#|pn#;Ζ$$h6d$Ihww^] 4VZ3#i#1 $q:޽{>=xpȎ=^zU+j[T ?u(8I|[ߢ,#+3>4ۋ9uX?hziv_Dw1y}d)Qp9f^_4' N',UJeY{{0`0͛H>c:yR]<t<|{{{\bn6661ϱu]2vF)^yww8s 4MwxY,;;;4 Ϲ5Ư vww( v!% o!Q2c^.X)Eggg|ttNK+Jy& !# ?!S$F+Е+W0_,h~w?9찔;F#tzO<)Xd(H)ER%3xAF8_0uh`LhB!#³fSs=fdcu;ވ %/X9 MWeCIXkMSME=, Ȳ*7F-Bl:j,:)j o3jmQ B57#y'n/Bޑ"(#80q̎+kl;&{m"c!dmZJ 0&' #{xb?בs=:e0|^C7IqwC,c5!LB:Qw@,ZX)mC)k=, 4 4͂I"kޱL9“1$AU(˚8^0˚zN'U*MEQbE1Fdly&14 86x H9fg.KHq%f1ϧ8 zvUuq+t:pր8/xr1EQ86S7t!ͺ08_{x|zBFtz],AlvB{(C59˫zfE$#SV/p0Z,}L "AL3@UUM)7ޠ0`Y 6Gm1ݻ8;{8鳻0L8N3zyw]r5jATkCe^>/"WN.\@j9_~u_M,qqs=/dB0>3~pwu o<4h_fA} } ׮]>Gb}}Yڵkt:N8::7o.5EQvT+Z>WUE ammRG˼br#}ݥa4(VݾZ/y2`0N_)%}ylA{,k!ucv2Nx%_t⾐CJeYiVȩ4FBJö̽鏤u{{fdeV,Ei-xlٺ1 { _̀l&[[6EHk-gw9FedVH*3#23"{=y~Gx^]kXs0ڂa']B0gY$H& 'B\Hc!-E4ЦBۮH [ +'ɚH£|2#2B6uIL4*4*@Zp[h03MϘl5U^FF%%nr`o:6ecA)_G'CJBG(L$$|xؽ7PZGÌ.՚,;bm@ޠ$KCp0 ڶ Pڄ J h@kӓsiXP䝈{ݡAgӜ.ZBq*MYk)IHH9>,q msh&8$T ( Y*+ZfDAT ApҠCD`,)Tօ*Tn)K:8UEQ$Y7P!Bm[o[ pL (Y|vvNs8 U5,^{ʦK!@ ƯX)E, d),?;;; ס J)/eXB 𗞽Eh@Il :>Y̊뺦iqxx(;%V;ڿ^}0rãcZ3>O?4e-I,Q%ױj0(>U|v?GQ>Ѐ4i߿o p]ǟΠS`0B$v'W`Y!f^~e\~J)T=u_x0Gawwio6կ| d$xY#\v 9i !V\(* ֢ES+UUmnKuz+t}a(pyy@e.Ћ-f6!Cz=\~=z!:C#.\^{ p~y_wy߾ف @+r>///#-Mc4kqImRJs gK  ,KQ4F;q$! huC~$qkP0ʺ #Dzk*1ޔ%?)!,0,C*X1gHt Ajm Td/|wzK$ @$$eck;K:>>oV׬ᅬ^Gy]%eɓdTf\M>q !γ$`0@] hۖNOO7rI-X:M7mۢ롮kq㞝l6Õ+W0E~!>;;֪( aH w86l0%{{{$W⭷[o>x{{{xGh2`gg]5^}U|oҋ8;;Ë/^x*R8<^x@W^E9pU5 .({a6Sgq'KɴW%hUd5(J`"0hi,Ϲ:fޗ!ȓ_,@"~ϻ$v88 tI= źgnE縸BH /q(QK@O;xÈnMr@p9j `9ffrAD#˲*Z&n*99"~?{G|z9w]Hbi[K,zOg痸z*~ׯu:L&(˒8<<(( Zh\BJ>}+jE<ߧ7o,K|ᇸv...F9KF\. ի\|m /yRJ|]zJ)J::y b(Jz=2p4͸ɱrDǜx8_\N!EDIJ.!uTH3$YC6L"l@'kl6CdH..;=+R2M -)$:|1;0?9+K(*X5Vb$@%ch08s*qBƴ4qh*S$II@5Uj;;;B1;Y$6=)'B߼NqThg7#jϗ`I$2obfv/[k$*@T,$d@Y'ץwvvv)Wd)8ٟak|k[Nqw | 'g8=;G݅PqX,VkteDj^,B)deYc8E߇RJ!lFł7Smʕ+ørCGFXPDjn(v`nG_|V+1`0NG* ; Mӭݻ<i:b4?1f3L&y/  `\rz8>>F4!GQDrQ c MS<źؤ{rߧreYRe}UU›Sh4bk~OHZ;m[ N&6]̣#?3>WuM7i8~d)eLbCi*}~w($plu]1?ݻCA(wŗ~ `^ v;(rOh3ķ?H>n#OD>δEy!$k@v=.\UłP`M/n(GȬDYVkwʼy8H n%T@Ƃ\Vt[|v8ikZs+Y*]T,[ۊD n;MU$+ IQ"v0䵃*km#kc\60Imc!m4J)6";"g"0;D>IYozOZă8A (r$eA! Aώ(b0e|^+aq`^`XqgghYBkFH>cg^w"cr9!X+aX[GZHIFeiA(B$<4=EYE۶89=N/!C+QTa5-V+FԘ^# /՗@HbV0{y ;wBqRJrQUU\l(,Kz> !v N#j53bAՊS!6mHkx8Q'5#lN;XkImSs۶,KlV-{t!lzFv2q ;`c5;Bsp_V$)WiŪ ST`HG?8P`uUYPe zݜ W&;ݡ`,y e+3Z_0m eYa[*v4FU%bbUcD AːBEײ,VZ֔s4yRd271r*OoJa섪(0 =WR7P*|Xf}PDȠϨ@|AzP|aL &|䁷8Lqy@ec a8Nq#`1ƣ񘛦ٶ(sя~LYa0`Ot:k!{NT5⋛RX,6Uzt9'듲,C]˲ y:^0 饗^NCȲlMTJytBu] f0 7f"(8c$ Cl6×e 9NOO*a IDAT Aaܭ7 MSa6?tzjjLg+8d2p8031F򯇂b6( e;)WFROb!bk,A笔mX1% ipKӄ!W)c S|aFR/$,4/mL&sM|+_[$ p||??OS{N׾$I0q}۵ { ;߹sq` @Ŗ"A$Zkaժ MS!McX@k "Biu/FyH~w~pLaz6ꦋ9:>(@4mF{mb&ցY!TU"<٥5:B75γBj"RڇP5eDa⵰)l8|v'.Ƙ} m(E 1Z;v""Ba@hj58mt?:3W5q ަ< 3בź gqCk0\.Hv[$vUKZ.Vymr*@Bd:ʐ9!Uaszů"M2dY "o KBtyba&)ỻDE h 0 8 st;Gh >::#\aօsMmנi]o-$F" )hd."ojmU!!\'İ}mg,h <7_}q b:i`<#MSupk gÐ1i5Rlnʦn׌8|j=P @wV2Pv|b1[~J6T!%pE(%ydW00XH$A&U c aui[TU )% t:EQK~Mݢ͑f8@W A۶p`ժ "zy  V+` 6HI Cc YƵk9ڪW^y~vx[~u}S=~I ݼ_FL&;=]LvȲ 88_EQ(|t:Ze=agg i" C\v DDu}ܾ}n^~e^Vn ~Wqqq͇-~ӟ{k_a<o??)k|c""Zs]tmJgXk,)9H勽8_z/J%ua,`@ԘJB4 CY's^kl %R8? L+_:1ܗ!e-1UAF 18N\@[*PVꦅP!OY#CLv{$$GEt [3!1dn&$ uG '%CX[o|+CA^<" >,mF?яx4]}:<<8{իݥ,)OSZ򒪪❝"D۶t]ڵkZ^,*(60 ( ]kx 5a MӁh,9:;;5d2x Îz-jW\N#2|Hxꩧ?ݺuj__Ȳ 稚&{^`4.Ee~::IEQ'''o|OOOO? `Y6݄Nm\%)\\QIGkiHm[sVXI"p* fRTVc"k"!`4$!E&7$ Z\+dL«K_ѵfH(kYj٢- v R("^ R !5B$:p_-ECkT[$K'UQSlZ JV5l5-hclLAmK2I`Ҧa aG$ ReQDļR"^{|'[&.O86dz޺k*#>592sKMi,YkKDr۶h{R}8,x1[ʚ$E@EHN5$Rf)tu }Xg՚uӠ*$ &" D-aX)Xqwq V3rGC\<XEK\\l~A%ڶ8 ( 9t۠Eck -$)Mk5@' jmPL"N3`G[ mg{uF*g b FJ㘫'#RT Sݓ 6:XD1KAѶ-ےXC%Y8a̎%310Q<1͗d`vk{CvL&`<>AUUHXZ)?Qh& @GDdmxcd2jZ-(CܼF>z9ubU, >4ro4=+>׆.xKy>QBz泺rIA|~ I}{ȃ^Ou0NOpxx嗾q~a6[`4c݀sڪ38M32,G= য়~^7o$c !;)xooNOO7vN׷c8qch4Mh`@DķoCbf\ݿ«Պ>Y,K03x pm)I:шִ6x<, x\h4?σ^Ν;|M89=eK/ q$WKB^k_2_?ܽ{y?Ne,sJy Z[Ѥ{SGe$ ]( ""BYt)C&9r2B42!& Vb5yڱ1 0N@@9"wW֠(+䵷)$Ɨ$HS '3Y#AJ)4FQu{YޥEuBrQ NHRAȚAϢz<rdA9ւ*@kE %߹!oxvv<qUDY ZkV8;[oǏW^yO?8xi 6Åx9IPyhO3抇( ?ͳqR>HC)~+q'l~yC q&95My<,nzo)d O<FnܸO IKqХ@̶%4 ,| m( !cѰZd PP2 X!:ARhXcYgY`X()$(ނ)$I hc&1f̈́\wN1 h!!6jp{ B0, 6 c -B<˲d($6ɦkAQ?XrD8==("I$I]m+BP4 ,nO+q1 CH8N9 cX9;hB fIH,,VUE)QSEu]cDX9k׽A0 H011  .Ev;V3GKZ,fXV`k8$ݽ!q! ԃkx\|Y.jXO)LݿK |qrr£FNOOѶ5u1]L~ %9pµ6s3hoЪ58 pՀ CI2᝟KRIQ<ϱ3I sYQĀM!!6qpVlia0(I΢m-|x|B\9{{{l4-trarNYn5蜕RQ[n}GG˿_&۶n`U, æi À( C/eT;A΃L&4H㱛7P7%n$By OQxaU'q~&fEQ^"%Aćw>pwǣ]ǯ;8:ys)#jK\{4Ňq޿w#Ïp77{9TFfEQRPнV[ҳQJۺ ZrHW8MABM:cΙ{/?CE ղ+dEd^{}LS~=\- {woĭnʕ+菆Χ_D GH:;#W!I1xy/}B`h @ZNzu"y +(K8ʇgH9 JF4 I! X&!򰷷O>9|0"}1*]wǘ̗ GXG y6C!ꚠ>tv Ԡf<6j!6<'Β\H{{>ʬ@H a;;Z- ᫻׷cƭksB($Fu ޵R-l,F8aQ)9,[mnn;UQ*ĽkkkX[bЏ\ǦD  X}lv BH$0L"*7/ IDAT} yF@H *aPe ͦH9t$Y"(fxn29 <, k7+9~ʕKؽiwo /try3 r,˰=J= Cd5ʵSc9_@ uJ;@]k!cI5\t }UOgJC &BS6hZGMx64@0U 2To^y|o!"GGGG0M.)k*ﻲj8.`fhp$!"ܸqÃʯ\_5LQY{V 9 G}Y7xkq*5˪~K!.V_kNyjru]n 2lllOS[ [߼A.]b,KN[ct@FZiދ/`6.߼y5bpF&gβ .b8E%w"x0_rwϲ ַ:u]< JIܼy:.\#q 5|}k?S>do ۿ};wჄ666( {!BȲ[zu]B| {r]~qj2L锲,( $i!#"rŎ9- v]Yd=C4Hq<kkCFc'iVפu͑BIp^Bcp̦^QJit ڄܚ` ),]ټ0\Q  HGBsֲAJ֖uZ DZ*uYh `A sU{KaubX;. FZkw%,QDDkoRJe !P&|/cJKu UUuAO&!T )-Y`5qK|!bp;&uBhj(JdE$09a:bwDjRK 1! 8GDDq+y) de+ L]A!%$(a|Xmmm떨g0k À@!$$ p[1d&f~×\Hn,4#,KI I24cd^: ǟbks'''8|0 q5Ň˿ķmWkJ Z.b'[R֚M_m?kK U mjdS>?x677#i]eIb|>|~xIᇔKB`4`f""ekÓ!~m 0 Iqy8?LaEȱCo|bjW^yw>|IeY:>>2)^JTdsֆhCsUU+QkuV66==1) h o,ѾuhlGb`vww)"L&ܻwaW͛J!mR[rO!qN,n4_S1nŤП{t;rs4 $ ϩ݁pm ; "zaZ.pm$;w>^%lmmam>wy/}{"ew|0ϭז (8?ҧ( +߂y?x?:>r Giңk!.#-R ฒ#4 cX{.^yRƍ11ܹz xz͒%&49o~?FqQd\.9R*`5mFrbe*pxtu]+xG L0a4{v]O ;f"FWm$.Dew7! ÃAY CR xDJB*R bh*IP6%331kJ\VmՉ,H$QD̂M>PJrDl JJbE$ј @ѬjBקE`/H8..,43H( _ A`ϓE75a=)mw]tE۽.cU4  @1`LlȐ*r5ĝ3^!Œnm`AHnJAn1p !HJR R 65zڈ%]t4[b4a1O(K׾S/yCwE% Okyn>vvvDl]ױR=R{W\{lՒTB Bn%f;Y5R1yPV%D _tQub4MQ%<7,x2r$dAM`kkFrp8DUUֻi|~v*Z :88O>tIy-M`2#""ڠ^{vvvkOygcxmmm[#{z iOE+pC3`ݧ^HH:P>Cŧ]=ؿG\OqE>|tuwIǏ+ܵAU+/(x{{T5z=޽*c}}^}Uhoo6q>̊;;;I@_4$Ipv 4ܽw>ʪBU<g`TH9@!.]4McG;ZG/$>~<.h>spMeY!X$fx4 q|F78ыVv7 'nTz]QD@[1KBƄԂ 뤤K F m6 WeZChbKr*@,[ZF[aJf$[xÆ#6`Hx be;`&ʇCui*w6X,0{!#&ʪ!%*ڮ.hGȲ |.5)nwpQha4Lki7^F>=\qEQ`4AOOOZt[8;; ֭11{>O~1Mϰ4}] kѣGtU #X<x.|n;fUɪ(n3MAD'WՁFe[~kk Y԰:S#WcLk|?D]טVriw UU1F,KH)1 ILD￿Ҧu)G"/v<ʕKc"HDÐ:e]s Pe7<We0Ӝ{˧siBB7s67s2=1ˈb6Qa6u qc}} rK.a{{8`xQ! C4MݻxAa>ݻw9 C\p<(IUU,K} }2Fb: ppp-y!޿u]Ç իWA Ʉd{4[.9+3|s\I7n><~S,s˳1qGMSqЕ̑ +S94Oڵmfl vgg~ܹ˿{'_[ۙxy"GAu]v?}ȩK B·6"0H BUe0ljVk.VٻMcoRkQ@)AasP <.9Cd $I$% ! {cH MSO+%&h~.Z-΋uA}Peq{$ Pϓmvvvp 1駟>;wKW^ ~0eY(,Kbyc%"_<*yS)yZYO@aEU"Y, Z,朎FX&s/^#RJ~¿kFOQ~,;Ra@R\孭-~:E&z4o~X)|N޽{Ν;70!omm'.<#vm4n HApeޥ6۬aʋ1>S""βb}}Q|>|N'K|'t7_woAa/DΦ:99l1Ώ)#TURNNXFfKM!fӄ7;̖0ܬyccL[U.pz,#ULd8LRg 8GeY ,2뻜)iP1Q+Gi$PJ1 iSs(RFרhA `׳ݍ,4 IeوA0Dhv78>G. i{Jy5M`Ie]AZ[u?pc 6,Z1,Xfs}~#6P銊`8bAw!Y\\HRe)y"X c]'i*l1 45 dA B)FWd IDAT|δ#$ΣJ50kڐJfhv 25#BȕqYpIBtiNR̠A0ТӣXMni&f%X9[s\lGG#-WlL؉6ҲD@h X!@*J0A""xǤ53bih98H_x~`|e˗7hz>X#-U?!R'}H{/d/EaqNVfBըmp;n޹~D!lf".y`f113z'K {]8a#rR"".RdUUQ$X,R>s\w |i7Ɏ$Yꫯ (˗|*9szC( dEAy8ǧtM}GǏhkk766x9: a\kȼi33#͖bXl6x[nʕ+(2ʚ'_~$,K3x"moosewzxwѣGZebAUUaw韊˗/o濣w]򷸰 ڽt[|xy647y4<ϩ3-b\8HPyiu~V(x}iR+|ϋ(r~66Ɯ>=>KpGT1nT yَM=kǔ)bP\`cQ+6f Fnicב"|yήc! Aw Wz-*Ջ|N%9,ԕ^)~<睵eْ%IQMR"^l681ɑNp/ZBQQM`6I + 9,ڦ^ykkK8Æ@$ -Dq| ZpVgX,ԋx\څ 4MDNp~,&<^_uU[a 1Aҕ8zE} ͎ MNqvX79C\t~:{bP?,A?V'\r8"KұEXyW*nR ZJT`VKRJZ^kM A%`4}L6ֱ̒01 (LJ0RkQ MWc"^4EĆi 4F ō& n@K")h [ Ì)|DX^yWy8R?BTD(9_br>~'? }ܿ*7Y)Exvww…m<dA&>imA'ƽVԍ[$ `7scjm"RJR,&~V\~U]qZ8LD=$I3C bWI$O)MS} $`Y ^Gn?Q5s$I;[F4.n Y]|txx3s#@_ywvvK4%)O+3 <ϩ_|y@: `ӈ/*&+䋪ߧEP M}ҎyUa/b!TcP5%QjC֥jtrzaMy&'f$IEQNOOwkFw<ƈyQ8!"V̺svIq9MS "9W}*VJNKG8MS~i:kwK/?bi;a8B4|]t1ǧ(ʺ%δhXv,ٮ<12e)jA\,kz(J 1]b$I2Bp%m̷`YMS׋:f)?jMɛ,7(8]0^ *>׋ui0 "b:i9 `f `<|3ê R_4$6 Sumբg,`,NLہ>&a@|D-"]yJFK.@@! "8\w#UKScta~:;? ܸq7n(DQdJ q,|IH늍1>!?, _%lnc{{766pq u[w@P[ ]ך Lq̟;?ʓlZMRsA>Og)aiV60sEji,\DQ~OEQp8^G?=#sE𼀣(0W&YZ`2J8?kkk7`f|xq A_*:į|^mx΍ uS5_' |0lYQXJ <|㱩6A+[6`Bŋm%I˗?3ahD|24F!leN݈cGE0 ZHD&H%|_h<?px&iS#S~݋[^GŬuIRYPLp]}$dz&H)1y"iUtuK`9 kD~e5Lc9B1_.3DX|^²1i*xdb-&3A" w\id.2 ܻCϱOFȒjm|TPP7HkhEj3!?ml ˲Q4xMӠ) fs%fׯbww/x!pLJ<^j7ML@ ptdС­SϢc +2v?p}4(%Ƞ2 Kا,7@ ֘-!X-+Vk-":[H$$/HӐnj{mpc5ih0  A>M!m >?*#>>>y8Bfpr"MS ="'|kG("lN’5 Z;CWw\W[&{v|m񸢳~ Ak{0 $0as^xRrǢ?w-1QuUk76771pppG6x!"4UUmV@ͿA/5ܓI%_G|o`bA-ylAoGUAWJa:`6u] eDd$I5p7obƛ@\彽=ƃol6c)%b>qXC[ 2@e*t]qj 6Q$MђDWի-&,KNӔ\^u]ܿ~֭[8<>3poзP fbGlHk:F070 %Bi">D]ۢӚVL>M|Q٦iHkAFq; -@57>){L&32>>D-=~/nl`cbI'-f@ܔ ۉRMXLg`|?\v/^@/!y lns\c2a{hn'q!BP扂E`^g%2}3b>[kʎJ,j+Fm?+2g؄EIk,D0DV&M )l2fcM *0ՀkD iy< }[He3DQ` .~>DW[4  ^߅R#@'IB$VF<"1Ygu]6ɆiNJ*B*KRJ:]D fk0!i7B'ಲ&vB.ke$Ih\˗?WVeWy 7V9gʘ+j_f4-WT. , Bq!V|VX; 4 QrJ)$ ϩqAJ,ژ-0^N""| / Css[I6u^\L&X,xmm0baF|ۉ68j$H  lJ""y*<}sT"} |* *\qC@ @ %d֞ѷɽUnN&|?|2s޺U9U=<_z{{QIN{̝;9s&Pggg%aJLk m}ك֮_{{{X,.X`niKGD*ODk/L7C"ڂ!J$:Nr9ۣ 6ud4PV@PV]zƍn=:=B---yZQ.CySJP B z)T.uX#*:"d34D]mH0y ͛7sZoX,Vr)Q"ig3:%a^Ғ`N%3Pq9%Tz u:=R@B-mP(끾~54؏3gbיMH%=\b4FN_ -)].+%F(QJu\f (AH&J]JӴn}c#4wO7SރN'Ә9Ey z3in3`#SܩکY%2J)RB$kR,ke #ե̤6Ӻ;zFkk 6z+^uLA u2|aT*F:ɠsTBCCCR2AJy;KC/V 5GCirWU]o{t5w^"C/9Zr~N F߹ąXMWRMePHJ /1*i/YV UDd)TG9GIRD~rI&^xa-bJG7RDxL(VҊ=xvtw>RI#nN^w F!?tttg.tO*RIiUυ˜FjUWvtkOtuT )JT*hllJE]-[z@elېfj}^r25cE{!

ĩ;'blbQ֊*ߝ9s:_*W_44a-V7YL:$=goc`I8ƶZωJ3E%: ż. &:~LR鲧=-[Ώ\.M(ׇӯh߉B[[UVL1M+C:֙L&L:VX͛7QؘnmmB)S"͛7kM%UKׇn imadd}}}422l6C>B)gZxڴiGUV땠t:~ ӳ>>`7o^-Z ؗveniihEek0<Z[[֪K(WPBsIT*X,jWT }cӦM(JP(N'ѥGrc҂]9>###kV|LەJUDGT*`NIaH'PRe t:IZ)ET&466/]ӣjȢB~ mmi=Z[[錦޾-:NBkJN$*5P2tKK.D"b=::>LފBOlP BŹiIwR:(Կ:24FJwєKНmH(ӺSJ ĘʤRPJxid:Jl$hQiLkhrR ]d9.v-SB(A06V’ɤx>mJ\_qkyzMȤhwtvM!&], qdtte\;*Ex* U=+ƺGG,tvv$<9E f3N&0zJ{7%'ɴlK;͚5z{{M-f:Y*ƑʚV ._@0G4urI벦2i?GХX B!"+{4 $+W UK ;(ndձ@(ʞdATHk*%J(J*eJzФ}e[S߯%Sd"IetΜl e=Z!x'Z062l6ٳ 6@%P[0AJT5BrF4JjhΫAU@Y+A($ie*벮,(*j*PӺ6AyZ\NctKK rQC͛7c'|BٳvmJig&>Ҹ78::\. &V/J:ϫzl%h4~<]ܚmՉ*_(Uj]wL&Q WN@J2 [O㥒(4::a=66FtR{2AVJ)?Ϩ*U>0l6;:*ו|`ddM:g3AsMm# 9K*$8.b $U_TImt5BP@*tZUw3JX?'rZzdR D5ڵkbhhmmmzp>5kGPpSR^===wrI6m;h*ׯG:ƦM0:6G)P(ڡƾ͛7cժU8#q7o|J*UGzzyy̚b.̝;gt+Zar9Tk:i 07&7/CT#MT0jb2J5k`͚5HF%Vv\s~e]vkP. cDK&T*B{vW4r~$S btt-- |w9s&FGPȗJ'P*mԣ#%ƐLVʶ Hl *Nde V3Xalltv\Lr:M>JcϹ3قl6l72i2 OLD*@ilTSJX,SKQ@"d 2_ujUOպK[T o~hhnP===ؼ|+1o<E<X~-$q[?Qp3au`I@]s9ƶ<a}{pI.A. Kmm~X=y\}PT+8IuYq3c@2pz\rkrHQ,J?궚q}ܺ>N;_[b5 <<8ߓtlK!° &Girm 4,k759@MdRN16GͥO\=b:H`pPMGȦ.2$s +P38L2cp2\BtS6pq,MxqYuG;I=J29iu>IuI U+m7jw">꜓'I眴P {_[{{+MgwR;n5oJ-Kt] c]q |(`\M gzm /D٨}7LɅ˅rɅ+| _.ֽ/h.ϛ8vOB~[OU?=xReUxo޴x[N.cMywG&THߛ*@¾K!Qv_uBv rAtjegu%Z"ߎ}Q#TׯĘ^/B'dԛ ^==γ=M,vc%` C.M'V.[]]\rXpxd}coG>¹$?i5zO;~B|'G|GZ~?t]K>MjG~lקm7⾫w7+>׸ckn±' Moƕ߻|ձF=NeVw hq?;\"L.o)Nw; >sp͍a}Wc:7߷rzc˅⚛(ʊv=vE_Nr}' ^G|Cq"l?sq?O9 V,ٸu1ȅiGVG:Ҫaz^sIv;~p Kxѯ? '/|{B_A.\#g]JNLl;˴=jr !m²"WWz}:uaGd*Uz] uwjCT*ҙﴳoO;UGJg)g6mx~?W/^wu_V=mpǁԢNQ/i@]r[Ԓޠ=M :񴷨O{aO9>Qv aIW:P|ձj\x\tEvֻ/U/>1<~ST[{""ED*LVS?y!r𩒖:HL /@.肄m7&g-urY« c~yS.<^_3U R#9սL Ϡ/ý8Ύ.`GoGr*azDĪxJ{zc+GV)Sr޷N_"L_vM0NL^zjGYׇ>>L'I[8P.I8GÙ=PĢ4 VkzC~ԉtAp9$pqȑс.6=st)gi@Vc՟ $?q)g5NQTm9=P,pIoNwh29q$?E^4}wj']AXO r/uy'Hm5o$g/}|}U׫Pk5_CVLO{xED"kn-,cʶN.Ÿ: A<7NЎqػ?&8~g|t_/|}A/>Q0U//?qƢ=O{&] Ȅ 90;J=ܑuhj}OCO8ms;D*67BUlغ a Xh1SKAʟP5cNjIa?,3j}psߏz>لc%W] Uk>Tj\u=Ph.i"7 [& b 8}7rcpLuBnUVSҾ\ȗ  [M^'_/2y1,G|HЫ|׿@-:6? ?>谣ޮbwYE ߗ =A.4Cɚ72s(I[ɧ/[ɧRjOj5?,:ugU+/W/ 60KWw ?C뮼.@yF/_zW7G|Ys/uľ"~;*"}I`ajŤ:}Wꋪ\/˅@^aNa)T>᲏-7跽cy , (T'~dzA:9l=Z6m:e0%6:B]DX "nYw"C47]]D >nkv>p׸{5oC^VM$AOox. "/~K; ky4Tȿ]}ү{m[O;bʏr)ϫkjL~yWH$[~]{ҵ2 L"a˜,Ǜ y%d__.Ƶuwx?v}"| _.lǵŸg*ԜSkyٟs7uܽyauG+yWF=߷ 8툙/JUJ]ye癲ϒ/^"TwZ-[U\_83|'ZSrA/jBOz߼v.$RPurA\(3Of|w%'.^3_ ނbD"I~n'#L[~\Po.—>}>> _j\z{V|wֶw}jyx걇~ᒫY@\k.-ڗZaHu*Bfϝ:,VgCT-jDwPe[r#D &ϵz̸q2S/8u. =e y }HeO!7d2q ]d橭o'!(fEM9:c %y1E#D7螜@\S8lǮ>L2 ,,,+LS`k=9FBMpue929WkCL弻kT{hqXd ("aP:0SZﲭc9w^kQmϵE@Wr^L;Ow99NVΎ>=%"t9La:ǥlImFa!M(Ŝ: F{]'շm^i5r]XZf70pFP5SSeWE[wvQQFw?OEQp#zr2m݆FJ&v5(.ߏmςEwT1 Q@8 缐Qq.,|oJ5 BP{sa;:*\*yjWmNމK'N0#H g4͉q!fsyAqa6MhZx&#ˑ1nS5tyXXk IDAT~aDb73V[t/`Ov 0AG݈\6l@  Y.Wx'C|!mʓˡ ^ϴύdMD)/ ]H& 8-qFԙ uA{f{IɆ=JdT0>LτɷЄRvs ld_s!DcSIqTظ6:Ŕu@3ɧx;K LSL%'ϥ2 ,'7B. D!rSآ aQEo{Ob= [ϗ,O_c|lBx79:XX^s\+)H_S 8eqt]&9CHn#\)nWg~frC\27("AJu c,8A Xaöa7bMsec[9e\|N4$MgM}Y!t1% i&YM~Gdם(VTy%ʻ42&ImF@Sz߸KlDۂ6.֌9|j.] ;wCc{d 6r.'2YE[܆>FRpoWEP`gD,4FΛ8/ hc>5#fo{G#s5dfa2î8?cC K=l mG)üN7%L \bIMIl^qPQXNՙ(}(6 8ī&^ Gr9rFH~c,.%tɕC[N]݄%N8(DrS^r np,R8I^]*?Ɛ#liB! S r=${&@5MH(gqm4è"9'/S)3]цA7h* 4# s ln1.@dɜV\ '̵£m0ǃ,s$&V^Є2b.s|SMv]yav`s@0ti[+m QfL{V4{vPP  |ҵk .%I)㰀9XAK¯ȸ& v5`(0NkpNVC;nP q#OκTІ]-rpk8ȵ DIEvX?p.Msf9y-h=q1*fٞg'#Yv2fM'm\3z^` 8**mXۺˎf|!4˃BWb $/<6F=e%p'srs<p?nu a5#~#$Q:GqA)(YU "]H1kdj70 킡\0Ol顸AGNacc HtrSX9:WAb I[nTE \Οv[9 !ڜ[fo2P k*T JQ~"MʍCZ6)pm%m%&oG?M6KDXދ[](l>*\~YDP\!O2 S Q/kj;]0}F˭XVE1q͂ܝUʰM-\]elP nزc*!0͕yiK vӼs}elK*ҦV#N$o>j6zhR>qp8.ee8%nh$6O &r4]l;dA S$rTGH50NʲQ0'TBN1id#i8z\yam;q |?hAg(Qiǂ:ǎ+܁mzmaapZ]= r⤸` ZŇ8"G*¨FB)fەrdE]Qo㲽\ضZhhD_Yɵ$mgյͮWr "ܓ:uy?2T\vy@#- 1ޮOؔgPi;p)L!h"nRnۊϷETmh|Ny'՞ծ8~tdYNt9|-yu .a7qN7n݀x6ýӡ"#9KFŁkX@\J|N? FEsDU moرK''>bK ]hXiԾbPw.6o^Y Fs;T!hf.W9\gdpM(rDhDOf +.$o 7B=*lDvrBxa(Y[ 17^r@0Qƻ2Eƅɯ)*0'e>2JDž4|0T4Ù#9j ⌕-0=[1T:T+9us7zl %29;l-NÅ`r6;N8[4@}PIPC``u@8W549MXXCyTDMURlW ڶIVs lUv\wI\#MmiA"&U<^>sl"WWs,قyrLc~ѐMۂVŤ%>sŕ;7EȲp"*`6gh0ىMH}k9 n/5|!T +O0r%7Zl.z{Vl@rbEE'uN\1>; 1e!jEF3jCՠ.s]9 5͍\Eux_B*~linT<2uԄЦT*iqw$"j3ʻQ\ޙ /w3@oyr\dR"mi4q{U)u#㰛|nT"/OikS\e%FFg\x!~i*SeM|A0tFE^.co7 Ihݨq$ύRA8, q9ð3n %IK$NO. 6n/:21>6[U({I(Gf58el;7)md\gȖEd;l$hdwUaOa(.כ]XC\ GR2dsє\R(*h" ;ru S-W%#̛}l~vM N}.$-)a Sp p༳nA*o/wI*R'l.i&u\Hza0ÔK.BR-g2e1TrYZ&lDfÝ&crn{2ȫ)/lL-+K[Q,;7"=Ȳ9xQp9 \H6'ˤ+ml8}qԘV{b Ti\[QXtMlG2EG\qh/JpVFQRDD 1.uvM,yelBQ>w'ZW1h4۳ʉϋZ䤨uiK7%1hB XW#86C^{~b]Kfl> v[ù}[PIQ!$m߇$0r}ya#";< U6z˶9ۺ;DEFB{ܓbh+Ѡ 0HjE +1ޗtƈ{[$h7E#ʊr5qatY4RQGF1m8XԹ1w\ Egq:=3_è/)9 #s]qs3wjB蚒-ms„[DDAMA3\"'#)E9 irr͹wMVDf `^ &I666M6'T9.I_8ror81;dRp㗚d6L))12`AW=+^ 'K[:Ql Mro'jL;tɧ.,]ǩf{b`s1FZ.c08317lf24@j$pԮvd96E&U)0Eω؟NbSf6_m<3gT2 cSCs>FIlO$u7wkL*gs&G\7buQm&'ll-%Ş٢mQ"Jbhn v5N 76#׬v5`ԈLWg׶c1ܿ!dӞ8eQ]#H]E 2f(e.Ae۪"L\87lc[761T- ۊ,48ܺE}rȕ|]ݦm T p񨂾ǝqdJ Zb/((l%2 !%h l- .JK8*qV⼟ 480KM\ecuŖ.8,L86wXhWtVor0FS0oG|# |NqՎJΖV1踍[qDbd9W^-1L4@83JIKn.6'35U#r5(cP{Օ(D~/72 s6WmT%q9 gbmSP[8i8+-9mqY\7R˶fΛR#c (S[i!ur-p#JM ĵ4 v? [fs6lUiJ9AۅfVZQW[\Tʢ̣옻$C_vv]9^r78z-Vq]:W`sm 9Bt3͙v $.mORL=+n-ŜſB EH! V9(ިuLHQ٣QG3J)^9*u83?.Stרc8j"u, )F4FemS#s.v-5:︜j ۍds-w<[[PPɎJf%sM8YާY?d iG#d5aQ6+)9SQ?6gϴ!KlDVqlGqQpy=q:Đ@F\\). *nNOWr<7i[fh$hvSàq"O0S3#J)̹̙ff/T*rFuR6V=e޺#W{y7[lM>C2(svi,97n%`m冕 bXK4-젣p㔅9lFE;s4)4ɝ-[ iV2ATvp s*'}t8 9?l/wjVTyOhlo₳0E/m6NIpׅjt{dm| 9v^c;'D WY/Y'lLRn׮Bn)T (Ü<#'s)B: rXȱ-1 f)[q M8i N0n0LƌS@kGeS2_$|vxq ir8\ah m&Jzptb:Ƃ$r92 bgr_.Qn OA v88gV0#=ѵ[`\, *CC2:#53uptq:6tzwrFGcMb[=r⪤h0K`Z"*"̋Fõ@6 "9[ܸ{pn^588 iFtks*NY&b L53Ԧ1wRI-ӏV/ c.Pұ[ _0 (ȜFÌ :;TM<2nwD*;ɑ;]jt`8F%?ȕ<͑F+ƥ`uwt!7:'](.YiXl2x0(568YR}vm@MFk'3˜T"E7&6Eą՞Gу\v⚘ Nԯǀ,uE)pkfpp[X"BIi E+Ӷ q1P&cęǮ saɁ-9' pVc﮻ܜR nAw._mAOaQ&as8HsDcKa&aA"7mae [ qHFP *ZX(ѥK*m4p5쮤eW2quXASѺ1,A@6kFRxrʲ\95޹Dwzkd,](v-Mqe`7S*(S{&fԪiU5O'EP@0 rgy7@ P!lFf4D#G]3s|5ĕ452rf+4*6*g.D }eQ"\dl))V$@XowfmxC݈*_жF8JMTR KBd6oSԖt=xK,a n&&3e=G1Mrnf|neD:@8`8)\)=V}>l0wܸ\Ю M& n8Ɔ˷DV6ID֜&$+:/ͥ"FX@!`lpֻ:9o;[ f+3z IDAT7ɵmczd7\Q9* ՃS],TmJ8iMY&\н]D&\\TF ΍W3w0"9SmeU[&Nbo#*$Ü1s1{MsBJ-:& U Se+=~nf=h\.&l $3/L1I)"H4^ĢMHl(ʮij8BNM!6R!(i\|lrLhlﲲs;>8y؆t8Vq9cyItOYPc9ym*z7m"1ڞk;clxLUJ C(B:RQVekGaȴc돊]Kc 8@DQA}U:?$-!#KuESu{;}‘\gU !IkFTĺ短}1ej箩/>Lں@i3fSkE1( r!vz `]~ ͞M16: kѳ .:{.9vo:p @ B@cGZ;:1#'),M_Do~t/ypioGʥRebxg;@ (#cN o\ұq2y￸A, {1:1з> ̘ k^+~F:V ;-dPk?3?]J|ƼOC:ӂ)x9ŵ7 7?0mnHҒcP ;=dP# ~8̜+wVק9p)g@ ; dP @B@ rd,i G@ D @ C(@ P @pm ĪU+jJ @ hPK7C(o9vt@ 4O 92@ P @ @ @B@ 8@ q@  @ !@ C(@ x@*xt@ŋx`Cvͥ .^|t@ ;6g?{t@|>/ l'@ 8ETw X'ԅO lX.#9i;?wr]q;Z;q3^n9'-q й'kW_f\sqp>I~,|ctdXV ;,X_ ߇q׭Enh߼vw/Oyue?nYEVG{{;9\wuPn^<}uYhkk#Xͱqߌ^2}3gNk gP\e& L*[]݀%><0~}u!ڎ^w 3:::ַD~ ^x(Π@0 ;m|8~}w_XtjGGWV k?oZb0lwO+bq0՟@V s ++g,ڣ%~}=zݯ^~:7*+,>?7 Nᩧnoۡ~3p  C(Ӑδs t{qMw~{;d {W~f,8/8sމLKq [ckg!|0mnP)S}?>~ٷd`tuuOO~ '`}{c[Q+*LqgZnޙeQ}3;"* "RkoVYV[i b;.3?gwfD<˗<3w{s=wj0=3Nz9=mA͍ j1eLAA !AAA !AAA !AA`BD !}EIA D )=! Z Y C@$eA !A-xăQЖ1AA)AA)AA)AA)AA)AA)AA)AA)AA){jmC  ]GXzS#A)Q{I eLao?xrH{D5CLkWLsM-|#ѣ u[/[}k69~#CTs{<65׻ׯ%zpK2 n=uo>;}Z9#+zagISy]?1&4X.ݸ^{ ;Gdz P! [BHq`&?;ZPZZ;rᇥknGu}K~a4C{1h'aڄQh9pnL0J=>DVA]Er:MMwR~ıj4wp]ٯ? H\UBLu ⾄,?}1ZC]Au[^FӤ&Ff s ˽w[~  K_%J(.۬OV`G//}^CABH \\ݑ:_,97o_FxA m= yؘzU,I.Bv7IYG/ⲅOp`v-X4k)A} CHѪ}wO_GqQ!7ky3$DaUҹʏAׯ^,a] `U0+q@z^9;v|JڵBD][bs&gOG8{_ am *-)L$%l`mVmܻB Ж1Qxg$4M=X[/=7j " 43yH;;Y!:whh[s̠18w' o=ģ ? zh4nҜN^ qfα̌8psD>jʜrڰmNA !Apq?TMܨIc $,$l]cQ}ODtᅴIoj~CΝ93/SNYzH3Oc΍o$|p)'[֠1hĔI֚' Gh˘ j)Zٙ%+Xf<-y4F8 FJx@x Z=nk7iV˜(s;~[עmҹѵG_!#vs! CB>Owp888ëgNJP&؛^wԳxx5ʣNn^}GHeڕj6qSݽ+ QaamІBx08{8.^S\\M*:PR\'lE \yTnߖء}I n%ZF/`2!OIOOC@'VEvi7$/s %ZP|%E:SQ;8RճE.=w'G(v(.*Q r4DݣeK !>!AB.dgAe F9Vwٓod4kNBQ;Wxzhg5DQ'-VHл($%l~5I P Ww/߀u,iu ө#A,)1AX% Adyl"2&Yj:=vߔ~eL[AA8AABHAABHAABHAABHu{tAD]V%ą38vd\:Ғb;?Z팀&),ȡh9R6lQR\!)~#tGVXԽ;uBR Ж1Q空[u ߆"9~#CTs{<65 I hzvvhܤb"5%:A!AB ^Lyn842@c6a>e'Vk88:jvfcf#fػ7PXxSQ+)nܤk|iKڎ.}8T\>kJZ-~3|y;8J5 ֮ #}VVV8v8'f~.mprv9GRw|G`{ IDATIM9?&a߿ë yn^8wvϼFM`eeu$ W7OAڡ$ھA|qهM1ldgѻHRN&ⷬAc/>6nۘ B-h˘ 8H;0cXD5Ǵ Ԫr6.1صm=~ڶ'DNY\Ý_x@x Z=nVMC*Ht苀&#tGRtm1lPZuOۏ#z[A:v9R-<amH k8?]{_`0.18r`w5ž?=1AZ 8(5xsh;w@Iq1+kzZGXXS3BHrV`kk7YiuCrF|=$3&Ep0XozlѢu'3F'espt^|Baߞ}Μ@׈ҹ;FރPKXq5%^=[-K氵GqQ/Ew`kB} 5)' oЮ;ii-XY[cKX^y"\z9An|̾yZX+e|J-4 Q#(8T' ^޾rɈ!s4ju< hA !AXIiuZEz 9K3ZU]zb߿øu3Zeev%o@B Μ(Ws")a=)sݐ{ ϝFAHܾ^aVB[Ǚ"e{> 7%E(-)ƙHJبeNطg o '(,yFUܺiؖѺCwنgh}lCN=hqO-c zDs{4w8"m٦3\A.eLAvg};;.nh ?zRڨ#}~f++x7CϘaزa%XpBc7lk%\бko$ؠjY 4g')(Ry:Ox@v q!RC=ݳ}<2&,G{ժ2ADm:q BЅ B2&jgO+gĴv[/z&'GT3;Ĵv:.G42,[6FGSk뎷^ר-\uЖ1K*;~d^}rF6nx҃% [BH(9g˜*#q:MMwR[уIh5@ii &DoïY~X:O|$n]۷  =QZR,޲bA%d!$jVADÆؐr]"!/*t/C`Șg)pxUf/JT)t2D[Ŭe}mIH&۽.AqBBFI/w{KP;6: tG&^zk><г=n}޹b邩ؗPTx"ed/d|d.n̯ڕL(..7E.QptragylrCA !A{" ^ݬؾuu2WjLi4nߨ2U$źо]еG # q.֯G= WwAu2&jwOog+zU*ׯ\4עq3~e,oٝi2Ubm]Q7_x 4B[A#u@HN:4A)OgM{2>y@x`)]?}9E'C<3Җy3$DaUҹʏQ˫ZY qp Z'gW[5~/inX7>k-}?.[۷ hAX2&j'7 ef +n{eBr&q VD:>b m;G@vL)suDA^..d/0>]}w8,[+L9ϢunF#E;<$@#/f!aZ G%#I<n(*CIص}O ‚Q?_!ppt3IJU{2ֶSr BB[Ʀᩉ30)Rw!wF_ I4A8zg8 F9G:w %}_4 @xgN#)a#=  0HێpcR{_Д MB[a`cSOJ=`4v(]{Ŗ +u= [Ν[nAcY_:wo,`e~3L/ B jF(t43/9V5vqߡ-.tvqA  H!$  H!$  H!$  H!$  H!$  4 Az,m6MM9?Q6AA !A5@mw')A  pBHb.dAʞm˽ '77`ԣ/`ο0l/ŖVbw W.ǩ!ծ>w AMsABHl G'$oD;o88:#`ץڋ) SQZR0m(ٟppo<rNNKd9s',O@Α8/VAHٽE'  mVӫq3Ox;;{Ƚ[7AXzﱳw@dؼv 1Cbg߰qzUӲh0w1m(|vD>XrLLjq/`ז:'n]qMFjBX@̅ӫ" EJV^ :t}u? mGFhvzyj;7AQX{UQ:{bo jxjtA>}>kW`߿#fH,ogHk̘8V)R [}녆CBx0% SxtiO^~s[7Ro?B#жc[QG'eL D'6KHg7t5^xx5@Hh+\ri{px"A G&!-u_&ZlY@qqDQD^5$l[m:KiJq̿hުjp nU   H!$  H!$  H!$  H!$  H!$  0 Aaam4j 4BwhNm! Sj@[AAAAAAAAAAAAqS\T>~#{4BP[߈.8~H/}"Ⱦk R yoIvbŻ{T/݊eRcAD]d»?l1r 4퇋;5AA !AE._FN=LoVAM-Hf7m~3 HaCKCHؼ=CmٴG'-Ox?W,ŠDʮ͏ o<3l$oĔQ!B wnDcۻ)zp/;NraEX().S1Eqx`ȚxylfZԼ@z 2M0'?~'b_Q IXvzCs:\=߆*++EP[\x-Gx~AӯMwp4Z hٴ- WYp2ZWf|L }Okg'˜ކA=3>s;#ǿ!]ϝ¹Sѽ@n\bPRN^U >pT`֧?cKCaS>_:7ں\U*e+OVP!⁆ QNU.F#)uK>xO!߭[l-2D[CM7Gb34:Gbq&ӎk{!uUTΝGg+!5y'2C)A^xc.\}ۺc`/Fw1㟘{0ۻ)CMr|xI*` qS?D=[;L!}:A,Q AA_>*! !AA)AAAAAAAA&6Je^+eN{J_yJϷ{F0V-٦JJFIs[\Kb̢`f챦MCsUʿRJ_Vjji WY߻߫e\Zd(2gnҗ1Qj 2y9VbNF,a|eFi虙*| KJI(ܰ'JJKKϞJ/}aQJX:ʥZĭJL~jz1޲) /;{xIiIqic&*9?P1UR3}T,cڹih W/GgZfBVeFlwQfd[~(>_f>Ҙ(P:i|vqx߮WOߧ]:>oW+H4OI|JV~WG}*)5$ڡl ԒkW>5SC;n^AYy튒%釄߾]:q3bه8Q'o?*bŲ35)2D 6{1xW$l^Ci88Gy#9Cn(eNc!C(9eG/JP;9 r&^s:dk_h}0Oij<+zW(tϊW_񱐸m΅/m߽KL%n:"fL_") x%euOPP֭):*,d % I־恤#`19~恰v]zI#*?}7TIMO!Ic1Ä֮'&JYB;\OɋjibdBԗ̏WIu3?RH#:Y^-PֹTT0)+B2 *>^3aQe}X t=ka  7m?ZGA)CHh+Ei-4ϨȰ"=Bq{/ 7YU_RgJ PvuѦSvmYO缊iOfO g1CbkR:h`em +kkh5\z vaݐ.~/I;ƌc4 ?o>jGdg`QdՉu+F6BFeL38V~_Ak55=N3֮zngJ [jՔ7}2S>CMi%_*vo߀};M= vāS:\\ɂ_`0}q9' C"p,XVxŷĔ\"3xsg{Ppqj;fq^^$(AFۣ" CU%rHxC 4O n'mVuVDZGLYV$,ݗPש5A(cp|/V@5 !3W^,1ԘX#xWjLVEKayN!qS*o\[]`әPMj!:b Q,HO8:%7,ؕ[>sr@΁goYV%KŒ k6zIDcYjeΦ8'7AnM=aNo>YØDxaY쑩A(Y<9YêA'T'.p&<}XJ[i!dUa-cc|jXqx{x7ؔ<1-! |%y_a0XÄ % dλ/u"rs07cv1ݨn7Ԇն[ j[U@z6U=gb5KT*||=g:娲%cWKê,xg_NW0?V%eKׂ&GYe帵}j8bQuS+c^+8S$$RWr,vv9x Dz=Wx-J,}ESyyn%W`Zy,l,>%cbe׬gƜEeZOp.+ۜ+XX NVkFʇRG19(\PПy>bƶY& +DmRyS.ۗrwdU3gVc-cj%]maVI{/R PDd{epKA8of~o 9kiEDr*m5)7-5XrWYfzz k4QcԁF'YQWOmsIq)<_ *d@XYby:ZYyM>'&mЩy&}D2!0Ռb)n1G ):3g^jzqdlꎩyPM%GB>JkY\ zV*^/WV?--0Xr9OPA\a<~$ŔL(<DƆIBn޼yG7V>^ȒbMS0SgrbVجs"TZyWY- öʘyp,YH2z5gYɀ}>D$lj〛x\QxAFJUb:grnN.R-H:jD O<iSf^ Kj!,aTJ|~oNA`g#7<ɟE9JNY;1˄3ʁ\+#  ?K*}rR9X /A2.xB=A99e&ƛJ"seՈ#!]VkJ ,`֨V<ՊoF1R Y7, 8bUZ'Y%E*)x^?<\ ^Gfn9tJŎGcTn9yư*",}G,w<>r>~r=<+W%)XCk_Dᙨxv?䆿}9օRJv<, \=q, 9 cxʬFce+0$=ʪJ(hΪ(LTj8:5:iJ ^łgPЎ,^9VnnBfUՊÛk/^"OYd/j I$$,V 3k(PaJYC,(۬AqYk⍡AVGt o|u)R d 'Q$tW*|1Y,Z װ9gA4Rr9F[) D#wFT}D9`wȑ"Zhx)^O+%jFm  χNV+JusT5^~Tbزj tuRhcj(`'y_֗}&Jb=jBheEyJhK嶴BiIy+EZy-Wr/XԆ9RQzjոWJZ2jS׹DVX磔t8nkXX;mUkks2a!PCWK{(Z\sˇ4qpogj a36)9}QFV/S=a]hʰ珸v"zYqo`u1/w1*,y ~S-cbY-,>Č*&T/tژeT7^@[Vd"7aɒ%Fĉ1}4@;9yE&#LyKd,u;i|q_ yI&'Y*88:0b܋il Sǣ$ʍó[)_Lo (;+8<,YTt5kO>Y0@qq1fx6lqذa3g`gg.\6XG@̚.-.]Y̙RDDO?Ah_Wׯ/>C8;;PRR"TqÆ$  9 $Z arb|SNAN?ɨQFb)>~J0œ~巅V9g3>s{su^tq "7773ܘ+ú صk0i qQ%f(B3f{M3f 7nO9.SH+3 PdL@Um) l)>lH'M)v}g[>o}OGhLVob Zeg6l-1e _b"軈 Ӽ[-x@A*mӤBM|Veff^Ο?wSS^544EO &tl7&2Dl[{{{'/'1c5g޽x͋^oTEjϩߵ؎͝.r\gys'&TIls09Ap)\h,d24qSMK6hoLeX`aQSN +Ջɘ&%#z09< %eUqe'Hwڽs `ٲehiiΝmJkZ2N+,MT|gcp~PDrkΓ Jq/v+?g g9_⠥f4$J$61uNHl!W78JE.7(2 ǩTz;*50U1b=QLK7 _)?;w+Q|5`bHjϭote5ٴXPVTo֮]ʗ122Rݘ:@PS^egg5ȑuq30|*ښ~սmXpQ]FG ۩b*`XT༤<} vJ ])ѓ=ʅϸp1nBf275u;:Q O4ѶN[CoHs~!@J;ȥf]蔌'`>e s!N8.U)qH$>7~ oO:TWon??%HL%\EXT,,%8( r\J@2Pi$ΆjHARQ>%0htM"s> u )G 9J6urK0љ̀8PRu7p:tأ|4;r" t䎗{ )‰|98q$@˩'N8qN8qĉ'N\@ĉ'N8qN@kËIENDB`zoph-v0.9.19/docs/img/ZophImport006.png000066400000000000000000002175221415176210700175500ustar00rootroot00000000000000PNG  IHDRTLsRGBbKGDC pHYs  tIME  6Z IDATxYeٕ[{7ȌdVXŚ"- D˂-mCoz4`y~_ dnZMJEVkHfMY1{s#Um .Ȉ{puR/?_g;_~FD :B܊4v S%1B"M,x}ʓ'O8==ewwBsT-,ѣGG?;UU);;;z=.//9::~^li^y>{믿NE7|r% ὧ,K$ χ~ȯJ5Uh[bI>IIX%{EGM[кk(6 C8mW3Ҥd,POsM}0/i[Op!ED@dYF#M劶mo_.=,+#X&I2(b{{soN, Ɠ!ш*-[;I^EpDzYcv67PmEQ7V~ph혓?~)Oθ;%R?gF7ݸ_w!1  b5cYmQ]!n. bO;^>w_ ,k\+LG[~v{~ȓc~o5^yE>>9|^F677y(^{7|!YSB2eY2X.0 OszzJy!锳3vhۖ-(f<~=EQ1 8NL&8<<$"677qQ%qx<~ld2!cc<I4ܺuizx 锽=f'gk-o [[o}岰b4qAEn-BkJt|j@E&|Q޷TU1(=B -Ic _y^ye1`iUwuz5 Mihۖ(250@hۖ8(8& Ƙ$&E$It InAi#xGZz~/"Jo"M}eڶeY#tUeK:Ʉ&T!Ƙn ښ\I}٧YAnrd0`,dYBYZWӸ4ϨOP`P3c@ hh0V-Xii[G%#imnlR5 eyUUZh\KvD~s.Bp8p!r8f06s 'e>ȲQboQ'J;8n29?$@v (UDێ@ UUņ8gF!ηTUs UU1шw]PU=Tf$ nQ&4HӔ(v}ΥUU ~Ɲ{7ޜẌ$APeUU4,8 x @=-2Ru@jE4,n!c "SkVb;;%s{1q`b\G{56,I '㷞RuJ Kll?R V+A2 'g<Oy?b{g7Mŧ|LV|[ܽ sakcJͻهy*ayx7xieIӔ->~s uM۶ܺuU· $I7xe 9: 4ꊝMFCNNΰr-NNȲil6p~~ɭ[x䄋+BlllPן1R}+%M]ÇL&ݻ\,VyF_t`2ekowR%;f3^umiۖ|vxs=ǣ?eg`o{sg/zq MF$[Tx߲;B`Z֭[u !WB[GjT= \='Jd/w> 9c/SGOw~=-&*(0Rf$ cK:lI vv0_jPWsģQ([R#1.T.r~`H4uKc#0k&IQ7Ie +;T,[sz(ɴk!~YMƤG\~5g/ {mV5%i^_S㘶m@vL-+!81$(2(*!8{)}IX.ZW ʕK$#^fJEV+3<<>iIET 꺥*sTbB@Q.%:Z &UU5cE dEr^IQZVU'F4S/ cMSKX5l1׌ LvrTBxm1! !mġ5f[ԸS;.n1U+esNETD28"MS"#!HhıpkTCN\T˲QIk[44$ZCNsTU%!:4 U,I*"$IBQemJ"./ɓcr%yo {ef >CYl݃{  k6O@lVJXu0$6Z uiW8UPUKNϞjd{{ݽMFi ""i6ᘺ7Vu)|{{olooq5K꺖UPdYRصeH`d\V9sMFmƪM*RKAPYmc(ʅ̊s4Uu^8 ,+y<{hM6xZDF;q^\4q$H}16,K]KiZixqK^U!QDdqQV+QU$e0i188%W `fUFU[ȠcZ`h<$IxؗO{LC8%F!"1(%I{Z6K&=Mc]۷or){{{}}y$">} 3֐>xWᮯ9_ sv/}9~.rvOB[\ZB3ewO7K/̇b^7^xEgDӧOZ޽@?~1F{_|^xA뺖=·8G}$EQHsȲONZ޽=>|ȣG\[~/,AnvЈGla(p,Zm] ޽{DQb9K|p&κV{Y*th「"WK8%(ZTmb`\R\Ҹ'@wyO*q=)b0֢ZH/OpmT۽&Z3`B11&xNCںpwؘ1Ykz|ڽ>;[j$EQ?wÇɄ'/V+lˇ|Ʉ,N<\>o/\L%agb0gD)j@%Ͷ>}9=~L/ˋSlh9`vw6򒟾{lmG^, >cm^}Uvvvx<}T,Zoo;eoovyit6CөK9uYuBlN C4|ٌ$Id:j^GUUEAGDn~̫J\^^Z{.*ZuH>iJEzrr驼7{~k_֭[`6*bH뺦$8KLJy8::LP=wcweYRUu]ܹsdBYX,K:V1,p.ѶuM4бiUv^5$$5ACP|E9>DbZց1*g!Zwާh^jEU6ke!xZWcv=1;|1 6ZTmG&eΗv\^^IYj%!,[zr2ALĭLjUPE5t2+ځ% :?~Ƥ5 ' EԎct6Q]ݸcV:/cz]iu:\e@9ih8 $iQUMb?3NP_'Oy YwX.> AVUr$MSw/~㥿*YSf eN+r-Ey(YYȟ'_F9m)&&2K-3ùnn-iɓʖӋKQFQVm2yWITG4M#@HUUGGl6ѣG,+nݺ%&GX,uTUD$(RU̥ruuE]׺h4(Ayۣk@Oe\\\l6`\.9>>ED,c{{_~k^ɢXIEqtt@ITƕ*>3)+Uя~o>i?^{bbssth4GflVo~ȯ<;jԶm%c(f5cQ 5Frr)QiUUiZc"y| hC, )!7 pPFvwd,jҤ3_]t>;,`0"XB4a80gĎ7❪d1+VEOjN5[+{}H&\$P?a!"* &dybDcP.*X.IDWZ%WsV˥g  z_|@gWO)d]J 4FU"."JU18m,NWxߊ.WOY%"2zuuEY4m5"w/1sDQ^Eq^O(u5uUW7fgB[C$ǟ%I[ Kr~~V풀P5$y_(_}|, Tc==P~69gaY_ywIY{+/>sw٧i,g:䯽]<ٿ[{M<}fGGG8xWocIRv΋TMz{ccF!:׵YV7ihl1Niۖr5I ]Zh4ً@kK9u]SU 輖Y4|yNCvL;CQLST;w3kqxx;;;ܜW^{t)vadM yӯ\Zv $TRT6m%MҵŁx\-(~5=zyNE"yQ׆,STI(1:ɪ׳i 1|k*j j4Fpc;v ]KD*GX D֑DB2E$)%qCڶm5`NsM^;/&d{LlM΅%R/XW+"Bq Mr4 "vͰWD]ߓJQh8h~g}Z̯0f c44Wc#Ռ~2="R_,ozV}#cL'c<ŠđZM"+!(yת-b|cn.ZVV:Y.uښ@Z0z B%g]jR6-wjITQn0[D:#4M'eFGsy~|q'ݹF8NsOL{Na lnnsSf\\\VTUEU7qxx咀]vŒ'ڶ|Wy饗(X8z C舵j/p5[?q|VTLĎ}>~6o|ٞN42nj@h>^-GG\]]}O>ZUU[o'''|[bssO?Tֽ \>w1`kkKRez||,wz=1:bPcV+XF]eͤjʲkggGm[Y>H$4Mnn0TU%{6N\8Iz=yn; ]uz]ǟۿlnNгS{?ȿ*DqD[77)˫zqq!w?9zLu_79GQ7nն \gKX; `@-މ֩҂xJ]Ȳ>yaiڒ9Hr.IImd~!KETTU1ŅJWHZ B+ޗZ7L4nnQ8Z[)GAULE/@_ǤH6sҶp A`C[3kF"DQIJQsNzXU;ϩCf λu/7f\.#qlu8w.:<ڶPB 8):TUبlmmx<&#VEg iF@kSU$ QKgCCvvD.[ C&.˘ϯdkkK$prT=TOX,VX,X.Ie7k36':$%:(J58OEqi998|*mqqvvBTU%M,-d41_tYc0kU|Rq,Ūe<}TEp"yl񍄦ѳ *=yKkdKCP/u$QIDC@2%M"m0d2p]66<;ת^ᚚ$p'rqyiۗpMryy!Jb'Uۨw*EUMRXje5_jq0vqU]PQ$:4V,~F-$xƤOד[[[iUU)o^V+$uZQU]V,K\4dZkIӔ=Q>>~{{{ܽ{Wwwwe{{>wmoo!?(ң#y7ַ?O'7|eY3N5p82xF"Q1FO$kgPֲQ15uӭ櫪輙A]5mqFk#`d3\ۅtU`8J?_:r%f5 MUQ )q?F'fl+Z#$bPBYy4˂dyXıSm[|C(XI i[GD6"44T%+MZQ,qVH kcTUe<1*zu_C#nu&\k嵥Z ׁ7u蚍v.G9I"iBm>],TuAL&VvUU9I owu ,퉪py9($h [zk/h4[A #ꪥ,sh\LmkԳ\T$Ij ^I Һ)cu[D{VrbEt^rbLD#/~_R z]cyQ+]\yY+ n_^AIJ]1HQs ωt )3 [;zwY,?v {]+H]x:?nd:ڠZށG((=0C2(e2'gOhXeӲZ?٥N&L =\rb\!6qUMdTDUbZ?RUUAT=]C1]idք|>3z8XO>;ՌhV^'wo|#z${'ȡ5,eE-tw7zݸO`sff -y _^5PR"35{>jA2#2✵Z?pٛH/ONǸpŋVEш³>iʍ1K)1LsϭB8HO=~իȲ ~)vww-WX}4 ,ڽ^.\`OYwZ,ٔ "1nv)f$Xudv '''|k׮Zk|7G.)6|U&7CkonG ">88w㘳,6>So^x%K޽.\']N6ƠՎP5'''Hӌ.+("NvdXba;Na?O c$¸]5 RqZ i%ЊKNQV)ai@X"PHI[`8JleR!4G?@Nb0$1;,kHR J)c6t TEeݎA w& P"v HYSXUh}DHhsG'8>9Ž{H8+e0'fM,Iu|q0eczu cآ|1y D_xiF}l$(iv\6VB]̓7R@ՆRѲ{.7xUHjts_2 Y^'q =oJ&ry.zeA-~6P"ؚQ8u]V2`>9>|!*`wY{!k-t:=loc}m3f!#mPֆҐ}\OlrB4Xl+7XMZ+6}O>) a4V?z<*Uq0e!{ޘ> :T`Clݲ`!s^3:= MSβmC:0ZQBj0Iil.R<{{ԈQkKC>9|EX%x {)0Kܽ{ .] ,/Dm'?af8*Z-f3hL&(o^YƊVV dj\d,Z& x x^z V xqqyy\w*@!yN/Ac9o7n׾5ltzr<Ah_F^yF#s A#Ak׮w>-Z8<<ă!>6660П*Uew[V%Ge)SH-ׇRn ]HBUU6`1FY "/tM+muYZ[A&"$VL O;x-'˨j4N11KfH9NN0%p{GU8`"MK$I,Kl5ggrU Vh8kj^CЧ(Q`p@ eY,I nzEsQ'nfĆX {UpkUUFU ť߬K؃x|( eI 8`B-'NON/~?l-/q>1;|k9'Qs$ʲ(«k׮SOb6dq~wH<\$Ixiљܤ}x":\ IDATh2UUҥKKbRJW^a։$ e,˖e|GUUws:{Å V{-=p:mt]aooo{ш>˸y&a;;;8<<?=d2a5/S aa߷ >/=iRɱt rH2Dd p$e%e( Hk!{! ߋ9xLd k$a eE6 d>Vw)/KUvfI ~h$npr2+Ql5'0̡5\ڲ5ti)kT0FY(t0.cq,ӞkSLDTV\e"FYp]F5imXC&],qV3C ׶ӓ924_Aytrry/ vk*:]Iy"Jp!S"¡ ~vMր}Eyqtt^yt˜㸋V܁1!1B/AF>5 A, ȾFXr Ti S=[D8hcmBJIMpU*4>AY)!kШ5l7-e+ À۽<ס4l6a!*82fr,+F8{҇+%$ψZ}2][ob}}a:Q5M(ju1?Fk#mZ, ZV1)Qxgft*tppxg.\X}P%q )%5A@hu]t:sݦ<ϱb.ygg4M)M 5gYF888:)GQDawwDa޼y| ܹNW_},E$ ^u|>ӊ?Cwk/| ?C4?3c~}u](,Kz=t(}%h Y*ɁK (B #ǓjUS+!y.,a)]"rVfu]6Zn ё$ C+4Ocea@(]n) p`$ 80zföq]V 9!L\֍P +P t1L &)Bظ\W)hׁvrh̆&mE׌<+(Z+w 3IW!NQdjbfuPEq^dżFiCW_6+Hk{p}7)QzW$KC>${ޝa?펇LS>NNNprr[—ebȶ?#c\pȲ(d)x}}`i <1vwwQjyc(X~P5֚.4 j--Vk,U[ eY=hDkkk{gxkk f;9NgRJܸqſpMq||Lw;wp8;;E^mn`<9p8|.O?٬L;iGGG//?j`1v"7~$!섣(z4\R7'e^ `w$H0IQA:EA0KALaA;jLT)6TmCq8/*. 5qI=cpBH3/Zdb ˟3UFUXP &J1QWa+NSf* IԕV5*4MYk%`k!HTU8AYԨU bB:pIlqg;7,Vr}߇Ob+2~KtDF LvlFH1sePT5' uH[&88؃15H0 iksȝNEaY{! zݻ,Rtjq,fNCHC hSZG(6JC5D2$4}aCϊ5b(#`4 焼6`kt90L&4_1UU",( UWA{62$aIUe z!*] MSb&ؐ i!VB̆EAynמll",KF` A $[sDjd[ex2K1-H `cPi B`NXzHH {|_w@ƚȮAa}EP q<O'0h}ߐc'e`D5rxəJQ [FkN9WCk$#ӕH;\&>) / [cyO|=8/?rp8pΐ b=/`mN<lmѕ+; ]Rɽ;HӔ3/ӈ?'.``ob^QUUk &"vT5!2X, t-[d)߿Wb0ॗ^`²a~7I_qZ-DQDZk~q%j|5G[C1l6[qu꫸rү[Z8$8+737nшȇLO9nT ת l1,KҦ`BUsY8DJ U![T52G])c]I2g۱lյuS3j8"`S #AHA 1<ؤP!GF(c) 1F3𜀳A.DRӍlu%ʕJIʍW/]5{2 eYfuY߻6y03^{5z58\;leyήd2Cr73 Clmm!MS vYUÇqm\zӓSytt[n~J:666pewyo60t+W`{{UU|>7ob{{I( !Z z _>}Jz<Ç)D/4)%c@)Pvi(Ʉ]W h 0 yC@Ex)mxA1vt͎+q%ʪ6 Bn#nE`I3S~ dz8osKL')@Wp}J0>0 np15f nc@N7֊p' [K)jheǾF?H! 4lauUE^fx%"juOtvI,oYīn{cz(xZҴ^OZK# Ca TU: ! |G+0¥KWX 2Xr硽fC}EQ\ uE`H 0Z70~hfw\4ˆkl La&9pdP) WB{>u:-h]dX VVp]?QulA(f̰鴀f4b'?nݺͦ5xǰקcTUֶwV)a|d)]vQ*m`,C( k< BJ3?*p搏v5 avqaO[9 iaNQF8>ns&!˲$}Pșg?<w?& ʲVg)ִX./CKp!4 .n޼^~88/}WD]{.tn_/ J6O<×EE'x/`!1@a6;C/7I2JtmV GGGv}?x饗0\tN{"Eۧŋf|%*ʌ뺤v;0!8Z$'?1>#\ty*r~Di6FD>EK|zm ._H>}3#_e{9OKOÇ/=;i yc8Gť OloY䨈?w ? O)_x~Iy6Q'vDVh q$YpԓX[kk͡T<=BA aZLfۂ:T5~bGƨ 2 y^Ckya t"TbSX:>_N||(&gCtx ?RCMzh<@]i 1œkR@:><>I ,v!:1Xd+EΡHyjgƧSt{m 6z' 9 V !Za|):) xf;|8Y(;2*]ͽaTYNHs<ɜ'Ofc@5q]kvVEZל$ b Ƃɤ`)%)Uj(rB,FPfZnd0jM%ҥ]nwl}4L[ Iz5m ha5KR\UE S3bTydBٌ,*Gc/yS(p`0`n8s)=W &V0P`G5F'U} I+< l4V6K @&U0(vJe &H'I4qJ{ɱ-hWtDxwNK/A3zgsǩĎsy¯<ݯ2yI!1>Q}|Xyv6e32{p&LbOB?NӔ߿ׯ_%NeX@7t y]MZktDkaRӁ}s?7evA㭭-EAKϫ,˸+v| z ݒe^x T5 s pk׮۷i{{GGhZ[w[xwa? g?q@w}='^{xc~!K(Վq,K0OEڝDӃx.ooo#<>Oa{{8~bd2RHb׎:Cyc>snk'߷ݶ4M![}!u]ZŶ($IFkE0ub2"9֚y&f IDATપH86`jRp]>_ q}R 5 ]1Ȑ+r) 䐴=B5rpf;3$!Iɑz 34` uWÚ^,tttcI2V e׊oouLu`1Kpvr(pnvo} Z6+Shv㡔jҁ8BK)ww%ahDD\ك8/9*j Ը\r/_k:9;Cq7Eccƀө5$ E +}Qr nܸQ3Ɩ!|M\zir=><<:b?T(Pv\ {`!saUCJIA-I&i@֫=33:xUcycw"{'+r'IIzG**u:$`w5._۷o1;㝷10/ ȃz" m_qqKQ8mnnr zqW+  iC &W v= 8-4 µ2Qi%N igPFMs?/۱)c_v !` ydQ;k=S[)tS)2y.Я|᜞>%ۧ,s+z!lv'>_*n߾ǧTך0^f#1 hDZn._gKV,@J)iiyVuK~r^bҭW0lWB*My/I,+)}ܿAK)%1Wt=sHG`ggqc<ݏ1E]t ׮]lZBAlAբ0yy5y܏ _{;e1yУ=V۽.%IҜ 7c{sNNθ6LNhkAR$ć)%zF=L&ܹs-wޅR .\@⣣#lnnRONN.}DQ(X,0E8fk) Cܹ~{pcTyi֜cmm ~)? Fx<!~gkR.zՈ- Z>? 9!eYƋłN'ӻ\=w}>,?_Ľiey&ysZzo$@$G!QpDSb MO`O|pB68Lyĉ3C@h6zgA"*ܓo>+֔RdY<)B)+3f \'8 qtxBon4Mt=|߾.sǧ1Q-X3rqO,s,KIJEd6$$c E (K(( iA8ԛ!06zH0((1Eb o"cF }TviSY!@x/Z$RRaPVDJW-a $v{6|d!H$SDd5(%Y A##[H%X3IҶh #A@x>k1̆$%JB(bSiX0p}.JщbآK> ͌`F4,T( b$QaoHvvq-١(q}'|C,K*qFVK哧"N>F666) ebZm*GQDay$jEaMCsͅ&OSmFB0 ePE%!\"R Hc}-HE {.`0HH Œ "6Vu%l$p8J-yktI/![.K=^Ww5.4?g~=|;(I/kヒ  ?}ݥ(8jIw%ǂ 67ˮZ䝻D!us!zmlR)wU]Qbf&o֥,CYVysDz jZkx4!uMq|ۯߢc}}㏑eTS""ޤw}wwwiMywk@cOR_dM#_ /:_/b"_m8N=~~[KJ( 9fSz=dYhw"I MS:4u9arwzpDŁt0ω}< f"CЃX,V- uZࢨȵ[(T: ,j1jڣ&Gk,sP%(A]k,)3 !]A) FB)sIQS̒q NB 2E@c| *=ADR@{b߇ h0C5ieG巍"fF_ss$15IR,XFY%g$45LFmP/Iqm1EQ FE3 zCxC )zQ׮k8ZkZ`[ӤN@l>_PkiaPw5 ڴX\?OݶJBVg{ר]/!3\8gl*"(645"jESh]2g|~>CY4p~r OW<>4ES(Ϟld(կ~Ȋ `#w.JSlLjt(H:@:(fG?ςucyi*z8==ES| GHdI#"<ϱXpvvƃ>|;[X,x2'? (hf>?~[[[8Zf;߆ł)8??T ٕRJ}c{{ˠi82:::/-z=<~M7 ܼuhc4ˮpzz#zzpwdOi?G?)onnlzJY|5,i0L1AOTr 9 ckelUa7HAڅF透u^fh4B__"H%!eXk&Ii/4MLQ@T1& RV_}uBeXBBP pp֐֖-e\tQVyCؘYaAK2K)Q5aZ$Dݔ LDsK(ߣ*$8`CY`@KИaS")Oa9DEuSK*t/d$;PJ)򃈍AA`H (σEE-$L.9 `0xJ)ZIwrZ7<>mcعauYD+@XMPG>zq|7nܠo"9{hxIlPno5uMZeAֺv%skFdi*v2Vl@ W1n, x]b T !HIRZRU˂'6-Uk,Yfev6BvrirX600.K Wn*ʤ=/Zt^+%__k88˖Ϟ>_?!kZV,IQ$,Hj I?&{= cRx)]~.U I( |j= Ƨֵ݀HEQZ$;R\usn֛te!c oooS|pp@6pl7ږWGaS4rW\i2$AǬC]&GǏS%>8ƥK( *˒>뚪bgpsyέB+c{{!uY>D4p Atu%su40[.9r&ml9_[bMM~X,ί̞!h]SF0AU+"V咫0Kِ aeIGX~CX, 4`69Ҵf|"%]vk]E[,tÑC:kI׬3Z9eaOQv5׋qqyu]r]4;;gX(MS)K!2ܾ}Iiq%,e6 Aԡ2BLShV.n` $dc f6N酕RLQ"b !``,Y:Y 4m@}\ 8hl%k ߇ 3Жa]6d-RCD  v!<Ik !\bӰȡ~_h @|J v1?W}ն YAume\?UU!5' {MA[}6xn_ϊ^9]׹j۸u}t~~xskB/!chH=^KX}A`ww~!mnNx:_Y9b"gIQ %@d$TUgώ J=dY"UUb0`c(|ҡJ aG0?Q5J Mh4\z1/90DGIVjSSKG +p)RJQg4v HzT} ma h\"|'|[kZMW[:#IS ſl`fOX((MS= O8}[Y[۸~n\A.]~XBc4Eaa"MzgϞQEdu{rIM֍(dmk]uB'!u_:D3?Y3,zPq/TYa49- ȗ"@IRJIx'f:v n'+rl8gbLĒ-l Zpڸ\ Enق73xl˗m =,S먪WlW&e9~pxxhԥ12/?x AeU4MIJu]k7 g-I ]Flq%y[l4r o{{S#OEa 1C\Gy^@J'sŌk.WJ"ckcX-!Q˶ljɟw>e-ܾ}W^nuQ*x}JezK0BjyFWyL-\=})E/;5H{,mNNQ}6677e$ WUGGG:K^,fL&{.n޼k׮Q rݫ?\_v wޥV눏>ӟҍ7XEmu$eggg%yM`TT.G?wPʃei>!2q" mnNe!hc3pgkܜt9;; 1i0L(2uMAnJG'v{,_h4hߓH`sf!! A]߅DLFLD%\dԺE9HBZeK,sR`Xi3c >Eq|R4+ Ab)& IDAT4džt:Iz2 )ʲ$IXkxjEyUr)򪤎z('? P=|y,Ŝ9N(~~7pX,KX._d3 pHc͝ $_H1ڹ_>3[*VQIɾ,s.+ghcaDc>=>hk~qڕw O!vwM2P'IBN;?~\&pt"nuLWR6Bn5dBRk<I$ XgrTngId Jsk4I z=^!-em,͈UІhqiyKd J]k]!b9ͯ]oⵛ1Y)IQ85Ga "BTn޸ʿϦQT5~~>|wy֛PJ>=z=\CA!RJ79{_tН.RR䌁LDDp'ȅ3yjíy\YkV+IEg'ǔe0dFE$ Xhi.5M?V+nob4p8l G>K)]fQ#Ý_û2B/bK.5AEaq>S/|T|"n#Lip4MAN.RWdL̡¸u .Kf3LSq-kh1f붚|;1~w>z #4EakµX'N-M!=q ) hB`0i*hPJ D=FȪq cG%qJX [X3n0a3HeZZ}*pnj(mG( Io  IuP 6EV,J+א*̦SL lJ顄|)_N!/筆 rms5Pkr"JU^/8@@yocqBM`1}9\G;}υw7`8tӆFW 0Dm?]2,,9? (}0p$,}(JH>${PHsT"F;DO[n_m]nݺ[n!I#.˜꺀Q!sAQbR0!I׏o~{89=bs?\vvFu h"3f_~O;s*w]ֺicvttVE>d+xj~$gSQࣟ&\%u !<|$Ia8tx^xͮ$u ǒ|~yV¹ʟ8>>R:Ef<7۷_#f'|7vt~9߿UAa0j(Hj!C6];tdQ5EAZkNQVO&#Bp4Ϯ}Єt[|rrBLJ9ϱٚ} >|)%,{{Jҥ{yiS+>??Gi8hXpjr$ԡ$ 0 l-H3P:L]Әp:I fV+B ,Ws|X(R ZטN8;)WuN } F0tZkM bW摵UY!#8R,[!˗CE!F1S0 G[0AQd]5fq c*He!XkM>I(%X*M4)%>FI!s6V+ΖA<`ϥѰl<-XmikrۻCyIYEk",HA9GQDEƂ| a)J^sj*fS70,7x{s<缻@&W+* . )nt %$/Zgcm3Z290ZJfs([[)^y{{{BIQshvv·ׯ^w]&}S1$8( ~.^Ⱥ]z4`e"v䆒+ Lj֒J8S l`Z"$fag,`@, 7ͩ \ Jx !$@2[F7û5 0@!7b2`/|L|?NN+MͭrY4,_qtQe z 2'OЭ7˨}0 1 n܀/;aCۡ;Cn5$=XF\!eݢ|(͠n3,acaA$HHbaۻn>3,VI)9MSXkIp'I 8$IE+T5e-f33t ׾5i 4 Eu]ej,`=z՜1]tN  SE~T1EU]JG-33RdXvJ)Ni6} DsZVx11@p8kǏ5ESV.}ܾ}{{{=fRb<Z2 {t6"4NZxZp0 )˲i&"~:_~jUU57z=z!| w<χR>vvvtzBƳlL{?{3y8 J wX&bf"fKpnEDYּ1$"p~~u]S+r)Ȃ pB:?C娊`ԥW |>c<P```&qׯzWH3EZڷ~ GJ)HB$-nuPflL+Xwi]U"8\L,k$CʮP+n:Co ԚZ֚V%]FX1TW%UCmn'?AD1vֺ VP;ݽ4ME/1%/^0\~Om\ͭh>mA GGG8;;CC!kSp>8x{تvq쪮~_޽{$A4NX504eyMQ)L&ܭQaww }\)w7ob:bgǩ;QI06)H ,KĽ[[v a:-mB75-"lO^ֶ.jH7x24k,;I"V+(fY J^pFh,'!dϞU EQR.p}n=wlHE&G KTU rUM8! )<R1NOAsA݉BHRyqvp(L¼ٲF+^^:`!$@0u4;;;' ֲn"1g}CxköQ?֏lA0FC4ҳP@gÁ6aؐ-Zg:W?@$RuMVIl<q߇1 p'(iV "GSx!Ɠ!DB5TϘr~luJ!dq~v&H$nzq ?g?#0QW>Mzd$N+f:3ߥt.۟u[ۚ]wgg u)TEW X-c8Tw͋_.̮S o.r﫺^$} QFפM0ReH,G(uZ e%33asc]0aH[,Ǐs>\nO&{tzz,*]P$hфZW:?|59DAvOlҫ.A}}?&l2قK11n aܐzd;٬SwO $@XbAL4,!h`,y !A<9fBkZ;)C 4 WQd M nN'b8ؤ\k]-@i6?oś_/y&1z~by\y m;$rIjEnГVf/%=ƙ.5ʲ亮)sEN9]Z ͍̿˯i[mEU2L:*ui :-buӐy`raHJ)tYLB˅\y\vW4[o~y^umnnhooy֚!߼y[c}~8;=.|?2]][oU޺4EK棴y XMJ5kW`eޏ Wn&iO]L^ʪ2.kyz IDATйsg\kּTnڄD`o1xU *+7a^&f睻 ,A&ͰrEyټy#-ZT7GIxUgMێT_WYEz3&$oƽUIy*Mz `LWbPI"իAzܫv*MʸL"ZOTF*UYWj1(2)/)2@:y+ͮ:$,P,F$E~`~u-h׬Y ۮٰvZqVKhش͚5$R&Xz׼Iʰz*ܳN;y_3ǴiZKǐ4͛7,6' Շ^|2xfÆ <2x`_UbrZT%))LzYzӦM&*a=U`ӦM(V~H9Ue aV}r=JyTTM6y`Mӡ|Ssn:Rf:oժUfٲeX`1-[-]>}QYY>[hRJKK.\e0Ԝd.9q{CPGy26۸|ͭ+ oNFXv,c=F_cZ𕨮ؖ(%eߒ,H&4vlR:cdɇ;B44VK-ڸmQz4%\4s'c6O׃ 6~pUCs$zT?rtlFe_"s)I:6!_om:2g5ng9*/BĶD.5F6vT+]{hެ\MY斬+I؉(4ؒr\9/}yKeb=:J7ǥ QwqB6y\6O3[qoW/5p[S_JՒMݧ,B)=rjk)u%݊SiHeE2K.Rqx*-=nTnT\,d2Tc%SRo>)y=RUr#)}8}T-HuN|*%ɴ\,9_MJR!2].>HVȖT\$"姳BG. H?Yu}3$bv[q#{ޝJhZrʒT-XTaj|z'euUK.rT-H8T8/rz-%ͨ]T^avIح8u{'~)ɯ>0!?NW>>fб6K.i}/Y"im6F"-Vڲ&15 sS˟ g=В-kidj=MxɌ&1uf2S'={^a3{썏ޛ]QҴ#08Kq/I>E^3 Gߎ>5Aڋ~fs5%oXdQӦp/39c;A\{˟<0c^{?uIՉOy4sݸ1瞀{瀁ZϢm_˒#Yk \ -#\[x3ƘI>"ߟar_ssf{*R?+ͩ IZrM\ht5uHr8Y(9< LP_yȓ00,ơvwɲv:͸{^s@\8vȽ7y?xYb)vᆵWܤ* ׾OC/|tnNӬy)^`%&>w獗T{O]R552{Ћ|oiAKYɒqp43v\䭿cg0 UL_{콿Sѧߡ{Uc̿z^ؓm إ+4/wsUy-JB捗/?g K^uZM5O{{7HQHd{<'LՉOagW'>+G.ֽ-$c==wh0Ǟk䑋O=y=1O3?УOqW}^ \arai 1]-fg n\HǸi!u 4coOj {}(~}39~q=?ɬ_p䮻][.5m ޞZnNrzuSi-!sŰ$yJ oI5aZ jHyɴ\$"(lɴ+sy*K.R~43A*K.y?uA&?zwz2;&'{W~E*/uOE2\(_ q{ty"/E2K.>/\$BիOg&$7nX|?&wO@A%Oyxui<:k=/kIZI;!-HJakSFOYdv\iG#Y%cȏW7?R}E:K<|ZȻU{]l1W`ܯ]w~uhs'x;gb2ܯ_ԥ[2s`G,]ƻe ;&?^ oUr zuw!*~|raƑ:QL G2CeeiY՗7k67;5ngZǼ3G^51Nאw/G5YB.ݣV&(h q( QׅBCcp!fQ25 Jq뻆ٯ! oVnTuC>l߂$% ˻+,LBN4uN(ͫU6O$ۈ-!vɅ+'z2G[S޲BNIA^ٓ$$F(ך\9Ȥ4?{ B) YqR;͒)t[ƒ-'}14DMS:L-Vv4\IJAi32IHh3eҤd =9[Õ;/6@~@ }(.,mZժ}Bn)tuPw&βOy=^DG]o mBBY䊳Tq(P46Wmç͋lj,Oji[O6jͲc# -e'=gB =4i{{fQH2WD *Wp_\v}uk2/3Q߀„bc{dE5nMڵfszMȹZpmK,Y6l撖نnOZZL?lJ'.}\=.!3nN%-$K,Ywmc^zSwlzK erq};i>>/ɏk%( #b;!gKڃFRϨd+$ePU60q1y\SCHC}Rۧ>6>W|S;yX!/6x4kY# wT5Yy$^#(fC5uUیWsK(wضhإ2%nu 0f{НDޤaWl}u T:` [ޗfi*hsazC"C] 2? #¨}FS l?دշڇ|K"]_[je` ݵ ;wFBȥmi.}\e8h%F6}VDyWIƨ렍怅 D7~wr7l[Ʉ`6a!岄bhC>J ճ9ʡM[QfNQeC$DzfpQQÝhf.$#[8<4apO, (B4CM+^vC#?6^qHN(NRrP'=#C ("mQ^䔖xMSQ.Yb$3b[Afg*Py ρDih7x$vC 1KLcR #;؈XN:V.3Wt̔lڕBmLI(Qe\=1{icģ 3Гaȷ,U\kY2x1ePG#`jI=B`ۖ(m%l)J2JTdNft[r>C ܗǤ15%{l!? d1m3\(>9[撑J/Cyڂ#Iu]$Ӥ9ơoǦyLyel͑lp9ġYviI\eYm?iKuffl=8I{@]K.Kqm:Z6y!l ܦ$ iNB$k(2͊)piiY\@3nquF#ݺPrK+`3b?M?p3u}K`Ƒ[;nZ/vc,P #G%$K.oظV( i< ɔKsW" Y ^.^RWD90ʿ50_=jF2'8=+ztKuw9Uq3k"s6f i5q%^ ϐd(#tOX6miٽ#ֺ;" !#y(aƁZc@ aŃ p=Th$I'54 [CȇeY绔fLh=Zv&y+vNI%!&!4Dve7$)nӶvd ۵ilM9ѡH$С5jV,]TdƵ˾_:D"fPXhRҴJZ|t^2w?6m4^m,6;vB LBiDx"6< PCm !40 !dd}Cqyٞjoks^+1~ !&!l,%| :Ygn;ڬ=mu='۫iTJ!BHU+aI'It5;wŊ%qz,[pEVq箢4k^+Ooޏ @jB>h^Z[a^}j,{'S_9%My4Eλb^}P$ehѲ"$LNBZ(+2_H4k4S)=sj>yi@6*!dLB!BB!$B!܃I٦apBz0 !B LB!pi4KƐB:9c<[isB![+nUo%rB!BB!$B!40 !BI!Bh`B!B!$B!40 !B LB!BB!B!&!BI!B LB!BB!$B!&!BI!Bh`B!B!$B!40 !B LB!Bh`B!-DU@Hál,̞=A!޽'*dE$)mWǦd>Lc={wB$̞= ;O}JFȈS.}ڋJBd=B!&!QC߮NzwoSS^qW1W฾Я[jKs/6sfsy8gܯ nWXfBV6 Gef_g {ۮn\fuNzk׬lu?{̿{~l޼ oyO!dLr ۋ!|!\{cfM~Kߞl[aᤧ>+zLB F]J $6m ~+#qbE7qW#x豬DBh`B!rReEKGXx`t֬ B.W1W฾Я[jKs/S^=Jzak|o)=ߒ듟iG썁{༓ δ88xfikk劥p~mп[ ^{>6_dž%\;v||סM Bz0I]wݐ:i] w^/?:w>6T?8AxWqY`i;&­pNnjW ăc mҺukXJ</0?L`y/6gf>~Mo>ϿϿW' KLU?oĠV< gN2?^|F~Iпk/&L@ իWFQQ<H$`͚5/}5lu: t.~tXklBh`mg֠I hnCۢmq[ߟ\3/[DAC2D2|0uk3uўK~},nH%0iV&\ܷz #GW_oO>$x iԔSO=܃"0H$pchѢ+୮n(l,EkVrJ6!40ɶ/N>v2v:c~6?Thױ&uG{cyx{p'w”>/=z49c]vwޙo'v ۷ '%Kdoݺ5xm۶ŀqFwyСv}w~5g=!hٲ%N:$o 52ƏaÆKh7h(Qk}ݾ}5FFۃ IeƎ+00sſnߴl]֭]{ɢ;J0lP[<,7pƹOðnjz|Mzy?餓p9/nݺ+q믿'oСCq 7`ҥ0uTkfd6 ?D$ys1b x53!_=upOCG@II3EgoѲ֬Z''y}c[7o5M/>ukVSQQ֭U1e\uUٳgkƌȑ#q7O;ѻwo w$8 yT >(8IH<LsN ѲvXr9-v|w"g! <{-W⯷T5ϵJ2})5>?ԃ+ЦM߿[=z4f͚uzNfM6Eee=:ww.]'D"C #}`y: | /_yWjmb&d UV8#1a<8CвeKV !z0Ieq}|>޻5Fps[Y楸;p5wD /gqڶmr̟?cǎwTwÆ ())A&M0w\\wu~ '+q`e]=!paKB9`Ҁӧnv\uUԩN8tܸq+бcG}8T/GV{S#%!̑Wžu߸kqX9se'hpԨ+YaӦb32V~"m8sUߗLB!BB!$B!-Z`BBK˖-qI'o[H$0~x 6%!40 !9l0H$uO&xGp1и$&!" wƏ_LR0a RV!40 !9d<#H&(..??%!f!iժ{ք|f{,\&y^{".]|E][k|ϭau 彌]k?X*m2}!>U&37~֖6Y XdtVXù~VX骓Bd WaIXG|n]ʪѩ1 LI^$]ZS'e#} b{>2}bx $_0x*^j |QyQatra3$(9m:YpyEm,\QF--S=4S!q)n U6Q?A'k\t#A\D H=>FQO7rSAse* {WlKZo; iM&PȌSfm󼛰ٱpOS'@Ч V{x=mz )P IWz$&\-O'M:3,}.\kB}'wxkz*;˃jj x$ޡ8,q+CQ; Q\^^nJ u9kU_K,-GV-4N( aܞ۸Vx0]<.ހ8z:<;$!.P֝46O3];w3\}zMcC@8÷O./dO]>X(L*3ސʊx+~j`AW^^nesE_֎quv7JBYd;OM }%x"{jt' Zݗo;>`6KglsLBoMxea]K[z*aK.{]=~&[,of3 UN#&n/w:6FZ3xնOϔֻ"Q{Z%4+>9?`F̐?xZ\|E?2vC\1=Qtc轊.Pwe;y^2!!C~{k+s|f+CX}nS=f_wo޻e0A~EZ߯ s\N~nI^*/q]̵}zA>`Sܣ?툽{;L3)gS͟e|gңE0x9n57`G컽Ή2f3lVX_si wKرk6_O&k\V \!2 ϥa錰G>"L7U3WUZ)_UԵWE@6}+-coi;"ڿS?!}ʦmVPkuge:Cd*B?x-J(ͦ|P6P;ϒp䲷Ɔ˔; ?s( y&;Eߞk02XS'=kVG_[%g ߯#\vOqWЫw.io?\k_7˱fgS'=gND;ո~ԈC(i,gKvNnjW ă{77-76uKښ\ҰgCm6vux:S$yf "+2LN.|ɽ3X3EfcYw řKy*={~mS<oisL~`&+?o?`}c3ez䞛0sI ;wN_jeЧao. }sߧaK:S &ƿڻrOol3?^盂ѷ>|I&oe<ʈi_[ GǶ PiJV8a{xmNI_zwEEEyYj;H<@5k\V>th_)1W !1'ȇxVʐ_"vI`|^ dZb.$8j7qP1n24ވcaeW4gukۯOxkK ׅ>{;od|?zo:W܂V.};N[Lk;}0 ?͚]s֕lK~},|3fUyhd2Ґޡ"Åz*9AZD"6lZha;Pe7~4Fj)҄kWNY0c2_Q乯dX6՗4i#[Y zJij2s˸+ WU,ՖX|iW4v;v|ߡ.uh%:v`i*~_7;)Vʆo}})W}O=z׬yu8aP4,$0z-^^om¢Dhi!x/,Holi%I %;!I`e˖椓NB2:,AFDǏ=XӢE 8_4¶! _'wR#)QS5kK%E9x2 IDATq7CIZ[M蒙 "(Mz<hBָzH)FiVX,~C96vb7u8_gk[axa撌w2hjͩsVKcsЩKmצ]6)iZ/鲙f.Zn<,wa[S_2ct0\†E-DZP}CJWW!Piٲ% D"_W}d?2d6.xx Tu쎢'\&TQz1u4p,o>{00K`|' 9gv5{cy/3j `S0sqz}zd??x ^~̽7D/[_]wO|]oX']={d}.+V,,:u߁|-W -Y/_8V4{K/]7 vE<WukV6yVg'W?AQV{>۶˥kfY>dzL9wNw(Ysbƻw?~ Q A*'L 2Ĕ+W@rYצ[mDWf>^#{?ože )(cW)'_l#[y^ /F3e{05-ఋ;6.olA%K U\3pI 8gB洳/"Cwd8dջ?ޙ68 }Va՞GCp;{ϾoNh^29Kʹ)/xN| N| c/F^i^,|mw;f~oG&]:2/4LooԨハLzW\?qSn~> U$s]iA~f̓Z?޿7hA֙Mh^~l~l5`mu mv;R>CM5y'*ȱz!9 I $ģj2Iꩶldګ'@9Ǻ!h>UVGL0'C9$hٲDd6VO(H<׵*jnLzm|)9//=Uj峵p$gޙ#H8*w-6.6\SO5mwA33)ƑfR3cV~%gOmڸ/<9\=;wߧZr<{"UēI1+=[YWӟ$\m/G~oE>s~1"|7zU5փLԝv%(D&+ 6=m),k~{\v;̑Wi KQ$t^uxɌ:Sx+nj7;{}fɢ1ƴi;)g}x64+l5#kJ*Pz ?jCIFzB}= ȼ6od2wejJA=Ҷ6xl^8i5giVQ4K_?B0nk(5$:4axi/MslB*-мEK\8v\8vfY(SBlSsN: 6m^Mr@xo4VRwp#[iA Mtn=jge Oj`J&&0֣G`wq,id>᲌( ȷh.u%9+퓶(f ]ȟ eӉ6="]1wȩQfϷ+"oRْ$k/99rXu ,]ڭ%S|.JܭMJ`xkôznB(Cc%WR8mAP QƦ? T$s5d=쐏t}sw6qݻ#ISIurez`A0kx$3Ւz$ |WR䫿&G[hdB:JWD)-LmV[XEQ#Ywm`&븼t*_yy)/mg;4\O!< e55圫'6l뽵E[ ZTq8R%/Q<.5].ҐUe(ͳ\.[/iF2jCz!e3M/1 ["]-ľri=h.HW>P=$=fkoEz%\&cHku'l6s eeiqi9"+x#gCwCA.Ԯ hl/*[2͘Ӽtܑ$z)-k:g򄆎fU)P芠@u)GkDBr]R^Aޕ(_4=;!|JBohg$p@6 .Jś:#u?# I_̀9|xLhVsǟ.:u?IH}qJ<Dn4аw7&Q9P7ȽF'с/fG+jpw9l,ԄƑDD9pϒ:x6(%@}@gHb˥#de{3pY+s>S9ݟD!dxR!lt&a(z+h^,I{H>=Qޚ|2 ;Q}xMcLsX:ϔ{oW;i>^ySLo׫wMz.D2y\6k:WI2h(M(ؼ6!!lO׬%] ɆˌBUsKx!F`iEڲ<+AvA6fU.w9ѝ Ҷr@ K^2%G$cdؘw zh|mI$2tAI~wP҄sG#ڃ)umk7Pi ]֤DY &?K>רڸD7Jg-J˼U+ㅙKЪu&yoDJDҫ\=ҤǝfWTrIUo!;.}DUA*@ 3hSz$wM n-)oKym8?ĺ`i`ZrQK ,ɕ⾿fF(Xz5@!T eK JZ]7 r]?)/ۮބi<fnݽG'~$IyޅlӦ̗N,5dK3&\~WqNolPmA $IA&dC9m3$e|n .,=} wIֹdtvfmzhÄuM)MnP;>=whOטѫuM~})ޜ:z;2b(  ehPkL%DGe4Fa: U6=WpWsB{J<=) Tڭ]QkiCiJ<.JG^YiyV.B` 2ÔzHC3;vMeOdf?/#/\wWbYfUߤAŊeXfU󧻞f`6pa'MCMH KnGjR͊_2>E5F/B}4BEޮ{lD]N=1{,̞=kk@h,mKe./Xߣ+s }ZBy.Z'̃ 1ІΦ4`a]d`7UilYt4D]|ehirl E,WofcIqջk}Jje$Ќ3ofL!.{Oc44[}J')M1lz;TKQ^Q)[luԑ zޱlIq߷/dDɼ(dȋ k٘墡>4m I]٘گ'rk;p@TPql@ ߻l֖3P,Pԯ=JOIfyҙk0w;G*6+&J$zRUWOC'.aQ \R#MڼC.'wt/k׾.=Kʞ{X]JCI?L650oD)aQ4rjD+87kgQ+k<@m^t]^Oˬ.CޘqGX m(-pnϤ)$53p z0z($Mo/dr@ޒ!)rX c}jeH u._zbX(u~v,ܼTRȥleHC{Y\ꑴCabCdx`1d53,>R 6C $}QRs%O.ICz/p3q]Ri?NoC!?z@8k@{'J zKW ϕfbrgt€xA&'z?l[ԭuk{[uΩ:N#U*2M"QW&4AwHr\]ؒV@p%9, R{)",.g nJ '!5TiZɮNߛS--,j"%gho(ϤPwM29~I>K z RR*kQ拂H;)Ѵ6;C/K"y./}I} H M4rIDATB &ͻr2T\}1(_2co@f͵E}"iJ/J`b N}&rݧ-?ϴC/s}[FP36e Qbq?V?)MW̜®ǘxLwxܡYpnF^[is>&쟴|L학{M35su)G7җTм(]3핞3c>c+sVVf9f҅쟴ѝ;49d0Ȑ{p߳~N+IpNܘI-mh7dmU6N\}烤%-9Cc廥~]׸:CeH}6Bׄ^r^0 y(=~QȚ$b#7i{()6HK*!ICk) .cǃcQO JOXN~W_>Qѥ׿CKi9:u\dGOxK=Cb3ofnZrB;Zv7رC.S+y3o_y_qIs5e-59ޞ{Xh;}uoɥʥ 1Qk^eUP#;2\>h-?oBލ]=ݨnڔk'NHblҲxR֥tΉq"uώt7A/9!?ϸ;jկ-O|@[`{p­r_>]J*m'sI}d $Qy_N\ )Pgشz~V[?qmoKpiݲ9'œMkb+o-()k=u觺+t[A^߆fť.9EEuupM#_6o-u5N9}A"QUX_Oa"?aI$CFTO:[[ƞ|ZBu쓏?9V%e-rm[793&w5nfduߣjU5Z1^ZSXXA652wyî]׽KWv3$hHp1Ps!.=q!7ЊI}43'PG30$̔eǑAt>TE>;.hT)I% YJ#~2l3vEOS'XKW>d>diEZC$C(XJw&aIf+e)tl[^eVf~ƞE__wy@qis/Ѩq []pV_`݊>g<^֢4kx~GRJܷȠjd~?xh<vr\JX(MI$EȤPWB978mKj݇"1_IOb#T#gLRstEI(5X6VT&"*j\QDB}%AJ}ΐY&o<ñX0a 5ɶke/)Oi?R]cՊN?U˞gs(f=wרL>;|Mqjy;p=?fKZl1JD{w 352dz//缌~nXwۊe|O.K|x8e<wi9A)%Sw$\6E\X]ҟEZZqzK3M#XZMKUUDȨ'.RP#EUG1,CH=47o() '\KрPh&HWq7_a[&z57݉[CvGv#yAgq0>\rXt_?nF^=$wLSY2d WxjԸ ;~ ?`KB# ķ%$E]MiATyRR}cꌻ!gi}9e%T[ՈO9Z|(2Nq\]Ȋg>&7\& Y~ӷY`Rz&uEI\/% C4q+ 4l-Oڅ"E|عoGs;a|\(g9$Z|aL,VJ?0rEpD> *P.BOEƖdݗ%MBrR)\1zb->yh-N.lKڍ-5Cڊ|d~aƅ3A"=R3nIJbRwH {p,|I. Q[]4 cQ\+# IvEMINpYԎA-(a;DAH\}mٓ,|[izoyY@NDQJƷаc(|˟Kiv{<^#Icd)J0l _iCzHz\b=);'tCl@J# =m+WCЁPd&oB;Cw}}/M\~o|}wχ&AzGCԥ$=l"6'؋AH48+Jb"G fӤ%d$,*SNBʎ.2<_p~fc $\] :io\A_l}y "ْ`G~61h^E}ὔ^Y=IIḶջ I:+߸)Yv.5#-h=.$:"1}_(іn\@_ԡҳR(/qjYQ\XsǵvB]ɻQ!H'&iexLCD-,Y/Մ/:2NJʞszE'-^װaYA d+AV}wDOREfX:7߱PHPCW&AB|!|ݻC@i26-->ais!t]'7M IS6>htE郤I屎莁-rVfGIJ)gR./"ml[蚍 }_ Q1nAaQ/Ǩ/J:B+'A$~ SvY&f)kd>%Xi\,c-YMo7g3V1k`\9EnHӧdFFHlP,4|k%3s;i j/Z бzGQ)Hh+(ߕoTjq)rE(6W\P];e~?U1m*>ҡo^_V㯾ڍ+^Ąq'ݖigXԧv؉@+ӞoWg/3?X4o&˜OKГȜ c{ 'B\C|s}?lH4ʆtwQ"! jYhuϪ9ڭh'~Ĵ{OGI?hǹ|qmoeå/q;fڄf׎mSW|_tzz}2m<2vm/kX\L[=uczٺy 浕^f[m祝ItF&S#-ӞO8~>5^3Я8본r]6Zu4r_Mju_&tPUmNUԯhjN;wɓ'5j[+kL6Mڵ8͐)N]'qvz.}&E~kNC"v_p٢'oe0DRp+׊ e"Z:$^]:`Œ\p yKt>ܽr'؄?,&,[Ri5dhsX{E~&;7*..F>}PXX+腅:u*zf͚_pd ST.@KB[)?-%ǖ!)m[7+hۮ}6}Ǚgvf%cՊЫ0cmfPm۵ϴߦݑjdGiYq׿٦E7&̸~9E/bo\Mpi$z`2qcL PURR /EEE0`ͱTlaa!O=zYfl>&[a\R@SpE81`|ϐ»uO~){e#G-sZd4gY3U ;RX):~sH]:YzYVشq=q4`ӆδ_\RlGC|#ퟠWоq82M׫P"GylPlIC,VQURR={ֺλ#,.i |w?}oC'9{r0]9 \/iͩtDc19L~66GiɅq^ >͐y]4}Ϩ*ez{oh.9;s1|v9=;iwC0?S yzF޲s'9] {Ϩ*ef{o.LN _yV{6Bs8ٌE|q!.g-&-'_%׸`yfs5ӧOׅw5j̜9SWVVbɹ#M>]9hRەڀyCK9 =O0 9oqfHdp[5F5af ROdV>NE.)&%|竕KyV~Gf/o% OtEWdy\٫sFjǶ_Цݑhպ_?>G^_V5/=?9dB#]KI٭( C#B"NiI ' :Č3pΚ5Kui$M!Hji _!KX *g4Y~RZ4-xA'j!=5k/A`Be2¿ƝcO2^qoZ+t6=Hش96-uhOo`n8STܺaC2~t]ѥnr=/Ǘjc4]5.sp6Q&];rEkd252t-w-1q L{EZa$̞5XwZc 1;,dc##q`A0OlBtn̙3ٳM׮]uIIITKs.D&Jv+D\hYA<mCXFaW>2%d$2 kcL+;\Y"K<%%M<[(kIA{@~w%;xbc˩v/+wg,| 8HN#O}ǖ-_> CU E0JKK,>AWX+Ta -\md4*j^פ>\E1`$EYTq12J҄qh,}|7WrRRB]B^$>ȉXtŅX| Sם9o-; ͓Exf&E<׮ en 혝&}*/xHcgIclLB􁴬"}/;0ÌOY̿,0])MC.`oSc_KeP٧3__z跆c!nlr_}}x"gɀ=5 ADʕ!TbY05I|I,ٜQc#G .TN2p>1#/N!`V-[+u(! E>I=,,"ZH7WɃR}h2!o_qj|Z狞ulҷ_WȾ]VgHBٲS "sԉA@|+zrHʟ "MP?ĄAE$%Y40Ez ʙVQQQr[-H%u9iKr,ΔgcQW#>f#IR)JbIg,pϲ|ܷB)6[טY6[?]3@v. 81HnyշEQฉ| /HJP/\>ҹ$$䢶***PQщz$| cƶ?ed !;h HF6<4ž#|}zRҾj|[C-.hC6 ZIENDB`zoph-v0.9.19/docs/img/ZophImport007.png000066400000000000000000006176641415176210700175640ustar00rootroot00000000000000PNG  IHDR|L,sRGBbKGDC pHYs  tIME  W IDATxwlIysNwaow#eE`J(!,#Y`#ɒ^ce-m^ůlAW$’vwos'u8?tOϽ3U{ngvvPSO=o~=7h|QɝDDDDDDDD\y],˝ ("""""""FT"""""""".q$@ ({ka>[=X h+c9:,\X]޵XĀg/jibmԏ6-EARc<"=)u!D>y0b_UK)|b} AM X܆yr^Jpz(ajƇ6S!)Uy`%qg*-m@˕Ð9W>ʏXnĈYTxJ_*_aedr ﻬfH );0&Ud{CY\q4v>ʒ32s1`cs>_8dw]5} {^>L3J KU+sQΣ>sܾ󉪴JGKĹ+ 3/U{żCJȺs \+ Uca1I.X Q|Ax|xB!VPSzh;`~v|Az /4?7|BD&(VxX,'O9m< 9C3 a @ cC߇'|,cЎP^틯t,)-|V[qF5B&n;ŻW>ڶ 6h Yt|aQe6 =+zs.Rh ޺U?;:C5C? m/(ACXPqd&+@ PgW>Lxo|Űt b#"092 ɫ+!>9!rv2ZKqUoUACBBFnVa߄*!?ˬӜ#yss>ȘKi!_ i heAA#Dfg AsQAfS!93/ o* E4B?}d q&Va`B2}zBgkmA⎯:w/Y*tlCuQ$VKE_nDDDDDDDDE~L:Lp*>̀]g >$JS V24gi;dnP%J2o|[,E>W !VX:q _y2B&^ {-d=d!c- Z=$AqhQy2|WҶE!\P% 4|kbZ#nHFucu q =Tr- rBXgGa='ݪdE&O֮"@}0CݚKѧ !n֐k42Xe؇GYFU* U2BKakN1B>I>}]êTj[G]B-*G 9br;|+׽C|ʲenA  ţJC¾jحD*}{Zz+u(eGJp.E:BTוQ%ϭD/D9|ΡQeˆ0Sn}}E0{}i\%ڧ(y\i&֬8 \ݡa]!'.ε-$ -},X} UFhB//!X_RQVY6KVU.dhsQsSTUgVcgY}֠cܪZ{1֨^}lՎ =yTu hU ">Ax*5B Z}v!ERlGT]Ol9U% 8a1|ks9ly7{u-t|UkZ֢ S;w Iհ!2KDqc) v 4Z BvxJ!@j<ÞRa/U/f 7<ݡzr|}R7L::>~aua4.9ߺ!UB-SS%\w 5U1֝d=WF̵#|K؊Quag}sc)br*G rA"z@l߾T0BCs*><ް<{(wfNj@zU9j^݇K?:~07wYՕS*ʡ*oaT3$-f4pB¦B\ 9z`BI<+LQP.z}\V4_װmECʘ]M8Z0GVjnEO*0adY1I ]Gцa(kH"\ˉ`W|Q+e.jb|R;j^!B,Z|bfV&`aZl0dy D>$@;4$c|Y)ú1B#B^ߧLo@LJ!i~ng_YBO O^QD:c]MY?_WޅT@ :cjz5bSץRz6tQ{"D3{}Q|`D{GQG|"hGc .0pnjm۶dUygXY5=0$ys`JMY+Y[u*}3/MdG"l"X/93i#tG]mM[SPkI3PӧʎV6 KlQx ):&dywc=Lo0=C7<=.ԃ~-˻51:Ǻ<sd"R" 7L P($>ŚY5\F_vyor%k[Wq9YbBkEC]oiߠeO_+rՠZ|酸Bv!z[h:D,_zO%&vnCH`;|AXI_{UثPO`M(xw6 :*Ьt7}R϶wV6զ횴] /`hbMdM$""""""""ܠߥ[ LfYf7*K(mU|j.ۗ#p ڃg?ۍse7!a$ =$BJo?VuV<>^P9_׍=p"PB,Thb? 8 sU 988 Co\`ac'uC #_GS\-ߐPeN 1\19QUZʜ Y?B6.b1 ώaUv0U-2H{XlH2(8=Zo璶q8 SqjH/90LWR.+y7e0c0\uUB\=̲,!B}7Œb}8p}]ܾĐ\|)d `7 y2hZg-6oP=c1\)9:PA|!hA8 DmUF5.ѹBG)=2ʜj̕K7Rh}ocVt]>c=[!/3֑*\^}>;_Ĩ\U|AB`uz(- 0!36><3LHu/SFX3TYv7hA 4Qc`|@U|w-:H)BQC(f caj-7UvľIAڰLW]|-~J0BR֧>>ٙ6>>džM U֟ag*1*Z;}v1H*Ueϻ-y ˍd)|ǝᱣ  ]%.9ͅ|kZoBb9[ˇ'}xXx$SBKmjnoArj1HBl,UMc6TxJ de$Be* }EJOC!'G:-;P5?e'4(no}Qr1A*E*}hochCBx*$ :$s =W:8?ėUd`ȼAJbU!#([>s&-/((4#Be0߇eDey]e~T#*}bY(H j 9^٣8E|QI!t.hRCsE%ĺ||K!Vq4Uem•XVrίTQԽ]I9:ʱu#U\%-~/*8l?hw{xm:QøcB6 c P[%'j.~н[rQ=B!ԅπ6S m6ʼrTW!I@ː,N_X,A+_+ h# 56"øN}UZީMa "|O& =ˆ|5o"tCaf'#|6_Ŵ?LF\(uUƨ.Lm:W5VoFUj_izZCv늬BCx!(jƭT֕oZ:JyzJg\ IڨrMAUL!;*;Z]}Ve}-zu}[n\=+1a.aI0S}c -5 :zE Or$'| G{JQE*|TIփģ_Uee;]aˡa>+J_` F /dl۠3V6:_Bt X{4UDpg cuƂQap uGҮj*aT䧐P5gWE CD*eeXqCFDDDDDDDDD/"""""""""*|*s‡?Ƙ8xe*|N'q G6k|ϙZK O?#G.|9S7"""""""C/|)|3s#""""""".9L6ELЈa _DDDDDDDDEIq˟h^}߅ܕjksvs!sK"GDDD/"""R7H!t#""""""".qD _D3ko, v_}=֬ ]x1N;LilؼmD*pS4gg֒f5mW\Rgz${{L-K!t8wk|qN:u{AO8q O<w-KQ\yݍ} n[Cwx{itZMc;H4_Zp^`b> G>}88Ea5ώ!"xbݦ-WmN9њCO?==S26l/~7o`v4`c\y^ 7z;mܲ_Joڲ>DO۲rnlySxsT玒VK+R'սB;y/đCq"DDD/""RƶWb+{M:=^x,уmwrK_8 ηzE1giו[w^}+!%qtX*y]g-)&߅Xs5o\?35cY׾Ym>>WWkVz ЪVwn3pqغJ~R<ĉXa\5qED<-|!-7Ι JUBC%硯~NŚ/\ީ+cˎ=\{mݰ׶_}q"ED<-|!ZfXO>-\} N{w]cl|sMZ ꍞu57Kc|fYyKH fO.>6~oy+Mny T(h8g^6#GZ?6o'SD3q?m)u?_§8u|>meко'(a?/A`c߻-]s3;#SԖgkYgIO8I֜乃PV#a_ɔNEwqg>֣ k7\6?6oLBED\p6&Op%Yv=XS'o$ly% N;Lđ820z-[w߿ne?2DIS<1˶聧)p~ly9;'uUuB;Uh5 Y)OvfyAX~Y0)>/""Şk+elbuOR16W6WAH5\{m݉Iq/bˎ=dBjv_ɵ7޶gHHvqn}ߵYgx+cR26sWIu7ΖSk!D% k7l^ !乃PVkn`b^?.g\qsٺ =aely96NgězYxo>H_G:qzÙu"""""Ou~x[x]EDDDDDDD\ _DDDDDDDDT""""""""".f,݈gb,ZUDD3̥savDDDDDD%q9V !""""""ž uѥq#*|Qወ _DDDD]|}|W~zR} }J\tTw\t>tT,*|CMoyy?K] /qE _DDDDĒ/:廟o˟߼mm|-zRO;/ǿd1k>|;gO?wCw5m/ʗ>k}~>i֥|_5E]??U{~'h5gBR.Ehg~?]w|D*׿Eٿzk7l=٧O7 IDAT',w\!B%s<7׆ϙyǿ _O>(~wChK} Ku?~Oη0NǛow>Rל Kݻ-q?|?^>ƍ{NZ|9X}O> ﺛ4O?>_?CL?-/z9XˡO>?Xs_(4]׫BPyw, ӯ Kݻ-.VDnDDD3;\{:lgO?g]\1Xꚭ;p?oxU YL{yH}{{?{zsI/o}~t>Q-a+g>}~2BR.E o_wN O~ǏϟuR׼];w 7ݷln-xw}η?oY5 GEJ9s_(4]mxk{h5iԽKbx[9V w(q#""">w]t+՟W׼~%mU._ZD|"Ź /"_< YpMo9yχ?s֧*:Wt|"EK /"_<y>uEGOy7_s޾޾E]έ9ӯ~W7}<x[%E/.("U۩_w^v|;~+oޟ-zb]9mwu7<џ|"E /.D8|`/72v>(0낞ضr}p>>ɿmxo/"_\|q1#E3BỘ7W|,߲}7?blu.w^_7E/{ /"_D`bG䋈Qv^mx/W/' ?^z^K]sMs-wu?ϋ>E/I|qZ.w}/8?}ᳮsX?^w_?;}+}͑/"_D|qťw1I%{\/"_D|"Ņt7{xo>H_tH툈K7w^[[78D6qよ{[֒HE*H# t֠bD!)a1tDA(@a%RZiVuF:Lk)I'AeA! ,-ҞMxM//6mcB~WxsU$츚{vn&&k֞9n%l)r:zS Ωfh66pb|o|;{=#'[$|n|?c`&jfM\[;^O'/?abFO[t:5e;eQfӧ$I2E%YB*,#I$J)JIR-EQ` EQt("@Gf_c)l[):NiO3@0 +` hJcPB"mF5V4ЎǬaZh1H@b 7vG*,2SHI37̴sTHI ۊBz!"GJA^1mrt^F!HRGSF)!7j$IՆ4M)BH)1@JIVYZGz"@Joc%,Z $0==MZ=NFJHmEniXQ@J|q0@$ S@h4hIIFJ 9MEYikRj4ѤN(f61;G,'iƊ,v RE^G3"$)4ꖍعum^X$B4<N'ٲ䅠ܜavV3۴"dXab*ڬx2Gl3e`VF>+ȧ[0ۤf,Kbbb !,*4ccYմL4 o'`u$!%5jԩqjj4U 46FiRFt0pEQ iiuG*Wzw˄w?wB.9s^$ (#mEڂ{SJz5k H (JP˧KYRbѽW Df^&m8nZ V#!"$  hknRYk1uQXdBaDH$Br*=ݣ˿];;v)(|yJhCQNbؾ9',R 11-A0alSdZ^q)=AIZ9ff$z:N;0jWUlWb֭wbr7IwNf8xSۮblڴGtSS2EH'Yق䑓M2tY_6ن Ӝ:ɵk$R'Uws!oA+SJMe 4%W2ՠH4H,׎s}$"IG$o)e[a P kOW6P cўE2kȭ?)6NlgUmϽ&O3otgk[;>$Ͻffn*nyYcS_{'grq켜\DnY#Ơ aWsM/d{m S(s<1tAlM6Jj6khա1^.rf I"ɲYRNB:Ev>YDC,StѳX,{ݜT+@ 9Ve{J$QʜRNe%RQ1>8+賐9(%T.HT +"Q()Q*-gMsѮ0U~+F/@BdwbҵJc`/ycBw⤝޽ZۛVl6uծ-YQHٳRyh#-NvN)k%kH"k4*󅣛ZiK5A:[*{}\h9 H[Qv!1*@*+FB/PƙcL٧w@]RJO%,IGaj`RӶv(hKKGJ [C$-(49egAR$E'eKc0 (,$s"R,tK)qo*y딼uVi J)TgnDR`:]eZ5rCF(ZA SrSFc-J]4’Vѡ %4NtD=, o Qd>-X2 R0n)d_10 1L3yT3?v^y)~ɗE OҐU4i -em㩧sW.'u?zZWC4;9 ʼnc'YUneێ=q&^P=kD)VLRI &dϝk,7aʺ4;-Ԝ,&uL 5IZ=0mmTHƅ$B--yVbK\dFXĐn`%R&(JA)9=e) A(RA3$I{ Lsc@u}t_uyrD[egZwyN^8wm؂vI%$I͹;E_ Z.E*gwZTY4RfD`r "r"=osV2I05EV(%"-j|p\.SŹ_1}4a2)Jv7VT5i+EVБIMʅXa0B A IG%"Dž7h]*| Dܰ*E jH!T)fhZ_MI/UZwٳ,]eٍ^ #.ԍk=kpI!z^,Gn_Z6̬L] ȮE#q<0Wt1(.-p%c y֚h nUX]-M2ϏHchlnZ U ˎPH,)A[r_`f[o}_LOk4O;x 7އ+k߼v[3&3mlC݈r?Ϻ֯Gx:ȇq<}=ס;a0}4v,jurc4WnIdm~ |=Z-(8s٪-Wp9B%LcF -DXlZ3Ǝ-[ŷw_44gQ (~'t*;Tz Ŗ/P*JʰJI k(,Vw`*Ԛ1,.0E6IL9B([,=+*ɔ'IEWd[Q ҭڔE"$F%tEIХqAp4Viow5J.4I]]~*|g 1dYcC3vݻ^ !z=e2qV4MKeӒeRAӢiʟSf;A@$mX]8k(_9BbUC~=}VH2N$I o#X% k{tt'u.4Ytj)%rjMA-d\V([ХFjDQX]ޕ,v1  +2 C:ad #R!Fm )j,Niؽ2AH5a\>ŬttCܻgYbX-F;lW^J[,dT T(7 Bקa0VbXCʵu1e~LKL7ZqXݵrRҢmEghpaez`>fT./%X &w$:gŰ-?=̔SXp y [6lw~7|>ī^y7hf7]s7p O>~O:[˩cSȎhv8ivoٽk7?ooM98ab+923櫯ogzCWsu/?kj0{*絯IMq!&taCc ;nnN夭3ei5O3Udǐ-Ff|#9̱</- >mgIVd>8XZ=E/I[t (5i+@9KWZ.-k-AjgvJ$\Xd;&A2T)dq{=+nXaK0DʄDgicn "$Bj6hk@!lBʀ5Z\^j|`$d ΋No;$O@1 ORu1)"]of ڜ®Қhf4SؾVkcL*Ս.u2۰)p.Y댚w:,/Ljp{i&QD$.ghoˀYqb̙ 'zsRHZ#5 ZAg¢&%GYH m)4h)QF-oRgݳ"-J$`4ASF `lFcMTzؤgP rq{+uݨgs(7]yIDVriԿݪ/Ɣ<*X?Qw5 !P^҅;&#$|Քϗ m5Z.Z"0pa/"J*$B UkƧ !b=KISRr \7PXkЦ؂DJw2'E %RtjeΝLٜ.^'>Gw46m ?iVmcW[??f$Uvc9ٺ.4W۟ r{JK-KS.\ '،. QHƸaG'56nI4w{INȊi;IĮ[֫y=y7$m%>>IS zf j>0sͺΩStYu4gG  IDATC{OƗOnp l}dbĮMYckz-6_wL~|98yG0>Qxg"OPK^]ea)KDqz>u,эW2Ⱦ@nf 涅[$#wI*$U 35SFښɓ4֍Cr<#)Nnq *=} .IH,yh{Ym4X')f@­(4~M-49aFAX.6ֺ27Li3RPUzJO}3݅W5i^1W {fQ*v]{_dƸ~u6yѷ&IDC(|Nj5kXuVVY\ N^ΰucKV; xJc[',e9Mݗ u9%VұeJmtDD 2 v 2nL`0dV"\,Ζq#ezR!= ХX(V@#FXJv7B8T+vf"Y@ߋ۲J2oJ8;Ϻ u=[*l4)\3Lm_ )Q躊_E^΄Ǻ1HgB:EO[TZMYu]-;іBR[*[P&)4^qZOX?LTFeDaL7\H҄$fuD EDv֣uH)cs ذ1af7ǎљKްIIZTGQP؛ٵq'UڸSJiS$H6Vƻy4:/8<3aDƉ]5wYx2W]:g}U9c?`+[cdK-z!6oLsE]5Ht -ź|9Ge߁;EK#4a 㵌z $J1jIXpJ6ӌzN=fj$Y$k*Ðӽd,|VZ)ch:+ ta0v6 wQ_ 0Z #9ǡgm`_җ6ʦ5ؼv#I&85uӘ&Q3ט˯gOgYUkЙCg$yƳa˶{۱֍YGl޴Bfl6{ISj n/gIkJ, )vXn5_}LLUhsp+8F%S8pl'gOE*kV ctOQcXue`1|FY*q~+wfĢ٢u4uk!Kw%Zƥkl@bmaNKQN/,y/ pŻͲ ruV K6eRKgǧ-]P1%] еFn./s:⮼_ `:~pt:e=WٍҤ̐vyp5)e5(]-@HT_yaeَ2WqNt8e$Hv>)ct[' BJ$e="[W`Ek$ X,X-g\u( ! I-hwh'vZM c:Dyx\ĒZzt]ZK-4ZVK˲yd>u["&_( \nqhYG'1}fb|O"@aָP4H*3H  paںxRSZXTʲD&@$-\gM(c{*k2;;1 UF#ZA ;s$ֳ΢h;7eX}LN0ɛsdPL8P 5`kH_iZ%-b.ƣpNEI I/_wyeЉ|>́ObdYӤYG1y\*}{Yv-cرm㇘ޣعG'ٴz3?Sx۶}+\θl0wzTqA.׷J(6gM0r}\sb>ݻ8mfek4qfb5lJ4xuWc3cN &jkE2#lپNaٲe;?vFZ^9P9IHHQ|~F (B.]kD_8˕_klX3ע@e shw0&B*u4RX5kaZVӕhYܩ,XHL ;n1Νܣt˾]u+D g75k$2Gwʤ WPqvEh-jeY_cgt:P[Qs{|@hmIToTL%t-,I5W>"ZBY@X"0=I*v5i+I9tZ'B}#d.\TPZ,yYG"erȆ7_WxR>,,οۿo sWqSF C6oP;|򓟠qF(wg'6'EEŬ8ŏ$g9ڃkFL$FJOdAhБ2m٫A|iCpNpB}ΓC-> AMb֛L5RעOV5ѩ$AU |(TM-Q'H%hap CƦ$}}=5w64VHM^IY{mU6”ֺjU3?}>sD1菈)<('e1t n߹o\gf~ Ry1Zk +p *jܼyD#|:>ML>kr)nެ=^vs&K+,,Sn)+Z 9!x'o}?}}tN]8õȗYkl9t,$1,˰DQs.{ qJ-ɲP4hXϬUld<"ip%~3]lwhoݛ߻Þۆ"tHp-1mr\3ec&a=ZV)2ad )rXAA%F%=I"> ZgQBTW ᄭAhvD'l)!TGn C3HYi"+M8Jhb{[i[|~q#Li82-,'#k|(OPVF1&%QSwP1>Nvwxsm^/^ŋbuC3D(JROTı]4mPb}DN0K-i>/Q 控.=˓w;\|!<,ӷC6XYY!y>_o}" qZ=ϋ\:q>@<έo%lu3XlpCıcaFީ׮zC4S#H2d.DamI'.+|5si—5>gm$'Zbqfkwq*uN8@XM2 7WrM ovvv쪫Ӫ!)=sMvVc*8DPH_!? Z@+pƋm\-vwjڻ(|}_Tك{z'DxoQ׶ A'.jVG1mzmpQ7}H)qLQ tyMtǘ,lТey>hZL .w>.IQCl-N傸8LD:Πe<945`E=DSvJ -sE$Qa2aoo4Kc15/ LFah!Q7^>+*Ap 2ySoQc$auOQ'jfT#+h5jzx_ !U휣~SzA#*"idiĨߛLH9OaJ]C)XnH' $E@N,,ʼ*kTUY8-7vzwswkWp8xOpGɲb>Bl6yb-rm|8Q|)zkyg#wB.jھ#:"=6K+Z`k_?3?ӟǗXZ>8N;,/}٧1|K'q#BY={c ۛ{Y^cK4>z93mMz:g$_,]|QQ(C[(WMN] Wb%%B:El,ҩE>954"}(^z%8nxqI |#r F#e^w񽐾Nn#ø!ML߄eXGQTey(b{CpC^ԕ* ΐH$ub%\BUJkT2!QxSݧWB7y)O;[-B3 >ݑp-u8}єYc|8j?Ix1p,'EcL G~pYU ƇXGRGeCcG" "L!Ds<}D |w}/ uM}( Zԉp|p< lLyOXjR8:<^qH]01S SG+{*k*vrdLY ;MJX΢>tC׎{ct> ^{kѠl"ZTR{^Aݣ ނ"BiN&qhRTa+E:_o"1*imQ $?Sr'I3`w '/8xA^{1Ր~~rmFFݼ5a8yb'/-<fw{E4 [gC65UUq ko; qb4yY%dI4dQV?ƓO>kt{344\(Wq\\:FWf,#z}d~f[ pzϱy/~tYsMOfu}èHsHW,ܨ`d ""P.ԭx5 mCDyqL\9H,cוT֐5ڬ %X_F:GXips‹pq3{+X^^bqqF#?D'}- {E@88F%.oe@i. &a1އe!OAx c?KN2%Ї^Ljq ^'kqQU-B$ `OPi_YUeHh ͡' 8BzKc,y^ck6d9E1XWwh4XW avcہ8Y}PR'Dw{6 C)Ny_ZM IkJgżQ]J ⑦)cZJ@zua=ɿ{w48stǘg5r*8 N>r,97|G-mM>x;k.')0^ 7IY}tfc|qe[od $ ] C_Bw {y嗉+++^|8`oƽ7X^vWQ* Bhmm T UU{MY{xpruM/.1ޥj"2WX" Z5c󀬱or)^~٤?Gv,.k{!}}&4] 3NrKyJSTN'cwYI8ˏ^ww6>GrZVMpA@!nĎm Ce0`7ݣl/"M.z O7pW_`U U\%VX lXOBǬFjb"gTE_rN3vq{CFG \ BbҘlBZ0G,4TD  B6FfDh"iX8c£&Δ%ԥN k7m8,>:nS*Ne.3%HGi Nkd|0ЈZ>'(zC8w+^f>ʕ_ppmKtE9ZCMoᜬWP+zdU'm !*-* g2JEgrP=2B. wB.(ߓӰ1l63Ê8nXJJ" p;}:T&\$ݑ QOVjTqh4fm6WE9ugw W!@ dPܼM6@J cUaa7S0tm m_Hub? X )%RQ{ndgP9ihb$=kV1=~o)\P-' Ei07;z!i+ܜ9&lA`D:H-ҍp\*n'K`j 2$JkT##-cZEDk- qalnn̴)'kC^"i /;n_ V>;."'Ai"4UU0ѶDgQX#, *kŒah,JZ-Jb;T>I_c#djx!L0Ǔ(.@3 x:h zXU\cZ$P'jsJRU ]-Mz:x&V %U^EZ^Z4c:fރ9?2<44ҊX6*zضzbPO ),Kڅjh %E%;x잧sKtKc":6zht+%p0J bZ+aa7wS,%j#,3J4Ȁc-<,s9MK>ʐ?֡P eH=Zjkր{ (/[E܋;Z5ֱRN1ʿR!myLUHtLDij14EnQ5t1e]ݽ\' 8f~v0gcme>( }t{:FX zFCJM>ޝ˜>u g2;loD!vc1q+~D֌HZ*X$1YemV%4 Sx@hIOq<9<ll h)8~I>DmR>?}2ge]G<,+op7+߿ͨl" suQAO٢5yum Sگ~Nw)F9vK4H~:yCܼ{?߂4aOdY ~S?}o,4 w7<أ97p0_COd 8:*u;%N4t!b-t,]H"j]mF AHGZ9Wԍ5AW5&I oINb"tc] !%-Q`)7n?w%[E ӄ{WFn p{FD[3** [.pw``&{8PWշbm`pD3II1T4; 7ƿSʧ/pY.>}3D20lg?N#~닿E-ٻr ~-6.,gϞ F4:M \|>*#Rֿ7T>ڕ:>Ɓ!B`c0u"{*b8 Mc+q)5H7Q=!8z^-k?Q+IC#U@^"ELp,.y}l,:W?'q9 t\)tZSäC߮w1%TxME-#Cl 2ZuL? Cr{STP?`,92V4hՓ%"b|;4Z$F}B[B*|sKﮡH93l#*%Iڤ//,2d9)вt ![P;'p0\|c1 M9o+;'($WH%ģlÐ424!<Ũ, Hh4? *LU0\ypow9w_A*K)yi},-&:ucW/n237 nE$GhO'S\~3Z}DG`w{a"i6^nZuE> Mk!?ȉ4kD*g'bZk8979D*5;zeڳ9ͤ#WUU¿"PDړĖ Q2YkLHA[}T%)a=BNO0O!CC4^RIwXl^9%Q&7av ,dߌI${J S |Y6ϟ< c=Jf&k'3o1Ò(ZOZ4[oд<57-0J$XCr5b$1 R3+i+B0a:w_d=m63'| eR~(F<ܿu|Ǐ[nz|\}5y< s"oݦA8 MQ112xO}WZr=-.2l,-.`CVϝA ,W~3wYp}#|/:*\_/}{WoӐ Μ<'h?q_1o ܾqd1f{oPWأb<3d9 | eMJ9oʂ2!pa%J+$[!G'[k] ^#"|}@&ajG_{kp^ ī&%;AsU: }bqБ@GAE:yr9s<5WRTGDQ4t|3*BUiuRSVyz537v"2a&ǥɹe 65Q/W_*KEBI؞##.Ru =@M5PZg*+i>u5`;7-x[!҂`7~-6N+$aIܐŖ٦d4,s~퍟7Һ,vHO#A>pH9Ν8Nstw&49U~@$%%Ś>,K'ѨvVUED(,B?N4VFIh:$ea'SC?~x? >G51ceMY<~K'yk\}: 3˜;}OfH32 G䃒=F4cE %l8w첸)Iıy{<}'u&>UV/ns,\<ޠǙȌbwqk3O3%I>_ nٙ]?ÉS'?8 mft}ڳ3$ +KKqBow?t(sA,R"Sc}[]9>̳<3Nᥗ_B7%+Z?g{ -HSf)Qca"`LO~"ڨ^a$*g»XQ PVCSeQS`E&"2+ 40>4TfQGЅ-C08_YꕏD+ ZQo<;8%@'(aQRb=!#k=EQ $V$N(t-B`50aks[V@0R;on{ IDAT{.*B'!Hc8c177G>e- z]6s3NR).GhEhhZ{ᐽVqhkk3gЙ#c0 r&Y[377GeG{JMcm1H >̇yWʫ?>ȉ38zi fwvox=(_ΌzcssiA?nKy'VHc3zp }ȃ7nK..ƗD9N,if-vkȒ`*kC}l2x0EJkd.ssK#{c<󅯀1Vd=aՇ{gB ֤2xDGx/4Z(*AJp8$NBpCQJCG՞4chH =T{GQ0gI 炏˚`M'/+2FVu3F OSĤ-CyE |QZi&PkI3MH"MURA@ M~On]\.,`ȪV, f86Kw=a"?s1 z:RNl ̩NP 6.ˮs3mQ8E>B+Gas6/]Zĉeko}:N1Q,i$jM;(%E3mxfFK(e͆&hD+f5Xrܼrc $ZS9ը(Kw/ 'IRff8EQ(KKÍh7 Gza+&{fwux! 7g̊cGk?En^3Wq/r7x_7xni[|0eyw 6{FH-u*I!:f/ԊBH} ~=N+:V]D"A7yWť_zC,~P:S#NV7U`a>_ DY>f0>[׮['Go˴>^<=;3{~rgt.TԵ _t ULGưw@'IQc6T8ǧ!%zBX}Zꝧu dOB,e{s9t 9Y} 0 ^tH6V5mXQE:Q#E@!,[4*|]!y @Kq^A R֕*Jẕiݚ[}]^g9)UeC=[=CPLR v֬zݺsN)8Ô)ZP,J <6y'7av5DJD/M##OWxP&x>!Zs&FT}E4=A3[N>ooSb[6I@ 33{-xs/f³lnd&DßG6f+ ߽nh4f\u\H̞ ?$#$ CZ4ĉ' D"ǵ%1O(lMOʌ tkӜG?Kۗx 6.Gn &EImIIBmx<ׯqztLXrzz  ϽHEE~5~0*ɜn2gss ѕ-iQVI?>idyN4VHrNbH{9S:t:繫WiKv6z5׮_{%֫c,崞"FFgxiPx>^a:.+<͐(ڲ"z a)_ ^:Z1 g i1ޑK$-ADbM/;D 7.l(軕· N(2iBWa]@RD u& 7X %R Xz\8$!ȟO?~B~̻qzDhB8g1Ia4Ƭ= VШSFXa|1UJYIP$HҔ8hZ%䊚b'G%[|P jF֫81j1V;HCȺ(\=ih.`'WRTb͑ u$.*,fڄt`ȶ3u"D;f{ZQmFa̋ W<#R a@u}]UXL341 IC.8|xA6w@taĽGєb{j3ӭ*gJA "T5)zβ"]k91~'|Uz?3U ˊAkG ]*vmKۖԕ-pDŇw?Cbrz>!΍bI!vS.-JHf5 P`TujjT, bZ$#%M݅zUC@Q4uRQ+E ulMX^6q$Z[Ms$KPBahk)KBbU[8't{lu b!"I $giL ]0)~N#^L؋'l}{ĭo}Ž)w?Ggxw>%=<Au4GTe«rLg!\.`rs.={n~/Ou&o J=|"V i)["F1Ie9EղwϬ.gaca,fedCڢbk4ҳ70ubp~WFp:C>s;ccvl[Sv!]5ړg9VJٚgʆ6>HdDm-:\աTDcV -~١@ekG&G$yX6 ; b-5sRE3~@0!\0Yn$K RH<+%zr[uVrַyj[}YZx  *v^21TtV<{֌֒} @DEc'[S7q"/TVI[c|W`nfAXAAp0t#mWCRb?宁nkUnu? XRJD$Ö0oi!(YpKC?Ύ}1݄^nd`1N0veB![PZ pvW^8Ŝ][S4]Qw%t4tbL-%&bUUH8"U:VSjck-ֵloo3ϩꊮVU/Y,*$ZyoEEY"BFH&2ágd4sah/_Ɓ[7Ex*9x?k"$4MK߹#n\qC.f5/̿̈́$u-_I1*e}OIDJ|iOԧ@%=IV'Bj AIpӵ1h(aW@bV(DSa90-%i+:1v <_pDU(fP8"p҆*T` V_Wmkóp0zG0p==@u K#VE1fɻo.yZhf3,R~EYp-s *$pE|9hK|KĝϽyxp{Pv uAO~ 7Xq7%~UkNpϡHAGuPKtW*1MX[&r[|*åׄ9 ϢSAӈN9+mHUTi':t"sjUج[pd7rxPHV] xj5xDV \X"3Yk=^`E4̭9* 4Z: pS >1[:`Y9;bvz~Q`YMIf:;ܹ 8c,=ZPU%u]:OmK,I砗e]8: Gٔoȕ$x)K(~24mY7OX.\<vˢZ| ftpRe g DR f>Q?$D Iq2 T1 8W^oŗYmې2#9#9}QED,a9NiRI)bvI[E099 `Yp8ǔqW![^6 UTG}8!]r{p>_ 9Y̸pb''|=.߸w~Bz=.]b1GG,K8#2NNNbʜ^m=mppqCKwʧ>>X?U<#vU̬ kt)0ȏR<]1 T:(d FGctR5]cR+AUQ (m£IOVFQpOo 鞽YO|?_ƲuA @(^`[njT.QdH|Eܚ!Sq$:&޴IvNDQ ;X*Ԫ"&><#2R' 77Iļh77`sGn} IDAT9~g[K8޹/7N[_:w~Sxg|o^ޛd>o@qX8k2[UlOUߜ0?B?}&lԫm[mAxrxx@Y-K{,K2đ N'?f{2%HBK"]0=FClmʆS>a&yDI{jMACjqFumK۶H3 G(BB ^Gh9U](!7n ZhSBI2*@XralKׅ!e][>$PIBUUm*}/WDEdIֈA HdT]WoWRO4|;[|W&E]b!x|GQW?e_k_p&{w,ϑ 3:k!>|ySgF#&ͥ  #M&5<{ &{c^ƯK0: e^W|N׿ʯ-C1I)W U 姢  O"4*):gT'Z(,_DH^DTt`ZHʶ{ ࢃxHM1mR1Ni`c4yp>K;Ks bDCי&'SF s~ƶY\)Nb&D@qN&أN_lqмIxild8StKꄡ$ :-d*J1cX'ΓwQHXRⓈ!RRZdnP:e>ihCX#2v-emMH|ެ5mݲLzB@x:zHy*`!FdBR3Ilj#Rp8:tLfMRpNHxaɕ~@r÷ gCnѺ- w`Q¹-Xdy_Ҹǎ0:IV)cv{ >/z3ZO-6viW.Mn^r~/l^Dy>yq'|"]/,$I IVg})eK ӭ[0(a!]D>mR5mkI6ZŨ8wʕgX,dxN,?bX0~F ao];'̦J,SU dep`A.bi%I89mN'sU91b3Wck59 ӔcLHtG Gdž;4R2H>86, t$jr%84)YqEykHcϼ]{!棏TmM%ӊ~)%]aZVT>YڈΉ] -( tpqc;Y[)-<'ublZE|T9hh;7,6#n~M ck[hk쿋?=eX0FTEKzύ^ Gu:;=у<~[F y>`\2m+ǞkFJI4(F\|"G?ImIK#]q|tz2wy;xn\#">sbؿ:)tAIRcҘ^N"pU5}Uo>TMd1٧#̘ws{Ta2ҙ[gP]$?@itP=m$\W3ehBNb[:̰Î k-W$jlƣM_NH%҉00#%! 8Xep `+<C| +S)Iᒈ'H0b>tW&dX1޲T{LpvSc}8=#*^t*z"PFZ2!S}Ge -Pa^d׃RiTaj ƴh!my2Yκ  - O*/;ON=l;QDX!QVXfubw77 zu̦lp~cH᭷`H^8ȶ aM0\/"XZ/a1=6Tyn9~ygGԨyEDYƲet%j.̈́ rH'Ns&)aĵ #`1a2K:LmRJ5>hCh(3!l`oKI?ZνP lnQ2<ddrB\]ж-. >XZaYwBtA5~xpVԍp> iZI:.!LM)e(R '{bAa yާ*[vw.Q-YW?$Ma{+jC^>׏ʖLJ' ;&i$F ^N?Q:M'<#h.\.\AƴYdRvnogʕ+lmo0 0Igp񘃽ʺjjt+lZ1LZxxusA^qbmϗ >|{{-c49i/*^ϺΝ;'+MMs7[҅՘%+e*bg^L$j6R6®|RRK3H**t*ܝ H6!^wX}B'FeJz뎈XDXCjYs邨@0~\=?\ =;?ydD~^z/-B:R*b=pNipCr|.˰N*&bbSstqpʭn.@it,!TPˢ-KpytY5%m EˉlNkd?c]vY<[ jS6L FVs3Lo)Ueh*јA)mK39-G .PcB*c{ 8q!1v8 t|AtP(> Yƍ[o|WrˠI)i=Z+u]Q咪.0.C+e=gS53hQ5gBi;7o@*dd:kHҔmL&7'!s.^Ul@-s,=Frȷ{QD>$`JQqR 4AE7@E)Ϋ70#  Ƥi^G{8::3WrUUA*\1i@ku.m4MIdQά<|0_8gʣBF / [EUMֳ!-&~K\vm9c EQЙfEA5A.ꔪi&2tA$֭Z^g:=Vr3xT Q,}yGw׿LK۴1zs;7x[6SD"c J^~,8BrGl:Md9*J1Wo]Ci =P iꎓ I=|e>)G1)xp1 u]ca8e>uu+&WL)J$VbK=:qd0:AjGܨ#D! rb/Y4 l+ p}\ ,ߡF'O&+BER "K֖24cbA㎐KUqÊ$]t!e~mC@):(#nuH:XbߺJ|sV/}?Wϳ}:`v":DI٫NB|D.41-J D.#   30Ar{)xO|A^q$|-U*'2&"xԡ?EBPLH@W!q.miKؽp^}j´khCl_jIq(C{c^3ymF.F2  ThgYZ0~Y'U+:("E[4%sM3B61uOL*#7 5;U̦tR WYNIQ\XZP^6+V'N^S#6~ Zݟ?GSժ2x8mۖ[Rm[;;9躚ֶ,%Ye *L'([(:e14MqS;:FEQ*-~]sEt}6" |qxѺ_7SԆjf^ r |ᅯ9:h],UV$Q>ħ}ݻpxx3w|2i((VnX X=8*K1η#lc E#륤Nwr@Kؼx{6l<{iɕI۷rU]1rT5e;GS7r"z 7 ^xD]A0](ښ$ gbkZ@-{e .Hb/Ix3Su%Y%> act%Bu5ts"aJbr7}or_ߍ1͗6BS sD"#H'wdJGǏ!J |ʎ؞;i9 Րd*ίr9?GS"1SI·)d$b Qˊ*aXJ6:D1-'quRW6!/*-貔\J[^Σ 6"hJTjv=h[l)li[`!ժb{U@ٰk UMЙ% ?GNϝ4v/cؼAXPS qıi!P+@aJCkDDR$Iǜ:v3RH1ُf}y]k_$Ei(d$# % +6'$s@ǖ/"ѦeII)"gLO]ݵ/zgvFn]<|o4*8obw1Sr||HCzR (xJ%Ewܦ9$fii~o!k GGGEEEx)NIҔ,KIVDDH5~rr1ޫ4^ݓas`0/'\=2p!Ҋb#gcen-u]3>k+cq %ge퇴ATdelVh7)09R _0peCO /3ܿ{dK6Wָ{&II"z#Vl0.&șE\RTq ~,˞ѳ,\}YG'M&4 f'ǧL}A_$VAXnEippp*ۏrtt>EQ 2Bt}!҇'gݓ?sYn}H҈*CmpEM0K&UQ )$5p(!5ԻH!jšp ֟S.:} nZ"Z5!6>JQDI+)pWvV~E޷?OQ-Yb>r {[T :is2ä&H^61"F)k\i-'D+),$H1st8AR"|qOұ8h/CPbR ab TA'g5c8jK^^:N&!6:dL>`vBaMW/>`uo<9ej>v}' 7ߺ+^$e(+䄴BɆ4hG괨 v$h0 JqJ'dL7mq>, V>)CUG*<nbFNL^EH"$TX^@825 "MXFeEPH2X#0VИ9!p ÿ?ȕe|ZAX bEi)M6Z QCMEUSQ0K&1DFBNHAeNF$O]I{t)^8nF"J"ĉFҽLX޺3WtxC?G4| N>a=>;O'[)%E]1NI$dt1Tw!ϧŌ3g7x׾MPC"pMC3?$R N1{`E'2ϪOJ*lL74^m,?E;Z"q=3_ =˫R^d6͵޺'Z3k-[$ԡN)3* :$H\Zŷ8i6a$$Js}1ROswwxYdoi)kpKmNF+kmy@&|M1>`432d&4'$=A0&tg="x3|oPqxI#:mZAA]58]ηE%-!"`JXZf3XS2c2Olc#ri3Z^[;gGT23ƒ?!^!DBf()I3=nIX)0xm_$RH+MhD܌:Zfccu4}R5:1yQ ܽC4,3g $ d>G(x[8OUR<ϱ!IR4!,Z {;AHOiT׶a:l6p8ja0._N%l-OH^U9ddouIqsIЏv|>;7O@c\i ˭U{T[آY g &1\]ٽ[w E^cȲ$[j,Jxq 6(K<`Y\LfaZ*Vxx.]"3q\znA]rm.^Duxuڛ[|W$-="KR_`9?Wh3,(YXbwܽ~|:(]& 9jJQuH[BI1%1YqC,-,q uνqSL/anP3k-0_\^ &ޛy<1\n!q9 !HEk吽Q+ a2XbQ&7E"Ln,P82A-T 6SiI8 zУ$cG#HЂ7ҝ9W:K!#R$)J 4eԡ2fт#>l-xrǯ_yqmof+Lg] 5؉G6(DJEэ+1^htudtZc՘$ҚyIHwaa-Bg0<|"OB&|/կ|~ \6u͞x/ Sj_%: $Ih+KϰtDBirVm.GxAxl2E6q&NdfGϦZ'm}Q5㦂Xo$JXq!%8OS{DB!d0֒JFA阦$UZQXAqF,Ŵ&Vd^24t.<|6 ('![h%Ԃi`94O9z/{,V+(G;bn\Ӗfڒư|5dJ4ֳ1/5WUٌvRNgDBQL+K &MS)7fng#†,ICo|3}._ .pm$`8bcuo|kx]'GllG_O|K@Q ǧHsgt ϞG<Ot4ɨKoe@!Z(Ֆuw8HYF#4Ejn%Y(MU4R)ַ֙'ev8 5+2N',|ÓCoɗ%s;}Wna/>YJ0*UJ4iD "j eJBd-:B(A]Vy[HoZ#|y\p#DǤ =I T"ov<1 :`gi_}B%Q&]h+b F 3zxqDLxל.?EүRO?0W8ygF$0eE" =gcm}ze+|L5= ]BWtq7=91Qjԓ *%VV(JÚy&3a8ǽZ$1gV*kh,54)8K_e GǠRL(7gQ)|*tIlXJ54ZnI5J86,!֐9eH|іij8r ;K}8V,:(Q }&ٳg(gƹ\rBGA^N~k~x_;%Ν&׮\3 !es~s(ޠnL[S1͑DnF?8*< D:"XXԥ -/bM9v+e}{ Xzm\*|{>͞-ktJm4Ŕ8RҊ$>]? T63L5CY)^p+|v_owKssXYZ.MQ[nex]66PJqpxHm +++il'_[atn!![lmac B`2wS~'CF1^kqn{wI'`ms7'G^dccnfV0l"\<}vv9[kVpJen+P%+++ :=J H*3+EׯeBi3VVV҈6^8Th3DfԧiјC7Y7kVJLC_֜? լZxP iN˰NK*&"j`HAqƖ ]JxJ-K[=4iD j48p%Mi\9.iiBrdWx͟\ "7^FB& mAF-¶{;ẉrA*XZ)f3}UQ>!U2d`n'cya;w"++krǨm8+_g?~VYZ#_]66׸v*gή+/ow߽1El7/,lmme)m֖ * RT, z<}^.q iZALENгOspG1EEz.9Ɲ[U2^|4FOe]`|ݼkپ#w~J)× p\ 8NF*kp):cag9dpGԄlH"!ВO5HN dcd^KO![ANh ~!+*س~J!nop{jHiAv@Н=V7 g5 ZcZ"z,F/p?Jf?U.9J ӈh24I6&9cƐu"EY|-t7pvf Aވ|e5 paDzj[c]|:'|B e) U.xj^'ouҡdZh  q)7\x`I eQR D9KCHKƆDkISoy ?*FV:ĪٺV2/h6Iwbvd>.\aTT\r u{9lrw;wn gx晫-ȣGoauu(&o/^deu[?_W?&cʲci%)B{>u1qA8lS3an Of-1{{!w{I3̦$Ij=;yG&q $SOPvt#T!z46R_$[$iLŃhK7전4 3b+K 6t=6/]0 i*ǃϦW>V+q5QLcvUWV8:W_q~cK6V;8L9[S8(|s~ી\|g瓾a뢚, ")QWr#j$4j̅Ah 1-:ubXaؕ3R!SŠ +oݷaOxo'B%2{;7I(IT5y( l^{\Q&3KW%OsGpq CG!j(S1kP3\<h..qjz} IPVG`.ꐪP[cL=~{Q FR5 "8^΄$\Bb{b'9=g)@b<8B/VS#aX:tAЃ~cNO PU{F.(qsF[IvO0zCfb-2M+ v0JA7TPeIE+ Z6tDv#v1P,QE5UD.hl`ucV*+}>?S"g4Fw RՔ,@yg@jtO~]3gօH*uz~gթ4iuqM$u7סg! 4daLC>b&NF)fjMx_1)u=ao}cm >?,,,𑏼BݢrCZk6ֹuk+8iCrNI81 M3bXrrrD57HsX%JqwpttD>J %q,I"oùOq_Ep<t"8-)li*xz5/>ϥo_{,,/0njNP yU}00|MuN„ǙဨEE!Q)!>&c|cvڅծx[0m:WXw)`,,}?旙k,5.>xd+]s{z}V#H  IDATQwxFxb-0>M'QɘYSg4nH'r[PM3*)˜pa5h"b@gD2bV%)j4g&9I*̴ഷz[2nph$UI,B9ZMM[Y#+ ]HnIa *2 a8GnF5d (iC5?s%:x(1MuhDc-B\AWiUpߣ=i><y<(LX5 VTMDy)(g67^! 음iSDiG+QAxu%^'>I4hZ鈓a|p7M?,|:i!LˊpsYL<1P8(f9SQ%qQ^!MUfHi"(^![yMU9 ,Vy x k^Luy)gOI hITL᭠ wN7&wHsvhS3[>ٳ҇s6Rj~_dۇlw_> nz9{푦 gΜaieW.D }V6Wi{޽C]W{H8=>&x]챳UKeoo~g6qfuӯ9iΝ9lnQ gq};!(C:*bsea{@d-لk>óWTMGrƨQIsO(v*}Y݌{X;?>k_owN'8 b? 9fy~\=10BGf5hHHj,i=O Pa,qزZ"sRB`%O9 Nso()i^`Xwo}](N g_hl\fPo) j2 {׿֧~=Gor N*i"aػ35cjxFi!i%G6IhAÂɽ1P ]kO)L쑩x .Ly%?A#5ԕn$I*U2Eh 7;o1"\'$R4(R=6 9pR\Ke/A8`86>&y[0nڣ9:fLOs*!`iw *]8C<G{̢jn}(xI+]fIf 0p5I.xϑjq(B8Xʦ]N=:FApm͝uɽe4{1(aVA05yWcM&bLìhŎp8gV TӜ{Ǐ( _5,B؟J4? d?׺RWhH+4\xMPˊv55RhȔvli^cO1!ƳAJO^H{=F)"8 .^(Z2/_f}}tLY8xp7oÇX_{ԵS^ Qxh2N3 {,/-sR`'v>jO8B'X,&v+ =|cm,-RUv.8dZ>t4P{au[U{k=AW裡ypx*fF-F *|T!!$U Bf._տ/%N>/Pي3LD[tȧ9lpyr%%=ZYAf ESs:']zeյ5/{o#ٕ9w=r߫Xd-,-6+噑d[ 0`6W-Jf4)֖E :LCp$';~\( !*XMis0;gs r/R~>1Tc|Ϳ'k\#Yq}t'0>H> j+-][=P- 6WV(yu\-Iܔ)}p|TU!0x *鏉cELjRFr딚FQUٙ/5Ә@ Nit0O\IeLGeЮ@hv[yigi$DC5`nnkמ'3{=|?z>yQ` Qyt `ykKј4CJ (&NK3* 5$"KSH4 /,^} cmNi5X I3@ƥ[P peyLjz +6#3< il>^:~?b+x/2?/YoV^Aq<)3M/< -!%Iy A8|<2{;T [h!Bjb]R$1={olAҘui11w\]]u:o}& :I]קZl-6B-.&;;{}''~o0 h033BrM,$I0Umj~@|`HK~/}~{|ƭރ=T~B^T~0b\BL>yzLvKe$IRF2)s1-6Y 2<_*\$T>TBp^`Ak }@P-.HIeL!0r UDAsf.?үvw&n#ڐ%htS9`z%g [GW0G#jeuw no[&w0d=4&*Q шJiC P P1yc4\S v" xyd †> fvCGHSqQ`Pǟ䌫9hAnS(r$!B4NQA'Y(vK`.y QqBbj1hMlD&+8`btNsL1h+/J4ܻݝ}?]<7k_333t:ΣqE#xֻTB\B7Rkh4\scmczzK}HjR#2KѠ p||̝ys3ԚU֖W8<9d~/~%/o_KH┕6(֞)aΌ5ScF8S'שjs),<#P%u*֐! [ \n*1FHHO:[G`0"Ŕ.h͸,-xH ~DQBfӡ(?WszM a2,dVYSX e% |UP5V|*҃ǎFHR@({v̌)( A9rwjBZnʤj*cȍ):)<ćx֥O"z3A,Fn5E*r7a`3H%._B "1n]pާ`_ Y79ϴ&=_z&I#Exk*F Z Cd5fTL>9Bϧi X B=v ZhSb|U@c}qpK Ba5 ,?c$TeEud KLFaї$7\ټBwlyJ*!QV<>uWaA+-F23m\a6OMĔN,AgSYnس[ٴ٘۔CYuƖ2BN˕+Wy&QҋH՚! .sGYV6=:A{q8tt8NLڢ))ꦬp|Z#yFO+-,9MMtf/whL8/G[k>qϾu#bq)|+quq9qE89U9x:J:U )',W":?Q{\vKѣʹqdф1r|.wޝ _<Ԫu8ixͷ71jxgRqLET5}9w_e%Dα+ܹs=4 \]ekgE666 dYN^gxH1#666q[[tBs|p 'C7k7ݔ 'c4QPVϋi+2NTO+FPN'4Ok#rOrTǂJR 8=1(zeA2H$eX ˨fPe/vE{C-0y e.\}v}na咡Pb#:+WJ L2G#z1{x *;]_~s:}7?>CY*#$2H(Ò $.NDolc} ܸv1BLe<"6BG̴ԧ+>mrg֏?+ll=>ݬb$9>9{LF63W#+|S?KK+t]ff(7=:S}}"gymst<lOhT }_4HHGr(h4g<ΩVܻ!#CWIEfgiI6W/|&I؏?f`jPO%iJgᝁ=ߕ~;6.Kۭ4B)pf"h! n4#cuk +DF](!Ϗ) 'W[yؼ|z$,"9 u ~߉*:{OenQmmjJO9B@T4x)qEht!) m0e6"Q%Px@@N袬g3롔D]Vb xVDh5ȩ/y*e ڪJO, $$*KP8NBj9Mdcfʦk sK,K/gs˦hZ@vb'9~+ uCT.ؤB #cv13;Ў-=a̱DB2!6ܦ%jȕL, thzf6 dB! ԍnA`ЮKBy)I4)\%סb6?fdF0]u`4eϱ#˩=ٓiQR dEx ܞ2cJ$(CO^=X'k&v6ӰlcHtOdY6KKKrMΟĥK S|?d4Bhڭ6z |gm,;w2ENNN ,.,eia Pi e,i1+5(:=gEr>=&B-A6)dg))P}jلq‡[|ʝ<9pptyWܧW[2Cz>͍MN=/pqst''}VW17ߢ2<̳^* +Ə~@pG:s l@e4mxMF1B9|H /R*AJѤ;s(STMƤi{R k,-peol2?HZu<,Io2~{\r"-#MSz'e8(h6T*yE39~5n&QeW*J3q+Ư+}Z`Sp_l;PJu;,{MRx;r2L]*[Nd6w@3-,`!T> IDATJ" U xYds`G < @7!U`a~o=:]BW">X]08fsSLQ ,ï+p $%\8 0q #ȄEyy.EYZo)(2q@X ]FcO5(tS"-rtfDrwPR(*OBȌ-)\Uš"/|,E$憺 %^5&4e]#ZURU9 Ig?ꠍ"Kh-9z|:1ZW\\p!3ҞȯsڟBw9|xK A4es.%OrYJWCT/pa\- 9uBEdT\5kHdxyQ D q]js.#x71CzAa#dK-8ңTV0Gږ2 F%8]p:D*&9m)=#SFQV?1;JQFqj0*/F8h^fk<\z_9㈟_{esL& \ph6ryҼ/ܗ~9^{q0N0(1Byɒeh alplpz[QF`Y{޴? ?g'?|?mϔIcLiXbx,p1){qƒ{71ƐWs8v_vﱷMQd./b n?. v_ro0fc}VJh[d4DJN%H~Ir GZ6AIK+:^Y2^h+sssn@lmmo1i6\~+W^(˺/@Qj8>>faa/ NHѦ%9:鲸C! s ƍ&oΝ;tYmn`ly k(3HMĩ7D|$O=̲ )a-4:/H2],W;-Jcl,)ͫ@VF(#i9R"ǫ;zR?LAJ@iFZ5Ͼ2OycN@Kp*^ /0?!К҂, *fKUp@g.yDXg1`ۛ.jkS穚U%t X7P.8'Ad"%E.%j Q|I,uQ ٶیD r C_P PL$nQ),²dT N]Gܢbfys&䕄2" N$a'j;$ U;;}҈KY>=hqs>jGubNSh0`a#I遛jm%TjmxaYp-$hI",O _ 6e_j|(PԤAcꞠ*<0ՌjDe) O2ZUC[ggp薹INsFj,"q8;O>{<[ 1i=7–:=3Re\OSSJ\91??sԛmcivW^B"'C8#2dѐe%Pk6,--6_o/(8<<]\t_w{.'8.(SmUD[?:.4 :XuBrZ'ϴyOʇE''sßQ?}<= 4UBb%hM=b8ĻD{xnϿe_xk]yȒФYBoe9g>jkȲ߼ɽ` Պ[7__K.m \lh3?ȿ۷oxkJAe\N#vQl\dfq(ȅ UIZHȵ&mx4Ft=}4 HGVhiqTR{2.2ZI|Cn|Veyy~sg4)VcǟF#]|jǗ9㞥gY)kt )5q79e8z#rpbH-6zi1t615(耍iφ,5UPxR5\@$0ۮNz=jS8Ƶy[dCʘ[)!j*NG(4UxZt٤ KX.,’ @22G9e%cd}y'-0L.љB J;.5|4?HN4/^x,vjEu2Co&m/gw8 ;ʐCv5Er?B1|,α}$u:|M˳?F6 %,JeLPZiQhEBC W%0!'')ݍF wжRFdJ'D ')oI\*N TV1 2 48ZzMހcAUQBrtمVY%?J,ϭ.è3fԩ ?/J}jRd?2!:'Rwҙd~T 5e)@aʹ|ihA^@[.alDVX33@:xHr auoRph4FHҜ,Kz*aBR6=bmm}>KDg٦uQle,B*gYf4S1z8$}f(=勧NLw?Y9لIW,Qi>A*.oz۷g.8gSdM)|4k̶IpD&s%~¤mƣ"}}QD^sϲ@O<~J5dnwBϱFf{;;{^'6ؼxn~XXCndHZY{8R*0Tj8eeq %]<# h6 G}P+;w0 4 ^ygo2LyFZ-R8o}56X__ڨ!G[XX]lm?N*f'lڗ??ta~ϋfy6+Rũx9wOgn E早 QNA#/RzL-b:8Y^+&ä]DcU(F&[S2J5X )LuM5HqT5&Opj&  jj耭SY-ʜ'SnhSf"[ϠME PcŦ",1$Z A% CS"zKe8Qem842DjzBjC V^Va+WY_8OVߌHv痩?? [CB&bqQc7Ƶ:[o|>μEW 'O"eVPGݖ )2}D#0×TK=@>يB D&’L皦:HظdV!\3ԩGa2 惃='{YWhg>&h/6x(;lV ɛ!Wrj8=r:q:{:|Z993EcL0˞fNN-tZRhI61F0lmsxa~ay%"f!<~qsH`TVZ`SGbH4& k }1Gzտ\]Y6ߺRQ^Fvx|/cbiZ!,|ȡbfRwcw[?,BftHj2= }bsg%xU!QC4ϣvEZkkj EgDfBU*j>&|qw9//7X} Qe(AV.Be;hsw dǹsHFgd"XdR{raJ^׿ď?@)jܣ8 FLjp aG{#tXрa SNo2?#*xSo<9*GPz0!Y}DG!LJISA]$(r3yوet^hҬ IR$W"|H\ QGߣv(xw{C"˲L&Yjij$wn9qz 08?8a}[HΟ@${<P)rP)jR\uAg|>ﱶ!.rt>-{._baqƛo IG|}jt l1;BVs|8c,ˈ"2 ObyjZƷ-]{/p.^=:ow&*&i F}|ing;wG>_iδI tB8?NvOƕgq-PJN )%)+imӚOSpcu ХqC54faLʇhV,SBԑl]1V "̧6c*HsAcq!20`&#X$ΪBv @AF2"8堦^zN)o`"lY\ņ8U`|P4O*c1`s4>=O~;ȝ EܓzPZVf+݈sN}strDĤbbW]/,D wEX<^o~{l\D'z<]s'tȊcf {?& k5`n0 fK*v0+2T{œAn&U=>$%6eسfxwN?ߤ]qᘝ(]R!eL$iwjp4Ŋ ck7h {ܹCQ-B0|wfLIOЁ"w*T*U*RS7"޻sB)Y_\Y&˃[#kWp8&iY<\QAkesN^gG?> pJ A2w%)'ywo#vvKR֒$a}cLbͩ粒xP6>]|R8}h<&7W)S1:yQ~TZA#ө)"eu* uZ7}V YӴr>Cg\Ez<^|_4מ{FA}ƃz}j+DA;%rnzqZ'& FApg4>V J$K!~[&YQJ-$ߥ밽),Za`OF^GH6-s|F EVc~CSWVE`%<~g'lcuSá/{o}<_>Gw'mn\7^gR&{NSfᣟUW^= +8<8߿KnhF-r7Ҕ7z}k&ȋ+\\zS*rérH(˔Sw؜g3Zu o[o`Z㦞 97P_.=:ԥa8c&O jZ}ɗ{(RO稃?<`}=}5yΗ}Zb,$6Jwe^%v-+ז!>f8Z?OrtG7o}1/R>R}g|~_& yB@!"X1y2A A.F)T?<8͔̘U5$Y BSx>n: p8d4"$k$ibL$&I0Nӏ?/ktOm-.\O>ν;`;L#t~OgiiDFT#?b4muc$J&cXC$xBej4e8;p5n߾Mj6GDQdee ~\vw5}j,ՠD\|.} 6/Ek:Ki$yEp ^X9Fpxkucb]@ Df*I" ZӖ,^a5 IDATU<-<֗ϧQᛵ`&b)2 ˠ=@bbqQuHȔf:+Q(A Q!{Xu'me܀S?a=!IV1D<*edc )5-cx8eYk&#dyE2'` _V 9Y!*9.PI%צX)a\"]?x|-޺vNj.iϝg,x[<./ ~#zz X `tn^p/c'F"!^>J]-lnhoKozb'A>YI"eBHUI]DӜ1ʢl+5അm/~JI9Ȳ%p7"@jl"B] $?+ Q(ID5"Pc4z/'IG۠Y)lIm:Z{BJ)IҠ:ϣ=>>)TgaRt}J|&.1Q=֮T_gPssWo1#&nW1%k~ßrl_|+|7M.o.YֻȣєjvBbWW|^h].n\R~pN]eSCfft<_mw998{RPmֶ70a|-}l'y|K)Qyƅbpt3<`EC骦} ^fN圵W8},'9$޳l`5Ѵ.J.8Dq4>ퟰvsd6 O|2RE%IaZD*exp h0ƃo+)!M%J{feutm^L^6Jbʬ.Rzt SY̫r}1&rI wh&In3$'MsX MƘ/LbQҼi яh' <p=#[!7_1]Zޓ%XK<.|KuQ3>pwʗG@o߽Jw>E3EIzU%AD{j<Ue ^!UF=\DeGYXʗhX," EVFQLeDD6 v]TUR,aiRtH(:F MHt+j`IsE%Ih"uQZ׼5VMfv<4@r9N4Rj8(M|bw >w o$*d:⍫|wyl㳟<`VLN))j>LJGdIt<7oO?e{)g{{i8<{g?&:Ki:',- m#_ǧ匲.PJý{OFdYJc4b!3VWW 8f kklo,\k1Lfk h3 *8xj6_TT7~`4[sE)o& /znG_ Oַ?g?Aɀ`J?a:5XIBEw$/N74יgvy\8;J*Mccn=2*t {NFk7*2IkӺpz0up2Y0+$S;E%\&TԢB6΢U,&GYF1 XA/ ONꝂH椹UG``;8y KXlX?yV,%RH&uX̆pz2y rBTŸZED G%yD9wfpa\ZNjF1dD\Z@(g ֬m` U 0x[Ϯv[Z[1ІI{i~\7L (˚?d`k]g9:245_ЩVəU{ާ%aEmOWʐAEg'mq ^Nv=P)#;[ &8Σ{t}{'|q3m ~{IW>y&lq>='._S\X- Ov)gO!W;VF+m8;‰@5BʡHeG0l 2$+KWzLNtׅa1}w\AAU < (MbjgÚ-i:%`p!i*A -x& &6ADmy$Whx!*MHU@jB-ȄM%ޅ&ϷIs4~l@Ʉ$ [/I&FMPTRh2TuuuGYt zȲlmbܫj,_޻c !!|BMpu!>D$-:K[|ڭ8=C8J0PfjS289yxϸ+xt!hDƍ()wdȕ+WiBѭd;9Nr:C(ɗr5T*NbwI@zV6#'ӡiDUG3䓓B𬭭pNNN8=r:p|zƹ-Kt]:qYGF#Ν܅󬭮r!:|\yTMaK6WqjktK^[o?/+#x ~H/W,҃iqx13Hd $єY5ʥ-{YZn1lfEcByw!Å gz"ltI @@ %8QGoZ\8e}3eZ[R"60s=L PX GKDGYu)Ui?HUDJ;m:YY8( I'ILQ vuB&S$OH{/gfƌgCT&H6Z̀ٔ5f:AIZm%Khfܶj|ãIEAua]jogx0q;-U?hLrJv%1No8ps}ʼfrW>k}OSe|2y;]Χ? \cm=V>cǧfdSP[=;^zOm!S+[^ZC-O\.j*hԉUCb33K5DqK p2p~뢮 g_skȼ&?XGh^Fq-b*c j5\FbqTeIuS#%oEtdFȹd2ayGtpÇ>|s$PARNOOmj.ưX! [nwb5P3XN_yTUl<]΃BE#!!EźLgQ "@6(p᳋Ŷ hL;ϙ;(Lza^o=7YJ+?O잠N$3C & iRU5 x+s >}zqbV=d +qdPUO8<JJ3aI2LjZѾĴQ~)T mlrjF+올] ]py ?Wo# ' ltbvT)-]RRi A66JEA75Ya/)eg!+ )TCaJi0TJH T}>h7p]3%M&jNqwPBԱ5"!$^TyHD' %*C 1ʧjo(jZyJ]\L'#ҬK%xm8>:H#^YO9+uÜ&y8a3dw|X6l2i [#ij _~r<-jVbi{`x^+G -Hnw;f2+d=4 h(6k\_s|%NGhk{7c1a mzCm-f4d<P/m3|3͹k.o!Ӛ#с4Qh=DWKrvY%,v\&Ȃq7e,+*dE&A !ei(YbV<]{/8S4Ty!hbRJ4FK#h>Fn>+4B MD1'K$Bq*lUܣN ؘTQL@9f:`*3X!k%\ܾL/~P"9ZGn%րey1"4hY!Aht2TT̊)@Rg醂p)H[99*>&`Ci"-V{L&\528:0N4MvdY(p}tosA׌X[8YIUԔe >p||8)(4M.2^LGt i+nm$IbF-Y\lGGG i0? 13[S%vm<_ TR<u#Zk+EQp||շAWnɽ/s1kNؾMJ`0p9ʫqşw.r~/wrqnW/\ɱ'lo{WX^Z߸2jxd z!NE!C5Lum |hpgDk%b( dDpM) Gr5lBG^뛱9峯Âl-T54.w?=f:jk˰Q*y]x'yyg?颥h@bW8B̜5 ƒp5*΢$8L3QMV8lsB;XiB 8eV T-(#cZQyo\|}3jNYNvNniMe69%-:mEl; ;)/-yeuNq`zJ1PLC4DO|g6^Py >hw.RjtY n>8oa9R(B(=kCUY5-Ϣ~Sg.Wjƶv$ Fk1ǜP%itƤi!R\jD#ȓB) N i#/x5Ot&0y:y:ҥ+X[ԠWkn3S7m]ڒez=֔eIQ _hx Ե,uuiMYԌS"vcyW[_dtf뛫|=!y1Ora"zCkoa(p6h̜nBX=7fW!~su2$tX '(|]mr.Xմ)!05Q8@j<2 Bb+ٺu*ҪF:jdYVxP I hHE }&F  G+hx43n=fHKD|(Ys6 1WV 4\ꙬԈd.!g=}ST*.*+ RA5G6*7E3rJ`HS NtKvˤ IDATN5CvrJE&~ ,u:`KbZPŘ;+BM#QG3+*/%RnA4*m"79ٌӢqo|I7M#5 D72 .*h}qR4:_ֈ(_Xi6%6EHyeLscvl73xW3Ξk6yѧ/d]Z2˒ڔJA+OQ*fg4aDi3O(`0yI{Ki2 ;X^^^\#leYb{"e^=y6c7݂b}eY9aee.)˒hA,#NE ƖL #677]RtJQ ]K6ρSF)Et>;;8kOJYܻJV''ﳺN&|]ٝOO˫o̥8Lg#;_W"I]'X]^ðÏ~NcF%15̳YIڅ3&Tg6QDJ(+hIp8̡: P AP;zt;!=ܸbˢw,%ia9%,EjAx%-jrk cBSTCvs/DWAiFB잊},riM|pD9U/:^T MJbWU4.ɀ A0=FuKhj9r8p.}h(1%zFQt.۔nbD3r NB4W"kbc9䩃֞I1&䚲(Qΐ=U]BMjJaqo-t:$ iZmAg,8*»em.u/J%Ip!?w.nͭMf唛n#e!~s[ǎܺ8P&R V $YJ𰺾Fq snsmm,˸{.keYl=OplFn:JSwkqE;'?!Scݔ+W/;?E'-FӊʙV5^~|Њowx-VV8>>G?!W_7[[G9{{{X,{D=ucj C|_vL_4A'y0IMJ qH༧j>e]is!0Ur 8HhUFgo] Nu)JJ:ƐbBbF\$>M  Nj BTGg&noF\k<\lufi (T繗kK,,oQU14M銀b<4'gYFƅE 8QmNFbMĸEQhO/|^W2CFϬY"8GeTC>s,Ujt) ܨF؄ZX)kxU KD`*Rh"QhI EܔvN* `.|e>Ģ4(^ !xNh &K h"EQ k%c*pǚ4Z/# 3%56sZ6|BM6P )] QM^R*]kZcNE;IEsC6fʸkČ8vQ=bc@IT-DĻRt \֠!GM-ZR$L NXZꠔTFX)NfgaSRC >̍eLH)uck G*"OKRA  *"y5Kdq+6Nñ(@\kHWyw*7MajqEldKepGQ1NYޓs\X^5Y6\x;ks\\ xUϩ9鴟[y\֋_?+\+}[9L*G}CԤe"O-^_ٌ;2~٨Gk~o7m6WiWJޒeC+ cz+=ܻh:iAs:6u]&5{LJE۴[-:B(˒Om>xxaryxW? ˟~ﯳtr::b8YZYcVM.q<p뭗9oG[Njw>d6qM%wЭ5ݣm*_a0K0*Ǭf+a4, R@k,bdMS{" }IT2sas,*WBHH59 G3'&vܸԥ=f|mu xTN""@b>w ,i/Ɂ+֍bELԆI5Q!J3U̚ԻEesx_؀|,vC*E4 :u [z4dJKJL]"',z+6vR!g,&$Iq*0&w4,6 sV!k >!-xџR$ -JXD7@@4MB R$ޛk{g9[ާga`XAPШFPYQ#Fk JĨ(.yݢ Qd`t٪^?:>30s}s>y<V?u,A`ع9.{\9'M-pe􋌝;v2)Si۪d61u:󋬭QXWeǏgz-,2lNfԾytY Xl6x)JB*R՞Y,+Ƹfm.Lg[dMoӺmc[c29&KӌSyAQ[b*&*w+LuT5`4Sc:vHyyMܱcKKKZT`U{߹s'~s ?~9x ;wj&rY( 9yQ{7r+,sjcO}Nw.#\;-'1w3 NV9wbמ޽;MkKscst.B浢.6U^,֩P|l/sz@ᡨyM,j*['mE!/Rϯ+Ȋ Ltp`Y^20u?/* P$b0X5wT,NP7?eL(1Mj2jJf*~nΌƱfbib3'XŤV˘ҢLK̜֞oSW&42S&m;5ER׋&ћΦf,9/: {Cv,vgIZ\q#ԖUX_ȴ*|/TTjTJkƣU zO"e]&e+(cqu`Rت s f*d2acbYwӴm^Iuc k<&җr)jӌJ}*]0Ѧ4_LR6&;46,(n+be16eYf:k#^G&ΣC~Q]^2E& 5*.E"3=5SuM]͸Og}},#t֡jUyQlƭaMv/c 9=z~O7Vј`2.Em斚TX}[#Rj[ ڶi$9;wܒG[u$T"4I?Yg[Nh^AQ-_ti9O= Ǖ/#uHDOTHZ{27VbƗ˹lGljS'X[PIg?ͬvE<L:B]w݅R~;fFzzfoƮ]عs'~;uݵeJ7tٷ(R عg7|;|-lllk\uU,.,w^M:K;vNk**Qe`)_Ľ'R9~NXc{,GNxN!sڱ8hOtA!tTcaa#R,^ɵNv5xBeKkUdJ xE|odؼ@Kf.ڦ: zLuw('XܱDUFDsU4msE٬JEGؘ1)duzש9PSvȭsԅ+*i(}SR':B@o~? Tr|S?ɲ{c+T3Ǎ`W̦ޠIcJM8@7acǎcMpnmy ͽ.+rwb}9xj>bnj.VNKY9uU,--1~xL19t:Sxp&f~~@e>)1ܑq* l~I4T _7*2%v3N2,F%S9$G*C[˨ə.!B3QV+,QM*F5+|I,Iw AtƖPytY>{ )G8>0 l(h:N#j M\'G;ؠYj wE*@=hm)է2^o漛HT @ʦ,Snh#t U Yln"YWiEk-IR21> TUS.sU9[e[a [OM 1.iMѱ*4UԪ"FS.<)Ֆ>1ߢ kfhS1hFװnQAa4mBC#Ӻ#Omݓxf"Z.ɘyOoOv`GT!]4Ee jʼnQS 6+؈ &ԴE#MN)2tLmC &'\ G^ǰ'DG= |U2LэţCAi׼d&I&Hv%l}(YQY%T&b\ZiSZ(FR&Ͱ6b:)9ڕ%a4"=j[TL&fkmL0ư00EFQG7ӽZ[t)C˝M+ʲ4]̖,+Tq[i@5~1.s;[ZmT SV٬f6@]:k4:{ Rj@5wv;".O|T3\1iOy;S]_焘VOyu2,ySuyϞ={QtVh4J-M]QI֮Rqp*߱cL-.*Kg>n\o"_0`uɓL&3ܹy̒IR f>s9 )e Q+iwJ q{!r1U^Й#" JtR.8m)o>2Ӎ2![ȴ֔ޑ[M^ZlЅ"d{OU(qOv]`aٲ|=&armeíWN1SI#O&5U&Cm]ն*| N&wK#io"'ij6Ɓj(GWY5`tlN`U7d~ԒEɛBrU6??YKvbdGjC]j|ȁxOBF*{I}<TjFZyzD0*YMz˵FTjR)$ IDAT7eBTլK疶 LClJl#`Ʀ4v4FMӦ(BclIz};ײڔbL6t:k"w,錍 /v3[$[ؖ*.6J2NQEV#eM߻f0vŘ" WWΒ^9q$[gUh5[{Sohio[ID&ӀӍ KƔ(DVpѣiŠI~e^tp84N:EOGYrqvɑ#G!Y__g0H]I ;*)=pӟ|Ώ̣LJ(C >*^j\ԘLw^@{{9˓YUM1nrI36v B<7ޓydo.ՁUJ:jUƣz Ň84g|b,P5QcR󆱨%/+46\#lc 8;4j(߅HQ(*h|,鉡Ȋr]ZFh qfi$Xf*1`VƬ}U.E5Ywxu+++Dy5h7v")Y5 z[11]7&=mPؠB hVȆ!TVnk2ȢrzEAc2CTOk&A*èT.uM7EUkOhj"Rx:Oi6::7OJ5:bM$5EOY}66PdvIGLEQ7sEf5KwCUUfR-nN_B DOj*vƆ,Eӌ!sRsDnyۘLr|SvФJM3M{t0y tFCR3sc$9llk,?>b &Ӓ{6::ioc5u?وcf6#bv(;dS@TlCHǢD좍q\ppQOG,,̱̦E9<;?ʳ{=TS(oN'*6`"Qg!T1Ai`QeP Y ^*L+4N$41|Va$A{d֒KntJ)p$qYl@ zuaI,#jŠC' \JPN+46o7</iBLI&% PU<+0ƣMrj< fȣ9sBlK46Re 0Rg4R ֨¢l h ( ۭ[J+#:j4PAcjw]ߔP: ]{uqnDSw隠BLǟԘ3&#&@"uv crOQ;Q i nъVdkMa,} :PcjǤ)OsnGNH` l:ԍ%}s [7qJk1,WrE)#0BIa{m9*:q%K/ҩ0vTuG1FiE qU*MP1bjU8tB^ f9.DXuJbT>t]&8 uS7MS0!ɦc277 -+'V&صsOr6&8J`~~ݻn LE&'fFuuoeYUR%Mx86?(oURF7[Uoෑv"\!+]3V-$ 7ULMщjKZ7*)͘.@eYn9eسne9k'N0NҼIcx|T̲~^1`iG;9qDZ ԎҨh_Xd0põ|%F8zGHYI1M)] 5 tW]ős^K8䪫0L8rǹ*pgCObayc<Ⱦ`6f<)"V `ʀkR̤cbStnM }a_Y&y<&cSٖ0ʕ!3yHdbhZOk0 5ty/)6jt)KB TDf3$lS[,WYl"^E|Mk[$vu S:44woY2 LR>GGsCSc M#ɺa#~^0++1Y %]tff7we-F-+++]rS.o^Jw@lnet`iipx69;FI,\ 7p=# WҎݬs,,. ^z>vpܞ-d_l@e }wa6Zcl!p|{ !ݤ&Ah"jkjA0j{II Tsi"`=rƕѢrekK|eac"oJhi^^Ǒ#sͷriIt'>rٳs'?ҲEJ_k1}v=dIsSD9^.ǶOfs2%զi[!Z&u@AeIX5wA)ߣ]$M:УZ]%߷ bxB8!=LH;JM_esdΑٜJR3v֥j2!YՃ>~J?y/*|c]k}y_1?bC[OXgчNෝwbO 8bW]s؅Dp-?3dy3BM/ S'xҭ"/z>/{ū.xs}/?su'6>q;_%_6=xϟ>+ɘgSJ|7xO /o_M['#x-^5H?K_'vqeŹ\\_Ⱦ.$wE5ޮ^Gxm/sx[~'_u7>yOgoUƮCpLJ˭|ȩcݶ_G[>o?_ɵn;I[^57Ǜ].]w~ Y޵7wpӞ-v!׋n/vqeŹ}]wErP~׽G]ۋ]\Yvq>V ҽim}o'=YްxsW~~~/ywoح_\~>ܱ͟΋_3_rVW˾pU9K=̫}]lomƟwm?X8umdy_\Sbb^ʲs]Oh|ojn__١ʹO݇9pK߇^ozt{u!>&<9_|-,-w!dZ~7ɺVxBO؅9\|vq!.X\6NhW&~o ?zYI&^/(?X;o{u7]>Oqa8=)?|˾L?m3iCF  \CȬ ABKw )]b؅xBf  '  '  '  '  '  '  '                      8|  8|  8|  8|  8|  8|  8|  8|                '  '  '  '  '  '  '                         8|  8|  8|  8|  8|  8|  8|          +c\W?r ]b؅ \A+^q_#CB.ґ#/~yy ꂯs1ۼǾaKb q{9ϯ'?gR......qW=k'o~n{y_]~=g۳mӮ:[~?WsgˁU^Wl{~5muֿ=6+n}1s>?uj~g~ r......X ]:r/=㛾c~mu7>au7>^/9?)ZBBBBBB'.=;0} ?ڳ:ߞohEPJ]^ywesr......HJQ_}r^e7/^sѯs1\̾׷}......HQ/C'xOr ^2:smbxںxcCOz<69bbbbbb=<boz´׷_: v!v!v!v!v!vX':կyc<)OCA䶥?/-կy>AA+qAAp5| ݶw=g#m{A㶟1y_?xw϶\>UQ[-ۄ*b"z*֚4!j\:j"~wtx<*RcPJc|XrЙvc9QcC!$[?kA(FUPfJ)&p]; z=Ηzʠ2v.0cL$4yaDd>G)֚ s.K^ Ƙ;Z^s\-&'FOf` nl6c8R5kV9+qYTg^^3Z;5BH9< B<1RYzsWo"QLS)Ε`Y16( 5 IDATt|,dyD%';5(B稪Ckg>d*꫹ӟӟ$E/*<_ߟq?VGe.xRQ}Jp.{8ϟogΡ rYK9/z8z?׶^An3bp&,YL&D,Q%'Ϫ Jk6')tO7,6¹yNQ( n#uJVe-ya¹=yСصk'. $ƀQ~1P((?.;ً18ax1Y1tǽ=;Bp<( EQ0(guru vѦr9{>/|-hVu ܼADpH B?Y4gCS<0v3Lۙasj48()PM EA=YgzqB:p#v/e߷=>cq{.f[uN߿1漫8k\=Z:zY{<Bdp\U3+)11pDBM5U,: ; ٢>Z>sss(L&<ü_ Wt5F51sb=KCƓ"F|! Cn *!U,ZQМ7Z.#:o[k[mU%kl ւPڠ?Y޹ǏDžȬ >9d E9s όMGQ5u]CiA+^C[tn?#Z3LEXA 2f:* cLFȲ 5ޠM)F{z}1.>RxGr?CdYqj@91LKPڣbhEn5^5](ܖٜ\1.R6qur}]Q[%6A>:9x* :9y/zj76x]PU%89"1Dx64{ek(FzzҊ\f;.@/ϺxYL&ʲ-J2zU(c=d<= n|Bw8\0FԞDg{lxSn;c y9j=wi^vvަ#`O)jcҀ*g<7xv.b-㟥ȟLÇ3Q&e5eޝ,2crǎmpuP YS=R$X|ld.7pG{PB|]Ta< pUU<{f6;IT:cԉy{{s`qqGaӖIY1kF Iɬ 0jvڛG|iY:[lugVq3F#hPƨ,ڽX^^hR{UMh?ΈB*CD*嶕@]gToeeZwKKKܹx:ڿR9"~qqʏ"шy<e JBx~aFNô-6٤((z C!<'?"eͬM69ӛ=MȘ; [h FQT*1恻?kg\Soq1n޷ldz EQ=2qsI6NCGQ*VOR< S_ϖ( \#t^Iv>gy8]reZkuϴh~.Gj?YV{|\eͱl{>L+ORqx"[8^LsJucm`0q\s7,snj8r 'b<377cĔ@w>2Z(PwM9ya麛۝b'N`m}~jW;vSD )Akk"h6-ը/8m6 6B֗^s5el6*)ǎԬcZ֮5Zlds( *8t'#RQx k  /|-~¼a69{W&g1dYjƣ>x?w=FLfO8VM=;lOU0dee>:GTM=A#[nfgٻ,9Q6vyk}x79ߙzo߯vyRGڈ Ycq.zEeɕ[1D2Qj$3RKm@qΌNgѦ+)ˋxܨ@E}|R(GLn9tk`V3N^G볱>&]SGBϢځLfhԮDkE<9MsM"ш;km'e3:-;Z-ד6]Z}*,tnWhԤ[#ƛQ!]3n{cBxl3wu9Wɮ,汶@awtOȳ;L DI 5EQӟz=_'س\pO0KNnz٘徦ڵPSNZc&c#GY^^,>ž9~A#hJ8~81 )fQ:Wgk#pp׏//|*{|_t@|] `>e6igT+F KܠLI^ZइC.;!u7AN=VlGE^.]k}"+ XYLmUIՔN-*'"b:O'9Ye5U1K+&J-tȳgs̓6A$ri'EChFgLqƣsXS2žnO=9iG!Sǁ#Ɯ"C0}ficץ,rtLbࡤDַZ ڕ lmEXNs >業W(gz(iR-(ZYZ՘+z1o1'n@t ^}za<+~+{ͳ׋{΍驯<{GY/t w}>|O}>uyʈ5S Gf 9P8zqH#/Xբk,=[eHKfۧ-ۻ!q0NRh3XGY>c6;5 ~_L=(=dṠzcƂƝz Y&;IPgKט/j<V3MUԦ$O°MIB%CCUԮar\8vQ3yF-,V&Mܿ?%#>-:e66#PSQ5UUR*$jul6s]]L o6BItptF`O}, @׼S餽|_|,CiӰOum6Oٯ|?1t|{ ]]t9Ţ{{6:ʝ1" 8;H$V]:>[+m(9eUcI13f9Aс9vüts>j;X&namM+PTutR)NH S ];c}J<3xධ0q#֚IӾ}(;qHٰBix iKW,YE 2k Lsj|-Тbfg+ڑ&W._duu+JuK:BRk)pp|Dwڽe78O [>ڗUe1 ;8+)!ݸKH'6paU^k+?x y뽯̇~^\S82'o}a? wgQ:}8+ya=CP䕥jj`C9%l=O>'9,\u~ +mA9 MudBt::«a6׎3Ps$++]6έC]>5>ej2yf9xQG@h:Pd)s OrOP(8= 9!V`XpBP!d% Ҧвp%^z>J y2e>OxD+$Qf).^DZ>(3G&/񃈕(UvwH XYqo֗\_`8uV|u3|;u3d/k=q}Cg/+*= l4 8z{<7q[8X,V,4h0\]E4 ..DQD~J)Ѯ{C$xFFIRd쳿xoa|A$I|>,SoD|+KUG{.=el>=ݸ1{wkJ:ENыCL kr"q۷u3zlBQQ،J BI> ciMOE;h0UF]UH=EyxG- pH#6 '$!D9`<an?a뷸`^\/o}O0"`wdis67}SJG Mbބ'*<5 BjJ BVXAȅX D g~s<зFSF#S6 [m˒y9iQMYVܼ}X3O%4emFX"Eŗmlَ 8  זׁS%A:ɄCH #8RNV0|̲& S %nэ\Z¹U.o%jʔ$oܼu;w(lNE퀥k='?|uŘd>[cj*69ZkiKR871 0O=wzq'?:{.ztz'd_AkOrm'V NXMXMȼTTqaژJK-f %+}G6)NL&/v 5`u1mXkԲ&It:v Bsef h!OU"wgnt:E AUEFغU9\{Օ%2痟sk툕R,Y&)T.}>qWyxgiFe1Bbڭ?w4̥t ^ZBRV]24.hjtj$~D cF:/J&E4Բ̳*{g=9C nsx7MZ E,/$Z*ʲ7)ٛXR]"$aHZSQS(BhCR΀I^` Z\3۞""yhjRjFi-3.:JZD#9q`YG#"_wq&#2^q_=|D;>G9xDQ"YZZ֤Zinݽ?G5:$$KKKF#G{{X?gy!Hȓ9KL B8P0Q֖YlblJ(%딺.Ё@y'ei3>y< $ܚjA蔽|_u'/3`ЏXzuo牣.YVsaR-Il)}|}`-߽Qiy:-IBUnSK 0ce)!6njPtb>s/ |_$}\D9z!.ؕ,tr/77h39B90x:t<.,vB0IfGi{7'YqbeJuX,@M[m;Ν~=/_~`)5Zy(塽 p1;4^N͛7 }pxL4#|Os(dgI, C8'ػws['@iha$8#Y#EU7/5ub}2bE PԘ6chJ$6b#$R4fjaǍe>()QM y$>=P(u\ ;z!@jsgjp x&nE(yN-a,5 u]KjA~hE; rq^{>rs ^oJ~~;u5'RH$ZF؊ڸ}KB!^PeY3(fvk̹[d<{gZ/ CIia9!ja@^-xBV9(_`]ꊺHH)2SPVԔE R5#5cjE*[A lIgQ`~^e }?ãXư"1N6>o',ߙhba eض# d1՘wPW9n rx|/ ۟tX^^jnKM:R6'mVL?>=6:5'SdZZ^cڏ}6wn|w泊~7t:^&-7_'MF\iBi EYj'Qң6iv@6#~1LٟPA\u0*=h>r3 kO[L Ep5jZJᩆi;î18aA/#rΡe3}{Ђ) XlE]8\ԏ*$& Jzhp8/qŢ-ZpH%Xua[RhX<-vX?#,JkLgo9;CYJdgW0)G? 5k\\:O>lhYL|l*G;aPƑ95RfÁA$(l4͐eep[ {~3n&zH:Xa4R5̑^ =YJ][w^Z a:V*ct8a2R V^A(P]qW/qun߼*wg_Rg%^x!GUdŋں|>g>6Ahdz[xy>J.ɜ$9 ƼY;=,͡8MVuK?{Z= >̄| ƫ/ZowKi4PTUG ɰ&Cݡ%} b@+G&}p`l:P^HXvђ~Ȩֿ!,?w4ԖyҨDWEx}Ʈb IckEcEjOQ:km_3% PtZtAs/&Q1Ns6v(c 6xxWlllq^8f,//DŽaiFl߾}&Dz&:ESҏ: dY$Iغo~8Aw޼F&>{ŋhD\pskDtSsk;!LZRT6QiZP|*H),>sN>M߀ƙa~; I37cXNx1v!G/2Cpk,o4iEQP)rSUM sm2K|zJuRt(N Z gyDxGYWLc"m~bzxȬnr|x@Q |K+ _ OkMgCdB7֬ B1cOuH |AimʷgI5mߥ6X).$B&CFbqdX]B#K{2#pn}7^ j9G v9 S 8d4\_{ՊN5lq1Oٹ~Ba}}=#tFQWpws ֗ȳ+Wp5'W-ǭ;ÿ_OS o$MS0p& I0LW{K+tR2 +0|Z-Bbkʇq 'w' +*c-<4 k}<0ut:Ν;iҏdyd2E.o6S0UMw(!A09ic/ || H ?_THd3l6]L(qT[+aa V;\?ckc,Mn~;wK^<.?ϕ+WX__磏>T9v0ܿU|' 7X^jZKϝc,{J萏?z0 zTńdp4*0$ CL6+J> mK@2\G_ş1Lx睷G]~M.OA>{nyrUX#s<&syJc(jU u6EE[R9ZjA+N$yt^QڂP'_|n[H ąUȩ`MMmܢ\Ӥd%# cA=lSc$,UU/̈%6+O{0 -j,0|GM5A` #31YY>l<RPՂ:Sĭ8bE&̧#4n"j`smo2YV^!?81B%4F( @w gָ&]MS %pclk5%D9܉~DGޓ`](aI./ڵ-_P)nz8OnGpg[\Xs39F8}>!d}}Uֲ%F)EQD??e<4e+++t:1A demhĿ?=k`msK.GO}g2+a^4dU#nVM;F{x:Ӎ>*E]|E]x\[3,]+մNKI)d-{tz*j %%uvIgy͐'&C%cnXڢV-Oh1P:Уi1RJʲ"+gB-MhüVHuyFmumIYÒ8U9!+R@b #l-=;6ztHmeQis:opnuMgL҂W^fpRfD,uB(*Ҽ"MSzYNZIXa]s(4@K|)=͕ dwXLp5u((*Suw^[&)xD * 8PxKX]鱺D$ܾue(`83R>gܺyc wv$IɊ`a4>,+$rܼ1e0Kt` j<&RjƳ+++, :He)H._<¥3UuM6y,S YAkjj㰶y X$8kfx*سHCN'A~qZ[b}K k2458 IR?&?Wgws~/5`:MHf b8yΪڂ|)зh> =nyRm/AH "pWiK s9NG.Z w,[[ƕ[2zq~Ҁ8pOC$!"wqαD27m._x89( #EFof9'&sפ:5vu`4Z-AO?G?3 D;JJ.`:$e xbD)`K=s]LGGOtnNv F8<=dsE]$%IҼh4kq8cr> Y =c 6nŵK[|iA/lq:$+ {1r~]vvwy嗹toO~mo㜠-n}Ϯ|N?m.^h2|.ἙZ658&@CRJ dƯNxZxNڸ'Z,Mt,UW%Jv& B*u}8@\-B\j݌Ra6 lo#zxfxkp8B A:z=TuA8x}OK+|;G?{RyTn|j'K>B)|g+Ol>yNW3lKa<{J𤛾{,/m3,esuN19ܧJtBG(kZaGd!o톼b?7yM|wY_[c<m{.&RjVָw>΀8X#qViMD;}"7^K׈f>/_{479^)( NL?;՗,#,?X__Zܻw#/*>n 0̹N{0-9Ƅf(󊍍g@Xdtl&%:]*')˦'!uKzOZYSIg Xţ^VA-jB,fjvbq8cY9kL[OݼtmczOgRY g,*h!|>kGlʬj <`$M`:3͘&34uht89ׄ4Stm+5>^$! ,R(p2-& P4B[l8tZ\t+k,.W٭\vOnJLMYx>AT}mcq_5j*bϧ & PO5zTH .IӄyWURgdpy:a|GO}D E5F8۴}RSxBSzM o-آrt<4e%8#%0LE]Ph)aBckb_<*m xG[""/2k *>ir*)(m|xMm뫚t# nEe`T%~7妪n] Kg̓ u9Gj5Y8|R=7^=#220 g8::L#͕+W$(9N; j5<ϱY .|0HqV!6H--o^LQL~w^KWp;|~ys""*Q'&{do<{:pz/@ѿ5E,CGO˂(9r.ֻV"FHnݺE;w(}cn||#Ν;-qKˍ6-+^Kh4MZt:e0`AJ4R 5KEh,B&I3k^a{{<1* sE W)+F5?>6&5irYZZbseYs~vh4<;|{{uL% |e@\g\LSfq,(m^J08KZKR]oYCj}ӅD~%HQu N;7zNYԕ6a}zdYƇ~H Ɠ!}Row8dEhrGt:oRtx8ikMՂ✳#NN!aB I l阐w%pIH)YÕݰjy~s7UO' #&ɼ,s6k8jN{TUeIrbjt]bUFfD ISm&Bqq۶zGo%um M7vטLl>sRJs.XB!P֥a\(U)h5Aي4s~nn}mz''ψ ?jM&^̑nw_jd؄E^/g=ϛ/j1j#Uz+s MNѺ φk4M1?¶ vY,Mxyp8,K\E:V@F3\5neXΦPS%ـ66\^R9Zk 硅E9\p՗ٛm;;p>ur,*Y$1vٰOԪ S\O҈" K kk|翱7opD4mZ>H[RkE4ZHlۡV- ߿0p Sk#x\=aBX]eRqGu"R U]>J Zƭtb64 EZ2RhU8 XLGLӆ1i5dyFEL56ʼ0 0U,ic)b108|O\ڞGUYx2"c*G>ii"P0I&cs]0p=}]Zø`+JᑢlcM_* .E` }F-᥃{Un)]5Nqjy +|'"f 6SW%+ a)4&GJ,6m^il"&/"v#ZF#5}P%i1 R \[]4t†18AEM("U+: )JQWPi(|$&.q“D`ISCgKT [ZS[3E_bu)X&-M aYD4ݎύ͆f! Uѵ$DQexTe4cŌ,+b"IR&)Q#x饗?S:6GdUҕVA3 ko>xLYXaEY(%I& ( U dkIZ6=Ebo y.Q m ]͈SF[\1u0lAQWl4x1PC0 Hdi 3U^Ov<}rk:~ 3ѥV)JY2BH J\5ea],CڂΩ8 l4|W^2,^z#\~jH3^{ ?[>kFgkJ ߠrM868N͍|n1L9<$KK7ػk7qSvnG]VKQfemm |Wฦ6-/lBS-CtsԌ'0F#^u239 gdjk}![샟˽{?f6w5&bJrb.y; h!*g,ic ,%.olu6PRXHWF_d:W3xVqJmzK/n(wB\GZ6+"kjQ\֛euMʲ(3,G^~uQUEgJaz{[ GCk[&.HR[M0l)qMY),g11:5MtY/fɔ"αUԏ؎ "t~u!jEHтwq,sJ(f1Xd9룋H6v(h98]RI(eP8F Ӈ,$TK'W!+bߨeZ-k,(U } ϝS爦'Tym2ѮBYi3L<(Y,S^v猆c4esg89?c2dAU]()I3c\_cdM $3*ed9±Z6&)S,[1,A+I])TT5U?WPUfm'YAQ4-l 䝟H`ETw}5nk=l[9׵W{n_ҿ):75uatqFA)ͣß7z<}V{YPWX6嚾eibe!,XBs:ZFE mlY0SVns >7u+ww55co(>76{2*Kvwz~trNiqzz#X*BdyKv3K 8f(ypt;<}ܹs>kZ-,u&Qh488xWNQ&x~0Oq i6CjK94X/xQCE^UV(hezml?h ʤV[k:~EPeL,F H>kf ]Ff<yhC26.Ia1cg=\K`O_-cf`.'ٜLJ7[77|M>aVE`%|]X| i4p8np>u~vu^x~gGf?x$b4jaYp18CѽI&qQP5BZ+qeX33d8) Hfmد+c$IrHRi1A)_Ny!-x饗m#pmg],p]8eE]*TqjU`/  1>ołׯ_'RFuYb!Ʊ1M\W8@kE1a>::Dh%H9ƫƫ/z|>_=%S1m_rW;Պ9H6ĒYhmcUNPԖ@!-A-IJ)AQ)[*Gl&{q͹vHg&n`NV?)7N;oqzzB`x¥4I'3f!L'~r~~R~OQ|fsgG*j[ :-%x4C`||>Dz,q]xcm8ܼ{~/?G{Mfp%n"[ļow:xƋ\\\j7t: r#|'>f}}vC,5'jJ'4{Y$6v r@m$YP0$?ar6۽.ЄaUprq#[R#\Ĭ^cs91Ltz<=""TU!㔠],X,q.gggt([z ppx[,KH){{{!8MX*E9k s&Cdll:Xb)'шZ(jR rpthZڥƲ#6vLFgXf< Bj,lc$4mSSSW9^S^q9)U#꒺W5ҭE^KZm}["I 38Tud>7c.tJ)v*4S8f>ex,1ՎRG[!J7^Vy% |ݾFdԵq\Dm:RF;WR.-zA6R8nϳUA/ja!Y,zt:5BhϰUUs Gi!^6)DiKx`{hl7YFJ^ŋίUƮFk5 ߗKLV&B[hKz : kSR~kS#t<"Xa9Qأlrzzmlnnh 0{)%o~󛜞vac|g!zﳹ!łxGGGlll0Еh$ 7oĥ2RJ(-|hm??A)&ahJTEQzm^ ,^o>looHưp p iysrr:Ͻ,ɠnbysvQ˞˓NOOi4C~+٤bw~d<.Lg{ #(B[KiV5p2O-]nԡnkv7eB;tzmx *]?&/2\.&(*FiNtJ500`4 xK]F$A@TUEDQt,K4}AeY.7dA] $K!1yzXF4l6 $Վpb2P tcY m;w^eUaitZ0^c6|>GȲ aI?X].U)b:l`防,2ڑĕMvN2469qU*UڕH+q̘++$QWu6BNtDfEsx_X,wk{iHz->Sܛ/>6Վ??1A'O믿)?޻&+X,͖G^dHᬜEQh>i)YwrM39s9e *w/sKa0]3ۦ#{>/JskUcYԵ11oå쌳U>NgZꊇ34ً3\C0h F㣚Vm PuM1=x)*I t7q@.}َmi*Rvq<]~ke ?>>\\}?>fH,46i^6Ќf^c2T Z)\!uRq3 5[_ VBkU-KJ6ʲ\ BPi5Hg4,ሊvdO$q3L뚢||LQrE#c --jmz766ӧOwJ\uX=8|:|Z\Nfض]Jm*M[tZ6V XoH3%lnSYdzYk#^|El%L&8jx6iшEbm3:,q[?uM]2窐b`ZDZm؂1#5ʄKΏiDnˋ%*66lo6V8^p/llNԌhw'?>rʪ|u1\e aI( mI|Ǫ2D]*U ȪBZllZZ^m$fl% GKBݤ>I\0L U=l1ŖGGtrI#p/ 4\~_l)y 1gޗtgG?4}>#x~ۜSn߾`0 3fq0~IU4McPY^&%lw8??7WVPF -%J+,$¬orRh4IcFC|՞WEQQ5YfS677)8N,X^Gyl³k-,+0IJkň0y׸M#4EE#% _UȆVMBH_)k.:<ϩ p5rZ}]UV,%t ܾ5<>8"I B6M$']"+._C XHǜUUȝgڗg-dkX'1Za K ^.e'Kq*k+AX(Y** vgmQ;z.u9==e8nĿ_|1BM߿ϭ[( 'r||`0뱳6QJګwvlo$ o ~]C͛1 lllPU>4n!߫tn:l69;;[?(۷ohWy&Fmz/^۷ʂ?ped4ݻiJ}^u Pv],3QG.K-\)v) ;K g4 o-{{0Z:f " Mv 6:k߃fhi8HMyem1ιy6m1',ضKӣK./G ^Wzy*,kTEł(VyB%a<#0 )˒<ɡ0&@$5 L3UPy(!v|J^$4[>RxhtQJҭW|pd2OϐRrujb1%mʲB 2AO)[k7ڌҶqŷMvڍ&vVI;.\\,8<09iQe)&U a8 )`gG]"^A#蒲HH)l6a6q?G1dYEu1|ϧ\^^b!o8$"Wڢ`:Qh\#jKYNH!eEa;k,yTae Z5M0WFaZ-S)i6ad-u%7KP dK7jc S,C2|&I`gYimeYY]:d{`0Aߵ4Xz)e@nY˩*Eic r(ʌ"#Y]<#wk_99 U$8^'mdٗ%WXG%{U_e#?Qik=+5IWiK,,VjP}&r%1RCgc>;c̨j1 X,>#!nbcc`6[__g:5\vwy 1łHk:mǤqF@۷wASyvP5irvvbhE"jkk=3zqj$ B:qi>_|ZkeQ%>^ǫJ]}<677ؠhQ%ߧ2?~饗p7rwZdƥVR%OLXe'峰w鱶.w:m<-Vl+r\vA57zm6t;!BV+J4%IK:u.QitPr}}kk[ضh4a1X3Ǹx<]/y b2Q €,3`ޚf32!a5OUU4UfL&cJ)+ZHZ-( \)k6n%?d<i5//l|McNNx 9h~><*! K;z=hL&*(H4Mu: +Rf<`2!S<ߧ\2-!R4b|)ٸxN6Qd h@X'eױ)xz|M& ͚/ywe:subqzr~qJEJEdzɽ{cvz|],llY UhԄhed[$K$PNjɾk2A܌mEEb {*#M 8}dUQUoƹ_֍s& шu֫1Uƕ BRX'Z//ݶ%U(X#\tm$y g%IR5qƲXr\1vUOAYdz.sA{ MN(mާvM(j6ض. k+/`'G<~ ZYAKMuֱ/2AN /֯~+|W.R_r,,1d}봍eeBIvN늵vG@ s!\~v͏cY__4!H) p| d< =!kkkh >T>7B3}1'''DQol6[ê ͵jȲׯeR|;+M:&L8==Ǽګ sZ4͕nq6ouߢ $IHuWc|tLNJ 24BJki8-\ *MaV¦DG?=b0ZuEjtZ!EX>VsUU-@9.d6Lq])M+ʲ*Mp]Y^no a3]/%uE(/ h%}˒iqfItyQK'WpQfYT}?zH0%pXܠaYI̽B861/+ |W*3P@YAcei*Jm+ҋ4",C׶ɗW{x؎UضхjIp{_\iAKk?( z$ILߦ%t:\צ]c22A,hnʘXBk5X]뛼r{'21zz:gX+]m] m,S*I{S֗'_" U[^YWiƹz a+PXB+aS"ɑ2AM!wCu18:֭[8'''U5֚;wPU nܸ>'+@1z{|{ߣlθwNvͣywhqQJqkKItjBǦAӥM'ċk/‹I+lWil\k:QgFW uP(cs9QUe9H쾝MsqtyWyċvܤ[Jt_Ume{{{]^ 5$ JAEdbJ2Z CC8k fȋ/1S>V?Q"tO/w|vTSry+=-H4ehPeEf@2pM\#4Yi~x8kZ9.xx)Lor91~{{g#> X~X`ي/UFM'8:<ǖ,|)ʲ" [YaL;W{HQW:qZHT Yfq1vFQ+mwԖe-#HhPXIF%$ +^*rl" McFDрT4-*/~؎+)GOq=W^SbA IӘ840^٢5gghedWN)Xv6[`ے<\G V,{e&gtIɏ]Y~9/C&95K-5JXmY - /,nJ+5J6zrIÚI297ŹU,D@$D"^w|* UiFԉXHӼNFgXL>0jƸ6kd1/`zn|ֺ$q3z};74))+p|CȖK"EC>~DۢS GfbhQ+=$a+~YF}_m ۟nE`%UY{oOQV%mqLQꭐQaʊȗ0ڧ<}wE1FFMjJ~ף_tz /?U`+ a=THʓ1ơ'"O3=Xz<ϫbwl(C. gxÇl#8f+Wp]xq]%ml6k0++ԃy CK.{{ ޽˭[8::^5k4MH\|>Imnn#8G?\rc I899!jSگqisO>a2ggg'?UOʲ?iu˧12'Jk2'SfudCFA^9Hu(S>ea{kE=_7x3g]aFGS˅KLg 8<1bv\U((ӴONC'yn8IPYo_tZ\(٬8ah&ڔU^_7; y¡b8Rܭ_bMJ^|6f8#cQԫe!%҉3@[Z J[s+~rr݀K}PX*q N ǧX#(aЏXS h2&bd:c2_rx9V(9E0VH Zջ5(j3X_ƍH_~38$+R.*:(^3 NmrA94%MCJUU<-GUVw=2EhC3\@o5)<c%[[[eI)^?v`U5,J2M^~ )CLy8 \Qs*Z.y–{ `>b^C,gS_b|`u<_Cm&iR!FAH7T= 8Cދh)=-$zkIro6ER Lsveta(kw2Zar xym^'2y1?| zB[gϪ%ҥЫ5bFR/?)ӬV ;ieiAN@B6 NbB;ٿew7dLFwI%eV > U Zk߿O׫W.z5oZkNOO#k׸uv: -cc0'ac`gghs]޽}1k>l?G9 !e{{pys~~Ν;wC IDATʯ`e\ny뭷zjÜzѷ\tw ݴ& C~7}ݻǣG8??ip{1xoK_i8aUQӠ/ܪ#K©FQXA: O";h\I*RM֎eLL:бјY+|Ne+t:L[iUK|g'OHS0"YNȒ)2 t1"/7lo;o!gO)9Y'g#F eQ7OsG,9$]prrJ8^E#Fc 9UT9UivKXGsلrl9 K8冭gZl@ڄ_IE)IxBԼ+ϗ5ƃ&n #\$nw-e$?qJs<~(a,KHӌ,H?KZV}8s?w8qͲ,)=Yٞ3+jtXvZn{v꼬uH|:):lm $EWE}+Z@և @I(z/M/QCS䪪ꃀRf<9a̝'DQ%']/dUW& } nwZ`L,g;MwggsNN/8_\ª֞sA@E5`sڎV9u^UNRP%BCI5y0%UJv:1=o~5( .8)LJ#^}u>}g}7 3򕯰{0tt:.]DQu2%"^y)_lmJF Cyx >Æq"wzͿ.D3|{OOI _VY[oѧnQOov7zxzRRJ~>Xytn޼_rr1҆~լWeglpӭBY.=Yj+0_~ɳ7k^a{k2*RfˌBC$EhhJVX /hwC$YBVYS2+Y&sq ,ZI8|檜ǧ,!ejw]bp~nȸ؊Cx6'stIY L OKi)Ӗ2g<Q[%x ڔ]'n!S>ֺ䦬ӚEQ_u+ _~ 4j 唪2,s.h ].FNNyrxx6C~QAQ<qq3#˖S[O,*hS`,u5ov,s6,K jvm^#Pc {itkV?PDq@H|AKc{T@ MB\z\,)ggg(OΊamk~EZkazFTs ɿ9U$[pKIC,ҼeYb SJ=IIZT2jZ7:pR5=?|inتEdR(sa^}xX)W=_)!l]]lr+E[Ӡ\+W+h4 %tr_k_P $ 4wW{% @%Q2 P.m9?*I U[`Cy1OON/sU>f7ooF#z=wN,K666<FLS(j,yi2rt>v;ԆSF\t,˸|r Ð_0HhkѺ(2@R(> ._:n-rvvVW-夹[E~ǵT4F{g4QkK_A[te*<7XH49hTٴi(Vج^I:ݘ;w?f}8dm'/ O5~wn{IҌe2#ɖYA$YJY  y6ycd$sЧՎڤrkjd h#Rj㼭+ I ހEy6+tADEBUfXr<I:鶺sMPU9Nϭ䄢 _(1M>æ!)W˔d $]:S $]4C`m &1{3B7 rSO%$' cul((!]@dcYuv&2V KƳ%tÓCB u8j|!msuTmQ I akV;$`՞#{KCQCJ>owb*Ɋ0rh: nutރJ\#}tZ(8??g4Q4)0&KE _x ׫I)_l9N j$uAU?@ Hؒ8Z* $`熩LE+M\ XiPxjMJxdm}V2)5?o 􈢀(:%}Z«1ڋCV j^W|V'A9;Louk`a:3}ʫ|>ի::j*VC߳gkE]ռ໫U֖{Xyӗ]frxxk:yn}o3蓏f8%vU?\wP%9.bסkR4 (NV8U-Uܔ19k:;u$n ;;;ls!?ޯzWo\%4YDP2تDb1VSi6GWa;J$邲1ֱ6ŭ:Yfꚨۖ`.{t|Vji7*B4,@W28V* cRlZ{n,Zk2S Z-Wѕg)uKH {>N7sK.,T 5"3PL #5PxH'Uvt:%OZ="El=Fթa2Ly%(aeZ7:ε+:X)@Km42jF  ~lެeө^ żټR^-UH//peXit!j/o\.cv6Ӌf`8(!n:/\MVk}/畂Fԟ'~fz4+2/X&)ώN)<&uF>Ra>_[|yv(DZdsWZڟokht:c<y;;;v[_PK U^1]!޾LJ|;ݗasxޣ+ Pg4Y &KΎT8yzJ߽%z(w^O|¿GerJ []-{y7iZ\rO~͛79;;c>c,c{{4Ml*ikۜNb<3Nt:3BݨxŢH)~ժ#w{vv %!gYs"wlmm5_i~­8I8==weccrPؽUߐ)719/}]eVVP[V- , fuZ {usSvۡ*2.0 j\w'#f)f}Fuoy]ق~J3B{Ae *Ѧj?ISJ htY?QW,I~ZCAp)kPy ]]BVDI:G%E`tJC к̗QJIWı_?(#$CWZ"":((|弦q!V>(!j>$ajT ÐݢB9^gI B*fA|c#L5% AZeHcфhVBZ1 ]Y4#j!)s ]NAuR9Sڻ| /~juDS.!$aUϗTU[jV^ȋv;nVt#դ@P=+8_NǴZ [XD%yqt:kBݷqye>ʩJ3>sGG|_᭯]>{)V1}ywHwyt\T=c f%4MtRf#,ڵk5pC!lonP%<{ w>}G}}vww~ʪ 9<}u떫 }h֮07]@1c!;;;LSNGs}ϏcR|ᇼ[>~KyTPY kWTQMU(lpfgi"6.`sI)7\ChEx{;{S^}zc($AWz(r7a+/ ( [ %jEM@Énu5b04kTc l6 S@6^IR +PjŴ[hbtP%Bhtk- src^v>,-)ˊNy<s3S+u m/wTK`[KQyrX0ϞaÚt &Փ) JY|J圪Ȉ]( >B+kyRJEqcdYPm.KָhUhJDUptP3 ]5nH lENmcccpNh4ԃyyS=)CJLץUUDr7jtPqkN϶L֡@$ :7)\\%yAq`z!&vijTָ>.@=Vao/KOnpu͕fhlRzu[__SڢIۻK+%(;̅vaUy{Y4aC|jSUU[T<##O8<<4E/2vFK:e3dL/ψs^ټZeW`-:b(#R^{〝-|1ˀկ~w}YŦiJlllni?d6O!ϝyoo,Y˂ S kkkad2ak c^sRu(jMLSn߾h4fooS..&cj:QȳOxwy1 󜃃iZY$K8-,a`.i.bul{eɿÓg\~ǟ?bk@OO\޾_~l9BZmE:%aօeoogϞI)-IaˊP t:-8Dӳ%I:ҥKlno HӌaOx=e6U]dtqFE9asz:vYdXQQc4\rla|r쥯?k^Q*ztZQwQ_t6c:iF4d1཯P18ÓJ7]{82e@ۊ41Ԇա?XC 7\J)-TC{?4̗)E:+|d:RT~b+]9<~ IDATCd@V YWj-x/XXK$%k4"E e:Gmw È(9y^RdȪ]:[4[Ӻ'pxx-ϝ֑%#$g1HҒ$ɨ* u"Z)010TB)C-(hIEΝ;\|I|@ApvvƠBkkkZ-S) `wwfvvvH鴁2Fa8tj*]..JE\r'O_4ׯ_F#vvvӱ<88[t]᳧ܹUIۿz=տk~WۿegG?[łrK5*V0v)OX-&C݄}EcjA)"j諧_ USݻG[0 8zvkWy7Y.`0p=׿E9zvSZX\L> |L?!;lllGz|΃fkk{ڰȽqGNFNӔl꿊`:r=!:q71+Dy ßKNJ֮|+}ni|~F[.4؀UEK2VrvvG^^3 `ess;F[V~PO駟3IʰpM\rm4Mi@~W'|W Mx ,t]KSԾ"AyE^1OH)F).Q!$0(k hRFUUָ$j@6ƐZs C3(堭JFuZ jASU /{+SK+B:,#5~2;Eeug0Y[zIe=ҦIF,JQZQ_W[^Gkm}f ØLi>[2inssnE>ӧY%Q#|: 6[[}:qԄE~@^t(ʬU;_z 'KgXS(Yc|"hƽY+8Ft#EQ;ah:Ϯ{.t6c:!]qJTu5rZK-R*ڝ>+g1>G<~|W^}W^2٭eǏ0LȲ }_k9}Q~c}X/0'$)?6%asz|J]Q^D$||HY}~m05LNwfMwwloo3N~e3(Lxc>Ge <.Y({@AnzoWCjC\\\4zteXptts7lP+3'N;??ٳg;*MxR6ՆzZ]PcER+Vl|2m-<]6Q\S ׊L&)Z()8ϛo`HUNշ|b?h*'/5Tyt:7q9pĽ}6ݶ]g{t:ܸ:-AVDǼ~9}!<|!嘷n^O={E) ~3f=\c{ǒ;3$d6VkRgi4U P71w KII=BAMMVE&$d./nv\sKj7p-2;kq5.]h4"]Fs(˒W^yQIӼIUxY$iұᰑ>5o(&vnd`@QVF3w\׵p@)E%i 뵦ԊsqԤ[eU5Rǵg?˿̫ӧOÏ4__}]hm) w899?=nϟR6󚆿V(|5*Z5Rkˋ2_Д`U,0E+|!"\)i2'lLPU9y2uuLi?vln/^ࢵM1VIЦ/"aowr|8K !*EHIU$xEPfH[2 v[qc{G^UUs`Sfw!,TU1Ʊ-ז͠f0-FP9a఻;ao 9Y9Z`_20CTfhs - -"NO8$I^Z-J7. 8 Q,mzYlfY+',3Pkg!]|`;'H|hTHAs_^?JU1LCL􀋋 V Υ\tx+MK/‹/HnErZ$1pu͐}/(˒mx,9;;q̺s8հfA^򼱩;!ahmx{d2Gq 9>>Ʋ,:sVUs}T_Pa B,a+SUd9a2Apmn]iϵɳ]Am0CW.3fez6eG\LϹtt4X0q!]߱$Z3n=>biNe JMpf ɰa6UU0쇴ZU=ƶj36olmm}NW2J85|4XBc8==%MS1X u82%vi;\LgqL?d{wp1\\̸7,`%i|_~?~99>w !w yQ\&\vӳ N1Çz=:NE8L&MgX#XqS_~Xݾ}`0 MSysrrT[AN}Q<둦)ﳹf3 X1gIuEJ]4oha+mK.ɧfze& :Po ^ܻwW^[omuk??07> I25.yi0O[{MiIa洛Yc5UeHCoYeX^) H"E= {P8¤U4yF1'tКSX4jYR2lY+H2$"~ѳLeRY-z꺦+\U[ڦC*ӄLT% / vL{QU CAX|@ߘSOߡ c6z'Y?JnYOcY[7kf1A" LӔhxm w \#W46(O>a6iBa27rKi-]e-,(Yaq9l6Bo T ˠy6Im&a eTY5:eޑAZG>{\&N)BڮAk7n c>3 ri3^{5^}镆e駟r%qkynsR;-L9bXlFA(2A >"R rxp ,حJ<VELU al6#BZ6S:EQ-Tä!ׅԔh gǸdggRE{Q YE346TUMh IDATJGE |?,s1JWVCZ8^5h5-06C(]+dY3y^ðQֿ׺~FEaթi۶=MFe׮hjpJmR5 ]`l:61_`i/h]%hwz䫘nOY*8y!뎸q^y567vHӂ"7C \!Ek#eF`z>õ=NNvYV-GؖH 2iZkTUIxmkeY!>IiB2fEwhwJ!l,c>_-nn>4)ǩC BZז8!Zα%!l {]&c666vt첱nRZ.V}*]`&լǟ nYyh) EVJyZOgpc>ϫ,tqנbuBqISyn%ΆT i:p=Z:0a؜+S yPR$w.gKT $)IgI gU)mCZGJTPʪ Y\TIKiA"i,VedzJEJDЈ潳f$Ƒ6#pCUYTEFG$UIۡ ٞlaBqA۳fY:%a:i$gGE*H"cXD+U| q\@JQfXՇ*N5mK$a2ԁ5MuKUTtqC|}9|sÐ͍vbe|#}C^֬e 3˲ݥQ?n3ytzLS4޽{ `JS^v۔gY1pnN۸oAy2aQҴ9:$I8:n~??o;5!5ēy/[o1L7d21ݼ%!͛7xg4v#Ʉ_*w!vww9::裏S)_ x>My8#j3Ǐ z+qGRkjDIJ$U (.pܐ*v,,i,ÉBe&܁EpA@[[&%]ڦ9nsH* |+UAkI:6PdY(ʱd(qX$YBUi<70^ԺDJe"J0,-kx풥gh۶L:P¬z kY,-Qô&OL3Uuki8;]4;!XORkxMTQTfp[b^\b[s$a6'SR/m6ITq||'GyF;avE)Ӂ!^uA D}hrF, r@Be46ߡ0EM9<<萋9ea( MV5R5$( |b>"D-}X&Z裏;96_w5w}awя~ĕ<0>, S쌻w_!MS{FЎxE>?캮I!P|P-钢,hR:$ ),nBU$Ъ ֪8IIZՠH֭)f՜H 6Jm^U0ofqA:KGXg1yV9֊RTQR@[)Z9yNYBبJϪ紅,&PĪRUUiQbMY: qdWy_DQEF-J)m9߯4ѬrI,*K ea|c9Ki]2ʱE 霃s!6_7ƼHPeaj%UDYYqWe>aN'$s$O ٵ̀UIDIDG6ր2*sltٺl[[&HӔ"b:rzz^,sϛ0Nr}<ڡE7AЬ}}/w:ҥNtBAˉWITtW%i#e8|}tqlvAӡ,K\Gfuygr*G9릖lV+{&oIqQ-U),u.U"lkƚb5a KIa521:L_xxcεִö7jUAQonbǟ&Xe|eYA@w0h%g \CxQ#-sKb97y&Œ4IҘ*ψ' #1|4aʃGܚ| _^xi~Bg.>Ï9==/[ibXFqK/cYVO~h4jn'|¥K$ B`:RE3lll4)Mד'O899aoo,, @imx\naiۍqyLaΝ6>dx<Ԋ><ަ]X,88onԷo=w NNNs߿Od M.FC677NjlFMsI EȪ -k!@XEiL+,>},,"`I(UaYŪm͏/ "bMY,98eoV6¶JXeNjTkAWx,Wv`*#Gx*K*I^fy)˄"=aaYSҕ*-P)ֹQidYj| Nx,ggw )(U` M*+Ft}. )e؞VJvM+ [ 4Je}++2H#-3@|R=R9-JT¨0Ed lOnbbV}JڳYX(7JFjE2׉ uK Go>ylĖSv"/w} vL&ƣ74ϖ52 <-2jϩžl#+r$vY."oRWn7}]DY%ϫӳɄVv^P)8 T R)VdYRcZaQVmT,w"M,?2Cd+t) ӭ)*sEFXK3ln%ZزE @6g &ዸOxTe>lS3 !- F8*( M .3ݳyye),K4!ifZ7 u4€+S&z;(U*QaQjE5>$I4i RCY>RRUeZ%,溈#X,".X̓&P%YV`Y,-<3YK`aR-Jn6FlO6G-[HʪU!-Nو:9:/k-soqmV?&:i,,Te[+ ؚ챽}@)%9y"ɡDђ46]IRP& te*BB,Tx5{29(*|Olmu\*]\9IJ@<K׉t]_[F/ DZ(NT(Bi*D Jy- $cjh>"ϴj88R5nfhOFQ[ZAcuZy-2T`OWr||̇~0FeYyٌvw; }=h}}/eY|??_IkO>mANnӧOmrR7tT}AME ƋӧOw7n`41;;mܽ{>?n&׮]{w} 8M~:;;;Byvvvj4MBj__cjlnn<}$xeLٽjq6^58ל3ө12Kp]A`i }6$B@4ZćHԨ)ehW68ҍnbYl״8NNheV 3<'M2N 4y}s=ڡGgի &Q*| 36!4MAkuVE-j5O@E*ZJsm/0^檺] 5 xz=ZV:߲l=it8{6UUxڕg4f0 ]8P(L՞Q Jdx*"_Fhm:(Ԡ{}w23)2`@m67BYEni,EX J<34v|^Yi\࣏nDtX& <ϫt3QYZHU U̯tj n>( Xsw)-.S,GIu ´,:TySssnp__oSE駟zq&)]#f//֨ǏL͗u ]SW]LIVQy|p}~vty^ǭY! IDATŇ|­[/Zm9ApppÇyͯsp}6'-sdo Qv1^?oh<fBGOX.#zi'p9o\% t 04O y#Vь|\z_Ayce[ˋ| s~'sx9c`mv^ӧ >=xw3X(u<6G>S^z}7 n"w+WΧ}]jVa'gZK6n{AY$)K>㱏$6LAA }9aGP1&!MrlR1Q4'I eT5a+TY",IB~BnY%SZEЕϏȫq1_=l%]QC!A]fR񄢰HGLgG,(B+tE,ȻOh!ZylJJ7; 7#(ݥDA<.2B" <ղ48&s*ef s,kxX=J;kndZ)*@uvbOrŨa{kۤ>rGk;ۗK2LvY Sg;77+BKMLS9q7m;*V-$izZMb82uۄ똶0pt%m?;e$UGSu:KPjpem TnfkR֪kBT3 E坈,_SVќUtj$deFUh<aXgytcʕ=vw&L~r#^xӋ3կt9?=#-嬖b5b{{%s:UQ2 Y ,.|%9E1,*-!E搽=666 5` omTF[6 2+)Ks@zrMnݾi ,T^`)|]R|W }T)m3+pO#aJVQÐx7nyO>=\=284uJ}}.f|+ox1'x}>#1* .kB5ؒϫGk'**U˶fvMgu6'FUMҘ'+JZ9['+ ґTDW'LFETEIgq zrD PBXspmqikîMQ Dٰu2+ &QjeVUIQj#$"(T- iޙ @UO}ПiU 1*z-omeV^Wh&/a{1afggx)Ep''', M34g81NY[ h}zA`Vuܸ )BUUnV~ELǮ0vYI﹪ZQh*(-z(f+R(]e}U~KTR1tyb,)*LOy6},fKrvr*s~3|Vk8Dٺ)_8??WbLAx˲-:yfJ)ƯרZ{miVpݰn|.s_εkWZIN 2X6Jфe7ܸqNCgUok{s{Ӵ,W yvv$ QlTРp666ߺjQ}9| |ʂVUK& Z8?j>gؖh<ܾ}ͭI:|{CC߸ڜ9::bccu9::q$nѣLG'tu&VoFss2Lq2+d{Gߝ``byίjlnnnV O/J> aL&fivp]>z,#+1z*JCb84R:-XUx\K)#<7K?9}4ƈꐦfaL.ugaP'ATfxu@4m62Xsc}vB6E> FcJ'TUD7u27e|leY^ϔK$EFYTFaHdž,oPIskYQ)EQHlmFB(Pڴ|i>YfqQ=ڮ%1E3%’XRPY(X_,~ڶRi]kB,S:q WZ6~ۖi#4nٖ]]ʽ{xM0D8}/E˗W*NsYhЎЪ~MAsPY8Z4 UV]B[DQ\ ړiUcK+ +#kOEK <泔]o+ETuUXfwܧT\6L˵1QZgCkGޛGYuޟ'YY{uWWtJ "#gAqTPG_QA3.(HK7KuST]EUeUew_DDfS̠]|ΩSYqox"CbZ.ksi5S|_gZok]cmm0 'VAF!{t~jyYY<˿] ǦZ{$E΢iZ399uW=CD >BPM@ac4 h4Z<Ü>HkeAC_[[cee08x3~)]i-jjpYnhS*h4WgTb;2 }(RIS^auez}erq*t]eOcǎq7ȽLF mjLZ\ C£8+I{/(]8Q0 %u? ,ܚ"Ri61BR#S,[NSLr O,'oRDZ<9&ZQҡ\mtCöMM:a)1~0 Q 4 DJX|BBZ:T7 CӰ- q:!HN$@H0N=r;n ʹa)BM R?Mjw.".aMd#4MIҔ`$ef0Dd)ulQԙ``vv<|웜8q02sӌM p)199ERAMAP#8N&nA05siMCC1iR@2Y{[P R :Pf$@aꓒ".@O5D %2':H#F%IՉ # [NzXo61Oۥ?2Vi:Ls͕ױI021>G:?=To?qlf~~f9ifQh O:-`˿g `nۖo\ Zqð˅GE-K3b}|mFѐUa"4~CE:V44:A066Yo`scPX\\ѣb]V$TJ)"fҡh|.V(.LLaѶuFJB?dbl#GdJIEZe޽u,`aaQ\6~}KKI6f˔M.R̙34qcb6OI_Tbvvl™S?~zmDLNNrWI4 @4rkrL#FHߣطoqpqR9Bۖ )A5XY_"G:u(Ia:cn7mjRY9~8Eo\D^N)Ȕ8 Zi\VkLӦeY( G=i~.jIQ>o0$߁MMT'Qdz5V} ^-~RM>k5:$I4"X Jݶ,AY1BLL$"RAAdhDCcLS>\,B׈LϲLGbHS8E"+q].k;/so>ixiBJj|})!A&n6" BI$8@ي096ITD#_\Wy M҇4{ضNQ. R\O yrL7$Eї"4<ɭȲc{$:2)C4&I3T[}7tijE*MEvsѴ$R u$Xa|*G*~@J DQ@hA8G)o{8ض㸤S=FqDhdâU!3va`.q2,JTO]0MFiIŠe1$E&w=I`䙁8Ns|.dϮLMMa6NS'Ox<[̰Hur}w~ʵ 2眉),S_.ɞ${k#*Jsޢ"4-䭎, f>тp$? CO ,ۆ⬴'$i$&&β QI]$$iLL,etYjO5 Rv_LW [{,{KߥZ) g299=;# ~TB0k_ !t9|0808{,{V#Id[*ViZ-r͟ -# (oΙNזwX)ځ$<-Zmed&vL\w,Jվk~ V2:ZbGC˽[|I)r:11=8x`*[yWxWd.0vx $k3MsnOKFs 676ٹ{7rDzhHWV9r{mZe&'YXXk.fggev}'R ¸HGQNj`uKm?yرpXhi^xʆ}y!7BG/EkP -0ZfRre޽.hțWs'iu F.id8x=: -P^kkktm9Jx1PNɦv!LΗƙₜZrq^W~E.R=,[khS] $#%pr p? QP.{h,"BK[Yn@E8F,<E&dZTrR? e- GA(ƈȞ4iVV1^Md}M]#2+2qmiz6l4)b1)e!&`IJɅX&IdHj)W(|a 4Qd-4Mqd8a& <qTdw򇢕i̅EӲ4&RD AZLz柟,&IC$pKNT BQ|qmY$$DIH&rDjR8YV(D(Uw.4"j%)vس{ժ p(L(ALgvvz&ȎYODL˲qɃ<ʳ}۳gWSWֵ,`By'_G`415=Ν h4ʲHx*6-eiꕭ h4MeN ߚ.N:ݻinvH˽n~ N8e2 tJn1(ҠrQv3\{> ܲ*njlJZi399 rX]n/tq a01^1;;̓;蒤Q1677f[n:+e (=\Ϧ? T4)jF#W CLUF>ZI&aF:aSŤ:0 Nis85) +Ȧ>s0v<6PVM6ItA@c12H}T0 a3WC)uCd婐4stHR/F$ɖ%wWdU4蒭6A&HG7ɧir= &Kg(DOԘ"vy IDATa}۽;vD#ʥJaUXag$Mװir5˃*w*^ x.5m/,+e $Hq܄lV7EqF#tC`qэd+@ʌlVD%)"IhiJLj$G +۶4 /rsnyj&W\~1ѐݻff{&qrM?|Yӣo9xE%yGCԢ_dllw }y X^Vk78 CVR6Zul֢VAX` KY{àӢ h)4Eu]go3=YDZ4za7_7:/ꫯfuu޽_TCN8aŰԔ4dǎϪvN>QRY__#CV啳055E{-jbl(uGt:Z9rgy;BxϞ=9r{yطofVkﳹY U ~z^c(E-wY 2 8y$JÇ#ٳeNCoooٽ{77<կg>;>%v395LLp$VEi[t:خC/d-lFE4M0d:ehp$gIFMobr~y»m,&SS=&\cU(`jj"2IiMl2R?+' } vp(7VJY.)oKf5]PU8~!òB.R)iR(u82_  gN333i$Bm9#~џIJQNCa:5,(*p3o?nt  C$,D;gt69G4pm ׶KE6 XET^9R^STBgR*Man2AϦv9}4kkkUtssEiʉ'dZ]'TץT>dgΜ4mPR S򌜺 Gd~hΝ;+yʸ޽>>ICvդdL^di^ 3vf8x벱AcbbH3 #5MСC\qRL<5 ፘt]sa D3tv4u&Gh4Z<ʀ5pK%MyO-k"Hi:aluaSyI2 t{mLKg0<G(N|MFD\ȃ];(h5;ZFCq<7B8A:&kSe~t&q\w03;Iw/#tC}Ѓ%t)+`n{DJjؘMdʱ^~b~1%D Jc(azbAwHaf` dA(Ev6rhp]n&,5-}^{@8JAH $ia'OL|6s3zAGdn 1ANLcYjgkr1ngUhjG`[0=]{vt\ۣhm6XYY¶m+bz?8/HyE bEJ$-&e\B/$IHthBe5BͶHoUCrBK5TZ "ƒ sеB6C Dh::i"(f6;eHc /d h˾_ːm+++v)Kn;Y]*Nѣ9se8̸rÖ?jy{c[gznw{0I"#g0_$ͳrmX^?) BhhL<|sgyqB8z +A^M{4VE..I뮻re9EviE0@>)!n$qpzBgDxd٠jaY˦BhȌӧz.ӧOKkqA뮻8u_=V ] <--3wf&*2,]MK/J4ylfJ=uϙ6Bw^[9y\XX,#GiY6;vTD,,19;"I"*lJCi*0 0 ''IbCF$@di!LNL!Nᑼiycc3g0;;-L;76׃rVZ~GT+5I vn7Ƶ]8EwTHЧ?hy6gWp\3T]4T0EILձ  SސڭNV!qL, p@YQ##^N딈 $t<+`mS<( )qDSS7Pr] hcVaZ.f1fbk!l`ahCj" t6c5<ǦV҆D0?ju dzXYY?2+~ߧZ.d$BҤ2`Ѳ\9,QL#4[1p\ovATF4t=Ƨ++4-f'wfy J2!i!d[' 3sfKHJf!TήԛfߓAZN- B'svrÐY<+͹AD#&c7 .g",h$xVN i ϖ~]#"F !1idCU֦A8uR;R[U ш s݇'>ə3gWV͂ALV<{Ð_Tmgf.ȼěg ~^̓܉%VeO QٺݥBTŖ|z7p+Hapt:lw'vٽ6 \<}۶)8pv0۶ $x|Ge97)JyX]]essShZT;Rbe|BLXE1&'IU4iͼK+taVז{]H瑄ZMz Cah $TuX^^gnUfUuig*o)׆nabBjyn ݀Ѩ LKHoՠGJZi()kY`/o:ܠZtFq_n@Lx>Qgrm( 2 V^,sl7L9I0hR_ԏee(5aD4 C#1m< ڝ.?BCN0Fe Lfr|=;w1 x?oxOrnrq/Ms3.Z@909hk7B$?/QV ÐQ(,Պ8U eumqJ%.sE A C!O/ŽPuuِ#BD"H5i_qەwpeycZ6jm5ΤBR, 4Z R9Az&"CCѬLGM˲Yq^R8-HӌN-3q9{-gsp@U*'Od|DͶ05MFs2tv>99I#l77YJN^nˡC8y$\s<.s&. SS.ǙS N9( ;&SXj!lAfR@:STuKL~pH}fAdpAaJ8 t[E߀4AR#Im \bkfAQGrYfTJxvRmrմdq4ȗAh$|:&Kqzŵk؎}ߧm2^o~WΖtR*xFQLk*X$zY*&3[fKNy/{EӠZ+yQ,{M$abb]0 'PVjxHCLKP*[rXT1t'Jm ^&u$@O I,'1OIAmv׳HR_6P.sؽ{U4c0n⦎U3mlӢTP)hj ZEJLNp0-K.(Wĩp0Wa^F,fjAHrȗR ?ݐ7m,6I,~H37=uK3;v4M~_.q# C ?44ILT=I\%M"H\r ѣG Ð{v{~엳縧lKVx|/6&J ͭVv[nnJ^ I jT\4l).2H5@r:[&I(Hc+3>:Iʾ>G_-}s =|5k6}]_+ӊL!_I!in `z!nl[G.diݮ̚垥l(\qY.bN:SsB,>E]S>lf}}4Ȧ*aX4M9x p |AxeI׌Mt]c~~A6\ٿ?3㔫%9,Sضm>֐8┢'aioh݃W8t8uN²&>?dƴ9;#= )S}xΝ{q˲1t7"ocZje0]\Wf:ae|,?" b, m nGmdg#00 V#nWW2g;wrrb(!̫ y@n{_AQsyoED~-{eZCQe9Cd|ˢ>Woœr׎Rg2pBdz&=&+m,/&^{-\r QiT*ny͛U]"~MxTğ|75N}[ԧP( ׵wozǷ>M"BP( ) BPͳG';?Kk_tcſ}{. o~ËCwxY;/|)5Ņ|s._?[eǹo|v.Bcx77/o|sj]\ G^y>ny;Z .:,v\^#o?w ϟ97ߊe;o`},??{ywt!Ϻe,.| ռm.u _?S3"JmKd4qݒZOux#<̋u AY..ux/zk1-yo}}!su7>SKnO绗}ן[[aKyonz?G"/xُi'SGo{$5{ "͹)7r;gN=o)]|oG8pɕ~7; O['  Mкx?wgZxBks$cl-!N.ckj]'O?h~ ^#_޻u?槾uM>~͟?`ZwuGޔy}6j]_‚& ?;?X[YiϺ?|[? W?9ÿ|诿Sd]sŧ|o?F..c{6癹|:O"?պ0yʖt]4sz\qV4l\e-k+ޯ_xP<̗Of8/Y/E83U}dqS0 ;>w|{>q TWui{\ʩ_ ^xkO.s3GSh]|'/}u..uXəb i261 <1>{oG۹W =5y*k?ͽ߸( /o^ {<1OΟ1~~=юqq9/03Ģ_b|[qw7Ip}\y^pq.iBP(  BP(*S( BPOP( B| BP( ) BP(TP( BPBP( B| BP( ) BP(TP( B>BP(  BP(*S( BPOP( B>BP(  BP(*S( BPOP( B| BP( ) BP(TP( BPBP( B| BP( ) BP(TP( B>BP(  BP(*S( BPOP( B>BP(  BP(*S( BPBP( B| BP( ) BP(TP( BPBP( Ii"UBP( I|P( {8߳oP( |9dUP( aP3ߧ|N:0-TP( I.R<}ČݥfOl|_fBP( ŅeQ( BPBP( B| BP( ) BP(TP( BPBP( B| BP( ) BPOP( B>BP(  BP(*S( BPOP( B>BP(  BP(TP( BPBP( B| BP( ) BP(|h%.$IP( BB4*eKwVdɑ;|?: R BP~$zKS}UR#Hw֌ @n\UFK+rͰܣr5-TTדý2rPИ.ٌe--3,u+WAFQŊsNڥ}(2mj^~TSakw#%%%<n.c@`0^n xS 4dɝ}P9 k%&]KFnKvJesFe2%ɿ]J;q~GtF)rͣI JB((PdF_TD:k=<ͯ+x*Y%&\su܍4UgSH~Kj#|Gz *GxgW}_)_Iֶv2 {ŇHoY|JUd,@A"{ew.OWQ^}rгRgyjkAPh0@([=F|(۔kAM/\:AG1)(N1 P| | @>@>| | @@agIZ"K^^cW woϛ&2O$i<)//>nEEE|>O |@>l{Ro|H> A>NԤ[ԎMk235guP* w{vԹ3'Zͮ'%jjYIw_ѕXaJ֐ޝ|]ס( xf-ݢZ|$INE3ǚ(|w7޶@/5~|]IB>Z7nU;hsiI!IҨ)_(u-X!I:NI/~45E1Z6~M}Vt</GNWeRUoN$;Nio񲶱4fkWsUѳ^S6z7$Ix@Ô. -2˔o 1/رQIq1wTmIRSSx@ ;v!s 󲉡}5qjzZ(Ϫ5eee^jUf=a׿ll䢧_׬[$I $7Aeem=E#|(5lgtrhުLoo9Y@AS(tZ o s|U[QQ3.?OcPVS _1 @>@> ~iG\m%:Oi!fmIg7҅evmљu)d;p :Ns?q(;R$Ӵ2蜤-;ЉPY9躽MgƾTIN٪5eBw)]3&SL>jTRunώ:wmݱi[TGl|:ڱ 볛nxUzZjzwk Nk nf5Zdh sj]X}r<]XdoNWxHn!V)tp=锑Z4su|\}+񱚾xcx\Dj4MӠePώJII$s^GE))1Ag}0UM^$i_8wZNUb5 I>.... /UGJjd^N1‡{eӔ/~$umk^w"-}6sm痵.2Ff}#I/$Ej.=-.m9a~X}[N~~~rssSzd(4k,YXX`0a4`߶ r?qDg*UѹJUTa%M p ;Nio񲶱4fi~çȥ\Jɒ_7Rh^efOI]b^W꒤uohaԺRk.hĈ:ykm۶7 5kٲ׈hչsg߶hbt(ujMY*92 رQ}=4ż:\o[L+qʥ%J$YYY؇[e6ʏҹ'bl MצS| Uemmʕ+kƌǎSnT|y)SF:u]\\`ծ][jܸߟջwoZj:ujn{ ;ڵΝc3Zhr$ͿR͑*YG%͑J*ft>@CA21bԽ&_[zۿ6?OѹDrWę?{*7z_H9}IyVԜ/@Wi∐<Ν;ղelwUzÇuAUREC Pf֭Zv;m۪_~~رpۧpmٲ%r }AAA?Fc Ss$hTٖ2\Qʻʴh/,h̩G$I-vQUzvl)}4-i$nk^oZp诋}M$D/ ՟#+zbbbm$~4ڵkg(3as!!!裏re߫tҒ>@=XI;v^xJ/֓O>): 𡨪lߠ^]n_s ^Oݣrs:x_ '<'Ii79jjNզ(7k)_*UTwڥPEFFմs \xs`SrrrTbEJ*y@n灁 Seaa/R͛7'SgMX$_ ~]M).>&mVKVV֪\K[uVl튩wcM|]}脅 [ea4ʥ~ 9#Ox{{kÆ ٮꫯץKtĉ %wuu7o~9;;uZd-[͛ё Fpϔ.SN[iY>uæmE>ϱ^vY?r~W}[o^|}}u)M8Qӧ՛ [[[=zt:uҐ!C4m4jy '''‚=a5uT >\ԩ|}o8m4 2DʕSvm{YjՒ5j; }=1ic޴QJt"_wqكFaqС IIiP8eMgȨ|צD~rq> 3򙯯% F|{@l++(X s|Kr+Jtp0aف>tqJc|!..Nf͒ CFQ ,P||<5{lYXkh4jܹ@( յkW͝;7g4h"0 ͟?_F1zKKK}=(ԱcG-Z(C賲Ғ%K(:  { TXX,--emmKUV=6wY[֒%KdccMё|I`d rPp>`p|}}DG F|{@l++(X 馏 IDATs|ah:___V#>ܵ?oRnM l|< 4pr=d tawyم][tfwJ1@0‡ft-F'ٸ}*Zd*KJLFsʶ@'> GSrru{G=Ό}[ʝ8UkɅSq?oQ/}TM٨E-G cMd͙0D}<Ԩ8ݞuII0UzMkKMsOR~ Է{K=u<)ؿЀԼZrԻ=;3InodեqM.G_ȷSWJQ}p*o+ !;SU*ݘ|ȋ{mu~=IWi42NE3ꟳ+dR|\}0cQv_zRM{=}7oV ]I ڽm='Rξڶ{]zEW)|wz- ||ѣM.2FѼP888(88XFQzRJJJF>Sm\E\TRߗA҅ICd0_?8Kf[&bFuРן&ȟsUwl̥,_t!z{TbrT_euFLCjZx kӏ_ߓc}B P맄IRuPygΜQ%tYIҡCT~}Idȑ#UJ-[VJ͜9S^^^*Q$ijذ\]]Uvm}ga WյkW͝;W9E^nUJPY9R%D=Y9R)}B,|;:K.s&2C(.&ZA{kZxwp$8o~4Uᇮg'N8.֝<\Rnu"B7o֥Ki_uIYF{y }AAA?Fc Ss$hTٖ2\Qʻʴh/,|$(16F>51vѶz75o|YŤM_z}B4H%\kӠV5ת#I6b/S0L0nFt&|SsmmۚTAgO}CգI_>C3j]:WBBs_ &bڹe=^Vmh疵*Y^y34ܴ׿$)jRFuJ Zw3m*QFڻw\={護Ү]t5EDDWRtoÆ UT),YRժU3בL2^gڸq裏$0YZZZK.UV{ZZSV* UkeMX$_ ~]]%pIR{T]V6X]7k=\VC]Wc-G*Fmn듯׿mդSlJ;7m:aV)7wYr)骧{CGθ'Ǣt:~tc#qܹsU^=999nݺ3gV*[[[I߯ .ŋ6Of^z/ta}ׯ#jݺ,Ye˖yrttct5oFeDʔGVeZ~ռm:˿MLeZv_6?Ue2%ù+D'5_/$-_)Cʲٵvsj$+#8<| O>ڔ!o[)]<0VTʕKLP1 3__V(#|@>=_Ft@aGq~,@@9|@>J 9m%:Op[@O%p1@>@YfB!WѨ (>>|A={,,n5b45|uYt @ڵΝc3Zh{PC_PPϟ/јiԾ}{@VNNNر-Z!YYYiɒ% =*,,LҥKժU+P@K֭[kɒ%QӦMH>@Q䤀YXX0E9(x8|x0hgJJJ##|@>=_Ft@aGq~,@@9|@>rH:Wmզ#|@f{ޤOЌ/6x$Iۏ̝Pp"U> }m/eV|H>\Q; ;.@>-<ϳ*( {Iy&s#|@bc.e taȂb/_Қrv)E 5FPF$2^u$͟2R ׮je4(UM:@n$I/1_2_WF6]^g( R{T]V6{ez7ָ_udee-zgLhוN:Ç"ſMga٭^MIkA~-;ЩB>| ۲˫"Ig]~ > jptAbJ| pMLUTzFܯpm>wt%.F,߮cվK -S9Y@Pw<*WU2dmc^ӗPrpt0:w4hc3Wo[W|e (ͫ֯[Z/Uy _w Ԥ=x n%IڼvW_+=XWvgO 6KMڴܲVpSF$شF_m@qJM44wg^6?~%\^޶U@g.2Fai=mt1/KzZhogEsm$ ~Sgh}(-rTҧ諍Ԇ&jJJL2d"o9!So]8o(%^:;Ao! zA}(b$ixnjjش|< ~45ۺ7,囎6:O~{Q%̯+9W64.I--~ݠleOV>'2WJǪXq^=J3ܬGjv%JKƴ6t~Muai'1 b.T@e2̡m:G|< ۪t{']OJsj>*(_nֺ6!_ڰlTiH_-›@R~3%%&()1A+RsVۏro?dJ֦ߨkGrՆcȿMg}O?cV^ڰ+--\R1rjñÿkߞ^;~҉@Px?F߄T:j]C H_-fՋi ygUG92BMhXH4_!6L_}(+ky7.K F8>`K@>ڰx4䂯l+}Ϳ yzwkZ ۈ#FP$yYjС8(@6䏜~F tquh5բQ?ϛ mUj6jQQ}u!z)Ys& QG5b:zgG;vWmojJ77mJ]|״WN/X/q`#|'NK=;*>yY?b[ٴۿwV)&$$񓦽_])ItH-9\O|\}+񱚾xc)].u=)+(9LPEGG&FpO,6ZU;h3 K#>RL&~f$;\e%I|C׵`e$;%e~45۩ܶ)ݣލtAm $ں.}n:MӒ%KxPӬYdaa!hԂ O>;6$9]%KQeu0ۿw$w?KI5GS_o@qJ~f1^$%\zOڔnTEvHivRHHF'O믿ֶmxPsppPppfϞ- F͟?_;w=PT\t^TӼݣ$)҅#N::̶C^J_̛#mJV$)55o| Uemmʕ+kƌǎSnT|y)SF:u]\\`ծ][jܸߟջwoZj:ujn{ ;ڵΝc3Zh{EsҒS':D)#Βɶ}5qjzݟsNl2]vU^taC@sa׿ll䢧_׬iS $7Aeem+wզ 6d_ԫ].]҉'twuu7o~9;;uZd-[͛ё Ç|խPVK[um5oFetrYs>tҦnr'[o^|}}u)M8Qӧ] [[[=zt:uҐ!C4m4jy '''‚=a5uT >\ԩ|}}M!C\rj׮<{=9;;VZUF=p'<!3 [̛6JN".{(6,N!:td#))){5"_#dm1P|(̸Jgs(96# #|@>䯈S`a| |>E***U[1v5O$i﨨HyyyZ>Q)1@AE(4~a|< һsCzwA?\Rڔnn(W\B;6jǦd'vؘKY.|$ɥ<(2b~.BnWd6]VK%Js >^R'pM3>5(+klwp$8GS͏CeMNRL&y=լZ|icI+t.{I~&Ij%^u$͟2R ׮je4(U ϒ&OI>ayz0Cuoe*GzQWv]YZt{>H^3\Ǜhq[&eFz7|=ZXy`ZhkiYZZk̯p:[Y3zZ{$+u$U$WPikV@/mP7cq ײCu~|{?v( |  - j+ IDAT**RQQti @ *ߠ6wyJ+t| | ! ɤazQE5jW:z`/k|< w@p>OWbZ?V2 !>o{wȣrU.SN65} e~-Gg: | sgNNF9Y8}uu^^OYbڼ~ܝaU_uRj\Z׮0/u^n@M٨Z|yA_ΟV^kWqUkUuowx4蛰jS/Mk-k ڔ!o[ΒC^V6;t=Gֻ$C:v6m/eKnuy}6KN{4zNУ,4zfl\$hLJ{ymCT ƛS@!RJo T&ڦD`JNWqpr<ˁJH6A/裹rnoҚfQak+ms,o0 NU)i.zo>SRbb,5L;moh!))]_LdmZx$Wm8vtVi9 n RN%}1_!6;٪a'8'o>GcMLK?{;)jVNIj!.Ј~zYyTc)#ԤtiKrjô{ЇQAjʘy T/`#@#k#/ڊMG I/It[y>2B3tTO>l|F BBQ |Pp(ZϷ&*UMش&S)Ys& QG5b:zgG;vG)7ޱi[y#zZ _N嗝5:G'%CQjL>2Rf?gVɤNy!~Y_D=zZvxB][2,u\RV/-robzwTVh\Z׮sPas+%%E~-ksZ|f*QSPZ2Btם2^ܱhsdJ֘_i 7LZ2o"w˰l΄!̭F?V| b6Cf%vTT#~.SM@QGLKIWt՛C'e*r)m-^69~ xQpjG&I5yV{=!(ףOEZ-Iڱ98ȫLVZ׏߆eXakI:)LBUq1nʔ\ĎA?/Mԑ?ؘKY.|ᵷGkJIIۣh[5oE+˰lyjѶ]MKN~eP8:$=uܼܩM huשּWk㹪I&|C|I`RW|X?<2P ~-I:oLdyVyWeR$}E_GGT#mwm\̼.dJ#0%IϟU\L$ɫSF*Um\L> ǫo[ D[hE}9Wr<5/eA2-u*>ܴsL<'Iե״״i#/1_wea4jgU\C<#IzP:Fji\ [5o֯R/U_8E{9EWѼU\KVV֪\K|ݰ V6vrprSkҴ[L,>FrsJz7ָ_udee-zgLhוnr)r੊={M<͡1++~ny YZZuPh=I=L_ 7+]>*/tm:˿MLeZvU2jѶ}Ge r#~N-2<|F Pglx--4zR5iT>@u)]rne=HJR^(8|x0hG^^H:bj|tDQ9t")]| | pe 9]W6pwj+Ǝw y>2B3d^i$m?zΝ8? Z >6+PII1qP>^#{_*?dŊ/չsJNN3H\ O6FO+^]KLd͙0D}<Ԩ8ݞuIvq_طKPjYI#+ m _RPv۽Ξ:= }Q$[bOԢcb;]\.u=)+(9LJoV RLiӲW[;w?-+z6Vl=~Oꊋȷ뗝d4Z/@oNUe`#|(p k͗$]ׂqvN>XK7ƨ+zPIҮfWɤ&ҚZ+xߐ72{. :D Ν>qrduNn?)w|(>E#b!kS{ӕD.*uiC(ZFk3jK1n5-:E)m)UtZFV3R-7{T1DB M7g}捬x$9/kkxbW0uskW;]Dת _7x49\!4*F&cuV¿>mtDFF7mt#Òᅱ3V-@Uг>(EAO Zvػ=ʐ'98!CWV@u.r[EF[}?+*VXMC5ѡCG>|z=/Funݺ3f Yjf,*,Y&MAjաi7q) j׮ZCRR2201}t4h1d\k~zF 6… 햻|^z1u;5?aUpRI||l~<sѻKkC]W,7M?z͛]` -0:uJĘ1g|_;cY̚&np̭93gKLqc OM < &&&0yb8=|||1utYDff&كصkr]Ӏٳg#!!F.0{lh4oi]ɽ_J߽}|%JQI˳w}–w+<<=q9'4̟> 9ٙ8'hxM*U"狿e0Ҭׯ_ AhheL< ׯy2kPro7 J;v v0ի?OFժUPRE?7n^/5&% _{yybɒwÍ?K //O٥kh6bS ܞ=aZ 02r/e鈊U¯X$ZtYP~3Ca϶l9oPqsUAAAkϷ~EDGG[/#GDD}1c&ҎիցQ.\@n޼iLFFWnWju(׿s΅=_GECqcaJ5Ik]6||>xcz@oR]EtOFhh(klIKxgw{p1:ՈLxx8vډc~Gqq8quۼWw醄cٲ .ӻtW |^z̸Լ(AA0j(>hDK5MC:((}f:<|YLK'0yLɯk.끁8~8bbbX˻ooox{{#= fϞms_I&%äIZj8vyg!{Э[7L2f̈́iGc#GG`` %:uz͚5[n9iCNڠң`~:7 C&aػm#zn(&A0c|4쵻>Iߟ:gKRB+X2gvmހgiªDC?QSeJ .^Td9byDGJ2F3^50hkɒ  F&c[.֔OApW %.  +;.iwAq 'iOK_0&C9R 'K_?'!ːT N@  '  '  '  '  '  '  '    p 'mlxT HHHoR"dOXc{ KE >Ap&LJ;ܸqC*A\  'Pl_둔Vu}X{um)g:5o랓G`̠nhWb=cTɃ  '%K}p <1{|{ܺu0ƿ ?g_Ϝ}{Wv7\2vk=;Νa8< |`Z&bþs?ߦ n`-|L])737 9{͟+дEGlwCq)+}šAA;Uok~pnOU=_.^&/umF;CɃ W._E˖-Õ+WA ><QjXUs.9P-:9E8<By% IIIxw?xzzbҥӧ,9s(j_p[v{) ) +1ȺxgϜ@t'êYJ<} ͛7{˗/G=RDf3'y(2ΟENv&Ohh FKߞkW%e-c,HJoI[ŇȺxoM~߶ šA D޽tRxzz=兕+W{b B)#3|?;#k ߬2vnº n4tMĮo&t{,r2šA ((={ }>ct RQP rԩ Q'a7[z-1cgxPU"1zBzOĒ mWUh$ݍЈH;CɃ w.]`ʕ򂷷7֬YD1M>4n+9zEﹿnS޹焦AnΝ;cժUA֭(#b AAAر#<<A0ZmzVAAp/dOAA >AAA >AAm>A0OJ V*B\ .7d7 '0aDAĩ#3|  b  b  b By$$$_ѳgOOҐ b I|1mx鏭^1Ƒ#GcR) /_ƢEBe˖ʕ+R P mp#̚5 QQQGrr2+{M0֭Ì3̜9֭c9{lDFFbŊ6l<0>|5k|Kw.\@ff&WyIG޽{cҥ뺗V\ݻ'eа״|븞nR/EKm3 [ c{zbњh8]Ʒ_I#3H3'\Ƹ{X!U"KysB#= ?߅7 ԸOlMh=<=qL[~%s'SCm-NaS-PPo;-$ V+ l@xx84Vh7obXg[HXX.\`L 4K[kԩS6l ]E7|3fSNrrrW_a(+T?:uB@@SzK Mϔ{}*a _Y籖ذķ邛Xp]dcݶ0t _[/?Wr/iذ6;-:RV>$)wgfAZ%> lwQ5b얭W | Ƽ[3I;e_iY((b0c?pޟ_.UTCpEdff";;j#<<O~)@4I_ >,YGƕ+WtwqXz5;7x,?AAAҥ V\ ///x{{c͚5HLLt5yBë"$4#>NY1Eg^%Bëbm0g slIDAT_JV˽gxbW0uskWV/5#7)/kd/?>|8N<|9rtL^0~xddd ## zm(MQQQh֮֬]O?!??_:=Dpp0:wUVaڵh׮bA >BD֟Vy`ɹ뾪kY֥>ukYZc%*>@NEy>{!U[/R6Hݻ=ʐ'98!KnN2_ͯyyy>b4o=z@TT .]8Lcĉ@ƍѸqcDFFb„ byѻޥKժUCrr2/^,#((;vD6m7K9s(j_p[uB#WG 8{kM?VŔg[ J9Cq9'6O N}ϟ_B]k۶-ڶm ^zW^ҩˁ'!3|nΜ89ٙ?}X7phր=w0dfG xkwfxK4k 7!;ij'OABB!qA Ots,$cwz?< bW? A&W`Gh:U[1MA?ؓ#'U~8}EUw٦ 7G2ĭgNYc^X2)Z S c GhC҈ƧAXŀّH a! RLs0РpE޹!Бi9U!QA/ɧc`bŢ>q6Yy,pާ'udYR jqgIT9u 5RQ?eDouP[SuSON7 el4etpQ/ԙt=~{uСNBqk|)bB^(uEU:Ti~b0̈fPn)u)*fk) 9(<djg:G~kݯ̨_RglFu2 bseaC?ͦP>8.3z*nI*>|k_7Y* >|\'=G jS*~ N`1)*Ո F>T72R*-AQ]SX@miEUv87~FI*D_oڂge5Ca Tyw" T 7|Dy@R}U|M.]gMrg#|j`\wSViR z^{՝|mmWPxr1_&|ʎIڍ ਍ꗺ* wUkvP% 壈Rp%8řad!1r uVNGc~fbOpïC eq F|EX ::Q5\ W/q".tW=()pPX#ʠGU)I b$$IT w隳$׆ԩ}tcnFYF@0:c,w}>-G(*J;P+/iiQ8/oq%N-}QuPpqp$} C@u,q(Czy0~]#@~Jq|QqWR>#ۯeR5ݡ-.#*櫴QKH+qlƮ7=Jy WOQ^r 4Jg`  9+jZXo]rfGȱꪀ̴UOBswOT:@u1UdUpc'GmbNan Fe eksl^' wjLR٥1ܨm)3ǵZihѾ;Xn\RL{g}[7Ӧm:{hAtږ.!8=ݨe.yʗ6;OqPfMZp%=|Ƒ\Q(zcqgL4U /L q-i#}?{i@ ERʡ, |wN0*A[eT vL;[χ1^xgV-ߦbݖ]ůb%\lZooӥh:((GtwVm4|U~9FjXU#*h:!Gv7K] ׮~>uښ9٘ځ},^hڢ#F&ChxU3\-(}?Eݱgk ƿ˲9ZG[-:/Vbоؽy]C(zn|eAݽϘ7㵗¯NK듥oYռ_2 4&>\0Xr}Ҳb};|>e֑YЭ/6}[ҾO>mMv:ڄO5| g7-'f|`?l uGþ0,ew{W=Pv;-+{`X г?،UBQv݀3p)ta+qC'SW8>kpf2K3dZw?_j>L%9T9?'GP @J@]*˳B=S AvzU݅i%7'4/e]iZ`peݾ-{m@L_,+.oSFu `Nu|rŒJ6ҫSR(´SɟWc=H=5$M2#G'Y.+S$2 |T|ᡨ2MN嶥7%]Fm81 mRt_3ÄVn՛OwSrQʗ@r/e!%5!a%Oc{U@N'᝙cqzw}}X锁t|*<]4oݙIF]ɽ_J}|ܜ, 銴 icLr]՟XS[Yԏ F΢3|pw#%0,qfҠ3Khԟs* |5ρN0FͰw[ =Yڞ_cЧp_X|/?yϲ鋕K.O("q`/Y(>MkNSbTv)աti5OOn=qڊw)'FN:`իޥh1'W'D}>kш~8/\gQ$REU g;8kʔj#hH4hmԓX>GٍKmwqCCivn[@[b? `s_Zi9K`pe۔q};7iV,[? W^-'g74);5E%u~,R#(sBӫ+wFۖ{aTZ76?p T-5﯇̌^ prVzdUԧjdS}hI s1 Mˌw0I9ki\5\U]ܝcFLԆ=KsW[tz>;b1b^/!y;\ibiPpi&㹡_&۔s!ԴqODʭ;yY bFdfTڴh>j07b RC68Y Mk&*5@_P UCtPd\Y~~{Œz#ߩ. ڎ_ ;8Nw6$ hԴ&{]TFeNlj ]9B/O䒛.G?S9\p':Q>mIU/rN|9QhaW:q52j 堲NQk3h>k<,ᩞBA5؁yS +" =HEQS E+ʮ[/yg?4" ?mG5E5Tb6)*ѫNC=AO9 z=VF/ձQ.Tz E)E'7Wfg/-TΎ-2öwgQ7f)fߕLڞo<YG}"E4+|: Ni/gԕKJ5Dv4{.gЙebM2)e\o a6~֡p71UH9cQhJL%WP9+G*cGȆVeQY1N5Ή3޻2.:ƨu4VqC49\ҽ89GAu1|QkQ/gP!- 9ui8>4R5hw7ȧ$ݍA9Xji\STtI; YSll@F2b<}m8B֫ʌJxf3gFnW Уs:8T$s8gǞLRڒ(q(RfQNU%F6P=6vV nN3tAgFS19ٸ> Dnj6PeEu9;Oha_U^++՘e͓n\"lZ*nWi:r:rAf AggQ5,aBQfa8㙞Q ΄ø.z1m (FB>ɬ% QY6+Mi(KmRC90"Nt >o{ZDgQqu!5b7'? Zpn~0"[*D;uVP~]r˦TI@(.s3>Pfhgt&Ft }JU^cѸ N憘/I 8';|{cS9Uqug|Pjv̄Q3 ( zuJ?^q,뵿J=YԖs[G5`8/g.rB9'9Pu^?a|:!$M !C5 ykzI(=K̩Hg-W,ݨ]pbqU'8sYUorE40f,lVs&܏]˨7Si7:J(=ݦkq fʱMF%𙽼b6!ݬ-FTesDK1fȇʲ;pFW7NpjղsY,U C9[!7T띳*}`F0z3꒝^6*Qo:VO#9wA_/Ajӈmkf0z\7i4Ր1݂\OB>M0b?=2⨞TJ UP+G::rNoi"=20KXϑc=A=qr WlFd7]_5#KP!x(0 0Ҥ64W}I*԰3*JW/o \#*QڔjCO9[r~P*0naĈB*Q9MsԙjZ&S,P Eh$(U;6qV5 1{43dXrcI  %U t JE 88IE'p5 VwۓmZe$,[ѱb 喡ڸU~zH 3 >A >8wACR[ƳO4DzHJRJo$%ƢU=_<өv~K1y3$ -jW@Ñw}m>Vu}оA &řSGc,`ۦSPtKoW]_%'AЮ?7ĸ{l)|ȏƈg;]}t ¤IȽ%$ 9(I1IENDB`zoph-v0.9.19/docs/img/ZophImport008.png000066400000000000000000003775051415176210700175620ustar00rootroot00000000000000PNG  IHDR6qvLsRGBbKGDC pHYs  tIME  )\h IDATxwEƟIX]rA@A(pbé"((rf;qwTd K 'tafwBUuϲQvf:Tx뭧ުM UyNV!BH]Ocr_OnkX|:B!P0B!e6^O`FdBDv&mIEe; ;ߥ|kmڕd~)Hu9>Ʈ<\e|4+Po"(H0Xe?^zYa뺈fmq$ʹjUvM5xuW HSؐ6-I]6u*O#rlmrvلIvTrdʠM>d0J-FqbUYO{ǫ &gԜ<%E]ezSM9Ӛ0d;QW+H<V QVFNgϾlo"u\%YĈx$CTh[R!kcSџ*=l?_S &{Qeڪ~Pu&FD/;9$H-Q$2dU!E7iңHh~M; *42b]ƱAMbhJI~Շ)!nٸʌ۳iSȮj$$-5L,Rf h&~e$ĠB|բmJ4%cF FeX5J &s2"4&QIJ#$ҙLB zQ$%*,}{#&=, AG/ҎdqҔ, J'O-;ٺ HV܎>DS"/Y ~8k2HEC=ҬD͖@ 2~>f;QDΒ9(-TıDAe*$MdQ&0Gї,buIHI؂@FL6`UD_E:D1% dfUDQQѲѠK"T(Oj!m؃Hi2JMB2KD - Ed۵4 ?,%DD:x(\[4%KѩN k R$6dY"M;HAVCN^,d#h˔CZ-2z2LCojt:OȕHzU=YTNt_Dddu"2+V5 ]J& 2J>F+lOBL$C%BI`1I+HZ%tuwQ'3@DE#!*8MEOȀ)2Q!!"xD[~dzLh' jS24菨xRV">Qth];,"*2шy@$+D髒YDuq\hJ  L?96G!tu.MdFLDGn]& u 2PLa[!YHfH͔h2O rAU!*VeˢU"[kMkN:?QjDeH #YA߀E.R46+*!fDTV`@mΌ|JZhj#1&+*]Ձ5S&X MXvDe#Ɋd"$Y"eXVF7E"΢*.'AL6gC4":C#pɊ7UڿGhUjKw5RjFUqauayiOU$QT0tceIV[)';4]h'XTۙ^;K2SBZ)S+X@ؐ6zX2U+VJ"2U"E[m+<ɶT N512;)4+#m2)\5+;X![-3Hch[֭ѨNBUSkpc0R;JX]e 5Pgv-`]'h?Қ,bvڹ]vZjbʒؙW;FHaI85-8jCkݛ]"UѕzcYK&J48 )e+tT%M!B*Qf`;TM4qRXS]Ogm37UۍjTM3kصg]U)W׵򪉼U*r/'MRm)kuToYX1fu5QZɷH#N~d.MngMSOEg"S(3;^[El.?-gîHdz{ѝj}ԽlKz`*^ꋻEw/L_EM Dv_D eRI{2"oj4Alm&8,w%K;#gVlGȋսUYof5{EH ^Zw)dUM3:#3k5%-Uq sDTTFg2IJIt##NܾQF`Xrj,ˮcU`W:RMt][|8g=e%"0ULQ;WIɴͪ ve.$;CdЖGZ?:+ r6 I*mؑ9%m4)+D |YFTm*f,G [QTm|*#j(MA4T>N2.ґq)_;ֲ"XFd+=(̞2>Bfo^rD[eKU"p%>k),  ُ)h0MSYaQ Acc{ M D8F52}'1dIv]Edol;ݩRq2YGXvTQ.&aӢVeXĀ3Y[-{tF.d#PtK ~сDaVCe}Hh2VR/էlH=ED_ДԣEf;'MiH'iB|jJe_K&(DFTɢFPpvE *NT& 6dbѭDڥ;'**L6:HAˊKHh."EBXd%NV"/ݎTfg +}A`0Sou!h9&=}W">OTiYT% $C)iхHhLI1 PD h$T(2U6DH8_d [Ȓm a ;$sf"bG[DOE#"/.E;8t~E%*Ah+ڎDh2}ȴh$Q$$xTVrv!h?OI%+SCT FL05W>y~ȶH2G ":eɌhEDd"j0FbeF[[.KZ$z*35INQTd ׷LeVO8@ODE=dEy2{W2 էU PhCVU% m֡2>H$($;+⽸[d%tx"Ge]Cf(2jR :pe;Fݑu%ҡ:CCzxz!+[zچh,+*ER22 TyPE4"jPdRutV#vmg2]:R1ГU2ȬᖹOa!(:$uQxPȌJ~̢{ѓLJGuOp:]O) Jy?2( ev X51)+4eUD#ź:xVNWmwQ}}y񑦢mX 0U3#4fPAUe@B}Wl1TNYj6jN%`DJSFU:T=vs9NJHu-%Z&5;pڠc9ζ:@>Yw]ˆ^)yad:枊zZi!Eގ[jZZZHam2]5g;T02z Y<_voevg*5UF~ǡpT]u?U+#4PB!1 VF:'r< Wt*ʮ0*[c/TS[lxm{*{ڱxS?ֶ"ci5Q^VKmYvGTJW+}``j>bA5qѭd )h*eDCUyuԱr"*;8k$Uv{'Fgu]Hy53H Acg~Di*Jm;8I*E޴oCJ!H"c0VŴXuZt?T(lj{ƺۦis[ݚNt`6Y=ت=+/D^oDnюًT56HV2+"ϼ]e-lD_M*!Eހ ɼG析,-4Z؝|7`(؎H̵Qd0fHtF xB =/BDd;c=>h)A1 63emI.Lv VeE|_2:mTL,EW6j!+e_À 4Aq4TH i`M{؍N"{:oՁʠHtrш}@QEWRB DhN&!o!d""QUlV> d#VN4CA)d*j+l%e i{SŨe^+3$#YcWQQ VU|F-Z9,:vֳHldSd/:ŏ$33j-"d Yv:"9Oz'+ﯕ-cSDW] :XO-hhidy2-`+mXt9TTDUF5 6Řɶ DEB6"iT٥P{BfgH4FA#*T}YhQYK*.QđFDd;Ri.Tm)3 3h"dlUe6At`T@blY'GWS"3,2k!&Ġh,PrfR$$Yl Z4J*@Ey: +S"dQhTI8WQQ"*d"_;͈ؽ1:L2a*DUYv+b[*! GU[YԎcv\&_,Z/2BFϕY!.B2?Q'2@gn"3 '2#  J>"khA&#Ʋb$Dt,]4Hgo5ceĪRKd23`Ivt*U&Eh IT%ˮ#2NdmOtH3h٧^vKD(S[c" & S$&ЛltWb\djtp)Ð ɋ4 n2_F dW'2U͇L)LDAfUuEuE$ [TDU#B#h]3K4 (ؔ6:$R]N(*Eg d֊}&`* USg͞b^!]UuJZ٨dJ\6}2"SMlDTFV2(QIїXtT,vL2iBkȬl(D*[v02O}E׼'맒 _lE 4dq*+׳\Α,cet5"S1ҖY_cZZh2rH& Yɢ*+NJCevTrALu<N]SyVG**sbCSǪE'yo`ؿkZw>(<>.O5FtT:wb ۃ ep8ծH<[7](ip{аi hB:oϝKY9xixyՎyg,\uޞ; ?7@c#/0 <|Ua:Y^K6BN'`EiU>*ʏN[^=[Qxp<N+=ىͿ{ C_~ޝ۪]oMh۹+:Ξ]p:]1"ݽOSׅXr|PuKKiڢm4BoTwnM[Cߡ#ЬUqeؽW#`F@zag{'{ (!Nw2 аQ3 l^oF0q xyx/߿8sqviua$BhZ~{7k@eN }bSۻoơ}{¿#%VG}h[6EN\HoK^{n~0%1\s^x>)<]zbҌOMfʋb FNx#ye%B/>Rk<ӔNkm{w]km;!^}V&!z<O]PWw0|ՎzT\=a*Wܺ !P|EOZQ='!4orx+Q?N8y6m]Oꇜv2ݹIH#;d F !0MEGm/6E}>uO0+#`}Pv:tmCD]/D&-XRa$XB4=5n.'m:Dp:ѭ4m hӉ8Lv1i6O@^f8.4nz `Ҿhֺ=3‘IۍڡuTBMjGkFehg7p@{غ8!f^GdKLU39,FPY>ݱ">].= Nn,]pZC|Ǹrt`w^vdtٍA53_5ׯI?ĪO6@!Q0p׫\z n=3^kL{,ng&>0z9̼FY,|sgO>/kK΢uh٦#gl"];L{,n),ZS;=t*T|/p6>F^!n-OL w<4>]}MG"fZ5~vn%bi];~4kV|T39=3|ayΥtxﵿ`xxλ250}ոu,,ZS;>9u_2<<]o6:|Lpi.{&B(̾hު9W0Yܽ7-D#r{л0Loxif,~ynÁ܆M0iՄP$"}yco(\n?33aUQywOZ:.j>΀AS'=?hܴh,>[U"abP@Hԧ9" :v=nO.vcк}ha#/[/?'>.>!mYY5 'B00 sZ_3~]!#ze~󼥛Ѥy+_o/EΩXxҫa龇Evv7ȓ[w/>{w!#^g>w7˞ !P0 giX4M|m8ko|YU:HfNw?**5;'Z)<(]x mBaK6,v3!{S^q3}8t`o?Y66[CE}ij\9[WO>k&}ݾ@&nUTQwM''s^|GQ-[!0-@bwckMHgsR.HѴxcX#iѺ\964v3! Ƹtc.O#رu#^yzƎ?5.O3.,[?ˏ1Z\~1=NgOEB D]'+;[6׽z;,_|>/V-_ߑ0*+nƳMDM(/o+Gˮ >6+}ϋpM&6Ngs)&r1:t鉼FMkMHgsR~B^ud}X| |>/V. sf%zEgo>Ғ"W$*v3! Ƅ\q$vl|65w\u?fi d+x~8G==uUyc<6=Fks4oO5:cߏ.]7nӟ^yA= x(azUQ.ƨ1cqØѫ837};z3ga[qόpӒgԘػ{C0ƊzJ}&;'U8v= ̸gŵ=L~/ocS`P{-T.{&bڸ S;!TfkVb0݀KNtz" " b%| !'g1'5M|Ϊ#R,h>)8_Sy>#WcoaB" Q}ᄓa֫ca( к]g\r8kX8B(I೵%, O{]>;uMÈrpύ`Ϯ"Kp͹}1c7KdTy ja]<iضe}<|d #{TfdZvv T玭qوb%^yDFET>+-al +3Ԏ ҅ 57_|+G yx,xjOVK^Ù=qU#yWF: njyUUR?_ia]0XB("z`_oa͸gKw-ħ?]=߈5y{,|xxm Y-tģpt1[~wŭ?EkpgN󩜣ü`>?b q?%)7Ƨ)=÷_Lm~_C :mt}kǯf- J}&;'VZbg㡿O~<3Ϲ?|byS&\"VXfONEkpԧ1;+Rߡ7XN q>L4fb|xoMF [>ⓓqg鏇qG]W+ !1Ⱦhު9W0Yܽ7-D#r{л0LoxifTGv;mnVME"rמ'>^`q=3_|띗qO}&$ܛoL{@Agߩ͓ߟ4nZF]4*1(@ QtQ|TSwN{' \s;J1q hݾ 4MðI {@,\xMxs=yW͋{O?.}'>?>!P0taHӺ}? +ß-݌&[aE"|xp,}D/qʩ:&PR9G5֬ĉ} 贝6 k:nZFw9,(SNg5u"R"爬|B;uvob;so/{}?||ފe {˖ CGo>'C/Z_.Iۓ![]aG5+¤C&-й{ot ~W;}D{ΉA^W9G5EG -\~]?E%;'-tƟ@n'a%g0N=W-_ko{P|TS䜪i'-z;{h^dg7:~<垙~X8w22}s=ڴlڰ>DO!A < ˗~W0O^y⍏V#ȈdqQGun܆8\x JTWv,IxL$Y98r rr+<ID1WşG /֚AQd爬_TA$9HAN#Pm?X1%TH!srlA "Oơ{cɂyݚ/MVQ'?o߲t7_~Y4t7urj;w]m:9QK?nْѭg߄i?dk[-XkDG>wAWFI0(ض)n,֭>8! Ę+㺋H!?vl݈W?OxK̸ ˖>o6#fL01|LSQ|%Gw0k-Q k`u?~˗~ϋU˗w$L9cQm*[/ѽ³MebE"<$\yӽ\ɷ\]z"QS*TP$W2/ÜwE :'bϣ@IWtt0M[6ŬRF̻WB FIqm0k:,~~m!3ߥɸW{qFz{1ꢫ0Ly5l{hzkuƎ]8nmݾ ?8{=PG]Qc11W,pf<7/n <w>,f=0gti/䁧%MϨ1cw <-Da)LvN""iХ'{̙q'Q3&]ko{iR_Ƣѧ4Z5;Wz%ΐNN{Ez3{fggmn6nT}yٚX+$_7;]?/k!>9ۏ}=1nTuVbGq]O9bϣ!~3Ocx BN'^ CG_gݏg߇:kn9_!BH-a!XɃ̛ !XS҄B!B!P0B! FB!BH!B( !B#!B`$B!B!P0B! FB!BH!B( !B#!B`$B!B!B!P0B!BH!B( !B#!B`$B!B!B!P0B! FB!BH!B#!B`$B!B!B!P0B! FB!BH!B( !B#!B!B!B!P0B! FB!BH!B( !B#!B`$B!B!P0B! FB!RwB!9DZƩu1EB!ΑqkZ'xhQBs 24K;B"BHYwwD5O;L{*4FhiB9ph&2h)B}WqgmMX}Wq !B |!B`$B!B!B!P0B! FB!BH!B( !B#!B!B!B!P0B! FB!BH!B( !B#!B`$B!B!P0B!5a6"0 !B+t]G tkY]1Z)?__-iiB9n1 cO{!tⷶ%hbB!u;cݞwƴ ƟѪ!Ryqj`,.)EB!QTRFh|B!u#`$B!u'7K> oҿ[_mnZVRv]y=!BH4@I9&B! aEiדaۦQVZ,ton}VIC^hަt# W4M4h-wqE֍?8x^IChѶ#.wԱ2׵Ž-QrÉЪCW.U4sFس e0 nO6mm:B[ BaJ*+->u?~}OEZzFUP^Z ;mCѭWX9g'6cފr) ߇N nU(? oylE}~ 8].YV|5L{ B4M ~ޝ۪έyѶs6B~pHKFMg5i7vm۔]۷EJ^fs۸0vn=z~֍')B>ÿ>/âa3dr5y6bVw4k.(J{ǯJ׵r+%Rܷ[ >Rk="WVZ\-M⍘.++Qn2dʓP CmϕBr݅FhѶ2g;"0HH]&2r) !).ED v-sᄓ&N}$ D qݤHiPFӖm~kN[}XjΆDaۓ20eYy+mO32c_VZzQDzM#hqH[iqUyR׬[~n* D=IC+wlfɇfɇiղ2MJYRVJ_c SZR&G4ؐ!0RkG `߮qEf-cvgӉ'qhnT8Y9h 6'tny)\|ФE-߇ wAt{$Zǡ{PVR[6#eE&C(Ìz(#'1:#uphz%, !]a$ӶsYa1ddC]S⮩ګ2Շ̮'K:tѽ`4mthOZ:逮'z9u' q3N7onE\nt 2:2ѩgw8{jOz4]DN^c{`C2MJYQm;wGVvN8)~kz"n̬pvivhݡ+!ڸ SMՓyٚV9DR=eEU'5M|>#B!B!P0B! !Qp-ˊB#!B`$B!Ժ)/Z!BH*_GMZ!B(ۏTsJB!P0B! FB!BH!N^^kΙq7^^d@ IDATDڕ>1civ3 FB!ǜkvLYu^ǒ|z#MǪQ0BIK!\8G=]zp΋w̬g#'鏘d7^?d^xKFc9W8R~ݎ3_axx'o {ז2g81ꢱ2|+\>:~> ?xC,S5L,( !Yw_{~߿f'=;nM 8xl߲OˍyK6%,MV۹c >0Mk];~ IE^[4^`i~T9.؊>NV.VU4޹ʢ)iB!aZwÉ}4mϿd#[X;Y˶X"Y uN,A8+p%p5_{5k++Y˶[툕ۯ>:/"y-e/ijh_+?*dDŽ_4޹ʢ#!0ON:| <<]S=y 8j;\ ϋ)O>{շ1Rua#/[/h["u?cWCucnLR :m4t]OHkK˳n<+Re2wnkh&LUΝ*lqea+zlB^@yj~~wb}<܇~O!'ؐ'5M|>#Ž7_gɷ\R"0B!kB! FB!BH!B( !B#!B`$B!B!B!P0B!BH!B( !B#!B`$B!B!6<cǶ{k7. (.8y?` Ơ]]b?a^^@cג^G93N*- vA].hIcZj,O*r].h P0 ,Nô?1o?2湱 C\'(g +t5睂_Vf%9/+vA].h FʳNă.'`w1yC<71ͨE['?%|^qsYѴ vA]8Y[giF}cFtP:&m;vNێмU;<{*ڎ>n<M].h #QEOm0аq3h!湉))>̬l$B4{Q` =#M].h qk NG&^ONwL}ow c<71{#Ƚ"9pvA].hFk NWLyO?fw#y;.Kz<0:t鉮=i vA`Y痩:̫_v:&|5n9.h vQF7az96[kB!u>9ۏ}=1nT󹆑B!P0B!u56wG>K^:վ g[7 MDituf"-- ia058 _GD |+M ÀN4L!WLtÀ&tpj0a0 & 05o0 L4a@~?t: Cu85\e7Pw06T.n]݀)eWU1FR[JB#M3d:.9~c<3w͈Wn/<6N.<a@L8u fSǝlC7Ä#:5JGYYJJJziiZ]IԱzvk&~*m0t:^aҧJjhvti|>L,SӄCq s󐑑ʐFFh4drpt=N0 +}C@{AOEF؇z;xnf ?.|2#33>F@˕]shL:R.؆f~ gp0`Z*p {F ^P[FF\CQK7Pprp8M~ÏppM蚁@F hh0 2V +ӉZs6ذ'F+Wsdeee˖XlX hljx< h-(L)C8W`#8&MhZ,CV(p0C;`~UTsxY4L^?MQQ o #()܋`^ |f 6mىm7gQv4k͚6`\P0 8aX0MT:\gx.+J #H}> 4`zP အ@S;#`t0A3i4-(tS ; :tР@@3Ca _r~]Cw"h 3P)@*xi9V(5=Ujt)XdգVSȈE<[o-~|EjQ Q ׇ eeeW023!`?LY m zQQQ|CB )#t"==YYY4 ()) Ʉ;rC1a`|ph1H@n 8'%&^@i4rP`S3Ԝ5=:nT=8aCȺ խD>^7l!v: OMs =Íܼ`߾}&+|0A1!hyS׎ Ѵ4|DhX.#RJkF߈n`At9>[`:i.qPV\pN傮pPQ^ =t' ?7yK/.֍PǍ 7>ܥ[ѱcG]ۯ6l4 .N(LuNǬj83>aDߵk) tp錜tW%Cw& `ھDpp0:;4 4x@if8 :,]?:^ DtF45#fajNh 9F 3P9_ԺGh,F$DNw Erč%)JJ7LUPRZ ?*~ᇦ;pp=qNg)et\p\p:AL:4rVQfg"^9K0 h,7]~~~ˉ4GA348<:8-36ct||WІY '<3(pB-Ѩqx<8p(OM34s4hGH0J;t_ ͌9&5M``4Q9΂'U+-XZp'2p9*Тe3}2uW 4QXxe%G{#]֍(q=hgD<&4G! YZbDmCP-0oZS*[ʺ2``f:t)eH-=@ŕaH#Řj0! QƑHZc+12:*c0¢2PYe=)+BZka0/yN$y[' 8(aϝ#iFYZ{ג-I=#WYS19zsJ)0$ Cb0 Jc_Z$|ϤzL錯Ii+$Uraswy߼J% ߣ C، |d)+ېLc_@Et]lVgRk`0_IT$>aEJ 3jp6bU-3`5sh$_`:M$.T_KFJk1BߐXBK*,s[3q0.t-}*[HK̕Km>Q]*F#ʢΎA8Q8BTOH8f.ٱ,`<>>Fn hD<>(3bc2PVCh k%fyW:hԬ0&3`1"8jjh6aȭǼ$+ l]b`QRB5?V,)J*ٹ u>U>5ݘx޸@$!8CJKEaLF0>ilh.28:! C62DQkd03 x\|뮎Y;gPƒIYk=7fZ>A=(P1j7Z1H Z ΊYۣDz*ޅ[;/!RA}QtB O4Ԛg;FBQ5XvR*Rsw,k=,ȜKiq&ߦK>AD$ XuOH CZJ'%q3H2c{\Y_SY|H1;~eAkRf+یqW\f89>g׀OS펷Irlk7)+Hu3cG2W8p4-禦0@h]Y,hDKDJAe :͗THj;:lvrsYvg L_{U<'2 MS(қ,Fg6%R(t]UUyx$A}x By):o!s}eLYY١XC;ڽ'Q 0n(jsbA7q:3T,C,U`j@*fŢoS8']GxArƘJ0rL? yV-11eI# G4M$iEpJEQ4!ҔU3q#DH3aQO^qDظX/B{Zdfe}泥7/;7g3t>J''QLoy#sN8%8I'J'k0k} 9Sw빡%: J3gß0B|2B+x+\F3+ EQ1A` Ǐ>#)svn7ȳ)X#%e1'L)ea0FÇ[}2[llnrxKtzH9 cqE՛׏׀񫔏s.>`˅-NO3(YS(o9<,R0&j"R)tQWG'Zb@ W+zlH,fULjc??qLS" sX)ȋ>Ҙg4]A”8A3n6ӴLB,ZiP 4؂ 7tx!VhDD/=.3xg-6ny!;O${1`]`~ $?O-feY{_d2IGSJq2!ZL$H NF&@R^@ƗU$vW` grz˼y+Ts#B74E('A$MS)i "NEFj7hBdY#sĢ4 /**(J3,!X؈s#ƐzAyBDU|/:ej/u}WY _8_Za׸F: VHȨEIIJYp,/wFK* D{MF'L*yc,^GpA֚hI1MN9<9=ٔy"-y5`۝ϞsJʋwt^/;0\fB@V IDAT2..kfA.)V8 yFq%d}9uSܨlE{})畅3\yyn1'3<~>|q3Qa 4˽.E1 Y5#%%ǥ՘ ޼qK M"QNB1yckIrQ$iRyɇ+I2N:Jq ޹|A],}0w/dO%9_}`Bn5 gg f]Bۼp9/qy7)& v%)H#0nX P9+E\&W._lRciJ%,V&Il?~ liqa)QDk*"(˂@7RFF>~H^:)Zl\xo!NRF GGkT0j;Evqyyi grQx^rupg5RJԚ9\ WuXJ"Řih hDj kAQ8=H57XtW^$( qь(]CUgSƃ]&C89>3~piKۇ@ bc* C#4>|2Vƃ; ͯe|dz~<7 ;++v}9%e1׺8GQ*_ոHQV9aBY%t;1F3r ל+r+YzIoM;~ 987.']h%~QEJ%2UZE#gK͐Kܸ~n\)yPr"%a ֒Q#fp<[ae}é`PX hV"Rmu$-;\|o~ȅի9>~/ϐa*sg. ;,_PMЛ^O:~( ya"+-yYQp(+rub-,]R< I I:I\pWV;,4 a x|pp4!VNCA2<|یSʑU {LO`oob$guuN,&W93uTu!7pX\8e̬,8[ ReWT *s'MEU"-Y"CI :,ux5޸y%LcIEJrJjEpUT,8un`fk@FEVPCBTa0jAKeɘVz<ں_3֗^]Kٽ_ e3R.zg&gυ>PC& ÉQgZمpZB[(uXa -NX\=uMX H.N*tB7)˜!:诮 $MSkO1;[wʄdO H%@srp/Dk mprt]vFlq)˒ȲkYxJiZէ _J.*0.v6*-λ6a Bi!KsZSƙ1c~Zk641qaʂɈ\VhWB& c40$w '> ;h1ND[9qTN:4}=i5 UǪ㭇ul~:n;㇤P8^#:8{i1LNEuæBJKY'ƙ楧l1w-,)a2:̀^+ƔVW~o߼K8[]^g4S06*XlibDX&W`+tP)UY"# Q7*!99=bҺOpB?$j dm.uWa| E)JJnD뻥:Ng![d/Ű,KB%-\m0`LQu;g-tg5vZ<L|f} KRn)[TN["dHWNHQ%lm=dpzxr3%fL(p6\X['MQk Xt!dpI/ꈥyp?Z̦|>>az),Yb!fQRh,C,c|q24nl\i);D1FWʹgyPč6JdE4MI$IfMQDUVܹwX3%4Ee9==)JrEbEb1a:5ސf:LsyY~K(""{=!1$xȚ(E]߼_BkIt]^Z5_Z#TJTH28=,rL!yy}fDAzKVo_-td)TeFe2(P NOO[`eOnj$.qxr WYGFv#dwO 4J/[& #xEU {"z0SDR%+cJζrʻߴt''umiKj1I Rhĭv0 i:4ȫ1aCbN$#o(1k1ӀMO~w\ܠ{`NJl6ѭ/;ܒ) ^P ͈p[ܿ Um\IkrؤөH*(l "N2kExP;/#R3[dw9+l[`bgX¶+*', QK:hRI k~LHKS[Bm'.'tD't:-8b8 0ɲ̳A@t:ݻ7umXkNTb2鰽GG իO3!ee*oYFF#EF1*qeo2Uٯ?Z&e,-[Ҕ.ֽ}>7?|qFb[et2) MDРeIGVa ZWQW)f:[=& LtP֢#($1\ATOǹ1}Mgf,_ITeoX7,hEH00icLIEBPt w hd)4.ИHSɘ•Vp"[/Q" QNiE KM֖4CEo[w88Fu&9 ˜rxGDDk\yxR7Xfoq8!RJws5 N/n>Jq":uQq4/(nkw|i8T(QQC zzј$ues)X0TyMZUI)4B n od1ahb% }W]aE1B("C0 Rg6cV'<zYRIzѠ<E9Wc7+}߅g|ΚX7 BtDCK J|ɇh]lIn\΍뗐T30@*%oqn{|pm흀hK_0 /wn,2- Z=TbUF$YEml:|q1R<|,coF+M5B,~lJͧ:gUf06!|)݌i4;!l "OȳjH:tXQS*vCh.;Rl t(`u3ιy-EQ0L$nx 8EF&YBy#s C`M(2P84B/ISxCUft]tq2gww#BaN FvMy]_GJ#vVl4޴56KsyMNFȸA54R- |g:.ON{Mh䄲Dqt@[)?S~H&/=&NF N(P9mt<@1K=F)*jҸsӌ;CdD> c- /;@(EUkT0^+=>n~4ֿp^X kvs&'k-P=I"_ZYB-)1O0ܼ~<#.cEUU_Yv[E s8akc xɭx>!۟P',IHF^+f[FLG!ꜱ@5HEaʕ9U1rDݭ5 `*q\2c[ZgqLף̇LǜR%qӌcL:._|đd[!_%?fɏ~׬ggC~YZO"\B|qeNU5,3L)n8 At1o6c/?Qި CX[E*1,e49A!M/?_.p e%REjuQRjj 8SbeE!w_UEۗ9}͇dYE34,$i:eg-"Wt4L0{8\ DaO~sc$=*̆RR#Q&8"&4iK!H 1#K @e E^1N #Mݡh5t]"7 lDk\*W1"$'Q"`4*m)I Tu{9[,}$pIͅ" 58C0NJ<{ܻ{&EQ4M rF8<>~*Gn=* lQ<{_ܡ,K>ǟ5~nWMUUܼy8ɲ č}I1coo$묭q!]7RyN// ,i*N2 뜜x5pask׮¤\w/=.6Z˸iMyiIKoH 4ͥeY"KŗKn7岮d(LS-% RrNOB &dfHA@R \LMs鍓2͸RKբ*"V,iHXtq@Ӧj!(J<%H(L DQvU nĴZ-[viJeUeLSgJP8t:)yBUH ZAčgRKB0J .ɘ -xu7/"eJNLyI$Io]! _eRּ+z+Īkh\Wq wOHWX*'^CS-Y,Oh9q =ezN?! u1%G}1&Cz$I26J/xag#Eێ^!i#Jo{xCz-t1 E=ż  qz1&}rdYxGO1ϩ%} vՑeVud DjVi2 2 ՚'Stۡ FVQFze2HWZlw(zKk~XuWW%i 0J!m(1E ?~L۴tv VB>[v\(%N'iڦ#+J,9H`/ Ѷ-ڴUHC|4`"R謇ʜ&1BL Ӌ&#:A0D/к$_jiD/(#&%OOi)1Rb;ty\)QR]4H廽xU@Z%P'F( )ܸ2˷qA۵Ԧh3M&YJ0NB0^x>Y{Q5iܼ*M[$>I/C& ʗjsw/NHz DC$$"YMӸXfSmPJ> ({//},.a՟eΙӶvV"|W\&}=\"O` U G#A9;=Af$$qF}R]WBqB᠏'4 lIc54eI@xpؚh늶IMYL]Xa1??kYNh֧nP%eF]HOvǡH7/˒ÈGGGdY:ᐛ7oe]΍xp[Wӡ E6b6w 7 c']~/X__'lG-o}GGz67nw?G?X++pgfyh'~:wOrX(ZSV u&6A_v]WX, ZGZ.c=V4<=bƏhQ"</(E BƼu. OF=qpO}YFyL&Sap8);Ӕ#|' YXIŤiJCeYS5EE)mQ14!<3;MK*&4$hH|M>9[xåqgF( Ƃ1ւE1^@:Ov횛"[PT-&'A۷os|bY- 9v ~_0YSP~AUKȲ_my׮h#04Y$ʈ6X.WM)zI|>*3}>#<%~>=Bi,2]PE-xhLtSf󖶭ƿ_PV ә!ƖKÄ ln3.hxmHUU.Z7b!N"7is_Tu_+?fޯYw:'!\<@-x2Dmҵ ]q>ll [v0H}b9{ʃpttO#sv\ӯIV rMU`:W( L> yTiK!S1Zޘ^:b8u 7^#MS..._{m-Zw)pHTmCD, ْG 7ǧܼu<ʲO_MTuGޥ7ӵp+9ODɀd|6a>SaYZvvv^z]!UH8xo4˂qEDpNFūsս01Ͽ.gV::mэAcjzQbk,熽~: ^,c0豘]K*bI LJ@19R-{ :"+;W~@aY6MGt5Eݑ 'ӥNn]W[l-P/@0eBw]tU!o_Ժ)X(/‹"•A8%="`ꜳ >O)69aG)EQr|X ~`ݸx/D^|6D:/o(m-]aOY+nJ3Z}0 JɐmB?%ٜ䓻w(˒-o'prrv]/f<=|;?;&ӌ|7鲖3I-t;͍WzJ{wFUOY3mRFCݖE5s,NV拪qux!yޯz=OWnά6-4î9~Hl e^O#0d?)|@ 7$PnZE'yA@+h*vYFU*ڮ+LkZIS 2Y4/EV:R5e'uYu?`:k& CE]e J)$^]=o8cPFe}(zz$1GFW $kCI3в3=l߸N*J]@Y_jW\u5{sln:XcNYRsC<8v]qBduMkZx0W>{I!<@(+"cӂI5bE"螅gPEeV.iJ \g3K+Ӄ\uuҳxC;uE*OuK/ڒjn*ֆ#@btɔbA6=~H,Wvvi%28zGQ4Em1c`0hE(_9=Jk(X-PQ- a[b☺Z*A& 5umXFWx}R 8KQLˆjVRvyRw-'ͼ p&׬ 91Aٖl)56`,ݘ_X!FalKUk E]Jz꺦k=x@YfNN2AB4rNϏX B NN9xS|eɲ%ł׶qskwƖdtBb$ BwߨBk繐 ,JYmꋪqU0ޯ}P[.[+96kl /r=Ѣ%)9ucXB(f sA>Lp{+Tuq(rE_4HYGgZ)}yOU0rݍYk "K[隚$\GoK7x@z<_E+WH 9??ʕ+ᘇ6^wqж h8$,X,aFJRs(wjC$xBws^{BwфH9~Sk4fg8boohyJ5躎(#1;WI7nO䋜^C>iGX:t])Uu]G KggLgQ"^B e,Ae=J!WE,uKmMYxJ`+tWx(uAW{̪9g5Lώh-)ˊcM`U!FX?B)Pzth;Ƣ]c_QW録ȩꌦ,(˜1eO隒^Oi/h[6nj7i[) ! CzKܸz͍48Vԋ;ruftٜEް(*ZYAg$H J r gg+֐?tc$i#6%FP =| }O/i}ϙuG#44& DkHwߥ(r..Θ.?WXk{? W^'/;שdvF#~mϧL/X !.br"; GloE I<@IEk<廐#}EE(ڋK3 &ŋ/z>hU'zk_hG?c, s<_c;x^蜪u_*@=u ~];%M4[)]TȝQH dT-H>F"=^(ۆk]A:+°4M*FK!n -T]Mk5]b`Q;W * |"!}uu#*iBv/x>hѦaccåTCX.K0YD4 g_ ]@ݟ=0&zI7Qbs5xa'J8:|F#DErrGB IDAT|?o}M>O\f4L<#cb|`#йO._ڶ&#{eEe8vg4uH FaA?e}}Szu.v)O7>ZݭR:^`|x^F>9X:IHa]&6lVH*ѬFz(D#i-h崍CT9H#:s!-hӧXCLS'b2NzTuEGN!Xm@MQB+O lrM]a0 CNPN)]X@Q򦗿/% 2s0p V^H6>sv d:bO6B,4͂d`${^H[uD*tyVԍ n¥؎)˂(mQ|2rΒg5M h ;y|Y`4U$i T^e&!җyS q㳊#/(» _qB̨+Qk-xW .ܿmyŢ>XymG'}z7p֫udW# 6B (~ntumr$awwu̗Së, Ǯg>Oh6\ι ÓB%1meY2SNa 666888Q69tSG]x ﲻ{ly)XO8WpEctu2 mXQJ) 5ƕ\s9hM I(0LBɐub2ϙ.*uGeʧ.H'kJ瑄qm'`>;gO=!"Nz *pfgRԆ*%^b~P> <O4 E)*ӄ "e[.gH!tR.^4 |^z]M۶ 6㐰Ó f;Ch@|@YhYNΦϘ-֋SOjh?0gcjyyXWnE4`,IQč̈́'V!Xc"kZ Z_fقjX.r(J˓'O_G5Mūڐ'GT](h"&37BE,锶ш=MS (lH5CXM4iLGExNBtFO@7Hxj~ Ak:: 5XHN*\ߴC3ӵK_Q?%-;[C JjzǬ {CL Y9#ۻN3]F)IңkL8feYPU%mE40 10(2a(v$J[J){MH-dYƕ]?~`/zLDC5=$!>*Ȧ-B7wFXZ-CI҈ۭ_ XDZ)νlJ|I"<|9Wޑ7Jk ivB7iԧm[BxrWMNu,9.k5Xn( eq#S O)vӉ[gRu,4UNx^g~.%IOPmhVDpG5PwYU!tԭE>p ZgH!1F`0¢ U.!3@uyߘ`.5"aOP)9*j *kΙ Ðq'?Dq+1Ŕ,پ3Mr uQ^ W>u)SSVಽDѕfkk;̬$ |/f0pk3Dm09XНUt]EUEzgрW|X M4k &rzzBQfH\qSV9EQ3#5U3#Ē^vK>~"Ƙl 2 f "!eQ3Lz =,j,pi]G ,ڽNOЙBZq]!c0쳱OQ~vi'}vAY,󌮩 1ϓ)U])kqKծ۪9 }0tƘX˲|o_d'v]{%wK: k6k[E bnrQܿwH77_fskE^e Ҵ^/a 2ׯ, E iy-4뱿'&MLg8y$qȇmIÄ~?A EtbA(w? i 0. ]݊ݓ|"3H*Lӕ-$wPg*AteIYk+κk- 6TUP3b1G'uUיO()@nmrttBx$ ֚l =LyဦiK)لPjԪ2MbvF6?%%Ww7wAʵ]45n9>9>1= 4Y:ڼ&HPNyOhM`;M@ :rDHV7D:++/e7ujgb5@##l;<8˝vk|hE5A@#eѶeYZZ.`{{7ot2cD"LiS7)˜`pzṽ{>(Q4i[TPUBHuvA]\\\P%[[[#-J.Y jnUUj#m4 E^4(L7x*@||kn\.SsٌeX.sʲDrK m[;cGl2?,kcٌ}fr^1:# m}m0p||έWX,Gdyƍux䐵fEeH8~h ؠc>O$#FI;%a T@[L} J"bLMX͒݀ǏgMEt^oUXhm)v-KL (chZ2XHOR@7IOrBeFP a䪳#á=,}, ZH -$ZURkJvF>!>&at؋dɵk@hÏc3GG^D(qr`qv.ٌ`a}}7MОa8<WnΝ;ٟS~~.sw>L>|8v ݽ,_7o)hD4AJ2&R~^0KXt]|UW>] 1$^@Qdc$O?8e.kc$I•+WP~ gHc#STo|0Or SFaXHli˖6?=BYyKJx2VxMx_#'g9:9t H>6Ƿc}`0.Kٺzltq-ә&wN3fEAo):t:e: "P TcLGYȪu,hu.''˓agZ^ԍ4-ۛk!9yp)sMz|7X.$-f5G$Ihܣ,}DzLUVdŒE⩀4|ʢALZV8MSTdeEU6r}8XkP(/$}$O }ZHMԗA!Z$sqJam edgGX$@Ӷ j^@0xQ,n_X#+Ӑ[e>UJD<%| z̧sBeDx4Ysttyloo_j3TyRs~mǼ\r- <99a:G!dmmY1Nɓ'lmmX,X!vE[g pG+HӔ5Aka c $IizgKӹ"Ӕ(8==ewwphč7Çl<,1y]K/r7=A )%'?UFd1[^' 9NЍ^ل(MB!m4 B g\dI8fXX T$hm/ݾAg;nW?!M1L899,k9iGkMXk9;;cggSz׮]s8(݁8I4EN_׶-EY*o[Mr$ZŢ*p0 ק,}ˆ S|eMh k z ^B׸JFIoX'? ^޿GUUoūqw2Ӗ担[Bw,SڶA R@ۼa`^, CBwyݡV/ tWiЋb|B2rp?#2vwwkW)W|k__r9'*q&?m1Ox{|o?g߹2% // FfniSx$:8֭7?#zi2sE)%ӧNUEeeu pj"[PU <ϧ[ ӹMݴ,K^}RyLgyME76l:\x@:+rew|% dBY\GC67F(Sdd#%}8E[T Zi:It(_}aZ e0%BEQWf6 ߸ƵQ%ǬhĬLXT%;I(" |gkk (bss^ʲd6G4 Ǥi[o`0Ç|;a<3N]|_,KΎ/X,ܽ{w3L0t֍>u'''\zo}[!UUC$ݸqҬu%Ks8Ɲ?`ss5c<Lk|ߧi+Wկ~;w?;HRV駟^>H^2CHB ).zkk ϗ<~C=vuѽ2TbUlĩ.@<VrA/i$ BvׇCb?fsdlQI',~1?8+t]J!fϧRRezeQK_ڶ\A,(\?$I.6|g '{y =>yJ/ &Uձ\e4w?ڐuŇllmS!?я\UQ)  -B!i;XEBjaM(%Q]e6,1[klsv$X9SQpvdeFAD7\fTNϸSesg?uj&)RM[M 8Ano$&$g. _8eٖef7{aյ:{y\EaxuA KTz&aKl:ի)`4Zk?7,3NOO8L1ZG3rNkTfK4 hOYNIx<mֱRl0\ԑ"GHS?#pLZgFN[.  (yceY/n_(J"4-](IL4!aAytlacoS`gjc?S93cvI~--, iwF^3d6!6<&SY0u?~h4իlnn2W|f5~xxy뭷^mkkk+ѣG+JG)Eu]o.3Zڵkܹs(x ϟ?7ߤsppRjqLբٴgYz'? /^\QGI j솟oo1Z-^} ʲ~1h1YeJ)WԠRZ---rؖɷZVu׳FmJ$&Mp-sttDYxd2eC%e&ﯲRCtPg? Y}_Ul:@UyP>?KV `݇FhbUП8Wگ3n 1 ܜ~#7"f Y,@~9]ƅ v? ze=<({ut8d2?){{{F#׹q(?9Zkm g.l6y7*NNNvM`mmNCe\p,V7XinszzlfQC>|ȋ/;yNv-zGk $ I>3}j3.)+9^-pQvJkZd$iJW`#=g8ؾH QkulZ>R:ݐTU.SYx@z1ۛ[<R$ taxLQT8i^kr&:M$M2EQ>eaԳ-c `F˲i J3+ \+%ZZZSiQx !.ْMCf v$A[n{D1㘋/MƓ 3,ēuAh{g-IL'(e0Pz<S.TU8&J%Pn8t|]&ӔZ]$O!_3,ɲ~'A)QqwZv@qdž*K as=ϓ Y>\ [%!\q ZW(U/[TTtJETEkh80 +QHܺ4!],Vb7j-|cW9Cs> $Vmfq,}V i4XP0)J)@.%9hZK27ΡVq6W^<888`01TUᐋ/d5FQB,?x_%6?;wv||!_Wj66_ǫ! Ry~&}~͛7)˒GORUQẮ*&.]bwkn 9ٌtyt:|ߧ GGG>GUFqrjgAIݰS* kv+-ZM;̆F{K ^RNGﱌ v_k$Ic*\_k78ak"'rйT hs9ͩ۷>@ x))~',JxA2Np]< )ܕi=coo1ah)> IFA^LSQvt24B)A!Im‘RRTe*V w{xKpq mFcs]9:>xxLؠnp}]˔*Ӽ=͸EIF{˔H5_8wn'܆GgLO)K+'8,ˊ"K#:d:FҥDӌ'X )Ҍw~d4ŕ;hea:Cah/q- _|o!w__ 6#hv6U3L3sWފ,Ρai2O)%ejM/ be$xN3T%ta0(ً٤ K7BJ"YW[Q O_$V5ᓬ3mt}6L#pG8" 儍HV$tB>J+.cnݺB^ut:+g;wuGGaS^|5 )]?;k|¹sO~›oE}!o)aG?uBuN_zu,Z`0`}}m<筷Ν;q̃V؊rh4Zau-R\Z7NCذ,L&ZۭVM>c`:~ C:iAFH`'8x4Qq\gŲ2 Wz 1qRh||!ܸqׁVodlrʅ2HG(5SK!-Jk=&Aq<7ċ%>T= fgƘ,|rHc8==e2vVb`\2[ҥ %/yF_+UZ&}G.cZ&Iwm˥4\/!hw:{,Hhw;<>w~O5>kQ&S92s&.A(M8<}>0ޕ/RX I aRI/Ц84&OS6^d7Zԗ\'iDdɒt6G`,}`! #nl#kD~\%YĄaH S>eUq՗OӟjMR%{{{3~W~~`ss݆ ۷].]zGt_ ''oONI%az)UN#D ~t[sP" =#f)^|E/Ƴg8 lKze jxTSҳ}y vW3LYܰ[ QE8">A|imt㐧`,]hVr簳dgCQ656Ei2=]п0lll4q_җq^u~mnݺEEqE߿Ϸm&GGG+,K's|>_e=z>ߠ{{[[+y&t:%> ?ysy޽r"nllſ6S~qy.^hœN1ar*~_ewoS]8p8w_K{;۔eɏc4e}}^x>*s򬤿֣,s7]WS< <f3U|>c>`j4k݆hc"?•KlP~#m-h4RVȭH(!)28^Ue>D>x9>E:zEs6div 0J҂xxFSSQn^K1g$-YAs}Tyc$J)>|Hө)M |2J)ode$|27oޤٌ fCk͹szܾ} :یF#~s}~m&ի& we>};lgg->y)[/bo6+\tL&-k.>A)kƣGxwW1CftJ{{{,dG&Sʢ굗Gr&IF|rd2"IcOv]t+ I SXR%UU H~H&,l>e\0_.p}hdIQ(KԍZBY 1YblTjrx\uX&A葫Kfy<(>#ORX8NȲ42 v\ rrcUu:]S}eUTkEK  BzmBEa>\!_.ᐧ9eU y0vzǣ<~g-O&gl)FupAfsoUNkW0rwFd5i4C^yENNhE f)g6<}bc,J#1)~8)R? !pyAl\0EkjrB?“, |Wb3.Qrn`D.'AԤ`8:G\rϟs]x =zĕ+WV˗/GnbH){._җ%"66(˒ Z-vww)vaȕ+Wf |[ookDv}}^}U^z%>UYuVZD鬾kYRJ$I"8f>8헾YV+ܺsJF^ IDAT#kjГS"Rwz39w'''|wOw.Ji'2ǘB(xUPoX*7B`L++M!['cq|_ūd>=eL2PIQr:2 "p&Q;@A%dAJ2+Y& &It[tU$֊J`f8zv6YBЈVtIYh0D t h1TH C{OLF#2c6gj%v0:ge-Gzl=[SyS)F,I3J\.0EPJ3z6<}nK*Nxd>Gx!RiAQYk<>eK:d3ME^ .0@F3B>HIk}%ؚB<6:P#'p6)Tepcu&|0 ).]Yŵִ*UTdn2;,HXQU$Mܸru}APr"^x֝cuja0C KRy vo>i@J-'~ kEVYZ}E*aN,Og{)\ۂ(hk$)upRL1c.\,͹v:ZW/~s<:=>YbPD2;,Aq'HDYP^f1F|>w 88a6XAD] 0mW[*k>;Cs?S|p@HX s (8B6 NBM<~Gr${8!2^S0r%}9ׯ_ӧ|dhDcхlrd}}ywhl6# Õ$I?~6.],K._??ʕ+m_l6#2 Czu.\@ݮ#X+:*%lK-u:9<~=6ưG}ĭ[(˒'O)ш|;,Kk6VyGG6*"#vwwɲϯ# `{{pH(cP(2@R0p]ȍVjt}i#l!o4rao4Z˿/~5¨I%(Yd1I3LY9Ų@!]$/ ,K9q<(2|ȨEt%~Yp)mjҢvF*$2 >kgé L--]f(4Qy0fȠױSs,!S0j9v 6OW"p TUN/kU}W-ӾB%craII@ c4'HG>O9`"qxxJ!nu,EHEiFVd]5JTeQ9h02sC\ǧҐ)2~6Jaו(c(e"H$r^փHs}?$q JTF9ҁ2qLoQJ.bDpijl|8V,kɳ.0DQk;El>c9~ J}a|`_*#AHv턈Z P/1"%QZW% dS-gI"tYZh19D3_Nmn;{Gwx/sn~fD(h3[{dY}u'Op}>뺜lml laam1&ǧx1% |Ԋ֓Bm4F^VK"mUXZh UkS㬰Jf nV~g5tRv0b6fåh]S%Y8t-.l5= ɠc)pctb^.1rl}xL$ܹs^z'O!׮][;V{M/2֥{yzdUI찱Aܼysyq̵+//|Ǐۺ$YgYza~.b+i4+}ǵkV}Ϟ=[i=c\39fkkB?իWY,\t'OVY}9>>,KF,Tt;;;ܺu$IWd2ٳg4ՠeiUxppsVu}qh峚³3*mss>lt} m `>srr>۬e6,;n[o|>-z}|l(ʽ .\>p|IeJFS><>%X{{Q 'lHW ^V;9]ŭ.BJTZn NF;mxx_xի/p%[u&K[)tA+Le8mUf TeD(XyJ 4rY6Җ֐e=tTJQփaXwϳ`Z+\ CZU 4ˆ(rUJEUds]|@(( qOqvۊuQd9vx6KVt]ZbR#x&/VY'Yp\z'(lHG"(htǓ課ti]V":=GdBݦ(,ҝfKGZ$=pY_uWѰ}WzHZ=h#=uYF哹BbZQ_e)隚pt -*aJ27p. Z@4}eDs F%-/xe$[v:̖1w.?O[\zMxN'h9<+%X'|>_Qah]<`gg(p{9IR)R䶻0m l|o10( r]-:UTEIH9s>9֚{.ZMl`w}4q< o<ն@^g_Su`ol6c<[Ql i5|'?"6ŧHC䢥3֐a)m%/8C  _I!(,!W*4ǑY1!cB 9;;'- NtFȠ6M>6Ac4qu>.kkqe8W`<(EVyNO 6ta '([R:8nDG1qZAقOa(hK]IJtNn6MތDSVfG(J Q^HI\ĥI v4A (Fa)9,bI3%[',e>J)rS>3Fk_ƭ繷 v;@k.DQ/̻Kۭ fss.vB0ωqW+nF'I|INOOᐳ36󜵵5sمys]]|֢( qT .kkk}yNMg{PR*( 1sTNJ#Pd㹍X(lu JmMy]k6:pQ*<wO>/Ee&e!Pʭ*+0eYfGuQ8v\lYTi-v#W6) A3Zf0*dueA+aQ%eP9BiMƘJ"՛zg?e}cȳ#^essk;7t:F#hvBB܋g<{^z,F[n+48}50$ C~mft: PsvvvI%eq^{=& kgAM|g:rF1;;;p~>Ak]$ |<+/3Ҕ͍ >;=e0)׮]cGŜEo mms58fcstl1pov6N xꩧx}6$>h"*<2gs8l`efuNxE &/@UnE:2582dt8n!sq3stt{#õkOpF$qhziNZMttUdyr4ʋ)͜9W-`Hcu 'X()\ p~th<aKeiD9 2ɻJXsEpeQFۢPI%4i y.ܬBNAlEi6azw)<]! F#ZVv_!.*洺!q.iY`"-5ZP*D1 atS9]0`trF,ae&]mS5$'G}L&Vy/ň<+CR1ZH$ `:BMDgop?DeveV%,UԚhpF 2$v;˅E5鈮Fm HS NSꄓ%Q<ʕ+o aC>St0y&c7')عRtn)~ΰ8H)_g:39?c3}~{\0=P= HCIٴ ƀmnơ%T($ ]n(1\c0b)J Rrv6_w!n}1Zeٌ^xJhq鈕8e<D9q4fK )hDq1Ջ$y|k\mZ"cuqT:ˣgT_xR-QJ&r?`cW;= ܝ[[\~/r|oh4j q֤dYիW)KN'SO.v-:Ʉ,KVWWyךԔbY~TX}VVl4t:mb,7T6^3m|ל=g+yY,$IҰݶ,;==e4q}|IFADʅal`ww~W^% CwÇFܾ}^d2!. < ٌŽ}ŽS&ӼN߼Žum*W׫ k}B᱘&she(-h,xD+e1εt:gD蜢PʡZݢ$Iq n+]45YF/-FI&I\.X__esk$d岹N۷r#Z}^d}}lLZ2cd2B ez=vqU`ack^פ,%ق(8xpy̷*>M:>'6 8;;G)eYfAKv%bR!4BuV_L=c9q%.jFRT#U4b@@y}*78;_| TJ{ v /Ľ{g ={tZ_Vߒ`^wHg5|g)Yv|~o9ٌ͍-WWSR9i18UG3 3>nZƋnV$MPk4EcKEE7P&ƻ={aq~.L|++cssMJ~<|>޽{͛7O?wARo}[|1wmmeLS>S!/V1n\Ԏdq8Ss5NBðmoogyfi\.\v͚jZKT ONN8==_˵kx'iKUcX__GnۑX>}E6dUבRru677~Bi@̇~ի q|>JEylBge[6VTD˶k"ltB͕T%ESⴠ֕8ҋfgmr):' m7p4zHiCҾIUוb{cK"EeőyA&3daI̾@EA8a5B9FY66{>$aǵDK.s-*t6&6ZkxQuIEc;uׂMcEq_.^^Tee6%W7床יWpHcwߵlmmU'я~ķ-!arh5fB4c- ǎKR<%5jEQ 2"BHCk (,"/J:"0zEJQplzHtzhmP{4OH]%Zh4)|nf(VSW"h M疴CNץ*\ #R>ڳPES jm(ˢ*T) nRv$v dYlV)6Uk)v"}G"KbNj|E N( i7rB^Ead2zUK8дZ]K4]"d*JuR1lJy(Jz-1*r2?!s\x&łdb1%EB;!>կ( 60&vˡ?h{|Ti (q ە%B9Hv]P-zÎ!S!r*`sڬsJq\iI3`>2nNUIYbKqケeRX+gm{T'wQ؅r ֲN cuF֣XڿX5)+c!/,J>sN笯s.ktCck o*AXΈc(H,^oWvocnbg?;w8ڂS"PQT 7*/JBIh4RvܺaSY:7ڈ[(me}J]kK/RHZ\`cn<O]ɧdY5nb8XkA[G~̧zW_}'|]>#VWmO>ILS(7o6C$kuWouu䕕Wj>rlԽ^톅8bH=*n:R X4 5VJ,I"*F̣enet$ |֚S/|ɝ8xeܹc]c5 +srr\iQ)JJSg;cyn;.c݅^ڨcPN(:Y>JiZ)x.`xJ否 0Sy8J){kUuye dL(,YPhEY-ʩ.m7,mT"JbtVyV]oWܶDw)FVk6-EoF6Rͺ^nZ  |\W{* O:!Jwlѝ䪺&->-sJq},(B˿[."2ɘHp=דEB^x^h {Nn,1v|FAHݥaʒ BzrK1Ï,sIC6Pd:8J |VWŲU&=m\V LVvr6yg(( eS&H(|(&)aةra(Jp>~<.&#H=^~֋1ޚjb&F[a3\< ˒҆a6嬮Fde`XF%GGfw}~w( ]qrZIRkK/1%-avwwp}O|'wgwyODvq)@H-Bj/*UECm;uEg.(0TMecr=\>3ϲ>PD"WMi{L&|M_8T-$W_}U߿߰G179;;kurռrU5\<0[)Y. 6FFet:egq666H\ݻw<+0yaloϷp {& ~:O=in :>>{looU<;?Ν;?`}}up]!wf [g^vxO?jv@Z/렣(j^RfƋ1`T>6 Fٸ( 6t|5A ,qױb&E{~EQ'W( Np]{]VZAۄ|l:24Cj2-*X !ݮ]Y3 㥮bʻT ֻغxyyY9^#|cN0u UJc(b(%E) Z},)r|r ;mRA?y ٜi!JMq/|ee9A>Cr|vyn34A4{[9H6c~YlQ_(%g?_ƫE?3\udy|sg8=;{mwޥvvEDQ&BL&MiYf7|>3IpHe4}voAt,$ A|S& ۬5`F^=r].6ϵ/3㰻۷}6ݳk׮:;SW9o?^)n櫯[o8vꫯ_awگ矣5qʰt_~$m,ljxWi&5EQV.X)Sv$u^52PdV:R)2QEnM+R > T6~tDnXG$$ˈ~ZTM.)*דĩ NJLM -LIQ&ybGm<1.Cʢ4FgdIL^]"$d:@kcFV9ah/_RVθxdi(#d9^w/oBE6иN G//]u7Ij ,V֑$ CVI~k ymV Bk<ijzC^}HKpZZ6c6ٔ, Х|4~(lj+x|Yr^Ml]:6aJf 1W)5E1OpkO'88{X,vqFaXp5~OE9W^\Gh`n 6Ǝځ< ݵ;(?Ck;Z#jcKY6qogk7tl뢾NOOl6go8==,Kog} y駙?&B!? CΫ/D)Eܻw]Η67hM&Mr2{L&])mGUGcZ { KU1],5"EQN2LhBјnrD)U1 6xRaChr*vvV8M)Ar3XN@ c#)% c!e-"#M ]i˒wY.-E-8l տ=r)ӌt;N^0l{)e#C5c\"q;\9a&ݥtO61tJL&V&eYcXm: oaiᐠbuͦ $ID;`:bLFDQBC\ף[駟WX!Irp#2,2HW)|+ЂW^:?Ōǜn/?Pꔃ~C8,Sxv!ӡ1O¡X\7`mmل02/1R:,qc;??JF:Uvsg_ݹb%+^ șf?/ ;ەqケzfG4͏>/u.(UV:#l!MƵ'⩼ %}xm[|Tوs<"/r%i=d2=?go`h=2Ѯ^,Ff]]RB66x88>b\CM2u[xTx(aǤo]tR tp/#0Rv,ZFRU>e>ucU4:DH񴠵ҡ )҂`ȓׯ3D_ٹ`2pHηq|v>|U}& $ם[n{yי縮K4M|>o2`<Sn)FR`|>'"]; vqbUfYoo+++dYF :a=<8Ax Uf /$-:PUVVcnYZʰAfsW$EK,b:9%<,!1&#/c(]T%ؘ6GX%JJ8Vq\*>KF+d@kh6u/< IDAT$^- FԎlH'_Q!\Rz$WYYYtlﰾEUArsɫR#2k|z )[ V6x)t՜e'ܽ{3TF<<%+2 C&GGx9xe\M+1U 7q3EZwE<8hvcՐZ64{,m(Zkqケzˣ#/; Y˨KGc ZktQB\))B(/v |IGDifo{ gallS1~6Ơ HaGge !b*73Nӳ3DiBW:NZhU RP&VƖJBVo!З,A54̶G /6g3|% C6w~s 6W_Ž{G>FJ٠yہWqt:CZᐍ gMק(+wVi(Rd,5^<~;* "[ *=\hd byKM1<.Ǹ誐,U vŠo ]&8nˋ/|> lGd(R̈+LAvS%?A"TIVIQl^oX]DT0^k}QƒHS]ڀ~]z۷ocww޸qc>C:.~}·_祗^jq+>666xxlPV5rJ2_YYCJ{?'''TxwyO=yN$.7MSAÔ<;;Wn$ [[[\zKq#Ų9xbmm(hԴ>9y [( G riLei@ )Ksu&w iXѠd;*Oh)mkP:ҡ,h]t9\jm"u%v,F @Fdi*0hRS.cȧV(B 0%2JAvZ yh27gv#W$I)˂,Kq&"ו#}Yo@/oXttXF9 bӪ& s$ $E~# GGvG `R%&V& Q lntY`B*W)& 獒,F#$gPzSJUi8^eY#v\JX6\TU.P$+* %/܇+k(颔c:-B(:Nq$ [%IQV1#/,EȲ9AP8mZݠ]cz%I3|Ys_z)vujJ1HF&tEr$AQ !buwYYg=V_L')@P P'#Ήp7yOY6:Dx({ "{3C> >3NNN899ݯ=N_>Y!D7bN]ҥK(޽{\r]6fTUA;;;棔+?~1W^*neY<zAk]qtAN;wۑi ]ctF=O?,K7zɿ˿d\rz~-o_ !t1|z=NɊ]ggL[!q3;w]&%MS9::{_#򤫺=X:m IL䜲;TͭjbXK4y5y cT.+tIr$Rv'\>ylUR7X#e@ CV/O.)cݬt+ em댪ĽxQHnKE(Rk|te)sR9[( rڭ'ZFJ<3'\-UaN|můb|a>{Vw FQuU^Ѿ1mez8Aˢ}<c*Js?e8FrdO?DC^败ʧSMg*& R96ۓ1ԘZ(R{(6ŗ-PK-qy] t&Qٞnq7<@4lru 9eӋDʂMurGgԥnR͡Y%=F_5ux=ݸŦT[V 7!EûjsKk/nWNc-Қ*&- 8ŀTURxA#a`i>D>AۆRR AShDKq6ڢXt#ֈ. htFEb;yog?F΀o2NqF(w7n>c NS& C9~!=O&,c4uZk|wɟ ݸɓ'l6ax!O<]7n9-V1iV))OykY: WH$%XAՋ04Z~)8ױSq!]+&g^K T[^$Ea0a/p (Yzg%~: $c1uN]AנmuH뺄rJ׭Wf~$/sy}4V 1Њ`b+Km1 Iz} h<0]bI}0 Wl}F]i?Uoƿb~>c>_rz [iF\|O?o\vhĝ7>}z;w`0p$Z-gϞ_gϞpxݽ]F,[7Yb4p}F{ǟƷmH<|*e4ݻY,ܺ}pGz1^IP|~!t_|7nwn ?tsN"{:eu>dzOy:_Ϗb 冫7<{n\ɓ'J?{?a5ҔٝgqMΞ.QUW'|>Kĝwo|~:ɘbh($]ZO/D`Pc g]}4 P>VHL)ؔbfDV2hĖQb(v)(פt:=g^nu,a Lj,u"uؽ8~<;jΆ$\K{'̫s'z\"erk-T .M;$F)X1BJA?HyKΞ>@y.jo" c^N]X瘊`/V\|1޵Fp:; {Қvvӎ?qOmAн~|k$K0VsbyuZ`iyx8<3.K:fagg`vv)7\p7M&MS8nc&)Mrz*zҥ5^Cb:̋ݫ) 5UU<8Ɠ~b7|v6fVDQ9Zۑ mA mkV]N)'WI] CTE H$6HaB $ )= IF2:<ݺHq:Ϭ( * G}P>WIMsF ܧEw_K 3n]5x+5I[F#]ʵ+W ^K.{!=| p|| H `f3V. ԩv~x<\<#/ nݺ[o^3R-RJ~qS!}ŝ;wxV=?(YݎC[l@Ǽ,g)ł%LS5Ƹ{oo>4uC)%ɤ+ٳgG?,K~7~$m!gg'c$!Ø|s籷|9:.w|rF8z17n) [[.""ʲܚZ[EΔ0 9R~]aAL<ϐ.3!8]^I7P;6K: f ocCFRy^tk{t2m%|~~_N2(&,.nPEF.0¹hctM³nr jXxV0J$QH/.IzE>MUcàCzP*)k%1jr4c\2t.Mڸ>撮B7e\paec^HkxK|iIC$N Eܼ~{7oAC)|y9 )v:ƪ&G;ǺkOت[[}'i"E{tx.ɤ;߳IcԺk@ZAf(ix&եH[6҉ v Ѥ;]|^d5C9,&!*@浱\'ki͆~.fv7iNR 0`݉*s62 {A=z;*:ߺ4lV)#lZHUMo)yݝ) eMH /k]@Z) o tJ|/r ne;$3Dh40K<[fuH[ E/ᚢ(j@Zl65Y{/hƭBJ$Jkh'AkPTKY0}u庍&Θ P"=t)jV"tc2.$1c W%x$}0y-b|]0gyat1`N#Xύk,]^eۂjvTkD6ωq|ɓ/:]"YNH~N}CnB(?~JEK"SUWkW yMYnb< j\iQ)=ձxJuP늦R(?_ ]iR#ׁkcl ךs~%/ܨuu9l7EL]l3ݞu3nRJia~>c0Ʉ?4^7fN&0ܸqxLzyAhיV(n; Zkц͏铣m.Hɫ'Xe\$5}lc9QdZ+U T1TvR)/ ƟIKF uZ1 0oa-a7N'n4fUe²Z:KYi Q՝,ț) D4u7qQHfE&=}~ҽV͊4G)nFvfM rQjm ,ma.7Y3U,%z^/yIo:[WѪ-3Gs#N*~^-KfY7~Y72l6|F#qn#UU)GGG|o0 xUU~[otqkB4 )GGG<}?)xH8O?/>tvgS4e^ ;.&鳔/\z|A pEM]t]0jz0tEФ?vZ& ???0K*~fcgKS7M6J)Y3ʠt]6%:̔8 [ 47v@k4umLCQTa/-zeV4)ݡQ)7lJ)*G(Wısr+D!^S+VBJgeeKݹ~ ;;;^r9|p{͛7qMrtg Ue?Kڥݱ$?wźޮaZ#we=wj+FhU O2x^3Mj"}aM@;Gh&j[Ə1zhRN*`ۋ׬k܀=O^M KQnT4]E?i Bےqϖ$.0=aw:鰻Ś'ϑ>q3L%n[-f{˂<[Ytno0bXz{}KwHzN$oPQN[bGW|=7~յ=akumѝ( tmPn}d.ߜlKF)EwД9<|tjbA+_4]wx%=rSvvq5W9G.e>?֭[|looG.x>7-|pݻ-veB@:4ec!ZNe٬K)ݽPu:o:wY]ewxh7$CTVUAٸvkkBie.6HIF9.Jb>k]1.9*N\]QdH(Ux PB>auZ8c@"a*Jbqlkaћ +~(Q֍nMqUf%_ŚY)U-Im,/˄h$?e[l(H$IO,sRk`/8a&_Ue^|iyι{6xXu p<7Ҍm_%`8 (rdj \?/vbܿw2tƽ=|\nR}dt wnC]X69S)_~eIM;ƝzիWy1}=>..$6tvarӳCz^g*9:rф“l&q4K[c5|]V5xppG}Δlگu0`B8J'!F5fHFEǴ#e{2i"ŘFlmm3L)ϗKcrm*tcZWe181A pٿQ5`PkwOTΠ>{zŁץ+:W7VQT%:}Ե[b7eq t!VQ1 } }$sJTF6WXlR~!3Z]ֶDcM5P؜v/\D:~ ㋤ nZveGe|gO;w^`jt7浛\t駍5q yޠ|9q(6|f=emrFJe(v (wЋѾh14a]7ÿaݰ-@7 B? Tg(UZnxF 6aFqZ#a9;NئTݯ P7f7È'O!BYyIŧt$pgm:p8У0xPuŲ=R%bk2n7V_8icYր˦<|t7z;Uʣ׏Ȋ%(/sjZɸpc|Nq1~Kۭ;/ lCG] tMYWn;ѵ?zqJHEN{_RW.AMSx1C>eoo4?)Fd}uQa%ݬX.s)~!nfY,<|d_~? ~'|ܼy).*0s:Sf6[n^e>W\4u}|B>}ڹ_u%eYr%~W~??ڵk~pewv| ;{;"iD!YY#}#`\^fm' ͦڰ\.Xl]ڡSrͦ&_EèKҦ3BA|5dk7ŪuIʱ|C^8{ձh|)sz|;f8D ׯ_M,k1pt/+Cϧuƴ 9X4-è[[_am/u]УʦR5N~W+P@ ׁMs0N `,v`jC5xi zsJIRSęFƣ][i-uEmV%V׎iEcF u p^clMg<˂M+ߓȦ}R^uIJȺEGI-c)4c+G?;XiYuQ幱Ue8,6gs8R/1;؞rju IDATx>Ŋ?^$gS9'2!Ey{fՒ,]P9e3 B;o$/{;&ۜvr׽0lbݸ%8?M7k:^n Ўklg,uiu\ a5V\=)8{^%AM^TzR6+ Xe}$PXonNq랮m@ F6yHkBZ Y{ߏF"IuՉ=/pIpӴ^;۬-֝q)tOy&O]ݏ]h^ytnX u#[:SAY$G JvKQf @^=ѣ]Y` bFQn@ 8ْ|Ir-+uAI7+ڈx,{JDqȐWvQJ\6^2uP{J]g2p@( 1awfX-;䮘iH ٦D2ur~eŒ(4Y/6TE [yys|`PجS%UI:/^xqMڍi&A%_[7|2aÄ^2b~v30Ƶ ZlZ]dtџ0n FT%ٌ/s+/h4`B-E 0ދic+a]X~ih )Jue jΔME9R %=<#F.Rg&uf>X'4Hy׏#pdq2۠O% ٭TԆѕ#l<9p9mBmt'k4^qRji-FTR:)i2Fcz/Q6҆$ }<O2¤[֛ YVnbϧ2'Rtϭf;.ҡFx+F (Fԃ"a-*}M^|fnuW~8*hC#FMYeN03 vu =zC7x葋˲*qx._̏~#>|w9J.y<A93q@uC7͚$5Kdi]7qFb+4M;wt?gϞ섻wbEglOS3 mY+<ư$ ڸ_)lB%PTUљorʹ?~ncj{ՂэO&~3j4R1J # ŊJ1F[|㋖UNY$!фO8 c"d6A#ܠt1Z `1_v<Ц$Ն*ØDbNS׮00ULbWaH?I0uII#]}!6yMXKzqLj@ZI MVk Ɨ>'P^ $ad<ޜ,#l =|dDC`.1Ng,?Ӝa?+`ݹ:9<饽APh(KDQ 7o^ڕK z}\awoiO9?}əMh4R /jX¼P0 A`ifBOQu;]w(`P'ۼ ]'tx i2P(^jz߹i LK$a؄chQXQ:cڽczk¤ggO-\{`jꪦF)/ԔEE]:I%$lp'G 6xRaFќ4=]k<RPjӹU-ݎUML؍%#V6a~?q1[uAY圞9t ' *AwvZKw{E`8XλE=lN2C+D n82۷o`rr?nI[MEhV4;Mn¾}{8~PY,eHtZ=TTShRgvf Νt%kk~[ Z^ު  Cũ/;X&'` $ebbӧOs)FJ4& X7#lb"! -(͐fK19&!cat^ 0t-.015MoHws2#|Pϲ~ާ,M^0JH!&/(fƲJٱ}+ đD) 9y$72~gX?Òr0g~~355MwafMP*V׺ _TSN23þ{t{d!/?9xNZ{᳟4>I0=9MaǶڹaTM[¸>vy)M^/M Dɖ-3lNdffVA3iseጥ?2侨b4M%E) {M5[u7^Ӯ nCsEP}@*x6$jq3[|kp*J2 GAjCYcCYb%W ZW-8Y#s+~(" c2P FeFj88"7Mv6`rrUЀ(a;,i ?#TaBcra÷sh#5:pUl5ǎjnͺ[ (aWk"HZAη1UXo3ʇtfp^ߴu'dHLe9U8p6 Qgxy256MSnۂ֚#GTqշqî妛nbaa?#޳jٺף~ 333 zqpHƖ:~[lamW9/..smUf>xu{t]<ȡCxSʊ+GWVV'17hvf0p#ZɊ6c|ʯUKH*(g~~0jg)F:i >. `8F+* %q BKJ9:efYYiPPRRFKb2(!e6b4ShʔP` rw۸ar]!a ',Azh6Y9lø -1QƤȌW~Y7s[H4 M׾*GkԻLDU) ߈|5'k7Vd!sm}][~mN{~gYVtZ(zu`} |U~aYXX\uUZ-Vז?~NK.ǧ?֚G{e0Gf8]B>0pT:ĂYP1-Kp@5;;Klٲ)nĉ]~/.AkllƯ]*jk-:& j $d(V1AHL(sįtiU- y1J& غu ͖_` NDa0}+fu#w h6SԩSp, M4 JŴ&:t v`fϱ/LW\q5aؤݙ¸a=['EaF[e3`d#vb|o?b ֨=UU9_fqaǎߏqWs  O0f$J5F-K|}cbb)&&&8x^F) OL,f}SӴZm>kVU:~$IHjMamm S'l4zjU`-zR5J)lݰ/@a^k}Qwn)}isߢVx[uZ菣j1A&8kV8pZ%-S P ARe4 =WjEFS( &:mjq8UsoO6֖zO9&0`|ظɴڑ$Mӟ̌?Ii(ɳҏB;VUPIgҎ~7*GQdUe4&ڕ*Ƭly;&jE゚Z685-Fc>m<ڵ -6hBʌ{Luxk-yQWqsNSOۧfƍt~n<]uzڷnkQ*9|0`ۍ`g~~^{XWW~'O$IeʲCQp'=n6}e{8-Aʮ];TUSSS8pfhO zO$Iv 1ja֫?hP[ GU!V*ջ%>|8̾#,eA5 €9}{%4҈8NNs(GJ+&& ]F.m@2'!$4(CKB鋈IlN3jJ'ˆ&S&';2%upɁp@wXSޜ!03]t:f'HbM#NK#@% j1QRpg(N1 &IB?`rr"7t&| Tkz+ !z ~$I8|0sdqa*ɉ{8=N2I 2kE. KAS7Z߿};NR9A8q~m@`Y9KK 4`ٻ04YY^w[9yZ6RuShm'OGcXYߖu}iM[7H~nqNW2~[Wˌ]N8bj&T}61BX"u~p"beX5"t!vѓ\wѨ[/xR#qe<+B!x\z_Mf}_^S'B!$0 !B uԃ_w~ۮ'żۯgwe+_ jk:/;wQY;6vWoGSyUMO~ N?w>R=w/ORur09.?G涛>w^>׹Gqq}g67|.oxo=_|ii^W ?缝ox+o`q~gxÿN>vKW?pv=~n_W^|^[~_M 9.?j~cvyُ_?s_y]K8/Aa.~u?̯_~r\<=W׼'kr\ CS<8ᛞ-m7/ iIJ6x_ß]~:ۿ7?AO{_]RJq ^-}o-/>^Ww;nq<^]IK̟:9._Mqr﹃_?@3Yw)F%8ċUAr\3 !Xq`ίēM?y/?ͽ^b.JA3 gY^<}v=҃eQ^/~@G/~^s>Ǐ_|u\|sCw7/\zŵyk?k~u˪7w= È}cr\^ɳ` X>?*aMy9aɁ(O7鵼M3y~?wnZ~#Z+9|i}ot{z OyAӞ|q:.;_BxO?-_ eY?oxտy#/Kj,m_b׾K/^qÓv3+n'ٶM?U}eqǏ!~Hzx~F{\Ϲa\y3_[n̦k9.ٞ}b募O~g$;Td/~3Wm;_]_gyX]^O.ox673Wc. ?Lg=l׳O}/7A|w|?OWPa֣<'o%OBC<M=|7?3?]yˮˮd{ʩ?-BH`j׾KB]x˻?;ص3^\h'zE)u޷5>6xK~7x /l-B<ɔ7y{7׿yOe==yggl=9s[}$_2-B<7^/?7.w>7|οqC||*^q-yB!Ƨ~ouzWȖ׋BQF!B1;x!a2(B;>o/Bq{Q!Bc$,~=6 !B<<槤' ߫sL>yy]!ߘS2(B <>x=7 9E1 dǖ-tC=z7VFCC Պ0p u -*3 " #\it˯wGB!$0^aP)tQB)!t}]zU {NG8&)ш8 رg+i)Gq!kkCS 5RD*asJ{ Ƣ(:\$r !m]}ֽ9_KJz:@k_0Z) :U)_Lΐq'â$PerM͞mlI(nO}ntW8zA`)-& c(! 4 )x? JGr2(By`yɆqeI>AhDY( ֔amL4NQ8efm=8gs(`iM!!%VB7:o|NZK7­efX[J @/QUɎB!xƵy fNMG WeYez=^(( PݎS`Ն95.4!$neLtHi v13eV[$a03 k=Ro~*_l8v'}V#c*483 !B<cDtW aHV}C)5ʹ:$P# C22:fؿ333, F~-a?$sTt47sgt/bQ8YZZ`vvhZ8rnZS vDj*֖> VV16ݚә}[xe8˹wr<8JpX:AYa{< o1PeVlns׈?]B!/:G*:ӓS$I3Sd4JC8t+(Hl4Q .?,XOvYuFEsUA@(VUUvs[&9y$[&2o'I35@1E"d,!T,t$QB+ 4"w[~S8{9WRv{ud@!q)MmcCŋE)%Q!ȹLLLS #RM|֚"ɉHh (((s`{yfPZ)e+uO/S񣎆S͐355E%V9Mq|yN;وa4aJ~tє:Dg 5Dre9'<}\?8?q5H&*9% C8Ea=JX9W`?]B ٹ4 tWW_\bee5euv Q05annfP%JD͔55hkp@-t:*<Ц$w~J[+wXDq$ aiD}1ٷk ++!wqZ.ٽ8 R9E%b )h7֖dِ~hG`ˉ8pسٹ ;>ϡc'KVVC^.AMՒgc)3F!BH`|VVuh  €$I8z$VvV4MpD-Qh $pib DDOAaTMDQDѠjhZDQ`0`nn5` Is,:Ξ={h4,#2E@YZ05e`8f) 8~zhD4Ð.[Z#'1N\zD$ einzzc{4B!_Ij4t:-N9 v KOg;Nck! Sta,dyF(t(& CP1`4 KdP4a綫bNe9&QEAT&Rv. !/9(튌,8QN;BaOg0l6 b4((EHNoaϮ4 s[fÀ$ 8h&':\9g1&1k`Ifq]图W] fr$AX*P!J:P(|q4AI$CҎK}liGǸݻw6C gmEl=_ thp8{QWmUNjOFB EfI,+P$QDbqqe޽ ]nPs饗mf+YD7@㘘b޽ ,qJe0#?U}a݆ c&NcS,//37;˓^݋ݻ, 5PS(4A (;h( БFjsbeJ2c'6*0 G38@>`-P6)/)ğ~ncL5%NZQaWV^B!Ƈo~~$I|_)/H9y$'OjnR%,aMbMw2k4Vqv=0jEر9v0Nػg-⨁5pHi$SF Ea: _ jkUԾڇƈ8ku|`rfHEdٰs!iQJq1f9mɽ; 0"aV0$qNA6*7:|GkYKsexنQ!/Ԗ-[DeR"lkp0(B W%B5NfIQ}0011Aݮw/ȷ3$!b@3e8h4R$b4վ8H54 E6&`( o@k*(bH4  @F\ I}( 9VvEYFň^oĠ1& c44W_}%N/s= 6%M $hUo4Z˩7*!jpHƁqb5=PZ!HOGo\7vYYYaii9s4x'q%ph(P[F$=3`[!*UXClۺfeuu,)eY !EaP:$O+Gyl}_\;gDQa|Xu%O199I$mĤ ?E}z4VfM*\[g&ؽŮ[I9=Ľsz4VY?#\(ǻŠWL+C mdZZ!i~G8FiM#l J)hJt#GENtZ!Z7!sr SSm?C)Q·1=W\΁Kn%?vl4B8 ɲz> &QRm C/-*JS<`kOӎlFQ |uh4bqqӧO933S;v$A)8dvz +kt׺{LLLmL9bm3mfrm\q.;t#GgeeaWFQB~*8 k1p 8]Vmwj̴F!B#`=4M8qE5]4VB ؏H+{̏DP ,㐴@;FT1gZkF#*48+T=K Ga1a9ٳk64R J)vM377WMwDkHjV#aգeAd$cِh@i.5mc_`@#*o!B9?Ɨ6Ѧg<u B!GD=f! C2. ap1ZAW?+"aSZ BQOQ4F±#iwِ9zˋlA) T!QlP:Z. )߻MZ,J4 fgعs7ys!gwi78uzh. @3ij4K?F[QM[Dt:Kرu]8k֖X C ⯏I6=-"F!B#8 ($]/m5z4c yYo},pv,+pΑ4dYƨf9a][ŕmF!Ui3F-rZ++uTANEiʔj^iXE|Sv,--q;v`%s^N8$Ic4$I#8"N|irF`7>~nzקdz/[ZҐc)-UexVteYh]FyF%1Cv+R!<~ : Ak̆i *gL EYMQL`8+h3A #h$IUHji?zUXzVRReIYzm`X03;Gա?ș4LLNs/6$$McFT}Iߊ*,V;h M!Ro(FmY?`zˆXk`a~EIjvcUIXpq]"B C0nl kӠ*aG)0+Y8,8&I#|Hc`-ܾPہuu3T,΁ IDAT~*n}k??Eme4ZW#c8*XZZeۣ?ri"C!+k]P~@$IDDQ@*G~; P~}s():簡U1[t~OQLl߾i:&6RQK-qQn9ӮB!$0>RWkU4w~ijMhGaPn} ) LMn.ٻV#_G'Xnb[i 8Aq,mFoa謪~f0[8aV\~JN87LcΝ8pyNa\ HhRmEIЈ ueh֧ƕcS`\s\Pd6R:G5mFb3AF#rӧOs19/inWB!/WVH){7*M$IBfuAV8YNgvJfvWT1'dQ%ibtl/5U,K),i(H] WNG񬵜X9^TxFз Ct+},: P* #)jGMMsׂj֧FeVz=d5) V =z/~TxnjFP1f-[(BH`|Bk ުP#b3*Adsv3)ٞBiB>1Ī_fnkٹc+P4O]%5%0H(?a\0˰(tb0frj@-X@ R0,G1yat{( nU299 Q^:[O'h$$AXF\1Y 9vKjh4g YB VGua>9Eo)U@cmJEdyuA B* SR4֒%Y^Ė iAw1E0 0<+)rC``T2Uյb4ɲ tp9G#Nhv&'޽e:, ~K48fmm [LvZ>uNd$JBj7h6H]-J Ҙ@Gݿ0_}vŮ]8v8^۷jx́SNɫQ!ع5V}rڀ "Cբ8WCߨ+&'֚^w@i ط)2p۷0 Vɲ!Ɩy $Q@3MpqDnʪCbu0C.✢`4kQ1~h;zDf#,2-QX)8gȬ/OıِN;{ffj*IFo8F4-nA`ȭƭ_;ٷd$@BPvT,PbnVk]Q?Aֶn-QIX$d_NNN:\s-? ;K""" +c4V1;u!z|d2N^mx$q8aP~5 T"Foo/,1ch4z>j0 [1'-6Nݓ5jjAұ0M+ l-jiab`ۭDmpǡRv1K:!wpu<U[Vka4VtxVR.l4ajTJx"Fw!G&&NbHfh!cSߴݻ1-t:K!Ra`&&awAmEV6abXz&aӨU:[#!Zmc[T)jne̛;e˖]hNT*m4O3 .%6mĆfCw_?X 6399I,Cd2]zvFLAm{%Lr g)""o|I" i֣-4 ߧ'c1² 3$BK:044ܡ98E5!SÌ¢S./F!u(t.]]>Ah c4vclb Hē`X&Z[a6T.fkK>#u腲mzўV39>J4E___+,XfģX3k˄#/|>OwwLF!zDz,gR^SoQLLLP. 0=gs̋D lr#8èV[RH&&RVƶm}EDD&XE`OG5.d*Zleb!Lxm}eR ϝGOo7c>iYd29 RQ[ GFXn###A@bq9FT4arq1 R)oN"0 Z(""&ǧqFGv)t1== F@ѤR$L& fIkn6&qˢ;G___G&4~#qډ% H |2YFiSk4.Q%4ami-B `XI̶p,JirHgts G.#k2q[V450~1hDmIe3XER vft[lô-\/X,35=M^7)ض@jbqB7; * SSSE:u ޽X,F6ejjJC""" /Tvlpu\ץZjb DOMƶm2==2iR8~ VX܉*k~YmY$SRu ^F8q wժ8,7 j 5#RȧcV]]]y~ͨa2N'9quZiYr{{ ʕ JFARRc6m"Jbxi!p riYSSE\Ŷ-ja*RZmۤR)c]j58==ݭnvLF}ו/PI%ԫ5ʥir X|jfI=V ˊ N璘7X&`BX|^*~!ORaTi10^gh?萔]5obJ $1/O&$_QfH&qU mb0hx, JiM^grrѱ1\7j{|[2],>z۶0[UQuq=׍i>8$`bbT2I.CZhxn$(˔K%vVN1[DDDCLN$R R)B? JD,mie66~?h!d5lnvbvg!b96L.J:SJ4dk@a00$} K̲e 1g,z퉆 L0CZwk1 CB0 nTY,'jNŽLNNRԣE Qw䲤\/ YkPwځ~r0mx[#RP+@ZV)cL&bxG^T.NY0w.jtIz4(0#W$gϦ^155A6~6}Hr$S~H4o05DFCv4!v<jcY{뺤iBJD^Er /^JOW|>K+EjJ|BYf###J%֬YÒ)""o c`J0l/G0 \\74(Y1\,+N ZCͦ4Mts3|&x7XA@<m'Xu4\Jau%jJH%dd\.G.%JD;8e! zjcъD*qÀzmm`t6K*ӢTmF`ZNf Z|>OWwR LN`""" nVlvI.P*1c;c<{-}ڵtcR)[H6PÐn?nJN&ߩE[օJf1jNUe]tQgb{7%:;EkT*bsi0,Z 3wd,^öa(FU=3m055b6fs1MJefdY\eYd2)f͚ҥK9bh"|ߧVv)YsXp!B0 jT%:]騕Q___c<'E}yQ`WcDZ8ahu0ZL:K2k'`۰q^ry8 9&Kl$n$D˲ H`j5OѶ-*|aUvxl_ncYV4GpFX9Zl6K2+NVϥV`tBWrLBh211ţ>֭ۙzIV^8`6K]=Vzj,bjV"F vAXIJMHLLLi"T p!Pj`&f"2 dmV`Q`w%R*Q*d)N s+ǭ$ Vn㴳O$No`l qZD+h ͋>ISLLLQW.f=c9cٴq3r ;o'lMvTtcoJp4T+b-M2脳d2a Z@Þ*c{8Nqt[;p<+ < |L&-$qkGFʣ6!Uz=R)Jn,`ǎไ[ǫWIbx*gd5GqD|>y<ì^*Jv*M,#  c!G%KLƣrx<ܡ9a4H$aZFX;DH}J$TJi ``\\nkv)XF44lM{ aҗOЍiر w^TeLIc4N3=g*v.;j<ʙgb \Z2<<Dx<mhEC6C2C"4sp]\.5,/,|>O__w*d-k׮\Vo<۱գy===̟?%K`FkohT*ՙ뙊:hXIl(""" ģ+#dzz6?o6\[7?=[vZo T*̚5ONh4! Nuj +`{]; u0Ql/iWϢE&^gt㇤c(r{v\nc4 {3gT*EC;C0, "T*SP<0hI&3nD\X,}vzK9꩘Jdrظq#> =CCC,Xiv5j5j6h>x}EDD]l̙33:|>Kiz#JQcYzer|5kddd^+^0pSa횇Xd0Q#cd2,u}+]-YqP*̠ؾTkOj3la744H.$BR$XjeYQ8 lXĶmr\ +HL L<Rrq(uYuQڵ|zN?K,a޼ypjCiVcmضYl>Coo/gϦ{>""" hxx3X!P(md sI&RA:mn IDAThaMd2Ulyz2}QsB'faH25fll(ضWP/[vO_ŋXx1Lzְ-d&Z'd(m|?3 ݛy{lbG}K_/fש홫lʟ r3Ccv@&gsbx ̼|q;k(m8N Ì])h{U""" 3O0B|4ibddzJ<$IST_pwkWJB>%HSWXn۶mc񢅭 b1D"U,wM0;0>>fe@{w;l~e{Es;(n Uw֬~\kmE8|X,btww*^g(0yC1V( || Z<6:!d(p]F٪:$055Ů]֟X0zzftr]N3vyif.\iWە(Y{-N9D}-xIR),ˠ5h֣h4   G&Bvv=3jٌo8>Ri:lٌVlG`x] Mzz;m:;wѨJ4(0Ys(VD ? |ߣ^o` `tFƨ""" jxxT*Aww/zJBZ'LáTvcd(J1q8S"DS*T*]̙æ&_vu"0j3s9yPվ_^/ݮ</+J9iI$d2ak8 C:^ Tg>vzgqqoU'65h\R,g.caҥxǣ>0f9w)""o V@+8@q]x,u^ [_sS?XHM}_ fR3.D@)7sux9y_?s8h.%1.~ّѵ+e)>R.N=8qV-6yy}O;ΩKb%%֛CDDDQp?+^?`]u==;.e}#?(g:>qKx?-/.'rϻ/[?ViY$~yˏf' o^G/}W|^rk8O}߯"""l=fCCT[8v^ʙ?}9,8p_u&+O: :旜 캫|Q+`j|7tBW/;0w'ˏ~fr|9sOG= mgQ`ɴ[a>BN=<׬N❜yEoy+MOo}^e=v;yO˙2~?r{.fblbY>c~s#"""? I?Î<{^m,=bm;uj.~?狟|3wtd;إϓ^u^,h)'`'~?[[Wsˬ{]wߝ?ؓNeG45?ndcxK^r/~ +tsܧ}^W]]ۋ;q"!l㉧;?_c-ZʥO{%>C5ӓ8vwqخa^y^y#9m\WA.x'(NOpwW}y΍!o/,>}^/9\襯ضU} EDD䏋K/ޓӾziLl}A}SsA¶qx?eȁDHPDDDZ""""" """"("""" """"("""" """"("""" """"(""""("""" """"("""" """"("""" """"("""" """"(""""("""" """"("""" """"("""" """"("""" """"(""""("""" """"("""" """"("""" """"("""" """"(""""("""" """"("""" """"("""" """"=!#ԫ""""8]`L[Q"""rIg Q"""r93~NDQ*9h &INL\v5T~h&"""IY %N]EDDDj#"""" """"("""" """"("""" """"("""" """"(""""("""" """"("""" """"("""" """"("""" """"(""""(""""ς>ߏ?g 9#P{?jiZ(0j bg_،JaXU&̊(0`=vnԹ,гg~ mg,tzV36xO1RDdS;nl6.<{"&wШ5=\2=ITJ_Pgɑ*"G簣O c&\Î:aafۉU̚X"aIf_aG,K.XLaX)"rQQD+O֫>Rˏ=8:9I?Jg,[y^T9("""" """"("""" (" (0 IՂBDDDNLlU`|!ܹQmEDD`|FQ`FQ`FQ`FQ`Q`FQ`FQ`FQ`FQ`Q`FQ`FQ`FQ`S ?/_[CщG˗`:B'BQ_ҿڹEwn zh ˗Ⱦz59NOE ]LDD500q:yɋ di7sw{=8乜rG_Ǯ:y1gj "ˏrQ/44$-q!`c񾷼{;G<+旯ܾ\掛@\7]~?h6]tED䠠 N<~tN~r.N>' xpcMxww{/jscE.I Hw~3~xQ`9P/7~sx:`X7PU̞X<^v)x9hhHZzt54;w/4=?k΂d 0 uED䠡 m~h!Kݹ+IizO_nyn"S#Q`9]{;=qu;ls_':}P, #ϻ)""rҐ^sailjq?W;om;sYMqj̞!v oU'X >X'ƒo#Vv5dO$x߽o|\kǢQQz+O:nZ3_.>W\鯸)6TaFy4$-- 0TaFQ`?ay,_ZCǿ: " B'BDd_А(0(0(0(0EEqˍW$< V"t*r@8yɋYDΰj)Oy}#s=N HFG>Q^?Y&{5u*M <5:" ""w\ ;[B`hF yiHZ^t\y'Cm>^=~5w_ l~͏c!8ハN y(/+旯\.Ms?R.}[^ݙkx[^ŗV:\>}˫C:>o:5ץIAe~+C04Y䅦 ~E5]}ɽɽX}f?˹%7>NŅS|n`o~ٹ}wF?׹\s~oիW300ʕ+7J_=ibƳ,g<=Gu;ʮ2kz^=Q`-|{}埧~#y?7Jҙ'ewO&紗@Z\7x~ ]Xkq.R>m6_Jo9eY7||n,k_^x!Loߜ(M?t-]ؒ#0E^F9w9o\>/_ }~|*ndOoCenGkx+~Wo^kNí7}9+3 pB/u߼y33gf .`ݝ뻺׿Ί+SOeڵFw̞=K/|aLy*\.~hY79UX8\w^A]WҳdW+ yR'_DQ&^NJӓwp~¿r˳-;߷ O]R|{dS ?Onђ#W\s곩VJ\K99묳.m_uq衇reu_?e͜{~;oΏ/tk_e=z۶x_"iY p*|5<<Z"/[6p湯cǷҹ.|D"u^Vs}&W\{{ɵ{ _4Nr_<>㿩8tu=u_z0{"/gŊ{kK/3̳=~>)?"O'sya6\r ~k8x㍼e/#D(0<_onO W85Vf]8九-o`?yُ?}g&O6|:\nq^ >sW\qk֬Zv[ :3p&I<{e,X_ټ_Wr 774wg(Ӑ>z7YuI.^rw!o|O{Ox8N.3_ˎ:s>e9b24MUE0N8/p3w\.V3.chhWUxt }b0lha|]ͥ_~,]Ң^zRXXHNNa؂0 / )))!== }QJ[,ez1 00իdffҩS'a؂0 0Jnn.=z --͔EðaaԼh4 㫃0aa`4 0 0lhaa؂0 0 2M/%QPPcǬ 03S KZ,\faa|%00ulaF0 0 aa- 0 0 a4/B0 d5ba_20_[_|?t_-UaAT v}ݐlٹW'VQh|a/Nc VQ8؃IþۧxnbhҽaS$.{:~NjMӯY]W;.qNpMKUvz\R>ճEMG5WsTgR*lzχQZ>|gMCv%}4UkgD0b,]e%Nxovϧfh~RwR격gq:;ﲫu[Cch-Pst~9W1sdNگBƱTO5NKA%~&]iR).8jf#ċW̱|یZYg}v6_ډ4ߛ;7?4]6kō#б[J߰<4c >eU:oϦfp.0b,.f*ןL;УW/ɽؿoWqAG^W3:lk89:m4mʁ[u=۾ 0>g|h,Q@4VD g*E(l^DQx˘rTN絭<}v׹ΌNbϦ&sAaӑC[l 5K$[3vh,]Ƿ#C'ָۮQ>}wЁ.4W6_.{ssk}6C(+/8f@FB{ 7 &QB%k 7Nx0A=4D\13xsvSj#t}5*i^Jv)xL'8νiӌڟE ƒ)O$1zI?ΔQױ*/#pIL{F^˖ ϰw3nJ/Qؿww-MѼu?eǪ[0H~~~*+>LII Ç{/MZZ-bdddAUΏs'iGqěJR3S(+u$JT]/'nHQsJؐYW $V1ԋ;UaFJo<~ǔm~qîs:[Rͫ.hڢqO9I6vOJx_yb|>'3AEu^Ge<'T6mYYYoߞ+VЪU+vI6mx7hܸ1o6D"KII Gw̝;9e|Avɾ}Xf &Lwߥqƌ9k:$3p@2dXt)}M\n #7:PoC8saԱP-HekUTqȲѮC:ԭ?b"HvjSˉqp'|;ijwě9|p+4{Ӣu[˾|\>.sR=R>o^wnCw|Mry۠Q+:3묔NvN.TF[n˲M~ӳ^fˆgG˹ƶ3esNWVV 6M:ɓ'nݺqFZjʕ+dժU?dÆ t~Μ9W^ׯ'ѣG3qDfϞ2֯_4hn&7gJg7cƌ FZ"F= hT 7 ƪBUR\_Jq_TTD,c.q"Fbtl2 ΖsѤ{Je%o$oBTרC9J3'l_UJ-[Q4}74EQT5ʭDhi{wx٧X6`ZD wviݦ,}y"AZ,FZ,Fy"'{vINn>m.=͒6z:73[a?ƵO3u!>=ʬ êgta009~2:t6[/+6>;Ƥ\DZ[6<ßR~0>cݷ=JXPvtWdbcq!eOڗV Y^x/R#y+/od: c i _򦵕;:ֿoڜ]MvcL_KmyPHT乏x7lc|vqoᯛh>xM7%/>R7l~r+k:83m.',Sm"Wwѵpv='wW섮>O' ݻwX,q4iž={ѣG}ذam۶e۶m\|Ky饗\VVoذ!{x+//9[n3gt[lxSC/riAj셬|ETjjcIU8WZ ܊+ڵ+xJ ]#+ t>RUUYmT#fG4ķ^6~ҝ(Te`-!UyiPH|-=@p7K_9La4|ۗ1m̟9 jۨ1f-exyZ22v)-ϻq3b,~zm P.r,D>|MmЈA&dؾ{xfw]b󨗑(jnMmX9i |:\?mcd( FS3]g~ɨ!}|_xV1aڵk@~?~|nݺ1f:w>su]t-ymܹs۷/ve˖92u 4h>-[d8Zk`_w-A0y&QjMZsj%"R,i2:R|Y&ܫT?p:r|xqٵ{i|׽{},OZҸg!9*|9(fCeN+d*fg&WKJW2JviJ琞ߟhv BHfI~4$seE87G@6L!q>K:H'opÈ4*}o qsNvMMaepjʊԢ#%U Z>̐%|~Oe6 $EHw}KGI.Q @gq:4QmjU/iXj|YjoŮqJf\sFEWtq$1PJBJTP +Qe̟PS\rPiX8Qt7I0ag$' 0/SFY! TwYcIJVD!+);JV5Y0$^IQznO'tREPU䒎3(0깤}tR$G5S'JOc!3h}:QYIFIvd]V䒮M.eM6.uqCxI^mk=*';%?.?>q\-Jej aa FmKIL&%eUۼ#Kd:Xa9\Sv!5F#Wc]k|4%k2]IdI3h:%ECYTʺX:EPD1G^ƚN">f¹@6(Hڂ1P]Gr<|@E:K3i}-˭RF%maaHu cKS fIWvwfůl"Z_ҜQ,+ҩPIOf6vlBJY!aOES؎Q"(DSm2ELF>LII>|އ:iii~ѢEndddh}!z 0]%>±H:7>2TM%DB áCr5)0a_RRR AHKKcŊtڕ+ 8I"0 0(իdffҩS'adBIENDB`zoph-v0.9.19/docs/img/ZophImport009.png000066400000000000000000001724141415176210700175530ustar00rootroot00000000000000PNG  IHDRsRGBbKGD pHYs  tIME  *j IDATxw|TU?w& HPQTTB"(]]UPt]W#TNG:BBI?BƴRնPIetsZ=M:O#Bym!/&Wt~]2D; JYSoqӠbc DZ8ې/ Pܖ5,aXNmtOi9l"m QSoPIY(o4äXq^!^uxҾmGqg+e~p9N5ڱ+}Qߖ*ꖁlVYhH싴5Yy%72 9N[B+ TV|_fD%ȥhs )qګ(t'U崃ٕbԩWbrΦv.*pImߑS+7;W@/cĤoM۶v?Z.ǭ9+9j=/ԚGpz,N܌+ɩ\NW͘gnt D^L ^+X:YEI%S75%ADLrbݒpBdJ;ErR`%N5b&Si ي1Ŕ9ivM2yY2f'M5SZ=IL!x%He [g)]:,cAb~&Yh:50(i7uZ*IOkLOmżLDVJL%u(5%8 fҾ%%R2H75\;i6`f 4kt.1`>_lZYG!w? QABܚDy;c=}>RԖ)cj6^Jw?4YB[c6دњ@Xf(ט: ;(J{ez;7p nIR['&qm@n*֚FjNJSۘIt]sb\@ЮҾԆ&'')yp7悚T@IE:ΧZȃc@Q6\}8ÍݘzW O5$4 -dmͽk"œd:ARnkMvxa&XsMd!,l.iUÞy~tɂ ޹$NfMd@G/vt(nGxqܶfF)\4.y4hO輬{7Jlòz=N$}ً` &2ɋq/Iw YV{9'y0 ١pp9ởs)#-Z}MhK>z2ؑۓ>m(nȫv̢VMUkyM5Yb5#G#=G#%rrbt$AGLf&/IـygL.z6ic].fwͫ,ҺqiMb9A.ص)xӤnJY*7$iqhNrI9FV$2"DN8S{M?49 ! %Y$[9G@ƀT"8-,e[K\&ʲizҠHPw"I?ҵViZjM%kS8YNm5cfHlH"Ca -̛MJM^<|uUGZ}qQD3ɓvy5&; 3 (7I,*,tɻ{&yO7ŴdvHooZ.<F5Uj"5ֿ4J5ɡm}"I/L%; d&͂ZӦp$s4gP>ЛnkYDhdu)5"@mPHglV2XL@K~ΰ6G1 +iBr\Cjw78wf9M|_L?`ndQgNz⪕ gF3h /s%MhOeͅ9ZA׼nh^k*뽬;Us5W=&pʹQm˲b"MN΄tHkr99u(e iMwL4Q (tGDbNkb%1ߕ.x#O'MPE/kv5&s0 MYldCEXAdũkTvhO*4Xd^c ZYhm(ult$H޵ Ji-bTKc iڜئ0SMS"inTJMCkNj `e)kSI^m,ksXPAae``w9s( CfvNdcp4'[ 3U|>U8[l=:u6NRzf;l@־(CBb6imNﯨlhש muS}w5mV5rwCuuPYk2rܻ֕"+hPV"LR\Dʅ֯9;kٚv4ҝv7`Ic*ٝYi=FO1, q/V@ݱ ǏAΡ,˶nFO-GZ}agrv/$թk)7kWZǏ +1k綍>>sK)l[AKiǵip8HZChGY$@sJOr]ユ 42]2A(lCy5r@3I-]0>fF qT^oykNZ"ΪSڼէTGiQ*{ QMIS$IA8f%Ϡ,TFi cërh6m Ҁܓ&s5d@:k6Mƚ4peʇ 02 l(ٸ2BКZ Y4yD2㚼R &iGY"&zu }h& +sU/MUv[z$1}AmVeYVg|r6`YEbM>u涯iqgHI"S»:V>hȟh:2%ye&H}2xW>w(`j L"v$ejv+5;.>"-5AY4t3}ya$Y yNQ 01;;Iyuj'QLǎA?0褱t.<~KƱcG*3,$ՎcMNf4QA! a+`N$ucf$5(dVyvЬ'5d3cN`HжӼźGoWvnM!N& dV&&}NtMRH;"@ ,&'>N@IEKiA.U lt%$FK/wLDIIWiCۛGzwMMvn`;kR7 'a>sFn ݳQF Qt[ w>ώOJEc'R{2azvNBNyؒsM,ht Za4pjRLd&`46yV~H ~l\(^6tKa&sFy!5c ,A.7 V˨EdW2$.59D2}鎱t*깇,[z?.[%@L}p`Nس3tt4k^^uHN6˕ް :v= U *뙕%sO4ӎ]7wt$&ubAz|/hU;z)PMIul>5^UN MvYK}%4NIɮ6; 9[+əV8JâK?IA+:(8 >P(Ip%36;nr v茄@/E v2s+k ︕ѼIqwmӷ|y4<> 3 lBΖ զBKSh`%!yŶCW4hsDj]@0l2QZ~\/3! #ـk'ZbLZʱ/%Hr (C18iNi{KNSkHGM_Io\X:*Z33|M9Qcg5fz*=L*~Qpr7›ɩ|Vψ..>n}։SLǡ엑6!M嗓|ucZm,C8%6O9!T?䣴_n3y?6R+ ]Ng9 I7K,G{R&Mdם5 m 7k2isfɦyMIG[k њw9I@S3?InŘ䥃P9>ǩ^F#d,H/NN5&&^֙ KZZ@+ Ɖ!^28MڱIuӷLO@!ƿ=LDnijLƴetsB14 崙Mqr! c2Hhn {Wim*-~ɚi~ӬKѐG@t\l ]¶mڭNW;^V*D3\?N;[0 ^u>v[xBWhӞDKFUv:PeD+T%PټW&F ye\M&KL!Z \>U,&ScHM&4N tK-I`c*y35T*Űn&O ѦTVCjV6i^ iI5r"p哌7V"/m Ikd3ۡIs#*~N}L  &`iĜ s5H2RЙfGO9iQ9ݒZ1A 48 k=+$&I;B׵WnNkb'+1I eGcR13$A$v(N X/ tuY73Ѩ_ƭ&6&*i~&pTp*9ܼSU4Čy|~*UbR{1*Ul^Yeb'⦿jtI&/5u[iAʦ0q:`I~iB#1uz w@L Nȴ03@g`bF+ Z]H¾ -tjOK9&2p{ &H8|zqk>"65WZ\&ʔ 7E l̬XL%u)=ԡV6xSLsrZK&'w0% \sjU>b7yM&s4;dV8&Sl٫C%}K9 lPs?|i`Nt@?IрP]LvlA[xy"qVw5ThJ2-𸽜q;)$DfhLӼ([;SI^5@Cljv'g]vӅ|$i`EpZ5,^WgmI1m+|ܺE:VELd&+dmg[ _&4/fMGK6rK%}Qc]R}'+VǎwrzusBY^0Wbu|#6 wA.GUMyl{eT&Ԇ2y2۝xloUի"EPr&& "h&.ƜNI7 NZk *f<+^MhDVZ*Esx'RSFId7]/Mtz]3d@.Ypث߫2yYwpat٨Nig@r%;~zMN9m+DX0 (鿦LIhI?&;inNMN4@y$;GZHAM8-%@M*6qtHv9~%ZxI7G%ԍA[+b& IDAT'UڹUEΰ ƒXNI@&6 ;IWuk(ƍ%}O6ךk7Ie`<.4m;&Z)&J^-֝r]X{v.ύL^/꠺=.c${}0D;8^UenУyU^*c<y^#Rϋ&ub8Hɗ(y* tydLHw759,a{kv\vBN/!3!ɢyN>d]tıg.^,rK@v |lƌĴiw1YX1Ԛk@S)i@ͺB*2VOڀ1Y`#AYi|db} &&A˜d#Ժi7~(TF2r f>#L| FE8ҾWY^&ЀRfjCКJ$1^կAl >ͧmUhnƂU }0hiI/gj!`*$mVWpLyU^$,,@%}NxqΓ؝uМr9Ճz7@xOmx`MkB'KdLHEyN:uӼ6ļS{ nN_-%As*-92{IN%Ц>1P#WMjAXnԙ3 ?j.`vu&k>mP:~LM_7:7PGj2:sˤFn`1tMf ATNkfWT1:jIO9 nBC5w!mKw# j%NҚ i[+R3Pm^EMT46vjBkwϕ@29 82ELmo*s3~ear5vb"#61^;5ZN[F_j6c*z<C4Ċk<:8iWVkzu"VkcՖw֩C8m@Mf ܴa{:6RVjԴ@bn;]Yc'dU`1:tPZ)M @c$\r@X.`'Kp-.āq-٩0f(990;/?;{HrcANj͡5DX*gdg!REm^Q)CLhc2H&_4p!I5`҆lΕpXhǪSߗni,vfnBq/(MP= ݖp\j,[f15yf~qZC ?C"XS!бCP{rDN3yg*h8/HGIIGm10{^#~uEXY[ӒIҫ>nKqdGn r`y4f馦N2ZxsUUnqO^Z|ë\2FR%;`~bu.59$hv܆ֆĎԮAoLфnõnhMpn."ct5;7lTch_+iW@vYNM2 7P2$^P1mx(L+ssB^T1 &c{!RYI58.#( ɕ=U\Q:5 a0wCOLJ컦;NugbEK6Z"-MhwMr2@(JB_%'YiR17)&DcVC MR))_ XKhF3LOnMyi(Լ]cҭic醄t,HC^I\d!ͣl) ]$* Sj(ݚJͤ/uh4xRg0E6NWGiA<>KӪnjØ@BՊ:hȥ:bN8^ƌ5=%fWMXo0uj$uڵw;; ?i/r-jX@8&!G'w>IbؘIͻA("g'aCg2mҸ}B@PδLǬe>eF Ƥi)I;=&͚ϚA^NB]BhHE0z1me"`fԾw/L|7c^2WؓX2mp[)=2 u;z q6w;ϚD_/eW.U1g6F4?\UF?tzoæ*i~]M={%ݘjg2Ǩ)Ta%Zdg nH ;:e4'^kR-HwDmmLSM-ȟ -9HKǾnnjq[pXhSb8Yxdb^L.gm6/ʮW5mRY)MOrMJ/փ~ֿЋ~om49itSnM=y%[~nvLDt ;=0.L$ !PԦf!&(MɱvI41ÅXLZ}8RJW7(ƠiO&A]78=u@L2oB_$.N!^lќphhإ8LrJ(Ba4mazDIIsY{># ⴡ! > Ph~N!M7IRO8m֩mQE,*v%'V(efykRYP2c*RMSkCszjdSljTFF7Hk@ADOɎāZF9E; w8 /F֒Š&xt3RuݦiD5 twv!B]М}QF R &f^W(\1}-I~(Fn;p7:X!Q% yӜLZhѤ[3H{h&yM24wk6:%Q0X !B!2Ҏ7߬cEBK:w>uNgEBABg%x{>پ$}& 7߬C\B[fZMyJ!T v/V$=>V!B!P$Zг-,[ 팾1nh\NB<5 :aHtQسk{>eYht-ۆ~NS`6+B!T[x"H=۷y΢0 B͘: 91zDxm|n8L!BABbS'a>x3 ޡغ0he8G_|߂@@6>9$B!@PRe/ Cw;7>_.,5мU;4h=;sScB! OIC]#/N]~90IuV_Y xa_oFB!AR~~}oܴ~fAKpX plؿK>'B!AB!B"H!B!&CPRm&!B!fDB!BB!B"H!B!@AB@Χooֱ2!ĥ>+BqMCI  ;0g+!qFaϮ]}OI] +[LHÙ._W]}OI+a|"O?^5T܏i j1xpQ6,!Q9p}$dl MmNFQ> B!<$QGa~ƲoHaz飈˵#C@_\{t;QYn(A?-׎ĦA~q+)X{߳_x yǏUۥ~8x ;(yl X&BAuޚ\R<_vnA#g{g{{PR'0.ˈux`߯__=| ׬(RT,~U.\޽{###ݻwǼy9H&''gφeY~?tsaرHNNv l@ʔ߱ |QxB"Hb5+?¨VKgf`_۸IM[9\:۶Y<DؠqS@||/hQYBbyd.N{vn/=oK{MU?1uT 8 h׮߷lق+-[DӦM1f۷/駟&MO>ĉѼyst =X9}p/O?Q3gF-R c'MEahн'8 ISѤpV>!BE23NBNA?3{/*[7~.⒚ޠuG~޷{G_B5}.xuxOpG!8z$3իWca~c„ ذa~GtwqGk/_[l#p7cꫯl2,]T|B")Gs=_qqq;w.F)VKiԣ _!B!Qg֍A#.CyO ".fL;~IIu1o7N?;3t>/<+ Œ{&yVQPioo^O}UFݯZkRRn~饮>}z'O?,[oq|sD"-- FB\\jNyK/ .)))(B! -t;7>_.eW?5мUbճUw?㯻-y}/ ^ƿqx9`_LR6FH>x 5jT?SL:֭ѣEVȈ%:u꠰P{M6۶m~>!~>|p̝;Ǐ/R $Bb s90IuV_)2M|a_#~'u?u_}CG]4$թ3zc/.wNOD߰ .z4q}zEk\{m۶{tM4ً֭RLzz: ya8p RSSY1BHAu7mYP .. {#޻ߥխMf?OѤIdffbǎ1cf*ǑDlݺwN3f ̜9mcʔ)!-- C I !DJGxpwUV3f 23u9s&h]t=\#==]vEff&z>!& @B!$v|wXgfދm>p͡VuRYRщڝw $ E^H`eY۫义 Zw!bx"H!B!T !B!P$B!Rc`PBDfffB!X'DI Ŋ B!1 O Im`!BMx"H!B!T !B!P$B!Rc !Q`KXBH%AB!U2.!R5D(rw lB!UO !B! !B!*B!BB`|,KGnn.+B"H!q'<~oCbŰ,9omN.g\v:c+K˕KRC{oĪ!p67j`_@R8e! xAA~i^e~mX>+#,:M<$z |((#9Xr1f5>: s{w` ܜl,[UڋoQM?ǔ ϖ2RlO>č7ܽvnߌ?'cG,[~?'^;' eU$e/!ؿg'yߍ8"ePJOi !Z%K(,8`0p_0a9`M= D0>[ۮ_Pޚ\b1:fYN*:q{⯳z b33O>` C/ B`K!!=wM?=[KM[{*~oA֬0/Юcg,zaڟw_mjrrr0w\L8Q<3;v,2k'0xy7NHx O}~A~6|+,FS]Ab} e_LB}>~}~v9V㊔=l*YRMC r7 {͚aTf+v%33E>U燲{~FVϚjWwZj_+qӖv#sqRسs^I}Xk8HMM_~>/8gΜX]30]{zę)bҸؾB<5 :aHtQ!3~%_}OIUtŲ,e+~ crn[嚋0K2uMŭn(uM_\н!&\֧rIv~LUvV##)JE0?/y!bs(Vk <$HIMG8w7mQ53NBNA?=_^mÇΚ}Ӿp߽s[H۱mS4F=ر}S(Mu e>1벵/_].3~f!28zh~w+ ]rË/Ç)9W'%bhP7<ю!?]S7~$0fQ{f~|I~xsOl&o5+56"` 7lyzg~oœu5p`ndt><sNcG;ѳqCߵ{OoBط{u}ru~Q3F=xd= /߹}l};%g{ wlooknp40;!%HKKèQ0g\c޼y>|8RRRjw։`IwǃOaƗT\Ƌ)o` y]_o5`.4Ŀջ`.=SKM˽OTނ_c>>2W)H'''g>g~<<y(/s"X‚`1EAcD]4h6 ZL;{>3^emkH1|9&^/30j>_S,&4oj=;K㡧/?]I.n'򾥮|_ԂbˆQ=phiaѰqS`-6iVV%e}h,yW·_[š6:{Ǐ/"5_m 5Zɩ`IP۶ ;j{ޠqƿ|9>]AeXV$/|/+Hs]||>?|~q#.. O<q  .S'F{'_G]FIwbo ]s90IuV_Y xa_oF#'N<N>Ѭn|v@bR p,7 ڞ|* dQn2O$j/`h>6W]=ΎCHcذa7oϟV $(р~x裏cǎh޼9&MkE5oܴ~fAKp,\85_ރ/1@!8;w^+JQ\IVXper9(W+'Eѫ'Nxs;9`4ƥ `v(z "(mK('Am%)Fy"H˲9pkGO Ꚋ30|̯YITSeJ`T 7^9~<7nƍ٩I7𗨠@?}"|)BK 6 R$U]`ՓѬe[CFָ[q7Ԅ\|V~YZ]>kԕ]SEse}0K2f3RvoQtxѹ[~_: qͤ;=_ǰ-qx |MsaDX}4lT{Kʖ9hr WoD~LڴiS{r >sE\\+`СQX/XB ,DЮ%4 %UNTicXW4l"[7÷cۦް1Fm9y>_ I42I7 ^Wг|t}@99[7|20)32> nRߎУ`L2ݕx94l :AZzC6oy[nEE۶mCƍ9Ixbb"Wi~`(1E^eɢ5F %$ 0B!x!+k  !1FH~g2Z? \3g&MN:aڴi F70O{~q+D$$&!1NĢqq/ V 5'BH+b\;ky睇={"77F”)SH%--ҟˉ_/tgXCߺfPB233C[*D]_3E oضF1x5on 7'$ ? /vUABffbEB%(8!#--AaR 0@1DQ $je~.kJAE1L@jCzw0~5J 5=B6M! x"H!$J:{ڽ K.ƀ~BH,S$B X#lڼg7n@rd}YY@Bb8AB!ACW~9~> !*Wc#Hj$kW-+g{ =K\CqGAA>ENadgYڇìB! L'B!BEj B!BEPJvb]eKnDރF o]J BRUڋ6S&\-Fsz);f"HbGY&B"HH Sx⯳z b33R{Y $&8!P$Dź?9W>լB*`5V]ڕtJVtxG"H ??CHvގ`0`0X_?AV:'*INICYxg>o !Z !5 &'5cv#'58ܣpQ||loa.dB<[ɮOr PoE0gf !T g\tn3_3[ga@z!_LL!~}OI” 8! !1ۧQFs'$Hak<N>Ѭn rV2!27NIGH\8.[곲>k{ŕR{wbVCЪmtu륨gpf5Yyf>㐒Nչ! !T !÷_Op~aHJǰexss~HIMoͪZfݥ~/[wa}\OW&0\VBlۆmmVAضv##HHHDJZ}k:qV.y=: V,~wNu茗_ϊ&P$8x`/֮^czɩϓ)#X{RMRЪmO%"Hg_J*o(h{vnE6C㡧gtne«~*#]WJ ,i)7k?ZCϾ^rjȴŧk?o݀OW,BHIkѺmRݱm#V!eC4qv/,[ M󕋑8}y/^t, ШI3R9zc]"7'gNq&\}Bjow#7tMCFBBft=OLLBS+m ЬEkZzJ9[n=z ϖ^C״h=zW/GӑWvV&ѰqSt86[7=Ek6=[§|s{ QGZfB!EUD0D3΍ƩR+, m]GcͰ v؊~@aaaٵ },Y'a~ߵc+sAkڴEܿwWH,UXRoN]^ )@!ۍ'e)#]N=4n*u()̄B*Q=p$Z)oT $#'7h,~ϮୗQ/9 7EFM%xN~qW#HLSe^wg~\5@=h(~ނZ ʴxw X;HSW]i !DA F=X%SRcemp,>_"5zǎKp8; eacSB){*k^Y^r*Bٲ{|ޫOH@p^߰ P$T[[7DT7'uXm?N>-<Nd:$$&&1+l+uMBbRk;Ĉxw6opf>Xj1>Y6 SJyǴ̄BJ7A AG″{1_| FZXv 7l!#.GZ!%+>9o7>^ M[bϛ>jH,(dž^qФVcRz9Xj;v$u`oCк]h">Y6ؾuC ^?OrM˶ر}S6hg hԤyl}o {w~XVmNVQsX*uyLL!<ضKg!uZE,/g,+gRw4ʀ.CAA>zr;h{)S|rshߏVmNFA HH,O]ϦHj<$$hа N;99}jss~;m°Qg4m5{,l۲+[꞉I8 HNIC3 D4ocG Jl9X˨[/M[amX]ƏkQ79M'b#eYh߱ *4l:uUuTlkϣuNHKo .䚲E!~ږe!16nz IKr~OHЫRjS崮gcݚ:7Cl|BERt;+ a|[ 9% 'ua_е}²E ҳ3`|hܤ9 \B7? ;į'܎Fl"zԭӻɝRH{-DV^ԫçi޲2 ÊCYHMo^FY˶nRahS+>~7dB$)䚲E!INwIq@W~Z-G˹Bԩ[=25\'$`GP$T>;8:wceQ]%,Q+j63ԴL5M5Le [:+(&)( ~rႀ,9swk׾+wn9W7B{{Zenv$uLQ˗ 8M ؽo Խo@ZGRM{K!D>}ə89] h @aA>ذz #VGPah<MII1fyزqyLl0H?L^n/'[!Ohݦ=l'qgP6BR#({tv1xxЫPwwbL.F]צ{C?xk~ggWZi3HXgSfٷѽ :uV##H !APluɾ`!h6}c&NcGZ֯Z&!n SI_C T* B!@1q[Te=[%傋K꿅B!DHo!$ !BOdf:CO!A!ĭ~ؘd6 IDAT{w%ȉBQgz;B&:vvq"A0ef9qB!Mh 'a#'} 'E!BT!B4#'3 _֜L]6ebc2{n^\8wʺ3ZB! (eNݮcf]Ku?<$3WB<|s= )+? (LF#ٙi >,vʮMFTjMپ 'd4{W`2o$!8#(D3q2;o?prrǏUzx BQ7j5Z3|d"D3 5AuPH* OEمCFqTM# @HnL&}) Mێj1d8$Z˘L&th:$!PzA;q{vlFQT ` Ҧ]'r;e'!:s@y~}ŅqvvQ{)垣o A+ & (nS'xlZ8 /Z%lXD>_`0$#0IL?OW7| ˹r,fxo11L&ΞfX$۵</l2ЬW xU)!ΞfŸOFbSNUCh+-)Oީ\Q}ɖIM+tL~X !AP4&Ǽ‹ןy YWy|-LF#={}wˮV}sѷGv8Oȡ_kd?tO_ ZO;mh4XR1'_V3wk޻Pz*BfUm^ͰQúNprve}spͫV˘䓝q 09r{H9ⷔ__oO6lݖ)kˇ㦐x_eV$et|xP\t79&g8:9sߣpR\\!hZB[b.mێRU>ALY{e}l=RX/@ 1weۗȻ`]vgۥ$1%<=~#1Yl?g6˛!G`L/WvǦp(7̞Av1zȔ`u0'=6^̓ !eG<%KKH?z-k괏^m=0]8K¦f-ۏ87Rl: [7Kޅdv.|:Zݓ\rϝ7 2KL]zqװv1YȜ{B0x62r Vh< _֌;+z|>f27qq]3 N]{[ˌ05$o[`@R1!Z^`Bԑ||"~SoW{ yמ ,"ZW()."~2:(LZ&'3f/t$qkYt@=K41 !hXhL>ӄż,ϫrr>~ |"߸EMAk{LCG$(q첹SPb*uu`_ǟ'iŦ}W^ܮw?<'zlۗY/Ȝyl&BH'gW!L y:||ÓXsj~5=yaTLÁOcr%gS89YmCy1 !hXo\!$"Sw>!I89w  㭏~C^h4ZZ/1_1 G'AfDutɹ3'ї脯_=Mێr{w%пv곍~B!APш=#v31 DQZR g@z  e:l#h~ޫsx__(JƷǭ≿CҖyaTۄeaB.=ӷY7jTj5&gp1ݨؘ hTiA`:Eщ5-r7\f3f:py_L0֭[鉧'^^^Uz`0z}/W~~>}NDze((([Sf*)ZmJo?f)'ڱ1sm>>K2K-W͆6>df˾?K- +#|ˏ='SFL eӦMծ衇xG9|0yyydeeթ#33 BXxzz2qD[{ƌFB !:bV~Gب EQ{d̵{$N%2j:]%uo"[7d3ns=erFFN!0(sgN~ h@ڥDL&c7RҎΜ%TFB|???a޼y̟_6Ziqq1888믿^_oh^|Ebbb0* OEمCFqTfg_vB#iӶ#jv 6})2& ZIB4 _&88hícbbx ;`Ȑ!uwބ3lذ:m/D} Gjtޗ]z{4s=;(.ʨqwѩkoxIRNel6q*'b0},gOg1bdeAm;eqރHMI$/|BV˅B4 DFF]ET@5g9??6]o{GGG,X ˞|Zo/ (hfj5~mʚ_vˠ"8o'N6=Uqqu/Uŏ?q'TY^jhx$[6.'5e;W\F19a|B !BHBԗeG ݶy5FGѾS벒:=hyGXKeh 8ƆKpB!!},G3v)O\̥m.6,1V~YǁALY{e}lw{w/z _.B!8NJm#B?tV/a@HZ żKI$4|#yjuyІ4 EQH#i6J"#!w-{ߡ9:H[Ia2˾ѩk:GP-!DNii!n^Y% !}š]JDIq6<`k~GM&##i%CQk͈YrqDL&nRf3 iSo LfW&.^}S=a*kH޶J"bB\L!D DqӳR{R9lk[ydN3}”G)J9ǃ'y3٘LFL&S ?L&zvIbOk>l6['66S!=*ZFAա99spVVCѠRkPըTz fB(!6|Yj4 B!WKїPR\Dѕ\r Tx˅Tx+ zWP\t}i z})FZC ~4ŷy hN.hZ=JUUVRP)*bOAގ% !)^fGFѠQkPiZffJhl*{lFBiLj\ᵀBR?+vaYB "#AP!h!G+o( 7?Ѣjju ̦U&䨹uPu%A,-C5Jy0T*A,P*-à1B&*7T*@K F28VNQl2ٌJQVk0MF(o*MC na:R~B샱k_*`0жF;BFo$Ь#j01 zJKK)..)*b)))^_͵[Fn2Uw[ E$hɁ~ZA5*yF[V3Xk @}I P*n C̭LE}m&b;*N}QMy!ٳZAٌhl. zxU^JQQWQTTDqq%%b00-al3^ˣ\'[7 L'"m>Ӗ>FPFUadQvA BƯrK)%yң_s!Dm!A.AZ`?KrM(4Xy+5 x[sl֯[,:Rn&Yb99FFEXkj5*EegLhZZV~e_: ! +tgR6iNgEA`S#h)Әd- 7&%66| ^g{, ye}k5h5lFa+ͰAly)/>1[46|='Fm+'3 cc'=Oxv Óq@ :v 8ѝiܞ[7 uiy:'mȻpWwzWjvji阉JRa>1!ݬerI޾yqqߠa$lZiOF.d2 fH|шZS놇t2%V wjNg k͍|g=t6޲vppl{*7jàu:&PsmKTBHMN(]H_C+89Pt WJ(ܻgx~|fΠ1X˞Е8r #dg#83{60(|l>Oܳ'tXd3lXΨ)įܷ~%.?͖+9mN'~>.G#ĭ3g {⑧]zh &MVhleF66qAܛN=αe{JCv *FUYG-JBfy B4*N u?}U4VRR\ĸh]hc$n^yA09)= sߣϐb acy7` @lݮA6DhX6q#Y6_Hn6I"4l 6 "Zd2YAjut=ɋ jI`Å@h {[jC`cθ7b\Kn] ҵ (*T#es ^P^mm.TCRTj+5 (D#y=Vfi3iFۤK,?K2"PeѷtuٟZxEaPXD:'sF._ȁ_9~ = 8s*aj\uv7$\gÓ~MV{Z-{:K|mg9/kzcYՠOr1X.EAqRvQ˗ Xj1w&_x)rdc2 nRmC{0LKܓGn1|ު{|rҥh3pfJE(<<}P'gS6 9Vz^_ʰQxxұ&e6% jv kSd2sp@ѽ B!hFP؎( N2t8k_Y<goֺ%YxIRNel6q*'b0lZtujyTFG, PzDjJ"yhjZ.B!$ !Zڌ脻7yʟ'3 Fָ@=Uqqu7Vw?.fz;R_R|2'gǡlٸԔ\rN臥B!$ !DMǑ{9i n߹6f(wq-7q9:9S\|'k᯴hZL clX̑ +BE !Mێdgܙ5vj,_p1,:պM{3>OvEϾAmB!$ !D5EC^ly)>~89X? );)-)F_ZBу$nYRvs23 v$)ۏ87Rl:ZB!Z i*.o6twݲ##i%CQk͈Yrqc&NcGZ֯Z&!n SI_C T* !B@1qiߩu)2{Lxߵg^޾D\p!Bx&JQ't5VrBB!$ !o>} >ANB!D%MCuЬ$!B4qR#(B!B!BHB!B!APxྜྷ5 !BHB4vGrAcP!B Bnq$nY+'B!B\L!D37ǏC5=K2GRAj6 9Wwk3RGLl\>*.;%~O{1e#Lcg& /Mhچt%B!APP ?}MPێ888-oY'5wo7V2ΙIu?<$;Ö9Ν9Ie8Nt/\,!hA>W:i Dc"MChF4 "yjJE(<<}P'gS7}9턆GҦmGj5A2lRB !D 5-o A!n a:mgG{:mȹ*1 {B'MC(7w2,7%FS)e~b\\ Uŏ?Tm?yJ9EQM!MPhJB-=wYmj}e%%uڿhn'gW8j* !e(Dͤih72#3")~bw_dJX0:kדfVnV{2#OMMtғȻpfy*M6sig[&0ٙi6rϱ컏V!o$ !AP4s{죓I;G'W)79w$&˗ Hذןyܻg/-dP!AP&ꛏd21|dV:]g Uܪ?`ߑpL+R8K2ֲ_6F7-GeF$Ώ!,9gXp>{#v3bmx7x+_o> bB4qkpl\? ~n1ٕǗӹ[_:u-®%˅m]*盏5nky&7z^{*A6eM; JGlnel\J S.Y f /L[ '+i:U xEWk΄ߦ9hmnżU7ɧ=Jǃ'y3٘LFL&S ?L&zvIbO_Es7l^_omY#3GKk]F0֌ݶIߩoPi;K9 Bԍ&t?Ⱥ, ] bYC]~1|!ؘI3X$f%49.  B4LTBE3 kͩZ=\*g`gq[&Owu`Myfe ͣ m.(]ڷ@.!aAIBh& ?ssT\Nf:c'͠_H|o]g* AV{ o(EW[=af7BNşbI,cfIB @!$ aw ZöM+O`+ з|pY3F0+1o>gym⩲DK;Ƴ/sj~5.4{:|!~cnƕw+WiZqO!D M鹄 (Mг{h߹'o}\u%pptËf>΂%[by]ZFsB@}ju2{ hݶ:vm;жCWhd˺h7\F@!)MБ:rB7ؕUX.iD>Jw2ذz #W!q(A! >_@슯9 nO\ڶ:^m=7k !D3 B#}8v.]ץ{_NІ/-!Ao?KK$nf B!DS#5Bp&g1v;)69M+YpJ_kF=a*kH޶J"bB\\!Bj(y\ؘe5\ .ͤBx#Os&ɈdUXgɄT=I~i$&og-y^GQ'R#(BFl6ϟ~j]B!LpjF˜V{j % !Q& QBpB B!eUBa F֝[IhΪ vB Yٳ#[u(㗁(@{l6[_S140fF ьS`k: Y}h9 BHC`7"ؠ)8Pܴu &B\GJQ<_;͊RVIhhAQ_A ٓ¹SILJJprv!83BG(16f .h$ Ʋ&YCH) ~},X@_e_qi]?tO`uH$R SmO,xo>AAxX"ӎ_eέ 2FNљ"2au{ʉA`ǐRr<єFkO|+Aʚ+ʵE阌FXk|/a9W.2Q\?k%W|`mtu͑{TZ!M{FN#6f.co),Û(*ʞx P фtf4v&n nބmHǕ~K1Lhu::,/?MPO!'X!Dח Pe ٌRҏPe Ց (fSHʡvmo (.Z?G.}>ʥkAeo'_|OV2#\JNrcz2;=6I-c뇛wcr?bxg-o sTAԄjpt~Sm;q cϯ*tD?}i eB ƲFdl),XjESݧA+ve#nFj_ul63;` /GPxѣ`V|Fܮ#˗.~r3qmHOAP4|ѫP֯΁(ȿ@BJ [~ٷѽ :uV##H&>fZf16iRߓ3LKٓ<;3{p}NƲǏYw;x8P9eOei\.h ar$Ǡ\9O[Zp_Jۛ45"?_>$\TC3UZZR6Z٤~( AJeDe2b21Ԩ[b)q;!P1xɌclow{`TCr*;0q@ }>_,xa6xh4ZkّS=E 8r`7#Nch1ٕǗӹ[_:um S9~ Kl\tֺGٿ'[1q`+fCӔ(@m d42ir>*kK*3pL+Rl晵H1 1l9R?,d4mg -۽ IuM4wv ?.杯/V&yO* IDAT8fSֲI]8˒t)lc#FG}nK:Fߎ{2}e5~|űy*hJ Yx1fͲnZ… :u*bRr߷NoXjk,s;jOJRSƒw]꿿cޜ=ŏ>?˝!~N>>>vgddpҦMkˋ?>}Lj#ؿ0ktܙ@?Ծlܸn ???W_}%/Qos=|5vP|X0~ݓ3 ah| W࿋`,h4MkSFXG68fR` B!h,s;]m3&$Wy)k>^w+*+ϕ[cХ_K_ t8^ļW?qnn..={{a֬YqQ:w̋/hSf۶m[ &Mğg{;([$@Ű("JBPv\>ܙAap7DqwtA@e;JE@U tG:,]ܪ=֭{==ņ dlݺ7nGƞ={0w\]V:`:=e/ܽ0yj$>#K0VaHo]qTr۵kYr%vpDFFo i&MBbb"jժ#G";;݇~'")) uq_ǡCpQ$''_t4QQQN3gD~Pn*}EAǣC/[z%+KD  T &bt5}*]\ c\_|h+z)?ob"ώ 0jc숛w[K5d=[z{F>ZuDU?G8}SʖѽPlCVa [_ձcp1۷9ZhQqWF߾}ѰaCԫWIII8zh}t 4@ѪUӧO… K/͓#X 3f̀>}T Ќ|W5Dq>vY6@PA*K]›_i6p8h;Gm;FuK:uń?CtǞy=wnꞲ[ϲѽ?"jFD: xȾoJNz>IW?t?ZȞǝwމaÆ_ENNvƍq=zǎCNNq>d۶mË/T}b̙OѣGDFFV??`P7EBA*MסK^uY5xqm֪m(LF+Ԫ]GOgqY#,, v3s~aL<ѼysI?Òz]Ć^,^9B\B~ۈ[xy}Λ Aj6gc0AA$J~ZWt酤M4Eذv܊ظ$>8m#tu+}u)5$6l+ƆuY&u_+ cn +pw:]hݶ#Z(H!8T H (…Nyʡ/h+"%V.[P={]p8hb_ڇ>\Ё3PafX$p3z}qێ^Al"bnAfdt AaGqI8r·Z4k[݀qI8z b ?wbho=lu%p\0nA(XOA$APsg <ﳳgN*\?v۷7SH×LCU.. V >yFDCyǰgV|;ctHyH  `*Aػk[g{vmC$߿c㓐n9"c|Ju!2*?Yc6ƞ9>/}3ȨzHk'$#  H (7ՐT жCgZۍ{v`pI.~`Clu{ѬevO(\z%V-[=;v#!,+|n\|7f&  Y*IMqu_d-ys]]Gbr_ ͆&/miXۀ@Al"2{ q|ǨU.uHGVm5x.V-[l6tf< Ar?H% v@P U(qZ#YrOL-yuqψ'K}Ҹ9R7g^L, C  ]jBSr[_Icw6$/b\L7;RR[/=[ ^8֯\M9f 5KB)  T7Ǝ}R*ABgSiJ܈[A~yغ 6gJ6}=|6Y;fˍXk9} Oo?Nǡ{,   k^usBz~ݘ5nw l> o|ۍ^)dqt٫`sWb_$2  BuC ƣ㧢~lqh~qcG_~Z{⹥{KX %AAAFP4}'4l 8,/79a@ÔϒRr^$2  U'N7ހfiv;ɓDAسs+7pha>_dt:uw<sFte#:&9GaߞHImط{; A<%AAAZԭ[vv;xݎw}F:uD,FBF}=vSLi\ xox=s ~nS9e vS#qA9W?Hޏ}͒PP⣷GoWh~T6i\  22r ylA| /䍠PilXӋj:.q߿8V-YӧbQ/]#IJgcq&DZYJjG/kܨ#J}i]'Wh֭\KeAQA #]qkRBae~yz4oV  Վ(\`};N̜9Cݺu/z7Bѡs7̘11"4k/Mm5Je`6ߧ]thM𝥖jھkW,BnnbpyF4Jm~ڔKlڔѣZyǠi_3.-,o~i6oZ9HLnA:e^35`p,x<8].%.P~tRAAA`~0c :6 }zqA C\|2vmߌf @=a٢'5xF$$5"_wӆ3VDE{GTPؽc z Q18{4F߁ ms6 o.BAj۷/fΜ0\}Ո`C 6iÚRAm//uOk.v SSzOlXu+AW5EAU~^!*>4MCDڸ슫qh.kx< a v PAj(QQQݻ7uvA*_kVGqi;Տqiތ5 ,%V.[fL } w<ؿgt݃vaPPP|vD, b0 @A FPJW6eFp*.+v؂+3y+,ΞAxD-ggϜVKJ6sYht&/}vx2N'z==;9GJcA"oA ,<ɍaϐrػk[g{vmCr}!Se=~ l{dT=cAA@P t{)mX;߻ᒎ] ͆o. s#k74,r\@|B2֭\"9e5u|n\n'aiAf#KCA(EbTl1 =&) 2% x"KHLN!X8ot]Gdt fD.%wW ^cw_f!6. ]{`$ݮsjf5CPy &ڽ#Q)O\nK/xԺPIoH]j:Gnfffn/~}ڶT}kC تE\\.R@]D9gfnwtt +,.W8# C+NW.v'6;lv4hV=AAA XOAZZ? 0   U> Aq@PAAzƀ?ϢE  T#u `I o c@#( Bq)Gs8w Μ:SN<;yq"8NS'p)=s!?<x g@AB]JҬUHHj,LjDWzEbk6[fM%·儁%E AA R&3sjv8ؽ9NfvC[GSu9V ?oX?Es @l\C\r$7j&$ y5i׹~'N'Namp `xt 2 B1^{#"{i 5`E6%E5 A(͖_7`kqyhӅZ<:6IE J.7N޽=@alp{܀wIhU\@!n YGg[!֭\o> @jֈo! B,Zdv:]p\p*ȇ9Bv|nEo Q:&5hۼo 7w;oߛ [> k(M pvk+O.z8w4N Ӧ<}`MB;M!Hmv]7cEATT .F-3{|/jծ 8{ |8j׉:y_}: w#pfE%Lm? .W8 A w~> x=ޜHTX ¤ B s sP;N‰OYT xPAvJ5?HZ6X6mX>nETt v ٵ E7 >߅ @7"!bp~4n mF±ms6w8|h/bʃjt%Nv_n]hp CAA>AXwm+\ Ԍ?u6yp87v63Xӄ 梴ữ 5C\B "qI%>6'?Y+BRJ@Ô.C€[ÇmDnnb@pP M4ZaN*x|;+66S$ʾQ`Ph}K>}9b;]&jWi.'5M@Sq[_Icw6$/ז:>9L~ 8Nd|ŷvlO|W.pYzw3 ɍ\F-bw_=;оc~aeϿ^= MZፏ]i7OǑPV\zSSzYx.N)-ǞCfdyk -ZGӖmp?vGڕ qi\4owlLL\9ǁR*)-r@lBCdM_CڇK/ 5( m]ڤ;<𸊗z}t*;.(DbW@宴kI (5x ۱ٽo1EV\V((ca?my|Ǿ{?q ;^}?l\^ ʂ kaA5˿÷.cGۍZw'?\,8u2S0$Ǖ5l -?cAxm"/,oMn#.!q @ع;~ٰ?]ڂU8% 7Yn#(|جe@m h"6N BM mA٠{lp8>Pc+B((ȇ]&S +V@NA]. {>\AXAS&`^j] fQw#kL}^6;฼cw4O¬S1uߐhOy'Sfo<9_zS3]dmYf9W?6xn]ؽcK6>;rӓn_q׀OBV/5nw 2{@֢=|fN{T X5Ӆm;b%tG$,Ξ Ϟ9 P ,,aa8~oo0l/?ViPv]8]~'5-$D` IDAT.\Tb"nw]wA/oJ?F9Wy B @ )Xݷ\T!iⷁ%J XxtTԏM<2n2?(uܨg@R˟  xbR{tTܽ%V,&h-*׃'5_{2;UrbΜ>5˾ |D%{ro)+,Yޚk˒Ytt}bmh~Q;g{vm 3OB刌-ϭUQ1qR'Izlywn{No@]yBbQuTb (PÌh@`S-Z"Sbm!Q-bI,`5".!wByKHN]F4M+w/~;cqqA\aU۶ұ٘xwpYLzrT2H۷w@7eZ|9Mw;`)UvɕXx.ޛ<?>Y`숛ФE3gVPY]Ii)%_ޑ0,Yk6k]9S|Sv]3Ip!676hBUP[?Vz U8  B~Qa2B`/X(`5bÚ^$tǰs'X|\wE q7ġq5?8Œ_`/m:t&X=Ydt e^8V-Yӧb5b4>A dBj7mŁ_9U<t9+ﭜf&5:,KMF:ՄWޟ{vlAixphզ<[ϲKc~":eѓP?89f^}a;S'XmwY1xq48㱧_õ$$?98>p3>i>S'^.^ :zhi|{:~X~ThE2PhVzEU ;P_~{f0!,w ~a2-LbDA{ Â/1j؀RrT >N c=ܖ^y35 ,I;F"19vIǭ AJc 4ZFS#֮G ԋ `X]A bA.GAA6ic2Bx Y>~@|RKS K-S't0`ݒ#X Y~T  H (BM&V >Mp:ѣ@^1ٹ7>R*ZAAAh^ޗӦkx/n5SOaA;L3V 2B`xV3V?7`ɆA N0@t^-gc3f hs }y A2؎J[zSMѠmCFիem=MHKk+Ũ/qXA¦MZrvB?8*G\>@x> J_5S~:Ĺoc38<71r A*M/I1Ģ`7 >?5j:>P.q/ RsSPC8u ߅!ծX|q&H)nbl`j<'T}n -03s&8ԠۊYPr-mksUϙEϔpުU_1CA9AUΊ@j܊L ucE=U #ߗFC(s>u\NJ% Xx.N)-ǞCfEs}^1[{_l=Ų]=W-@xdܿP?6Wu mX Ô3E2ά=mޏ]`^h 笛{k}Y%eČ ǶsE8Kܟ/\q6tЪٳǰ*nnM]/ZJIȱxjZt80G'nPsrbv(JW𭉚]g~gEs1z ̜LG @}3|fmsgϠ[y=2{вI2ZE%\8&4;9[phni%ΥGbiLߟ,>>Uu:-T8π2WL 8A$p=P7r4jm9Xks[4Sj`TN'cd5_L{r|gNE}Iɍj6OiC܉u eF!>Qpqo)3bSF? tFaRJ9<8JY`^˨qrn|v9`숛O|u-/gh |os?G}G;⦀{C USF,v`'(}w#oQϽ ؊2Zsα.k3kj>0z`\Q W2q(o)vc&W['"ZԱnQÛ %I.7o|(ܼY57(8YXU{j<`<+#ĝ,NXw? 1ei(ոZp UMBRª_ 7 fJ KA^nN9mވ%huFʮ}G9bfWrm*axhڲ ׁ,~ n-ZRJ߽4Ƕ\ 5cT荍Y;1ΪT&sTF$fa08g T$ʻ׆RflN] `ѬX'ԉ<mjVuw8cU{=}@j urL_ܠ(GFe<:I'LDJ{][eG*bN2öpꉡ"+7xk*ՉpC53( Rd V~sI?yuvm1 z^wKl6GO=Y pܻy]~bcInT9a0#ϖ٪M)Fg jШem)yZeӸE8z S'Oq Wڧ)*{*+`^0&Nt}qTUpኛp(j_8Vrڝ:g]%3 ƳR p8}b;Nq۫ئ̼Tt UEP& 9ϓ;˵Lp}qu: *;Tɐf}_=Q֙jGԋ @dhh~U<`9񝫬k[p'TZEUAXDX<'1vufV:Y!j1DPU!xfP qEQmbs1):1;)a`J( Óނ5n%S̨J60T7?͹CuᵕVE43Ԋ Qڠ PͪTP É5,`!Y!lj|F {^U6,9{y`6 KPws<@hMJudj&9u޷3s*CX?.Čcm`g%3-Piƙ797`^Ȅ* 5j GL98U "[͈IviV,`Xܿ/I("pVBFj{fĿyUpq6p9{IjGDQg9.8gp8͵ImKDm;ԁ\x؇#0'/ըS$PD(T4GN{VAU8ϚZ6U+ub@|`t ?Yx9OܜNCB|fߨ\Gj^Au&Έ!`ZǔH H86uT LOeN.^UEwnن~8vJSxv\ji4- ǖPU[߀`'9Dq39QBJۤ>3'TM`A۠ǔjq<gA?j_uE-?-+7La 8~Ǧr.$Y~nUNV fu+GU0+e*Zjl(U4]m+C$jֱJ{B37U$ V'0]ӊvdw.cUc8muRXKUY VMZնm)7Uuˡ*ul4'r9SVC֊UĽUF߮*eE̶۫Tw(lKeNHa( Ĝ rvToJn/}:6+e"0R'Z\ \ p3kP)B 8 Fu"Ap8A$W՗ӞU&(*fmȏMyVޟ\%s*jԄɲ K6>ܺ`52[E]a6UpK,f4 nN1&P˫ {p.g`P_"0~5j[y8zF;Ł8Nu܇A[0+D߸4gO4n#8yvRsc\BQUG*efҕz]]B.T&8ncF|𙖥J8 d 3 (%P\3[5׃Ƙ3S׾sJ(C]gQzI'h5r(3[T5NUS&s5 Pq_q2'Q8ŖS2r 1ȎxTjߠ{O9`n>/wk$E2 T+F2cè*I3$)G2.P5d4p&{6ԇ7Iȧx\5&@u`TpQ@G3P߲PgL9 9aifVe 0nTDNj6Ge{/^.cE"Gb7l`hBOM|"35p8 W+@ULQ J߄TmvoVȇ+RD7չAٱƌ"GٞcT?w=jۡnk6d(,GCUPEE$VT{e\*%EWĀlvr@UP!SEeՏU\Iq`᳡LqlTyQN^'G:Nr5[/p̨؁OAb$UjOS JUNߥ/gdNhvp!P0#PKGEW/g `o9rSTUT0 %|D5V,*j2j/\M*Z9L66m1zHQ0U+;F85^5>9(ϙ&MXEձ*JsQq, @n}Aܑ}ZZ;F%wz(pRuOQ}>IhwfELc-NbF qD8vyQ'n*of(HNd` 'q P;لxP9vHIIo]B,ÊzzSM#r_X{wSxKޟ=eYM֎~n0:ViS@QZVmbOT03⨐r+ae.P'F؆#aS4m8,iS6;ITەQ liUk-QjNșPvMf&UhI?N,QثFuh6D4Uěyu_36'>wro(on l2Nh (=V:2rnJ-k͗軶1bev )-ǞCfr?v~X[5ElJDQpj,mۯ){MM݂:QB*;)PB{ n GhL%o=]t%iUG#ƝFAVq'0v*N0nH 6U3;Ծ:=?@9(03YNST>WB@X`F7UX&uʠ`U.EYOg>_s+\^sGULbp)v8"=+T;bh|^]b.Cu8271 nj& <1 aw_\÷&jnw 2{@֢=|fN{@>G4i?m#¢ofO<q7ލx,݀,-u?n 9龢k<1l6<7nd-O}Ҵڮ ,n=dqt''}x;zS3݀k^ک+}57ԧ1A{X[@f vg4qZy3W~)#[q(dUqŐ@hۀ5)zWq̊3Q35pZ+KYx*S36pH@Lnnoc:D:Lj&.Pb$M?)W? v9`숛>{z歵mn?Dzkqimv.j׉q}_@Tz }"jFDASQ?6ȸZ4?U+-uGO[bŒoJ?7-׭SũjV*-J? PҞkv; DJ Iን}D@cC)vc˂ bHhظԏrsʼ#ze=t+~m#ϛySU.ƾo\}`7\^W|-~2/u<0aJqЙsoWuF:B؆`]C=ٲQ7n*u+nK!u*yd컙 UreuY%>"fڶUj=p"'ΌiV QRvtb  p}N+D([Ƙ41[9uŽFwTR.I?Ԇ*΂M%\c]A1nF5*ͫ:OdT UK?owrթ905(s DӖm7ظ>KZ8n\KFkk=vT;&4 :G6ܳs+7HCx1Ncb#}{Hf4/uMAT0e3b D|k;? ZTDzfZCTQ::x T'ǝ.`^r ׾rG{GɠATnxbrR (P Ubo1,&iF?eFS+(4mŀAAq {n>@ΨÂx/F b:f?,R{1i:핧*uKW,&|"7I4?Va?5c_z޽ T67( Rt G!O5]9Uq2Ӧ)Uq'*`m#J?0AE2sN@oևJ2y 5=%!M>&Tj@J UA F}(:"Z(`4'U&(;fmT+ɝ HPb1wNDu9Q'N jzXXl񅯄RzYӧb1wFX{2NXwΐ (7݌l aP*+e$X`E;w,s jY)" =JHy`sddJ d֎P'5ߨV r[:r2"JVۊANPrĔoC$#KKfcaέHNmp%bWa׶_ C;fQ?.K|#]OKؾcx1ؽ7D׏Em|? ؂&-ѪM#L«~i7D  Ad1FqژJjrpҴIWk젉:s@s鬑Sչm7mݳIeʛ t5:Duk5n*IJLօc/Ymj˞M* '8Jz: ^IFPuQ7ޠK'\>]i*-;L[rIw}u?ԧK?VnANNJH׀nܧ*St5atŔvJSӊM_WɏP۳;ݫMtzt=t t׵N,*)[L5**$ı U~MɧSF2ݗݮeu]k[mw\gl766-=F_!SaZ=֣ڲBg \{YOejjߺ^kSϫsLҺYzǴ`\}yTѭT{)T^{fClf%c.z?j,t΍ܮkr^kUݲf2a4t igQ6KJե*O {<:xתkRѳFyW嘸wuuUme6ɾTOw]/MmyJmsDȥ6ZѱX@ {6gU( hӮjӢoJǎ^y=PA6ޠN}L}6ZRپJIu.( jm5iwخkڲg^/ugڼg6\>} W=;Y*++բ5_V۱ڴkYmfKj,heuM:Mac]9kZfljPv^T.lw]/sثP:sD~¦$X$X1%'V}$9[Z& 6z^#>VgW/C;oӇKc t׵NtYc]յMZC-*?lPy^_Zi_\+>1?^Ϳ3kH]K\ǀӯusM}?}rNL\BwӺz9dZl߭|u ?mk=:eNDYؘoL;91Eם{cbvoGEbƒ'9[~Z=}TyqnPHO3i7rѝ!Sxovg S\gN"|CMqRӴ;{>2L@wv%tg7NeI'iΎڝ}2)vgG;m&b-= n 43 RycG tidb` "@ 7>XuAJ>hA$̻%A$!!7]:ƺwN 2/ynbysV9t ugh٢/`/S]9AN5x1MMїG_{[{q|?uW [S}TvĆZ1fȊh^I;EOƝy^*ޱEz ]s=z_iۦ5z=gro}og:u#tm]6F}:lS,]?3ǜĶ}i~^~w1 "ycAzF;[~6^+^%Qk&M9r%%UVV\nyإ~ 5A,_}=ztѕΫ)ע4t?PBbFqV~8N=Qi8#h$~ΙO{U_DWpgL/dd~Q%JSf[G&_ú'[+ȋ}D7F5M$n.L.kII%1Oдf?^= M֬ "73ZOjzwc@]rQU24. Oҿ145N_VYY >!#hώ- O}cDҗs$ ΫJ>|1-)⬺@sis55ssTMyu3dl͸f|.3 OWM'ェg) YHVt5+蒫n?CFOov-?3/$?B]{Oc*}3s"xQvӥcc5^:{Jmmlpӥc4}}a9>V2WpF:J8X̴} Ó$ @hw6c89 km!GbL_DCK$"@ HD 0Ѡᮙ'(*ׂO/?)AC/hA$Nز~| 5|Dg& yʟ]wϵڐLy9Gբu-uK@/\V-UqQZiSN}CnUKkU9rHصF;S-Z5hUڰzֱ"%%MN3hzຟ]}rգmZMpj:t` صN3Y2E$O*mwaAV}5_;mT*--UbRZfSCoH|)>KڳcK4+oԲs}gvw^9߾VsvmnҮm4uujӾs勋 ;hm5kOϛ:;7s$~UdGZ՗K+}+oTˌQɓx]޿۷kڦ싯& *Z9(+oX%8Y2nAKB \XI6[s>PY19-W׀a-VkέǿSFkJkBy9G/n``uCzXv[eY_$IFMvl;"u[%EZ8gnȒ$y͓} [i۱S$I۴ׄo}[o$i75phjmY$_2ڶIs?xS:g?M= 5ۂ+e[yoRrrI壂O,Rv Q+ '/#'\?>A^nC#ǝ( sp,~z |So7x^eTꕤ+jջzm;v;ÍZKzٯpD2Oo$֝ޢ:C^uMz:w<{.~pҾp9w)m-Q;Xo$X 9_ϯ28IR#uvFjnp O7/VnA)i{򗒪>""-g)i?} K?GhZt~g`oq~^jK_7OBo82r$iքo}[-3ک08Sy:0Ԕ\L:kնC=zЄs)=:u{_a0$GCꭗd:j)m|4u]kW|{1ԼeFSҚ+ou+i eۥ"%&m7d9=c&RݵU~Zi_/pmY/Rמ}nReۭB'$E 3HNN4u3s"x@7;1z+ >Mq_#+@ HD 8Fg7A' HD "ڝ=6GhDNİ"Z"@ HD A$"@ HD A$"@ D i!kfg 8jQSzT 蒮~F^skˮlJhiaV}`ˉ[/@ ;n_Y 2kG.% 49kv6'ȼJhrr "A4) DO0xάϣ&M<7:%0?Oա{43("V.G&hptg-rwna}+%\QVzLnԡ{uXIhIR_e콻oVq%$&UW~.!m۸FyD{r-땟sDqxe먮+..j_+o6%@ذj r*ѺKTTX@j wVqjMZ|Qэ+@ qTR\}i劋 nW*˩h勔{Jh߮mڹy}D~A$FUy9hIR{o =; _ѮF?[u$m'kFIRus5hgG|``0զCg<n!x{ۡ{Qf_@ QԽ~:u[!s8{oW?5A&MUڳc_6nO{S{Hg"$Ǒ$9#9|~=)9E[I 6jZ/.|^m\U)}y5T-}JJN-s\b|HHF*UN "`p٩5H-ZcBvm^uA$HJLJeݪ%lߘ,%5o%+.:D-l߶1+ز{p׺fOh"DT֪В?pg;6T{vl ylo*~!m*mrK_A^^e]N IjѺMuype"*).Ru@x&sY2[Vێ]j,A-7;?>^f:|` upnwb`H-ձۉj^{wI,[X^%$1I孍)ij߹ڮc޻K{wU骷e"i=*IZ3hCFZEK$c=V^ڬy0+5-]]3|QJKo!_\ҚPa\W5h8uC)|JJNQب*uw=P:*W||uO~^ژ~NSj)Ys2S~x٧HkԬyK䏏v۬>>'+zT|h{Dg8B@#V*7d쮛0$3 {=ܖ A5MAB IDATrD 7]:#"<5"Ad%7;0Dhԩ; A$"@ D  A$@ Hol nQBWȡI4h$Q=єdDYzE 4ve]KfX*r*uDZر3GZVUZ 22@ N Hv'@&"D"X Z";v9!!AÇ?Hdc{.IRaaum>%Ɓ "F*/oЖ-[ԩy/9TuEPQQ~_~ׯq;v6n hϞܹ# hÆ;v'O>D'OVn5jhn~_iȐ[nE9d@ @1WyقBoװaê|t2͚oߦ@V{ܹ4w\ܹK# MU֭{u?WFF+ed̙o- hҤZp'ٳgٳ8͟@&M z۷OԩSG+T핖짚5k-@ AcOmۢm۶hݺ5;V]vU]ڵ8N?}/Fyye[tRh:rG5w\{y?~f8}鬳VϞԣGF8U4' FήlbbJT~uiF۷oWݏ۔@ D]/f͚iߟW޽@ !Cg83sj⅒m7P=q%6p?|Uݺuf|P̙ ~>qD=:rGg1^O=&N\tرiJ]s[۶mSii֮]3n~޳g*qH}D1Def$~ 8@#U_}]?]/ФIS$I7Uw}gpNwdWZJ r2}nǩu=n As־CuŔԦE;Y>[{ҕg~_ "+|~1&hDQ*(ׂs%I;]cOu0k 'J]Pe=+7/׼Xi #h0<њmkҾ{%Ik,*-+KtlH)l̐5ֳ`Qɱ*WOKijˤXOaq!-$5)y'AcJRjer8@y2ww$il[ULQWZfՖo\["% D;JeZue}la<|>6H#R^aWcPMmR"6 2&Tlw$}r\98^X__́lD Ύ2QM-(["tJ=Z"zj@jt-)=ԞS8r g"@ HD A$"@ HD A$"@ D  A$"HD  "@ HD  A$"@ H  A$"@ D  A$@ HllOc3}Mn[bsuͷ> bJ_-$iuiuF]>~V3Wf OTkӸ^qptGs_[k}:O7!=I^c߿'ru͗ @6KoV}ɼ*}ކR=LJ&X0Sprzc?B%$&i铔,Iڴn&OĤdsZji vhʯ$IkV,Q]|^[+S߯s]HM۱c%z;t M쟬fvm k뮩WEc:o߲^udM }ݩCubֲmW^չ#2tӴ:|p?8={tσ-y92%rz %iN9?ǻo6N[Dhڞ}>{_|j֭;gj///Z\u>^g\MV?T67$Ig]x붗/\?Zp@DqQh.Ys@i͚DBx*7u=9|=9&L͎??Y=&9}K3Ͽ\i鞺oI?Q}J/S7}C@x.k3]}ӏӯNK|ayi30Oї~GsgU66WgF36ϧE&D9%YYJ;zN_c;k,q]$ԯov}{պM{hubGq@?ڃ(^|Mz4y@ݺ򆻪^rfƹteWlc\8ͼZ?5<%@ m;]9y Tyu#!~IRVm%I>|> |MdzSNݪT>_ 5 z{Z~W Z~e7nu~nhP÷.FohӍ9]ԱsIgs/>+}ѿ%Ic&{jtI҆zgjk:;TVzL=Si7|(D7sͭzW?ch^$?u=k\ǐc_DN9]񿏫y͗h5i{ U';X 5A(Do"&n鑮!*LlAԺϸ/ HDԫy֫֬X‚lN^)i0l.?B2 @,yլy\ZH'SĄ#GsY~L! ]ZrkӚ SZHw={AGkܔ H }L?x0xrRs*)9 HM/苅_{^f-{LIҚ%IL͛itQ2"l ◿K˼; t'X^}We謉+a}ɗd6D Gsrg_믹D~_JMIѵ?>[Rzw<  hͺZbf|Mpwqt-*.GFA$uU ?x_n7 Ps&QEڽ@| zE|-[s&(77_̚{Azk"w9AHAJj政;Ԕ())Qoc+hʵtkjPGrNFHbQzVvmtOoko}k6ܖy7܋o{d 藩K/<[m2Zr@gSH}{={fRV-t1bʁuܫ;~{23/ԤiaK2@8t ₖH榻Ԡӌ[~\2@LHMKat}ИI!b͸)hܔ hA$"@ HD A$"@ D  A$@ HD  "@ HD  A$"@ H  A$"HD  A$@ HD A$"@ HDhAd&b"%26'I24' ꘜCIMFH(l |Ti+NWAY4*~T'EBQاƐ M"bh* A$"@ D  "^R/v;+VrjI\U}_?GjC-'e*cX Lˋ6|-ݡb*{dz JSܴ< _sM魭p+>l{/MIؚ3ӺYϠ>)6e|\+C /* 2=hz ' ~:Yw麭8T SPyuÉoئQ}Hc0Ei0p?N7S4'|Q,Jo$L|)/eKuzIK8t}s¦+!x}>Ѥ~=g"q8Xwn78qq Uѝ}GN_?E6wͶ][6w:eq3WC90Oy9ֶ{mՌEʹUQ2C9&ix8-&e$ Su8#QMZL˹Iؖpm[Lt{rk[Si~3Ӷx#3z^SUiv<|kr5)>3O;\nHbt&-A>-_Rvḹ{-Y7cw&A9/LnL.|^V 6cq^\BΤdBKk[]X\ LqȰ.kd*hZָ{2<i1#z\^ѝ]qw yG_4z9-{Qdˏs~57T47ºZ"֤KCD HMܻLZ2M.APwnXtUˠOrfwhv\"}i]c3e[YqKI46yjxm\ZlN1dF&u͝M˝MWtUnudX ik o&u ):1Q ]2]pSzyD̶[PȰ>ZNe\D3TFP:#1?[kW1 ;闼0ʣiZ<{:Pf m<p #!Ks/瀻H( My#cPf4ڔpt6㱈ĵЦ! pi7ζ9)m8k|kv*/@@K û}ca5;=ۇ#h5;_Z8LZ9nFzpcr 2bM˛שH-&=>} gɶS8s;14=^pdۭ9c#u$Zc87KՀ.>.r5vLM[m 0݆cyle ö0}^,MJ'3MI6M""\$)LQ5~EF2D[6U-lnl9ü>4n5!/E|nzy?6RHlignه}e+Zo:h˼dϽΡ7X>j`6i)6Zx)^ 眱,G#v;i2{(2v]}f6czǬ27PRz+H+mf7my/c? m/oۉԃ^[LAp0ܻH>@4Gz0[q!oz (˾lL"R99e9$#=uЍ/ǧ@YYʣ"|uVFo69LP]l~4mɦ.&VO;Vivm'6w67j6o=&f\y^Iݸ2<^_uD:;T919NnoԱysn,g9~mm$ǣL[ \kM˹746?2?l&grQk5^xMdc<Ĝ~|,Qsf5ij޷<:WѐhkזI-V4Ӗ Ym5#;~;g1/uya;SY4S?ld.NۋAP^ʚ2!y\@>7ιpVnmm}h{LmpUHhvwoaH>#y ~MDkzP=U9nǻI-{|8nQz P~QؼNGOPFNNמ}}9*w6J7QaARR{NzVUvmݨCXIѮ:u8?\7X7Ujt=\e*;E`l~DsPS$6ԵwȶUSe9p_ef-^Ee:ܝogӊgx&#MlF/zn4I][%Μr6-r'eGH>"va7fI d>Ǥi iyrmZ(+;2ߵ)*98Nbbݱ'Qj :eee>I*q֭X|x%:|e ?BgM:z87`h5~ۆ,'>Yjj1i*2g38ȭL?6ɣ@&-M`lz>|7٦mcPu͵]m(yzeo-uugGbn"Cۑ||4W#њ!ύ,k-3کgA#MY%I9U7D5+$IQjU}iU}(+ӞԳIҞ[dFw_C(?voٷF3wVtHɩisf+- UoDk{X_4T 'my좵Ht"]Gn^e1C3myۉnw 6ֺnfrϑTa{.hSl|Դ v4Ȅq+Y}OpZiW=9_6ZᓤcJjV}~_}h燳,N{=ܿ%oM~񧦙HH5}ϬL$ T8xz{M.V$oӽH漼d0cb׺fbf*>ZM5 M mv5]/m I#9Хe%m~ePC%tHfntی"2x~S$Iqq*iIJNIx^|=S>_INI n8ɩi$_JJZpe%ŵ߇W7J-Q^t"P/$2jxoP݆m0S@%Auׁ^ʹj{SdyuM.Wu ڂH֚Hnæt2!XʧHE(mX|/Z1lU 2s8mՉr~3R>[}5Ф5ǴE9GHP A5U=4%7΃uG%3TE.QVөL_,l3yXkK%ir`,&UTYI,nL_]i2 9S+Y $p(:[#}EJIkkoqpM MM66:]26}& i&/ߑez$o]ڳ)V]Y~$dsH [*ЭӍL/`>z6f1-Q| M4tVΟHtk[G5VM_Edd@~[]$ M(h__۾_^+2}˾ؾM(&NL5m 23f[826b6ol2m5-V s,g/19d A#iΡ}:V\T>xNԭ>Y;H6d9y9sLȠNTu~FX`@t[ndʎ׷y-&-m 2 ⍐h}[l [gӥnHR펎pW%!oHXeh:Wi:mH.MGޱVumF: '~F 'Fil8Lh"kNl*bBgGM w2|]^M[L[\$}^פ* j鶼f:Ii;@!aq+L~Zڤ4^ڜ6u͹uù۾x۾[Y5wo p L2 N-3&݄&("Ѫ`z`Bh&1}u#"2(6ǘ,6we$xr;gBMivxVmFgmZl9u&#z 9neŴ 1 c^էxˍܯn{$ԑۼo^&5C4ylnmt7# `+>wӥc8BIvh١&jGN2p>2 IENDB`zoph-v0.9.19/docs/img/ZophImport011.png000066400000000000000000000707051415176210700175440ustar00rootroot00000000000000PNG  IHDRY$sRGBbKGD pHYs  tIME 7| IDATxwxE]z -^C l+ ("N4 i"HK"E$@ z#mB<>){ݝ K>[<7/CK4 @k1]~zVQ~|C- 32y?M[ɿN~Za`f}k#[.҃`yl:ڋHɓg Q'" >}#C4XEk*\*QsJ)V 8 d:õ6$3eY:JJD.G~4dێ030#224)A$m8P+6tK̨H 7.ڦFMR:~X !&eȴCˇfSL0.z܇oҏ  %3 !27{ ֳRf>*K(c٬o꽨Oa,DSu)vPFQHDjH(R/_ћeG@.iBpB>' ?2tFtD_ё 6f2Et$K'oDG[yD~ bmŔF] _eP`CU (F6FELA''Ѿ+ >+3J(3'5[U*,<0NjRIh2"r7q{xVX#C@ZWV=i`dyy$[-7D}ڛGUtJ ћTLSrmMO5|G_t4 ;(Ӗe~Q:{V ͖㖚Q]v!{_ȰNFdP qL+[(;=h[6LM<8LPh!:oƠ -M-9ut:/zTҞ-72Ȱt.2Ԝi wC@^S 0m9 `dfe.%ʎrY[Ud]\U ektHO@f U=IQt2ȮcQ)5Hi;%:@L] ݂a3ܪEAŹf]bb:nvHr5ZLb [ϾQkuV-s6oq؆ӡƴZMS\ۋ) +jG-wMi=mTQe|RWp\JZ(rٳJNHr=_ yD=Q;:#:.3Ȩ1 ? D`e.ڮvAKƶ$VEǂS4vd yCW1HLt{ XfU^isNBOK#[βS<R}BhW\{eޯ-.{c-V컮͒}K${t]S;ZEl%0/4 ֩dRgUۢbR!YFZA4)[ξEiudS&-ڞ<4ꫲ3CѴԼM0GdZ9~ɤ#{T)?Z[X?[=$sU:Sd ʴ'[jluMG՞EMEuYl)6hlycaԁ8ZSqh^q9FUQ:y@07<5ѢndQ0-E !5%XL{9TD2>!2͊KdF8Sm_4j26[mdysN-z֠2改((ꉔCJj#:3-%sƯtEvMiT "gja O".hՖhY^n YBiMݯ2oYF."oLM;ECɗ$"!a ;+kU ~saěQfi7i K1W8wYy%W;|\Πj@?HT<=MZ:YYx!le/U1мMg|dt}Ԗ@"gӼJ2\=g ##t^kk1o Pn^L2Qv/>;L ,34l M<5utLD]g%cp>d߁O4hΦVzM[۷rMBu?׮^ae20- ۶G*I|3/yAM-;0u0ݰ, rD\%D]8 pTgHDeZףR>*sب \&n^7>1m0{xFz;8T};%I AhN-vo1w~t7`0}DD HJbۯz n݈CZj .1Z{ki`ɉoE5Fp#'{prp@Vf&HL 3$PWpyX+mmc<R :a3]qi}νC=8j.$P6੮uС:9bז6i gEx?GngeeHyďt mLZ[j_D]:gz4n_EHLOsmnJR}Z-gGn_:{b? Xt6F!';)ɉO󅲞&~ I 7jW.`ĀWNd'd 5ZJݹ 'S?ĶIxm"?Gcu &cۿ9vf^boq.b>н@K}=v vTڠ6FH>m0H?YVʧRyjnlj+a |hUF/5 ]z> G'jн|숱psDPiͧxu j'g x> _>ggg ~F-DŽW[wO:>aE%eT>0f2Ԭ@1<5 ?c,ECU/ɒO֟Wۄ.baW~ %J-O-ڷ WwToDmѺM@7ۏKYc.lh[2|+T[?ݮMCh)4=jwNgn@.ӱ]QJu܎{b x+Iv^jUu gw. ֕֩c$Fq=H7Y^{uXpY jk"Y>~YY j}mO>6;`;#N?&:"@dPߢ`}[Zl~\?36uU9OLx?oIv=5y̫BjB[TJě7ӵEVf<|L>AJrʕ/NrNN>TU=q/= "asԩ }6v^jUOG'gm%'%vEyy5f^|i lW[w.f2T*ppt#O O}vC?Km{jGP~SڷA-f"Xl -O-};1i&eggaC0f2t:'jۈmjfHN'OtQhEso y#W/D߻O#g-=x>nM4%)>&+_ofQ{z eė1f2t=IvyɇfeX Uv[dqD .Y͝C1DZ~Y֝ i*Ēcq-6 ;̷}mO?? oN[6}>RmP@?PWD[ik/sǡMP_hQJWKїZwƟ7~}c[dB g?n 0_qX4U@H쨕`߮&ݹ ^'·y.o4o.q:w(nj1+N ?m&ՖQG ;>6ziFvB}eN4G:=ϟ{jڗd;~yh۹T[,*ɧ蛭uf[s'ao aK)5wQF^ _V#vBFXj}j-x=Ykz;;>>Ow;>w\PjT0KphD]<5`OТ]Wx]3bPqKYj7~_Q L9gO1 Zsy}ZW=5n0΍%cIx_;dggaUSC_ô7! 4nso [#9)ZuyGaQX[H{{\V{u'դT>NB[v@pnh=Ykл|l=m0H?PWD[$S:{GmtL)#KmDdrF:M b?[<7/k YDɸr v W1yAEmhwyrǮ-? .Fynm:xčBo"&9~\kW~ _}wF>{G07n eO?Z[fJu+Lsⵘ泊A`Iy}r@\[$&y.7>,뱴aHk7,kx~DTEhyGkXj[OgLŊ V>/с G'k?5O}ǟv'0o.++C#?!$P6੮uСKovmMѹ36DğHGdCvG[S#w,޽G mu0({-$P{Cq1[ʇ-4hAZ;X}5%5׮FK~&xl+dg||juKA䰁D=᫏XH5tt MWjsֶikGl>.˺ϭ75۵e#v B G "XnEx?ئhZܶ;(!:sFVGEC+<^xTRJ-? m!DnǶ_u2VJ=f*G}'ڞEڞL_[XGHo$u_~_w :_5fFm&K1o8vh2Ƭ1CX2ޚu0u%AػsO7~mGE3~8b:"_WѩpC ^_~d+=+?ޙGXWjѮKo{xCtsDfm{O2A [ &YYvlڀ cїH:äOms)~N;YcՉ X2ޘ!Xi?ywP;ۏ%#lL9:vd?~܌s9fZ$ w-?~Ĝx*l?j5jcOaypD7ڢdz05D*m?VOs|W*oYIJMZF`KBju7ôEmΫtD7ڢa!3Zd-YW+EUJ)|^뱸s6 kQT@k|+T7=_&Oy$&ě}{z#)eЄ[7f3%ї!rXp˨RSUqW[⺞>?x4>h:^; wë\}pطk`0_CG)]'A6W][6ޡS,ֿ6'ۧ6n~OoXi)y_ƘЭȫH֯(lZK=žRj#"{wn¼OF#bCԠ HS*iަ3"wnٹSG1wSeZtGLmQkج \oRh_B}T͍մepc{ğiV葧qI=qW@*hQQ+X܊f?~OԴ)aOsfcobⵅA [hk f7l-4'ZwQih\8]cɟUoj򥔦R{,}?w:JmD$"}qBף^hЬ 2lFR&~{w̌t=/fA#J-3t$|h*ڌ ڷzC™㈍xŷdX'233p r;7Gc1z5.dwl;$o+`Qӷ YWW`ȨԏzD]UUiV?x4^|)I Bԥ2z}E{0O>oߠs.;ї!N~\43~z~D,?Dffg]i?p<۵+?a/DȦEoԢ>]4)I HMI¶_`QU4jW~R:ɗRJ0l`3DZph4ڈHEboq-6J$Uwn0s,7ϞoE?<;b\t"Ŭ%0GQR }E2z^Ym놷f,©u,׬?Av]-1`(,~-IɈH#Fj|t|05$'ނW9h} 5jS,JUѹ8o'`Qp8jlRWQPv}]M;iQj oc(_ 6 Sֹ~Kaxl7¿fveТO^ sy}ZW=5nhW!ZZ,㛚|) csCF:@:=6"o^~{6ޟ7\$8>1,=1M8z0ODDDeR\r-6)9"1,|+T7ӛMDDD2p yΈgNMYDDDT&7Ztǥ0gp 1ODDD KZ`ϱlx\<{숱}lr!:QW5uv]zIIDDDTZQX""""DDDD@""""bHDDDD  1$""""DDDD@""""bHDDD@""""bHDDDD  1$""""DDDD@""""bHDDDD  @""""bHDDDD u::v%().-JyX)-Jy?$"""*5Vbk 6Z9nvKlGdtl9DDDT pKG%dx9)QyPrH+Q2Qii`@""""bHDDDD  1$""""DDDD@""""bHDDDD  @""""bHDDDD  1$"""bAmdlND%#%6rrrXDDDTzx~5ԭTHt:.D`!""+''I)_ %G:N%'x- aGDDD8Wrhyߕd"""*uNDl!DDDT$1|Qi'{Qɵ&[w%?+NV^RSpz,n^AV艈 Qiw=,"/1$*c^״5.;۩pquG: UN1LD_. %UjԆԨe,Vun݈5]dkW;u^E̕x񻬬L/'Zfo5/ϲyKt,w˥`23U`g418::#ALTtK:U%Ȼ̮pvq3~'P1'%Gg׻]?)I 8{pϑ'iS )e{8ςKKUrr{Y8}xjzgO c-޾P5<؏ `yGv>;{ %yES(8wS Zw&~tINij2PikxV4-!:ڋkW#A$*uw©c.nHsqu3~T{Qu' "X`=K,:MZo sذ"3;Jզc,_l Ͼ<ΦɿfV"'' ƺgKiAvv66q}K{q)SK ?2z?9bo [ IDAT r]2g,-7eai/eji]KeJ16~Xj 쀨Kg1r`'K\p#6<}5ܩHMI|F]@b.?hɯ>'>S>1C1ὕeF.4B^W\dߋKZg[:q"* kq,SKZ*N7~2"dwDԪJ4gah2_DDD% G5V+m?\"˂, @""""bHDDDD  1$""""DDDDڑ{ "A܁p<1|7 ꆣ.)zqt\l#XDDDHn wjwk8j1z^m{ ^V1, 7Ėu!OvofM4S:vD5s}"""bX¦uEeC6w숬KS5M}1~͚Hݻϣ_ǎ>CDDD grNe9u Y} խioZi?pd^~:lw\;DDDD 5 NuA ٗ/ɓW97- '^R uid]';t![uXQDDDPtgdc@ǎȾr$37nC_MAྩq%m!3N!e 򿮸q +֩tgLe63ׯժIƉow` X AɓȾV¦uEC8"""b`#396ol>^nX~} pQR1<} kvi633XDDDPNC%S}YYpm:;; aoSFˤ?p F^E ~v@"""bW!aM^d:;58X,v F ЫjU\@h09vzI:;;><OTlښGDDD Sam^d58+_f\ݑ ѧZ5 `0`WpjrGD p; H=@"\M:`m^ l`<X]7#.9͚`0+ߝZ5k/') :: ""kvB㖬("""R@ 0zGFD" n-ZX 4/#6)V@5Q?0#+)ɸA ['\1, A;\77#GA`@ Pggז-_zb`Ñ5kfNg1NL1W1˱N/q F-X!DDDHA>)Xl/#A`zL Aȴ0W k 9..>"ל\Г1|0A` Hvz{õY3t:cf-9;\wvE5۰9+> p$y`]xrʕkӦBAbgoo 6 zXDDDAÑ恵k&s?{ڈ 7ec]x õqc@ #"↞kw3#"""1n><XnV@K#Z!Y>`qUt،tD_ wq1 ,蜝wu7p=;:wP#,1,:okB*U?rn6 9$??U ~[g|w0bX=Q7 #T/y,?WVr2GmU gA 1,_@@ʽV? 0;OاvmDDD8}QJNdߏի/@"""bX<"77A ,ժ1$""")[nn44.[nc^ ή>":xh >({g&!Oޢ p/4S zم[ήXxz½Y3t:ɢ G2$"""jp; t:\]aΆ!+KTpkOOD;z OW \><rv/TgN `鐑d,=}~Gpkzww++ڬ ^^X{w=-' MV@;mqq ; ;H3#Y%[%K%+cX2o6. TPa+ P!dXb;elI6d 8!d,#ɒ~ؖd?fyF39ȟOՔo{9F?y.={ʖ,{իԚdj4F+u_ޅzGsUWl-Yk}Oq/ctѶ'Ԟg֭ʖ-jj-]?ڣ::X<5-2&ƍdJ]~To2 lݪ=>vК[ު>'Xx־6=c޳G\ƊcߕWlbWt&yVVfsTui?lljie?[{X,U%矯-cH˗kkg^=c־6{b \F`O_?\_uGhtѢ1o9vS5Tjgڵ$QR}G7ܠ XB{~ZܩoM7ua0ŷMڥ]O=6,k@ÃРF54tbgxό oxP}k/эwd=Wd ٽ[ܩE7`0|~FVKŇt޽>gnTϪR+o9:pJw;KLi*IRenu~HmK_s.Guoߣ\\%i6Gjwjw=}JQ5[M5GGj5cj$P43LYfCYiѺz?o:uGXʔ+# `CjH ܧWfsTQۜ! 0FJTY)k4e 5LYPh6ƌs6l |A/\$U+k*mfJfSv6MI/Mٵ?DYm07Ǎ4;?4M.PQsYcʁ<3VkloW4pIA1Sv{z{v1p i,&1:% XN`I;I2mm$Id|wOwr`;& A =Ff` ` `|n}2/ 0ytWN'`xE7mOMzۏI+7-[F$}[wjݸiW|in}2u)׾n}vRmm_B~Οӆ~X_xy?w/ݟ~~~^;~򽿪 >i{tWՏޭ;>>_n~ 0C%#ӯ}_tn~GtJIҧo>O:qŎ{PG_O_>ޱ=w:}M[`OIۮ\?؇t$7?q aۏMKwѯKKm[銫|~|Z$m9ț7l~O0@`&}cn?:yxo7|ŋ\>!% 䪥}b~}2@v000000000000000000000000000000&9g7w4ucywdxS+Uyl^rqfF򼠯Ȅ Ʈ躝dDsJd 4xF&E̩H-FR ]VV%yXVc"?T+e =l`=[Tb|J:enyTh'LI U yu {ڐNmC1puB;1*8^, Dѳ;E,^V(? @bQž ] râ`ѕjΥhYau^&k cX^.;7Zns놌U&<_V=b@`oxmм 2yf;`b:XQd1l5Yu}=/͚yϯ#Flg-0,PݔϪ5_k̛˺{V)'u^s.zS;NTP^ݝqxne7~6[fy#eh׬9C; UMr&NW<;Yߔ헪xd2fg:v~Ǻ$˴y-[7uzZLdsZb<\,-Th…:"Q:Ჺ=ŬRl?{c;^ם2g1 C/ ֝?y2*!&ȧu CgGfNֿ{ңeleG#wmm})I-ҋV}o кky%>*c{bN;񸀪r5ʠLayT).&817eK{,qG*/1şS9Q= ٰobdƕUETM*t~ߥ8v[x\umc`,ODl'V7yh˚B3vXs#weB_ku(q fc1y<8u˻XUA3/ط$u5Ncэ'*պսs}͇݊;cѵU뎟 ޔ&kZvb>³ʰS&^[A;vHg[wc,}f Ɔx+ xǨT1^zdlZG6VU;/ȹfMS 4$WbFI~]TbZ\b'mcYI:][˒n ߊ )cM5y~OzO iY%Q!CI%luY(Deeۮeqi/eŦjƠ/C%khKx!9HwYeRT2EƇUVC?dbv]}&y^1F]nPI-nQ|@+yCIRB1@vAzoCt+U=kHݒ!IDAT}[#(S{(A/+[- VqϘ[Xx^BM%eYӪHn%rk-;VѪ]e+Ď{=I4_?|bmZv:%RRM{*\_q2 សT~^v%uV $ Y>u[85[}[&#AW׍yَn4lUV?jt }c =wltlˇ]"z`kR9Xc$X*o{BL~8STޗn2EIImڙ> myIes)g>]EZ0O,fbӈɨk(9vx%D#tj/h9'V*e@RBʫܧ}3eX ,=FՈ H:,R{Ĥ'XU&cSx/I\#YMRL8zc[6yxd8Q|ɌǽwVus eJLzdSd`VְqmZsBᎿiP7unCT/II%2"tJP@fesx[F=/U?O1gúЍ=sqԵu]%-m.t#tM;ڳm{e9^+ݜhy6T59U6Ms&*6FRU.I8хUQ'6ީtKz* f(#UWk|ˌyǶj6=kŞ);40*0T0b֭O!˗F#&+}Ũ 6% Kgrw|&0tJ14GC %nPk'mIx鱐,[Zӓ"'X5&S+vJ ohHN,iO̩wzמt mȍȭbKV[Q9Ώܩ'ƒyRO``M_dhQ5JbRxs,1|`]-Isl9d9?i3~LӖ[U $[21V17BLjO,^1}O_&c0ϤV*tcE4;Vٛq$_~R_<$IUk.٬Ggv=?d-_Zۘ'iHʿsٕ[wT_d?KMi=uS' |j^{,'u.~{I:~Ȕpm`!XE,lJc1<.keJ7Vc7\l .V]{Jc1/V9+Oe5XoY*(tv+^&L\8ytM=^ czu<[sٰy5j_c1=tկ¾u srdX̏y!kfٴ1j)M0U=O< ,k/IܗKұ#:vWݐdV$?~IJ=/pi25yeI?Pe5UAZNJ0Ħ7JH19n'fhYO8'#b^e+GkiϲZӁytml $ݦ6r/;JTٗIkR]cNP*$i>I4Kf<;]C ڿ;ud] >7'GflD}΢-[Gr1-3U1o?ĖTbPSƾ[g͋zl>-u'D1:+t+UNT峔9:UTsfCN:dߒdf]CeRL^5c\sA.b:᮪.FuE>&*eKGVu4=XTu3fãǦ1r]cHLʍ5).UGlL}^ I?*{1101+ɞ*"Ruh* v#bRlXrv? Wź-dp7rko/OKjuz%JǒK+0^{ڮǒ4M|ꡔ\zʫo˦wLz Et0^g= yR|E"+PΈUfYwq-ΪUB)e;ʓi* jj'H=g[:y$[rеv,ot!˚BǓI?2\ߵ!^[ E$[p6zzffٰpnN״gXD?^Yp!2w2E2;d dVsf5֗^ UƛUwKJFk3䰷5кcy.f8G-@"u0UlƤ񔷉U._e,H iWU,b2OI$U*I*{ZJ**x^fR.T#vUG,vc=)[)v \/6 Lmǒ9Yު~2QF٫{̘ZlӺ#\U͍rEؓu[5ݧLlhRz{j0ґŮuy$T@b`L)3o|Xd\%)U[,W/M씧e!8*j$޲z֒|oe Y`h|;1gZmfQ4޸:ˠ= 2֠fB }=%&mT]JO4VD>qzs>T7w(aQ.Ź>C1g4_9YiLi**XuWrPFpK˹f)RG{:NX)h.xy pVSbģrR 200`ay=BYIENDB`zoph-v0.9.19/docs/img/ZophImport012.png000066400000000000000000000767461415176210700175600ustar00rootroot00000000000000PNG  IHDR~}sRGBbKGD pHYs  tIME ;6 IDATxw|ƟM $NM v )"v "` -Tҳkڽ{fvoAd3gfΙ9c3xe dB!R~ no6]>~!B ?B!~B!!Bpp0g(Qw"M<(Rÿ=c e)?(3Jfl i$Nކ6m2< 7鴉U gGW264=肷v6,U**7dGU/*c+^MiojSEUߤ23<~+'U+]e%kAE ] 3=@%)TJ12+AgexͰz2p$< Nn#X *z yX$} r[Y[ @FC8)d5^' E6Q)n{X2 @ {$c 9P8-bߒg$V2 cg=U_s%~3sy|OiQy)9U?;iֳe^ȳ4a'.Szb'_h,cURc<'r3&GʓoξLgJ j.'V"Mm"YZ@]f2-tƎU׵7td юİ3}]ueZ?rgD:TZT1iU@mS@=ȦM2x{V:2`Xgڽ2T~NV+M-uHMl%W/k,Tv%S@}Mokz*U5#Sjlla1a OU{1Cg6w$]Z$#(8c{sݩF'eJ2|\iOy/?'diWHL_ӎk gU_nSJ{Zv ,_}i|4Ǥ.ڶHIk$j_uגiIfTv1u\˪m;o5bKN;*S],+Ew)[; 4tѮKWʬUz*3YvXrxUB5;2qNu~] '^" ˅J,Sp.cG+KJUFkV=]B.*;*V j@/Lm[{KSG+s*3#`P?Ϫ) P #uIf$;JU?"%{G8%Y;egt'= ۩LEfQe|Syx٠0|IDYL_gt{O>0cP=UHg*X:PANSVp:!Fta%SSκ*CŐrH:ZJ'sBkm G(*cQiZ:i]~|ÁLW!aT6Gwe4vWeRmWv¹&NTwf\|U՗\K2dm%otwZ2{n:#*v79QeJJ K{sjWE'ݐpeKNt7;~*џ9Bi6YyHd8Q=/|i'δR)N53^:i7h*m>'%FG`G_Td( jip'>"'q^כJUCV%ܓd̑0Tޛ:lB3#CV"Тeh)?gFLm؝pN$[Sb8;2uUDy4+B|̵"T6P:Ar(P.Ҕg9['//IɩXO*$WCfAyl/Yv_z8ѧ Reg:܃W+Kej0_iH]]̸*ćԵ%=L^ .}Y}It^'u'*g\GTfTT]*;fU\e̅2n=]H JǬ\5i~Q5rU?%pK2;+Q#6Z3YG$P%]Vӌ*[[5Nl7%kF]]ҝP:a%T멲&қ\n[ ^' *z(5fttV6*lUk se( @+ 5%Tn,s nfK]~*ԀPiktw(iOǞ(-)fQPs$Uy)Փ;lv B!tQ庌EgTfTޚ2)]u}:_:U 2m'TzVuPTK˯ȩ0?v YJ!=Q'٩rʌTVY0He|3:!/tO8E%3oNC\:xfG5:e^yҙ]wȅ_pOuYj InK9=R7Z'vz3$#{]ʀ~ I{Zzx ^U6!x$UU?6~PD KMnFVWz.Q +Ӧb*1 %%ʨW9DzB-)ޥzrI* 8{4`S$fcwQx*/"ԗzk,7۪e@"fF]T8z'Ds{)``0~[L$2'O )ocSVMʣΖIM k㥏8&V-3TXgtU޾:TvUy{Fw+.CX,pUwLmዼK%dd;U@SFvRHfay`;v b[VƪOv]:n6lyS_)+ ^Sop*gdyGHJj6Q%*t#MXҵZtwykQuZuDx頺'0H47{emw^D^^f S6'd9'M'a2Яt,8:U7dĴ)w"z:|R 3tGTtu4l;6HI:?'; lCfFZVF:'E≣hөV߅ʒaNkdM|Kٻ9P=23p`NdgQvPoNUS3T\򺲲zIUKUiS]7LUTmc--QvGcޣwǺ7ϐw'2NjZuf5hq:Bx '%쭓ZY58*6u+k;A|%Ϩߪ^@Vr+JLwZԢƸgfr{`.dfZ7C8~873PҌ {w`FWeJM17GŠ'epNFVf&Wy8c%WP ] XlV1*1Ut;HU>$S9Ee&I2[Ps֋U_zm߿aLc{L1imNa9 m׍b 1t(.U7ʶ@ө)Y']v#Jd.|UP$h[ i>vQ׍ssrx41m@҉׸:5TwcBWj$ [p ҀfrTvCsPUuHT HGCYbeKMo]_Pp7B^}`g&4M3*QQS kTN&(TZ!4,8L:qi"Q#1Q6F:%3aa_ܪKnuNuOX|켗?>{;wJue,՘ϳhOqfm\=[|W" ȷ;HrY vv򀍺Hֻ膔DVmW4 ]=T=D U"( kiRoǿEEWs?bY._{w^6L04uUcY598׌߽݀i~v1Jߤrn?R_i^ cxǶdew<4{)=t˼zaA[dGƥr;fQï_}9m'tjiL{w|!"ϲ,\JK: ??Ԭp`Ny(~7jkG;#ugͼ<w! ЕXp*5iɉG<,;OJLM~:-3.Ӳiϔ;,up_sj2 ?P&[2{ P_ȯ2 =ՀW 46(꟪JvjVSnvo-=֬۰)\!׮kTÉ5.Tx)kdg*oݾ!'_ҙ'SCv Ptч}]껤z '.e83K) q -51}%BV3t,V rbDz~3~vQ@ȶK\5Qgkހl$¾ĵ-mF٭d@= *t:T>!L_7oPeݫ4-KJ\\*& PwaJ>T |e>:㿊{_s{ *VVt0o%3ilb(Ҏ^թVes/X^ʼnrTf}]:6|C?-1섳1ʙ~T}3Ts;jʣ>YbVsA=JUcUGn^,LFH`?⻝R iN7v Uyt`Iu2J:jzCOW|9:k,ic >fWtKIuZiH>$TX3:TN]vb3:ch'8Vuuj|xwez,n8=|;9EgmiPև]q䄬KKN7U#]Ql\C]vNWy+6wbgLWu,^sJʪ.R + )= Y A 9JUN仵TꤲSRu7{{'d~K-cgݒ);aE_ _G'1>If> hoF+,I/Py%%1ɸ$MRݥdlV /T}'IwKv[_:\$c4,l#*: ~ꕮnxAɨ҆VV mN ]uPpUC醓0ˢUW2¿UXIMtܼ*I}$i= oӪ~v_K`d >v]YAPohQ:FrʸcgNbtEUըL̚UٝhC_J3?Ug.TmY2Rsrpye5q]ץv;t:E' )!/QiaIx ؒ/+Ҙ6::,=[2z3AFn, TcdAS$t_>sR%Q4!P/#uѩ߉-TBVIOR!M*iJMۍZ@·!:czUŀ$dmeH.U^#uuxW'Y2橲첳DEoUVrˤDJo A%~|*L IDATLF:,ӶLȉ *|8I{Ǎ8zΒ0I]v轇cu5N<^B!R!BhB!~B!!BG!BhB!~B!!BG!BhB!#B!4!B ?B!BÏB!#B!4!B ?B!BÏB!#B!4!BhB!~B!!BG!BhB!~B!l6z^a*e-j9ˋl픻,,n^vGJ~>z'܅ Zgl_OQyJ510jjvxFғ)5 EۿS6uSTҁ kWy}+O;2HdOAD%_WcqJ=㗛O=f׾ ƏP o&?U?ƥbp^`+pQp\.G^;:~!ۢO 3j>-1bP7[Q잵Ёѧe0o#J%OaH62Wk/I޺ȗiQwzX 9z7waeG_a@jsZ泸G=i7]})9ߕ#"qk&:m#eKqp~SnN$Z% 1glW힓{G≣Đmؓzk+vO_k.J|E$yUw7cG\Qb2*Ȏ-Yꈴ~ 4էfȡx"5% ?}f>|Oˣo@r z~`n'~KJég%Ewbrͪe7n<0i|1M^'_OCӟ'qc÷ߍܿoW; &'awaӆ5|O `4b+5l'Pʻ (# NJM)3q>?x nPo?{KW„/Zl) _>oPs+1n/~=17m 5~ޑ>1mYTVo?}O ~ٙj녮8wjUt4''*G ./HU{[J*c7>P^bnv<<4 }/ 3,#!,<t/GDl _TGFBBq͐MuO~f>gYTVGf-q _i7|sOx0|EfG߁b]e=joi9鷷}-BBQN[㉮8m>_R sOL|]:!a 5<4YTgOz8];lݼz/l^0-^_Yzǟ~]Q^q}4ޅuc] ӷxaD~HWʻ :pJF}_S(iB9==+!w=~ Y]b[]f1ZG"4,/}6h~m@X4J> ngd/ftKW<Y%6Ŏ-iذvu@.Jnr:1x kWYY0 ㉮8mLQq@6oA7sv ]]'F]n@™uE;tYI'P%cxq&'"JtgVwLZx".Iwaq T.N9QJRrԮyTj:IxFv"npmnVQcq8r(я̳_ˮ?xN[˖煗)Oi{;v۬<7g<O:S?+/=՛w{е~Coy8~SKIcxJ.zs ONGıX8^@",6ao ][jZ;~^:pctqg\ܺCۊ~; S_޺2*Z_k~ۣy^]0u۠ : vjv*i%UB"¼ګ_!CGtɛvq+?Oy}(/i{Ki5fJZ㷟b?(JÎtD~;nEw?qQp,Ё}5V ?''` 3!帢[m\޵~q9bN-tXyQ?\|4z4i1k)/i{KYTNY} qgI +JG$ 4M޾ UTjWo`wa] qqҿ-^v&>*LfޏǑ,~,iԴ^(/̛xf񸁮.G$cnH/C3cш߳7Ä/S >70k0?r-u/|FM[oѶ&yϝ=; Fm?6!do>Uw'_g1^6PG*! Rwł%_Sg1#t]iR%y87x MGHń^B!~B!!Bw6-*e*U邿5z3~բ Ё[u*>}E|)~W>7 hծ w?ћtxi%<O¶Xs0-(}|W"bjv̕u1~ygَ){t<`UMj5 te.īXJyڶ <5/.]7 UcjUGǥn\bz_}(>aoNNN6@Bh-=b \7\ 7/Ŷv7:= Ɛm_KGrφpە]л o?\O1[i!ۖOAl=b \.mS9Ьu4hs}䉦-j?X5j+ko^SZn²D^nn빹9:dM3ROT(K|W>-0 oB[霷ne$sojU.iR 'gEHI:_Yۯ$HR g3XyG,|6+fQ0ѻi{mý>R^]f2̛|74ߘ&/FN-n/~=17^R9ןA(9~<3f=-Al gŭ@=U]WɰHp~bK6=ތl(+-7aۿѧ5xt ߥ:)Olݴ ǽ`Ŧ<0i,z!GU=KqqDZbS Lނ6O_v(kIN^Y VnN+^æx|],] f$.}-Œgw'o;xs_?m<駷v*DQZwa#rŃ{p- ~HRI '?}95a.@W:v1*sqϸ9е7]AwFMTןяC!_{?739!w囔 Cpǃ3hâx'F>DWW'-5-f<4;E٣P~c]uXZdˇo,µ*s}.gᅩ{}"(8:o~ΩYw_{q1y۪%qCk ¹=/=Yϣq-I>[1vhи9]&F?ݫ(-#!,<tOھEӖϢis GTĶaZOUW*N~3vX鈤*}q߿o? i?w$_l{/8+ ]֫6Sg{%&83y_8 bz|fАsמ_Tl\Rv_ѥE0MHewƞ,iw_Y} N|նSw$8vlO$8.=.*s}.>۶ti[T,y}vW:ZozDQ}w@y4KWB:.n~[0o"n\df.kYsI[4mi=аӺЬ>]u嫢<;JG$şW|^xoS}GT#ۼ-:OI:[~~J$<*~SC9Y*OdT4RS<>3j\; o9аQbݲa& l3q>q%ھA!~?r(</gQYtG׮K#0p͐˅>DG9;7N;ȪEUq[Գ$,VKtp*TYtDV0m; V.}-GIP]u嫢<0eH-Ǐ‘mY`'OI*獘7^hS'*2 %YI' ;OCNHطq+;Ƈvn)b;|15j{|6J4:>3SoG#"j}`o<~i/C+o*ۥncy'ʰz'YCCq*5c*U呜xTY)I]E_|NCGs'݅3Dž/qꋲJʧ۾5NJK:3[V:")/Y s_/ &ߍ)MF`$ީ\:v냸U_sF ^.=.Bʯ yͻ Zݰv7:Hv]Du۠ : v*r>yX_>7bc_Uq|>XN?R}vmј֫?}*(}3}:6oѫЮI pRgIq+B^|}~mŖoX5O}u롊S.44sFgtDRnI_;Cv]кC7|%j#YhxX4aY52߿1{2r=G?WN ;; ֮ӳ޽}3%tX~9 篼\{=F'F|j.3 v"3#"^ΩrIxaDlX YX,U=K˴v7X̌BBjNSd|X0eeO}u롊S.4u4M޾ VNJG$ł#O}G;~HbWR-v9gԨG+KNrOC<5>JTYw<0scF?;:wXSG#~o fN/ƌg)豇B3H-GQbūLS3CJIDDUE.3o}MZZʹfo6]?evoߌyviFM[% ĤH?Zu+ XPv:/.nj5k巓/>p8r(#뉔Dt8f?lxf?][7Z:ixV,)߇i A҉chץ' 4njNGe]k! -uya+-T?T=Tte>aMrQQ38_9=+[ '>~Hb1c>b+f >G3\XR'r(a/ye_at2TߊԾrseseoPtcGBk-|HI:\ٱ u} !zP|Z/>Ƌ !'' 7F} !4EN=;?T:Hz˲%73K!BÏB!#B!4J?A.\:Ǐ,_B!_`|W"bjVB!rWoNNOG BlT|7nX{co0t`{i!dB!_y'߭x7+fQ0ѻi>!BhU&zaFMn#`xl}B!LuDZ !rVQlKI:[~ aH!~7YM!J?ձ[ĭе[7b IDATl}B!UToUХEػk+0CFcB!_eEN8UȎ@ ?B@M: %<~> Ǐ]@dܿ {mr_NjAO<~7?jA\1 ;+SKNÄrν.aG ,^B*2q) 0?OfxPp 8$}-R] )|fFScŮ@O+SE*#KOJ Yv]g+CPA۱AxǏDۨ"؏9!39 /~7F cSуZdef *:Zw|LtБ2 pZf%@v҈j DBH3~Tp5oMZ"4k~cH6ظ!"%͎ \գ.j_xv}+\޵VPẇo-T.O%u/K,:50ΫзUV/DIK;`(=E]eYO6B9x~#w.|a'EON>__9O,:=Ubc`骝eGQT^~F^^.] 75V}Ԍ1iai Fnn.}yO%u//2Tg0aeܺiɎ@nty1`8}`x.{y:y}.aV㡄brS.SOzEE^B9ߨ_t #Ԫ'|j~^a/+i\߷igJ2?p3<L}zUn#{[ר)sK^^d`z6l_\uq?(Gzz֓,**#OBvV&f.1pa/.sy`3=vdga7xvfJOA|4.T0,,m'EON>__ْeG]8B!#B4 ]߻w /YB!4H)PabFY 1yV66hRIKMFXx(B!Ưsxlp|?0iL:\߷)F^׳g=Ӧyإ8B!8WqeX`7{^x9PyXC! g* 1^cB! Uk<#B#BG!BhB! @d&gT!*.BG _XnaQдDUφ&Bh#;FJSljlc:_r'BG \LK4 q>ذw$OۗBG2ғkwNs#<ԥڿ#T5ƵQ5, Ɔ&BhL_u5~>2,ӞiiLu׎fcB!\|Aq5mѺI <92RBWY>BWN}4!Bï\}]|v&5!Bh9~ qY_7 P$B ?g <4_܂O^h17BDh B`7 M~(g\.qW3h兌?z !ùx3F^ïםj"ھ D6;<, B3`DFL8 jEmդ=rB]Ճz !Bï{w]ïT W?-7AdX"\u!mBhK딎@wZvs!&*7^zjW(lxvg !B@W+]ɳpQ&Adg""F`{#"垰v-jJxg[5E@RJB!4as ['a{GHP#p#24~?5D`6uB!s?tLqj~u0 Ԉ &B'EqCaJDDs!Bh9j|GiB!K!r? !B*#BG!B*t!\0+B!!BYg<92B4֪@ % 1Z(jƲʘX1jF)0fǨ h8$)) QF$||o_~t~}Yk{ZU]3:mv?0 0 ;~n+0 00ջ *"Z%YΉQ/L %̵162_5!aa51n6~xm1>zj`1=|3-N;qq7aP9K{7YW {1=l s-.tZ\uژY\ ~Ƿ%]#3~aaK+ބY,6mc/ajمژmczv9is kmajfake܈-ы728+0 0 ;~$q&/~5w_[n]gu̦Z`uf\ S3v_z=Ώ7ure<0 01w{5o-9?fZwlήnNu~7JMoͷ13\kgϵ0=niჟ.~?m^0 0Y< z8㭟Y:/bqq&ǀB]cF]5-֪B]srkn15l{I\sg۫<`gxG L\}*꾆aa;h}\}W~vɱ G\*Cvֵ-uGo/7^e:k]rW_0 0 ;~S3 UNߺ;!;arǘt W9ysm5y6>0 0 ;~LʹOo:o>x~dcFO_;o`f03Ҋ;0 0$|Rۣ5uֺl %u Зݸaa4\uOpvqm0 0 ;~)^w17_?y5v>0 0;6kTkwW_xzFG}i?_]0 0X\lcaG{#011ʦfffP5vaq1waa;6;|=֙=ŗ\C1,[Аw 0 ÎA^ qhpW/\}:yt;+ˉWVP6 0 ;~)GVFFp1wA{ayZ XZl-=E `dt O(FGa;&144jC8&moq++0FG0::M:v0 ðwcl|cXZZDv KKX^YF]wX\c'0GG111ѱ1;|aaUUdK]@]chxxeyyZ1 0 ðG:[y Ωaa}aav 0 0 ;~F 'Wmkaa@| /aaǯqg!yxd\M<=!M7ͯ9Oxqqуguq~?;~xk|\kN=0~m0 0 ;~p/'6>wxހ7w>}:T5xYgUwg} xÙ|+7?zpsgvE}OG>% 0 0 b.<'Q=9g/cr'WpG//G\#uԞ?<uߞcb$W<daFfH,'?3;3Wު/{pQIx!}g;^xN= gab߃,K}Oy (.swzpӏ&G~^׉8]^v;6w|x*n͸oO9daR8 _"N=0Ws?x>׌BIDAT}ɿw<~Ιgg= xxcS{oĿ͡G?O}}pݵWgdaFs_!{к7z]8.a1a7wW'?g0 0 -}aa3 0 0aa3 0 0aav 0 0 ;~aa b" @ɿ;l{򙍿ۨORuNwzsݏyWt[m^U֖ݞgѡ6kNm lS?wҰguƘ 06{T]=:g@'vn3F Xq^c(u6tIj0Z> K}*b,l*}׭RmČY6gUlԺ);Sc9.d/Rkێa8S v}S S% wS۠DF{FIjK^Gqvע=(>c\yոAPghF9kj]6%G,y*SXT20jX - )`؅өD,72O5r2ZdTvɶsua8x)Ks=b)7:CCQq06-f9(;:٭QdSNFaB/"<{j< ڦ9_S)r3vja(c+l16pGX*6`ɝg*hOJ_!}g㗊J{q`ej Sx,M";sC$+mGrdF%GƠ:>s:?Sm :־H4kг*cvJcZ$/uuDLv{%e0R4y+ۍLrzB.Ͷ5Ew"kMcsǠFooi9'VmGyTƉgK^|ٶol8h,\*3xH-em]j1*,2Q?>ZBp4:f $¸E V(jkVjΦؽT?L8cMQEj61}oIW?* ;Nl߬_~(l ;e d$K=26<&~po~SkSW2 K$jB'+%TD&., #F{yv ?Èr:A@fGMdۧTN*KkQuӠZ(־N36?tdHIbbվf(l2S:~8%*>kHWV$U綍~)i^oytcyc=S)"YcsR8(8JhF37]4WQ%ķKΕٗFJdIxTʠD"Ԝ\)P.S^'alS2en6A6fKQ5]G yQ@7md)iP[]ke2x,ON)SK1ҶMeeQÄhR9FMcsS⸥2Cp . eJ&J3[Ohl{F0 Qϔ35RH-3Q^jbFP-ϕR 3ߩ1,J)㈐6;ya;ol۱v-ӕZ#b̪9Jn2  RnNMu1ݥC"J}0UcĊ2Q.X9:pP{D05S=ZbDw=J# 2 +i8ETXfYs1go0k/@">e|E[aj>yR5W1R9QaL|˼ ;ǎ36g퀦"~S.w޻eJG(kw0ڳ~fz-EQBev0zݞOeu" [klLHu[E\yf=PΖVzAAd.߮"`島ݞ#=׎MVS^=zRvD\3$;ʝ?wWqa%J^ds9|]+ HEDXV_JY\RP`/6Z)CSK);hbK@)b c?Ƀ`?r(^ WeD\HyPEVY@ˣH!S- ưJ!a9 ;f.D)";@lCTR9G1}%n+-KSnBȷ5zY mJEஒklaSlvX?t V])-}_RУɨܖG̲b%UU>CT\ #<ْ@ 0hzjܦ؎TT$MoX27B$N)@ )ۙ:9**=x\ cʤckH*"`NECWZUbJ&RބS+{}%fNmmpLcX*8+ѹr3 ~HHa9|'-B͑Q ڏT}>D5)XԤۯ؆\W(-9cTJE^b%ޣi[%J)0 l7[3_͗yi(}x#eV I U׊O7U)yK}]A`):yѩ{0ɔSM1/>KMJlswvr <؏u]f#Leʀ8sDUșe@*v,LK0,#3"Q$/YJOT)mpTGf=ga7LhGɺiɶ6'1=16$cA˂F( F(Zɰ() )ʉD1xɍ\77-e݆aLMW'"`4+ð]; CDp-RU@\< WRSqWJ9$)c댶gBETBQt֟O–Tr2Y {?qA(ɳ|gvq*-91(ѥd]bo1 h`\7%O'eS_/Y=6*<P뮬ŽZWg}Xi\)y_92O 0yjbqAxQ6@TPcl%ǰTԳ0x0֕1^=a0a;11ӷl؀O1-jDM"m.U>p)Vqz%ĮjURaBٝhE<+Aʺ S6L6^ETHo*1?7i_S@Lf}jGީTn[4'qwjVg6R$T>$}e";af!cvR)mӰ1SB]'j*JN.!q_Vm~RW6m n9m^RT5GXVբo|RN[kLsyG,cVR)=|&źsׅ~ѿ.pUZu|᎔g1hy;xl)([zH#0_{!%_(G.!rI`Ufܱ4-xUM(gOn`jqnuL0e!a +:,8o>Bd"0&(zX9Ȣ &IA,[Ȋ3BlT\*aٵ.)=9t5Ep;bwd[q5>qaal'5>=8::c&<: 0 68fb sN܎m:scX]u0 0p GOх-#[36 0 .raa3 0 0aav 0 0"6KH'IENDB`zoph-v0.9.19/docs/img/ZophImport013.png000066400000000000000000004514021415176210700175430ustar00rootroot00000000000000PNG  IHDRfH|sRGBbKGD pHYs  tIME 0-U@ IDATxydGu^Y{uWւH! fnc<όgߗω$dĈ-Zho>u`IHVユLɶVH3e("x%x7I,s1m",k$ǵY#drڪdK[8_G%82/;-Rh߭dH$T{2b9[_ܘj~Ge憸{`v̄*QqR]؃HZ̠"jb(8Ueꔆ"V}+zc185/'I掴vI1:OuR'65>"149/dEk1sNr]]Z­2*lU k+RaQŊe-TlWyH"+HZTK/L,E= !ayNܖ,#JX*+3+$aY2's,%(a$keB̘e}eo$ߥ׵W$lWYUUPnP$Pzm/6Ce# lem+cX@٪zy*K,kY2s2 [:h:VJ{&aTJu~Uiˋ,'`RV,1֓I字wmOV*cT}FUn{Ճ.ymU_^،X^9Iv3ݯI"lv_sV͵wq ٶkN%OTbe:Z $U?:U&hEWT& ìf0ɰ_*c'%GO: o($=9 *kɿl G<rO22vHȒ,K%;ewnd8CFeE9AcdvOT. 1' k3YF3ɮb|/[NdCE_R ]#oK"j$k_--=hy5sK˱Zkdb.gM>A$*c代F2ք%M2A8kω;}GPHfn1ıa*!d,iF] SX2~B*_8Rls+$ȰxqI%dHLD) qRMT ;w9>e}@>+_lze@ewäJl!2@_e8IFdPfJLh3pFZGi{s%=mu>̜f̴z:*CXձٯYVU!`罴QZuRW|k[5uUmP}Ѧe0+L_Q X/ITIW%8Vn+ R p,kM0LI9⯬z@ɹE%%\e}+p`LU$э$IedE~9o1nK״yTI5+N*cc-ڼ]$IYCƸگ2$q,%de_KI68/ Z5(oZPa`BAӰҼNv`˶]1+#ױBzsUNư%w-bfrAМةsiw$ ;Z4U! HKVN껩 'S6}FӲHe}T5D%fkxaI3,I&I+@YJMY-?OK?k~ZʬL&22ewlwR*2Ln҈IeVMuEzsV(I!2; K>9ٸӽ22s[@|XISo%IMRʄp(I2`Bc I6lP䤖|xFcEe8aUB0*d$LZHNʪ2T1>UR]#D=d{i}IK?:WLjhb> 2W'ƒn sF m,01V@444~0bu R ?!gj~u,_&cV]iIҘxJ:vz~\D -Jk,kn,vFR6>ԻomĐOX 3D$H %>aSI$ {u%I^[5J)`B90N2L$,lfٔu>^= jN߲)B2*,P-U$W d˦1TdX9A%ȸl65#spȇ=[2}>&IH׷[1d- ` a7O?}M^4]~j0גLrNXr/7K_;ZNt+{VSmR0kZ~?py8IBWIYV)a*IzJUR 2nl )1I'݊m886G\G&q cRPv+YvYRYvDgWFޓe %ad$'cDPIQ[. nFōbع86 =c_ٵR5Jˆ+{G$El> %,Ge:h՘R+z R%2vJuA~X.m0%/Wl{9sY4]fv{r H.0>q> Ѯ\9g j\TՁYqLie*kINȰXICƅAQ  UuFV5eW:D'נ2HpEy6:]keI$*UuOH{$Hv03Iқ|&#VRzMkzR5r " #vdѠݑkfHdC+涸SiY۫{ҭ긿fگfpZ`IJ% vg9zaPC*$aZUc؏4io륮2ՒyCbRM2fl#t_$ޢ|##&I)%9aSezU,HeX{(tʉഝU%aE Qe’L NU^, c+i^}PU3Ȝ0$Ӡzz]oJي@}U&M92c;?2cPrA-VeC%M#UkkJf![oIaΕy?4Y官3i MIʜhugZ~YiZ~èau:jл_A(OQucSqCQ~fzU|FJr/mvP5Q?X$Jwy+ ˗f?&1l*##P/J6B0nZ*L;8T}T?$~t*# eXdHal$I3dl?MY0* nX/=U* 9M#e^se2$ӤV/돬|`R]#ȉzf?][#ZJŢWv3i'U#FɖM$QP5jPH/T=P9A.x| *L\/p'Tv2A{d80P2c:Me+Ŧ cZek/$`jUAYYR=)0IBU3$dٵ4$Z%vOj%jm)嘔Ras{a/<+)V^d l'W^A!R7(GqQ; ~ɗ$ Z$)#%omZd2q,x/(C&TdUROJSZ\*Ǵ$9HjdXbID'k8N~{M&\ 9G̢l88/:&31Ɗ J# I{Q)k+rTR@&MK(c?ہy`Teo^iW+^ c_oخOUfݷJ#MVUvU6MפHY16uӀ[A*gUh1 qLA iR^6PoE&DZ,#\ɲrGMّ ]j*n ѯ' (.3G$ e%9m4 EJJK9IyOFUA!)+zWQT?To1H|z`Zf"ָEW/l>|%Ȕ7}{;~+ZVmdefqVK%㧷e؋Cy'*k02V,#%P N,',`Uad}2P -]Β)'i*v2 jT;WWUIC5Ul$ ƭתsf$9CV1T C*JZi]ZssLlH#NR 8I^oj8O7m=+kЯ4H2W~Z~ܮ`gh:ɜR^/ad0-QEv~^ v5 5őTVT Y+NQ :"cj90?a☷8vT*llʫ 삣Wa\% [!8Özp45ٔgz KgRrGJc- {N*T)^`ǃ\ RMdIq ؊$=NTlXdji09nѧa-O4U[ Ga9zIOZ3W>iF9Q 1cT1t,ZNws jA1g{ʘ%'I ُ*ʗj=$}zYbߧ6*G24tJ4$J/UNYʖ+k)ƀczMG֋$g2~}ILJ*"cG˜6eiezcD7wJ*)Җ4٨F1I2]$މZʴQGJ:Ć S$UeQ0iNjiʜ%_IƲI# MMrH5, T\= Ȅ'JJlfZQT6Ӑd\.I8ٰOrء8I¿ӟQ5J v6LuQ,XA=j" 3'aUĭOȌUE׽^8k:)evyr/L* $Zl^R*&dɒ2"%G^S"a]E} ~eI~Gy*ie;ҽkacf=iClqI$MƐ֎ܩѣNiܐ$`PPUՏ_'m*mbEEIG% #%üDR~e$'21* reQcdY4 @ĸj5YE+,#?2r*kJERi*jIvdMn~ɆJُ2Jjߤm}Ǒg zM#^bM:L#v崘~_i AuNˇ[kHuנe4Y~`{]?^nD/%ݩu-g+IY:e"|U-8Nf2V " 0R !cѩMc*H?NTh' G6J:T^5l*KX&TS/aqTSy0TeOˤ œW]T$:WU ƭ+|@2rt]V9.&M r#Udhk{[ِ4-0DCCCCCCC0(e70wcé-/[aH7PM7f?eyUŹP+N[Źou)ϟkiиpl;EliIиPo"o5# ЊlS+v{k~k`i~^7iЊu#hhh\3m DЌ%X{{8s(V|oe`p81}8KxC&ctb0K]fn4f(3YvM)^YbQڭZ]6L~<`Ky<7W9w% i1:1G0 C חխs~SYqDaH&clnvahACCC+8S+ [/>M-HVs3Tկ_Sbsg.u:MOosډպlsyt.p;m榧,skòmV/|q6 ]榧B[0~:<ΟO\t Bbx|'wݱ3gNlySk$w}ӷ221 @^pWonkU~?vM۹mܰgN);7{vvs=7ͶYi r^~h瞷|g*073G^f@Њ:v4ٵg-Y^~˺ /*-{򝔇F9{/=ˬ,^Y}7,xaɽ1 u׮WPUf[>msDmF?SdkJ]oV=4446и!lFQbխeb۳<|q3w³o w:\ߨU8W|κ[+pmi??\Yv3sV\wr?Ťmk?L;̙GW/04:7RHCCcShFRC:ziu"aZXUn+2ʫ$gq/:GƹoغD1e3K(<7 m%HZعGщUyqY=4446f$54cd9~u;6pk oJ\0v\~ 봚%O~Woz %*sW(mC)*ύCJ zFyx(17=ŹS 8w%&vՃICCcChFRC:zFug|'>|ϱpїpt ??u 9{c+ N\̉v+k=3jԙp ]"SA,_y|W[^qHV<\g<ڧl04bDHB31FwVYd~ܦהFܳwE߮mZQ^7.g 0wI?lpjhhh\Hjh\8p=|B$I8C7sgȫ800 R IDATAyO춪eg7s2[q 5t[ihh}a#x{OЊV$5444444449m K}t[ihhhB3Zиzv[?oнq]GSHKum HjhhhhhhhhhERCCCCCCCCC+|&C"ʿI\fn.eݯFU?)K~ӵӴƙV$54444-~&>z]u-=_Wc?Qk5ƴ"q7h_x_yoyǽxǽlxf}m|㫏Y؛GӼ=}./%sNICfo~zm̻_6]_Cw[|?Ujׇ㵣|~D-6zo6"QCCCCc /|b o~aO]qf|??04:·G'xd}Ćm?|0 y-?S?\^oҿq0s<zAgݿ2]twx?)_|\v_w|_>yҦ3!}Og뾷].voݬ-^)[k{F~_䎻/ع{?=|/X=xKϳ_}wKHQTkxKǓ={} Ϸ1oT٣g\ˈFvkPvdy?W3z{ŷ~|_NſЃkhиJI}FCCCCCCCCC+ZЊV$5444444444"I HjhhhhhhhhhERCCCCCCCCC+ZЊV$544444444"qz9Vܓv?{-Z.4\hhhER ~w?W\hhrq2>\%jYmB˅ -Z.\hЊ:?|U}SV;hrB˅ -ZCwv܏\W[q.q.#޻52j}}tY;_ x}޻uGkrB˅ -ZH=ywC~w>onxF=|+W\ޏdk?{ TZ}^ͯwNrB˅ -Z."=r> Y3m|>i;hrB˅ -I.4 7_Y:jսq]⮡פ>SЊG}xbk5 øo!߯ik׭nDz͵M0 Ls XY;򘦉i 30" L30ڵi"0by6al,;´| CQlNeQHKi O^\6wN0?W|;x Kc;8;{玽ν{&ّZGKaF>QԫO?Ş;(2,-/X,l6YZ&cB۟z"\l60MBEq~z' d3yML2"2fBDA q0`XD Bʼ-k՟0\/~>u׽וChN+[cuhng~k-VmZ$LP[t"tۦ^kjeU70"ymlr>r :e"att 3 FG9η޹ryynΜ9Y>} C(W|:&Q#>xua)cX4u dHb&aij8h/^[ Ldz "VA#E Da1ͫ݀),ʅ||qr܀| MVnsmz s޽Fe5B3dbGi9yGx=;'`t@O=_}SsL_8OV%[! !M^ՁbJA`c N# MLC$劤~6^G6#,Z.pC}[s"*Tkd`E ה-V(4W!Dmgq]4Za$ C ֔uvB5y77+OC+WL)h&fVi]JEX) V&A$3(eKԛ:`ᡝ:sb?Q(mm 3 <0 k43}8cXԩyZe?ǎW9|ϽpDU&RG>)Bu]  +ΜM'@xK,ZkŒ>a|l]|飴Z-Lfl|Bn>Y2<&lƆv.\J ~X%6e+"s2e!!㸄DA 24 \-À'ZMPyټG۹@,?4O$B>ϙ3߉p& ## Mp1N?Gˁp ?9vlZ!ϑ1#Dje&5(j5H{&a`a\')bd-545ZjuÐe:cc9Neƅ OAq<(e=8;'h-\1iNGum Ghw|&Ӌ4k G0By@5A>p)&n2 7OR>8E"Cc#L<ô&NǠ& ڴ;M扌2 "Z l493M P/ l{L͞4>S/gK;(Joc{!L#'o :78C;=^`8Duq/~ ٽ3]ayz +ccmu9q9n<4I ffIZ<8{c$#k3c=ΝN0CCC?gNclufYX\`p@>RO,c2VF2}fdxygj1ٱs[g~ackxmyXV@&ap8x0V fDFiXDQ' #?""BϾ EإV,|4# 3䊓|Oaxdw};?&3| n`[*-8n0ע xmХdD,U,[r` "׭eg1MRYtIJVX V4m2 x˅ Fv\!O&^4Mm7މJ%ʃ+x@qBvAQ8wlsϝou3:hy ̟8ñj ;aQȵ#rI]D>AB%? Zc7#LØk[ Cؖmv8z(ct:-j*K'p]%*|AC="C~HX06<)N{=vr0KY|3s ]%e-S%I˩C. WҰ3u+ufϙ<\'og5] ˝e<ȱ'D/U=-+Rk1lŮ[e͇!ZQziZxdsݧ¸X#Uuװgm[0.[V\g0G@@)56:;.as ō^ܭ~·׭o0 1 cm{wUЊ#GRFS67S<hm[{q@DG!Ě)פT٬Zxn)FQw xMbPuZ T+8̞Ņs윰Yn1,FŅ:l̐ PȔ\c Uַc?#>(0 b~0:l6܅ruth5=|'|Al&5i4 9EdeA2<< CYXVDڤ֨2uf?x D@&kG`!a -Ķ,(!У8! hmXF^ ma ؽ{C&pAKg_̟s c[92v@aExK T\rLU*MJ2=Py !/t|0lJAcC|];oAXan7Ù&daj5C m7rq:>9+׾9f( 3sD ˋqlgn$DrJ4]׏BNveb[8E2b Yv%C]"מisO^# [ ]Ο:DžyGGܽJ!, $ɱw^[L9E6kQ,O fBFiok,/U:-̂\$_̐˛VbJG#cxNHdgL Mal됳KN 2qjN&c)>!!%Hƶf3 !`"L4ysF\aV~ֱݻ5[c-t ^007~$0WFAZYvvy"ɮپ î_curX: ٳg026"gϳTCxAwn3Ew7[n WV}gbΫ?"3篮9[CZgMƖ[̫[j{m+7pEi4" .*]?vBL#kqMdۘv\.Ci G`.A;gqru]'8uND9K8{8aI&v{Jc D,v^Jte@YW[8uzw|'%Pdl = %w Ȓeq]{^MaĹY=iR.q>333wϜy $hdmrt$w>0vHcfE9lH|2^GP9T|Om0t. VX,d2]l4!KDDa!0-Ϭ-&nDvO[+ DZq𼀰lv ms~f<cw{?4?#ƃq:F2XhSo%QvMLFB)a{uDaן4r#G&+|JQg]Ӟg9?S׿?P(11,hn:Nv$7YӨl,äѪagd92j`jLC (CFdReYvЛbG\c&b˃6F̎:(D s K֚#al הRxD."E[`tt!L#լ8.ШUp-B[ycK0MUf6XՉϪ0"V[F Z^QWw++^]>Hnm@)*~Oj]4VB `FaR( ri6ZMðeK,Οga" #CxLӴcj6IywmaH.q4vi|ʓ8;sP]=j.,,w~:*'Obtt0X^^f޽޵R8E\ץZinysɅ,`~1t0iFd3y:QagL… AB!G?G ,CD  :m&l ݓ#X8# ]EIOi Cbk ^YV}"b\d#C>#EfIRly!h/Y{o{g>m#_zS')m\xȱcOj2yrSLa (A: ^@$lGF3,3;sFӧTs l+2O>,;kn'vh<,;vWS,,Q,1k$\ dm.MT*_M^@3<,z+oz[ygLMM1<WF?>QB C^}*Cy]-S{t)R?]bN# C.;OZ-c0m'Lsl% npG4i EmvIpDlL3fxJu}((򼶵`D3Β)/e:q홧ͧtz?wAHU)P L-ZG7nJ-Ҟ+}dJ٠jJ4[ {(p]PJSanwA a,P)뫜;whei4lmmayAl1O 6Q"օE+ cמzj'hSN?J)ʲ8.YaLzP%<3T)G~씷=};'Y~М?E`}ۡh'7BUUuG]U_xQ*[|,w?O#lSN";sNk1Ǎ}I&!;NT!#,YQJUh2agw0 "_ҵM+WK_/7g%sXA(K i1WQ]vem.׮]Xe#稒H-& Bw~?g*ή9o&[[9c-M6ϱ;c;R!^ >18N]9G Cןe\:J{j,ă5וDQp4F/M|Oz rNBc˱Bhtppj'NI VKrza<#%d+bo&Nr)[H%+KK٤KeE\'d6,-/jjoyMpQ%T;Z+OXz EFv+B5#O=]G}DLX?a(Jx^iJJu"a$] KbڨHA 9JW.*sk¸qe49RAc M OԹMΝ@%N]&%!88/8QLY*iҴ" ]J7``2 \/RqFF?f\xU& qs ޹/z~W(jYS Vx\뢤( QڡQj_rnsZA N#Dk}R>8g@ɟiOK?oT4Ӟg1r€"Ib1M_zoN9>=($ BeBQh#WITaGݸjwaXyA e V@Ud: Joc^?GEa(d͏|a||,I4Z5}" }\nI$Q"A늲Twp}FA4П L{[;P* e?`JS2GJ\-RV.XǢ"$].]xvק(|vvxe*fӜKy~|xcY>hop}l .]N{2gTZ{ 4BeU/qkDžyvx"5ְ2Le[2}>sn0OO$bLgIDntLBژ_/s6gef]rؠqK%zelltLѠ7fx8fV]6668s {i*>/IQKK#)= 鴗<{{q)g<):!V(kxQc ^Mv4V~a!*mhuZk988bcsÃ6hm#D7>lTc6wF]X12ao\xpI`L,XOO<}|Əm{N˪>M}˯7u]8l~3 I+c s[<:($Z{cϗڜ?dNV1 vql<E3uE4MFɴRhX"8 IDAT0FKME+ڃVXכ m**s&7 HPP<(uyrlrj jJ3^GV=Bmɫ,>fϕC6q%xQGs )ď(<5H6U)s BE+;lR>K4Q>ÂFR}:4U9q=i+P鲮H AוXa(˔4LSZU >6BiZσ$h4M.Rܼ}[*Uf39ќv[hG6bZ6+۷v,/wRK1/\mF)i"`٤,' Xp#+!|?r=q?_o~#.D3 aFwvs~26 Uč6HNwf{,09t^(PVp yW{iv r\!{{H"S2Unh6TUtZp'G~F|.@[V̖hc魭s4'œ8B>dpJ< i UﻸJSK# gV 8XTet\.2'LU9y6#ITyNݑ( 5[(+Q{}ʲ$sf8HׯlDZhm݂{`srTǭaq̉u!ysh4CQd5QC'J뛟ΉY>@%$ woa2Ǣ8wfv;`.Oro-TU{H6t:(35L&c[n |h#Hbь)tN4lo.s[k$ӿG=VNB1!a0t:t:v: FW\x{o`px^z=&agj%JiFo8>U[5`"Oև0+xRUL&QrtMw+4d0Ɨf($oɪ ʃ9Z'v( \U yE.m{4Ûܾh"q,ʺsW mr(mp\,{m6ϬqUO_;OMocmN ʌT; cpbvGlL>xe]Px8DGƲtɲ9(9̦*sΜ`{{0llQ񘵵5^{'!gΐ,c HX?dZyII|=^:0Tw!~R9Lv֒sRLc `0$ V iE앬,(b^/#b9. ~-V \WiuaIl7q -ɫefT%ݯhNlF l-qm&@!N|.&ЈX[]"£ /l<ܡT evL)aPYn~A6OW\AhxX H $º a Nʊh4$9Zαc7|4q$?_H$ p]Gև(h&1>dSsȦJLkJQG{;8*xnPSI$4€(>K)HӌraڂE,VH<ס6#~,9ͣ<>Ő40NZYZ۟v-9?$V\f3̈́(BMxub|Z2T$angm^whwk+TEF ~]kأ%QM `yy*%6u58ip(~Қ6ǑNClsϲz7-vƨ9!<ϳX*tq=|ȳ ?g套^hb20 䙇IWY>3hm9 0JH‬(ka"TcF !IB?R"<8"EEi  kfm_Լ֛O&6cx->6أ(qQH*mLaL- w?aۘnڢ pЋpp8-fK5X7!M cP ~EY*Y؈^wxN*J' \3Y[g^\#8cSxK^sKFJpc5JqpcU(jn̵Y]]e,Knjsj=?8+1OG ~it>fȘMcHMIs5^zϸwyθq}=y>2 ])ԋTG:4M\(ePƠ"OwBNJ^ Co2q6/#I^|y.]By{!Zj/H9מy-FiSZ0(^Mwhwc~B yqexܽ*{vH/B$a.{.5L$n_U޹yZp"vw)lz;Pk"riY Bƣ9/yٜ;FXC_x(8wZk67YֲlVϷVf%#Z&٧t2kyWxwuPiFqs9)P:\q%aE>9wqC~^AcE[A8;[7}^~d{7Xo^qH@:a6SP YM (Njwi?@Qnh< /R",;"`K\7u,[ $Rڅ'[G 4*,ݦJ'$H-T,$;Bl^k528R@FS>gΜ!"\/ZutI^V8H4k@ cj"(U9 $I :V 4ego{[[&m̭Gf$!C^zALJ*|2/ \ ^{[QXf6~ m0mB: ],>i^& \B .H'ËrV6|<.!s|GK6"KQ"O'8q|m]{ș ]tqgt8^֛qv n]Xm69{<]<[kx4ɥӂ~H:HU(bueV/f43Fmooq-f3&EagaK]rg;|/Oh0exg/<ϟٟ!a{>Z}mQ \0 |봭8`@t,TZͳ/b..N؋@ܸ ׳HG1&"䓫sZZ R#洟q4qD ba];_'lȽvN<AFzPXTA RxqaSuR$%H,#/ڳViʲ@iR]KyTyH3Ʉ=)p\kJ=u:"sa74ϙKR=KKY+0nZklyF P8Q'i}v HM8>Sr/;nOZd8,RIVcۣ8,|/R1!5xFͅ?(CϗO. ֕e#( t"@, ,֜@Ha$O ' 5;쪠v&st9 4\|*do]{ŵ TiI#ipy<#ETuF{(S Shոbܺ*(e8FU+1B%E+U!EΙDEQ*Kg$&(qih|t&t5 I#(M }3m{>Ǖ 3Lp)>u0V#A[!WUzWR3UM8FUSU8{~&~X0=y7~D倡,jtߕ)¦_Zeg|r}IoRL<.+=ᔕwSY 0`wn მ1UK#$9!qA.Gq__ lxK4+_ ϟC 0V^dyyѤ\fП32ïyo'-{7[|oԥxmX[_r$fsc a8꓄q1;@0gemr*}xo7 ݻGĬ0<I:I3QMɴDHE<9`腧qU 0'%!D>8)2Tda $Ocy` D ׸:{CEE* p8N8 8yW9d*BW2!tEE(0,*JߥAK.&OU죂G!Y<*$[: PV<)j̢b\G*B2VVQMf v3.UUfY$u]VLv_1M.I#& G|GOGiKբ(y]Myٗ3=zk;{4 o_n: r>cW.pʅ'<5,V>*X]QYt2Fx7\4SJYf)Q`i8"oA"GVy. |#9"?)ڠʂ0[W:hTHGQUpA WV~Hi<*XW`#] Il>"Hp\<ǔ9VU8޾Ц5>kY6Y_[ccc!ULW_z}O}RsE8, qpx[' AI]XIO VVV{EjZO^`0 #ȥ z+2lh$,ҟտlIZ"d<ʙ'sH9LޛIvg~ss}A܇"EGkHf,vh!/<#G_옰"-q(Hľ@/z-*spHZ#:#2++3{HHOY&i}^}-ڻیF1Z+*U@R$+aCNШOsu~&fĈ] FC ȇeo% b?9~:_0Zsgyk(Aǧd{cΝ峟 _e}}gdۥt]B(I8dzv(>[\t WWgGR> }Q {HU'/Ta16|Q IDAT:jkMQ?W`Unq1hánR}p]9DEbl*A5c`k;֊Qv_8dʉu8F9\fN&11ʑTB$)ƀu]\CJI w`BD9::' wLc Z.f|cPhTwHd;4yL?i9NJޢwX#=bw-< 4y)mw!,pBr6ya.(7W I㨲.QJI< B$I5+]pZh+~s,vWۜ89O x[%I:Š$F=s/h 29}ӕJ|HǼuMn, IgoNUt7ZCܰJXaQƚ.xVnwg)2>˨M*AЯAm&sܳ297ҩ}z!v)%tky߅x+ş~Ru\y2o})DZPk/G\/,ܳhKC=sZ5"|~/q]wy~_NB4R; hL4I.Μ:^z/}K|GxXݼ:#Nc)q]1IR$#(ˉ [6RB5ac\cRFUjr0UN`ҎZJ<]-30 w)=N-6唢|umP6ycQyV8B8 #%BK.h6HPqQd% j}4K3trǃqÌwq~.Wc|$abR39$C:#!à-, yH~_ #dqUIqK|R=R!h<| iBa(}U( њaLS'B!Ni4lmnu|/HJlG!p!oVLA9 l&R!&ͶP ݽ66G֩ :\ G1.W2KVG"WcdDU^hQoU2P`aAEa88vnhD$A@J2;7p8^'>1"?D+J8/0_`zNaeNMZS;l1;D(BFmA!-bV*#[Zv79N,V`5K[6gΰE`U&BI&''q]hrnM.c6>wQ1;7G ,y/}gx%Nl  _f~vU~mZ&?{{{45B/ۍ+f0JHjKUַn{gV C37=,(lRx|V*e#׉nVb0‚ Zbǔe9gV&qDZF稹sT'l/aB P>SԇYCWq4ݼYF8Z3)a%̡?0`  hm`<2/OCy-i͡L"5n%:|GITKS֥YZ3ھ$L#q*lPrZ2o1&_ascou B7$Zf'y[, gf'\p|z++kuzXu]F<<GEhs|Zlll!(r{|ѨSJPEs=9u߾@98Kqܡ[LrU; 6rfk0w,/Nۥ[^'kB qǣkE<–wcC*/ ZwBK@wљ¥@ cw k |W% ˼j箳h-a}v׈]HIj|1S?w5)̔X;Α%*P#&40p߃KL/La0i3{ĘcV<&|Gt]:jjP' (_VRܸB`wgVԫL d":;=&JHt E5B\(tTk 49s}8CI'w^7W~ɓ )Z-%O |ϧ^.[[R1~33sXkyqDy-#px?dJSe4Иqmx6PV!#kqsiZa5X(Ek1HUVGwЬ8 £#d/Dx966(pB| P-EY*wdAcָ|N))FƘu[[hq@|r,̏t D Tvd $JFkp RIOPJykbwG3pQԹ)TO*e4=Sٻ)sw=p z7WG>K/?Oj=0E9hÐ0 G ȲFU(ዐF%(mX#lI &o]y~s|O9[#8$BH0hUN$]xv^(~Gy7^r4I!hETXZ^dfō7m9@syV%Sy'gr|61~'~__a4Akb 5V'O8JݿJ: 97or:/2O={[7y->㭷>!^}EΞ=˟/sٻE>c<KoYu![:l,jiMNb3MP ku֮15B׮]#Кit8y | [ctɴ?k-7NG1k-N%x<.S>`u)\!˒ @nj5ñ\2#~#M08Ȳ2R\6)%wDy::ɝ}`TAf?ݤH},ʆhecc4EQpƻBĭX= &+=PB&#c;M{]vYF T($R:\Ag)q&K FETs\{ .#YYn1^dtF@y޻|"޽C<(El\ƍn\_EEdZT"[#I:/:Fd#'fb򑏝]:{s~%}pF^FٸEFNir<(h$&YhM ?fuivTB#BZԘv=/iU#bнҖڭ4ӌNddIF'DrIyɗ[{ ^'챵hˋϿ//ٌo?O?4?w< w/'_7/b T+.iApKy~+J@DX$ Zg0/ů// Oc7Ï(G=3xo ?bRh T5\Shn<+\ ݈ЏW^!"aF^ɰF!kLM i9}i m_}AfY4_/?:Z#WͭC6R׋<Q_w8k\iVٯ$hr'|OivTy薱8GYES9hj=^^TksiA*2E;>8rLh<͑1wyA8-_Ispw<|_׶DD@XZYiL<S̃TB(tJQc}284u[{&+Ah4`qq$ɘ]RM3DɈ^Ag˴5Z:ktv_ <&'C5vuq YΟ} GCgyZ26.2ģ^kPFDFٙyn^#F[UJHPLUxm[4&ګ.fxg9ސ^H(l 08=x (r@Ry龖H'ΜgQIi<(_yCA#U ( )U. fgg4Fkd@ JJ((3pϹE&"vw ӄ Ri+3KYF٨ID(yݹcv;BF.8!.V=԰v ai6%IYųOƉD674.#ST!-X?ʥkji}tzM/z7vt w~zӧGt*Ak!qڵuj |2ڡA)]gqdYy4Etai \'S!yaI!^PazbWjF\2ٜewg+/asy {{{|#uffsFc`Rp,GkCwBa؋K@?zkF#v7{FK\RREa&im>؇9<.x6[fQ)SqAEyNA!A;bq RJz_9?H=w.߹}&sa":%~9YCaJpsowrKu0Kp na3ϭj}9Qs[ǭcם =t.cd&oa$oQ6ˆc9?3]jCSP$)p@hGJLOVLL4yTv{ .:.vi#uGD0M-Lΰ%!R {4&hLN\&hthN [ptN,)IRJ .Jd,uGBEI(/˅JhM>"wQɱdP%GheBXiVLp:M0Y`TKqj%Qc` Y>dfz"`<[zy8[cR&9kloLTfj0??CWhf7h2}? ټu}V]$yR+߾|5x};/E>JTE{"D \2Z)IV)._z}|+X>5KV(hN?G$aZooO}v]ӧϲ6OuI!㠓x٨TKȒ ]LhL0ј0 8s |IyկWO~}3l{,=һ~SHFͲ\y_c> GXVsSٕBJ\T2\іL0KǷ>Ir4-̸atVA8R)$&39R9F)X%Ahkc6js0ZܾW~h~XXDRK)1(ေ2gC;@c8b>MF~w y1h]Oھ5DJs;>.4f{&^NMeooNCJa(%='GDGKIF=;cM`RM΀{;XcugjQ2W,“x(xQ(7+%PP IDATP/Ȋr1v\ݓ%#9]ށV B&'ZLMN:䷪!ce\`Q\B1cYƵҖJG0tJ)!:qRX0\`,s/כy< ,ƀu?č3K ;R!<dEe HGQXC^$6㈢IdQm E G6)%Bw1э;_Y]SF>c`Hv1qRX5H^Hoۂ BR;z)R.;lo"Mt$UÑF(S8q=*=Ξ9իW C~&X " 괚xStޠ}R=;ۻ9P脗^zss\r9vw[8O? =~3}\=/Igc)tL:gZRu*;F7n ˅B!-  a}._ =txUT術2~/H; RY52+nDH()p]_H(5Rx9hNq*PTcg~im4 FڃqEQEZe*%Q .ʮ1ib-1_8)YhWU߃\Y{ ݄N:@xNW(Μ±.^F/ݔ,6GݤKΫ# h(E!{7W \C[KZE=3w1==MUy;|_ʐ￟Z=d=BrT<.|A'J<ŋꭒIr.33.JT3ݚdjrӧ z{aњK4&&xgx܀s199PnKL'Dc \ &#Ib+=Dl"待6]A=4l1?7'>IsgΡ…xv)sx3EWǕ#KV\ 'c\a2C5#"$͆,//)~g׮]c>>.V֑hsq֎;T8:8]\8(ymk-3|5@<Q-q?ڶ̢8ex'`OP czدJLJ))|ukd<7|\Q&YJhcP.xK@IRa4tPkXxp`.D*ShՆq^F̣4f\{*z$YZ)#ُs$u߮<H~݁-c_?G_K*=6_#Y-G*4dɀWe.^zaϑx\ .r<7et$?pƐ fSh,(J]w݅Fs IX>{F/oٿYgkc)\nn 8WeksXTna<"{.Kəi~a~b0C⸆ _3o#zmYj@2I8vHSiO~ J]:x..2 PB~Cr .cFR3|F\E*Ɨ \F8l5g m^`YG^r4.U93̂8GHzKc yHk}$P:/@c;;@{3GkێM.u-L6cy,[oe$Of؇36HHҠu` u^wѨ"KR$剫A xHTk L4\tj塇bkk^]="BOԪCXV7x^AREOG?ŸXk 3N[/3hF^5:=vKiw}CN}L5a2 |Igz99?Vg95ʼnS8݁ #}u,cqqEQ*P7pO?4Z \pHi:{]M2uJ0E sVspx8هxT!(DhH̱Ql 0ٓžXcl=OXK9q<"˲\!F(0RiW;8%#j>O}]u#Q) qL9p*5Z᳴,D(Ð9y5ypt ueY ~)>vecP-BIZ ]Wf(({B9pjߚ(ߍ '>ȄP8tcLxȸG #@hpQIa  :+ʛϳzRZ*{C~odk`bxɈZ%a?̘PXgx!E& I݌sNjq^e}wH! ?O=$x.FkIvm*&QY7Tx+q.;ŀ^7+,zb,AVgZMβ7d0*4ͩ I&)Y?޹dKIS͉*EI}/XZgam^} +Wdg쿠54&͠Wk'q-ząFx%=Tk+X8իWGSle7ֈf̰|ܿ{Ō[ٿc ևc|lGAP2b%c)l16GݾֆИcq'}e }׳dEdYJxGR\dY$@ ((LZN=49ܚccym ڑȪCPmGE~Z {<)I $4-ȋeهA teK{1[!RbJRIIӉJ,#yH~_F0{_&:ּ{mKN^m4G?-[G@*\&p\C^u]"E(^HX8РU91{!x~fkk|5ZIPbØkV4;LϜIs&ZĀHn;6s}gxy311AݦYoPTȲ4MnMl7m3(fiN.C$̙oi'CQ)PHn\\,skl.MR$EJ q!d   `2HxIIdbG3vZFQd,y8cݸdUݺﺱh4ࡳNo͝YMNK'( 3ZS#'As@QiNiLF%kc%硙S9",r~k9MUb8EE&}y``;CNͺ^ Эd⬧ʚ8@IZS7vu<[˗ /auUGce@qBe%Ӝ$JXCsta1݌~;et"R5O_|k]fʭ[[<я!?uVx"ʼnӧy]F)ήK_׮\ F~J2tc(k8b>7˯ʧ?+;?!, (-PT8MYad1jѺ8程7y9YCquE0+zBlt8cZ \Ŧlk^*Լmͬ۽;n~>*A'^H^#ݓ34D !/hHes[*hkq _E{-tJ7i Z;:asr4+F<3 FiHp\r9ugG>}pSU,\RstxF Ftz)e1Ø sl z衇xK4MC%CmH޺I\vyx5&c<+?R׎h(nfIlB*t@!IpVP%yQ dlIЇXBn7B^&۞W}٥EY*KOdiC(ˢE4v]׾ MQV@/ ވՓA7[&bz0#JAUB_k)m-ڴ+Rz(Jb-hk2@%bކ 0XU pH2 "22;K =c*A `06(=ԿY (c !7o^4Mo~EG{'C&э26'h |䙧Y8F (.Cl=S~3 6>͛o]>YC^%>ɡɲ.#"tKALψz}꺦r1RZq /w?/3Ŝ^?oBɕ?ϋ/b'V5_1^sׁ:lcҷ`Ei.(c UYaLC^,򊬕s=4wɚC:hM}#ZA1Fܝx, e2R"_|ƕcā QJଥh|`Zkr{www$XN׮pjǦƛ- sYQ15gq6GX/> &;tv<~ϝ+?ɿ );t u ]u?'>m<H{_4(ꜢXP48C^%cE!'66Ӕg~Ssϒ>PbJ)=no=>S\zӧ?&O}"?]BgI[[2|.hEGUplݾLY2+9NU Y̙3lom q̢\p 67} Zξ%L&V CʢҥwNp<8YΫ2OIo<1΄đGSK>H`2 " $N"L&*U㺒||M(uM C'Sl:Yk&@iYd(ge@ᴥ+ζS負5s0*拊h#z'NDwŐ14E8u q#Tl@f,N8;䈫7nrM~5"jWat̙eY aStssw-*8AqbK'Y_ȃonAah@L&懤Y}OsQ{G{~+xʩ7`!$ SBʦ@cI:" $RqK|o|ݽ*)E_'J2(G?g~ljސ$=4Ih͛7֐=\th|\A$Z[Bc([r4?"Ĭ=4u`mxn b/qᑳGe~?p.b<4$Iy㍷I Zf͌(zub MS]U i%7$ l.?pY(dP<ϗ?G)v*.Wމ`Åk;z0bCm&Dhcn#McvF{sݚk$ZW~^ IDATJ:.qxS¢CC^$p ^guyj'}^wO>KcmCO$kpBDD`cD;%II?Pn?$}$`yH_z_Ea)dcmӂ$K9u4*/~ k8+"kbN]eI4lXemu[;D*`ztǞ(7'.e#`0bإ,f\&mvvP9~$ImV#n޼ ^I*zVWW9Z$2)V[+0F%4!Xk J` ߸~|wO!WN@%2 ;A( Aʊ)Z)4\;$YghjnO] N$͙,V30cOq ]&Ӓ1n@yykv΅O!OڿNuY4 YG?ۯ_vL& \|#qb,اpx]B"@M"ࠜ :nկ[oq6qr'PaďK+q0sOshk; OQwY.gΞĩ쏏LS8%08:YB? iaNqlLU5DaFcH9s:Ao}>]8Je1}W$`1(!_# `o[kY,`;eσeqӣ1na|?9Nn?~%m ]ږNT ^Jm)/6 o8јB*,4~o)Ν} |WqenRp<,{߸Ng<>b';Lfs9.]b&:W^perD 3/2E)@(ImCh"4 Myԧ>w^qVX,X[{Cֱ-MN%8nb5ڵ (a`WTuCWT FCVVVHnTC/0>|0lP;LZ-|_?`slmt(@\.ѵ*j\mE)z&Z0 {"j 5{<]t„$ \(pGmr}H%PgNh]2a\r9pSۿxA%ebܔy f>& v8&H#MC- IC#yoK|Ƿv0r9ܤ*lMƜ>gem4M|=:6 C^LG:{'(1B"ML@ENkOJ<[(tdLx^V AQF CDQa='87X!*4:lko;߼0&MQF: {ϟٟ@Eop=Z :"g>ssJdFWqI~K_dum/ݬ~ u 7@#ř!PP^)g&TRoy-$eh6k^ ISA/(Ð& FzǦu`p ~H'1 1a]~ V3=?xiA$hAcy].n Ig*}hx-)Kn7_'/X_٤鱘mh|@<9 m&mnܸ+Xa0 %Ե ՕMF+=.^|NLG0L1M՞( ~l(K8ԙM/k1+rnw/#̹ss8#hΖrv>N/>p|e[[ST%yN#$dm]?qhzYz>])g9~%" Ԇ+|c! tEwX]doDQU I㉋k_7մ EQx dTuD4%I9.<|JH &*,/0VcGcT@^o47hmsBxv9d %1U41)طTōmMW:S蚪i{=hE anI{8Yx9u+%IN^6sǣd!"ݶs< ɻQ)ɟyqga -)>gdZrqq}G#a芬Auvo2L's!NmlQZ8q4'6NcrMzSNsr!Nc1VOP fz4 y#O?p3gpu 8(zC U 'NÍ[(O|DaBN+lmm1ŧx]Ko:Y8(4Ұ=iHjDV4cQCe9zs \v#5 ʒ4PKMtG%qh(&T0X2#+K)泊BJ*[j#& QRWQ?dMm(K egc"i<'HL)r}g)]vp4k,SM7"0X]]ыX,z]) A2=k/<#s%.XO?[ F>J+@!Cw H ^j,C5?xe_T7ք=02qv8_[EAqv=m_4 ]hQ8HdR1x)wum.-T>t\CtaR~w>W*c4Wq`2>VO!5Ql`CCr0M B(I4$K׶Xo! Fcn QD*97.$eZ(v{hTc s>$~R201PwLhpWt:} pL?D@ yXݥ y#?$>C="a '㤥{#Μ>{lݾItx غ΀O>Ef8 R(0A>wxW8>gϞ%͆$z {+ԕfa{v+&c}"Q˯W(ʜzg%RaNs>{SEO|/[_o:\ZڕSNlnnɲ#泜lx$qťKWx ۦFJC] t5a,t:A@Yri1~!$CDu D>/' TkV ۍ=)̡TPW8PQu-J9:RACӺWHy:T4Ԧ! XK NCS@e18k5U䨪Xf̦3(&Hb470)h ق۷>̉€vvvstYsԥ8LKdYHY#vmՑ |4t6u-Ο;ɕ+og??پy|͓,^G>] MS҇EwpSQaA.ryÿ|+_! I2vyo}/֛!n}%E. @4 inN}HJϢezҍ~X"0 1&{#OSOpESo@^:ꚦ$H(v\Atf TE6 #x2- VszN~ˉS\xQ?鱽KQDQB(4Ţ+0r8tzڈ|đ֚E]QI e2;I,fSm\>#Sd榺*=dqB7I#yC]ȩT`i檢}Mi'IT% jj † v&Q_>k+s|7/UTĐvWXYSU=$Qpy|^~dz)׮]c</Xj|]@͗ r^{tX_wo3tm bj5۔!IMACW5 |F}kȺ)ѐYmL "RI+g.s庯: !ODJ:(%P/! Pj<¤M6?\9\kEgg' SvnlqT53)|;?2YbC _¯F;(nm4X,xw^RLJR0 O"t$itA:H$Rf.$m#ێo[@kCT4UvR *J%I O6* Z4ZR G[V4JдV8>Vq$Di⣺.Mx\5BF U^S3dqHc#G9:z tXQMMb1gZ(cw^wI!=0UQu-v4-5w]"q} =Z[yR&)Ң4N#KbVWh:rU![ X iHdLOXH9u$'7Npr$*Q$]:ـ$NZ@9ʦ&2:ސdbS- J&HR$i`$XZ2 cV̦G4>X+Y,,W_} 2ҟO2ll'>DP* aW+8a;EES7Vo~Y$#$Y'l 91Q#Z[Xq2L8' ?欏qƻmK abVTta7P1B4VWX vԒ0DAd*e!FKԋ!VXbq}oqō# T)A%T=$BQ., i;u{Lp›VF;F[o]d' [tUQ[٭2䁮51ܾ9@Rsh*oA  Or*Yg@ujP Ar3-zJ$~3QQr8+GLҐʈꐝ泱\Y̧Ė>s,JH$m`$y8lgGaFM"X15ʏq93zLjȬ$5\~2NMU aX68wkt:=Μ9ǍX[[ vv_%ǃ1EZ"JǏ&gYd2a:MuJzpM1 ovѾ8XIL;k,(F{'r :*9XG`}n–~uu"_)* #@Qy(qURbI36UJRUn qPn"*!a6k@)ؔNM|eG>st#ꠏJi HbpM9TEYطC!07a %ʭ}qYiQV`L{Xv#|x}8Hk"!7]";PA" 0JS״mRz@\ C[wŇ?|ZQ0AB@;-v6iB4ZT "4lNzsiEC:stEHZ7 {%Mp._¼'G=~r(]awMPAh9 :ߢ__i IDATG_e-+l\؅8<<#&X'ȋ)t6O`(MAm(}-<ѧ=N tN1^ZWԦhLCpsIMNàwʒ@7 PMQ]>!PӏQ䑥14dqvR+ (KAe?MK^_㡲u *ĉai $d-nK-M3N"$Ke9R, EcCE]הHy+*w~ˇVҶ;_w,{ϳ?,JI0 {XPO) iBFS% 5*[*: 0'Cﬞ`ȞbTqDZZk̦6byFh |3 ՖU4 EKH4%:DaJk,u˗yȺ+ IRSΟOpsc{ns{oBe V, E3m(sG^Zzʴnh*ޘFb4\'J~H(w{WZ 70F!+++Dj2Trb0Xa>):ࡵ (ZYVGgyw>I:! C4EKx6ͼXqBp{k>}{g%]hB8e&`O1>rGݾɫ^ϟ`;lV8|p63v{8+Ip[nn, pԔeBjmߧ,+$'M|aF=CqHO?غ1j|("ͼ9G]0)E*%Fz(rN[{",QZR AEq@FdO???bg6u@ŋOQS^{hz:of4N(J>BHsΜ=.Kؚ3gR B[KYL! Nk-1F!hJ@% @754WW&0=.Q'۹6%hڎnhD?TJ!q.ºzeTe,F0 NR$>$^ dxskpn4<*vvyq(W)tH uvw*BS,kz$H4v̚+ЉS>G h2?w/7{o555TZȲ{p-1ڡã7?FWu9*IJi[lG)k'pMEc&Ǩ0S$'$"Sw/7Fi5cEuFk+(mZi Y0vH)*aH9AIfj*eC:AkР,g>G1]HJ8.>I$|ӟ'G?eqaXf͙LѦ gC1VX,H҈ʦ@>8kI϶_Q^G-+K=G}KL}F@ތ^I>9̀cۺ6~pø5x@hٺi(zQ ae8b}8DA6wcSX퐁{$/{o֣[vZ{Xo ԙ{nR2) QDHV)W6 $Ar#_2 h&yNjq{rު8t7nS=!a/& v:8+"SljBJ5-ӓ3"B>Ɠki|Aڣm[Lbu[# *Ł~1_u5qk%폶p#]KD+@ISSB84H㣍럍|[nO/Nh:翔£3oQdP TFX]oRdKU2Oʧ>:G)u5>oc; xA%ƍ]$B$y=>ܽ.6wfY 9?gд{wBE/>ٔŲd$NzJGG=haX~|Gaw8u[BbM6\^st*󐞺~WU|>EddNEk֥Nvk/6^AOŌ=Кo}G~ē#^}+fK^~ۛC./Nȳ)IZ9G<}vB_ UHDpyq&ŌX,f ' 7v_\+^Mm =n${?$}*- 4f= B˼4]SNXecuxi=Y1/yM.<Bݻ}z_(U[ks윣em(&B_2wo!SZ~cM*( +IkuVp~rB` t,ω}^B\' 2vֺ?v-E]`O"ak<_pQ?|YSƝ1ٟ0ڮ!%A(Ц"/u50ɄmJ]M\}GzȖ+5&hI/"8䌢,Ȋѵ-EJwhwb1kohy qڃcz@k's }~a9r81i)B ߼Zbҋ}ZZ^][׷zD>Dʧk[ 2waĺ(:tS5IPF./.2&ܤȖz6 z6mQa_/B1WG]V;W׭:W ~. \6u>r~d+?77~wO~DY.8 >]y-?C)e,& ApG~sߺGztGtBU[FAL;, qeB弉nMr=) VXj<&6NVFP|7@ʼ/7r _wHYLEQW9awkE@jΖϙ}99wfS"I"VJԋ%2#r2 CO9+I0l oLgǔA:p;X1NB%i"1& jcU1έ]uZ6xR!HൺӮe")pR)JXDMK(mO ꁷ'Y[ϓ{L*HH=ʈD4%<p#Arw6/֧7^qmn)`?Պg ׀C!^s!x78y1O?xB](a؞x; c]MeD~;]N/zO4Z,9[[78_5 %#~m ΗٌhĞړ1[b1g[Jߤ1'g(_p>=9lea% }m eI&[1nUA0Eɔᄪ1&U}}>| yo-~O|f DFqЂ H0hp6K׵ٱX2|4mi09e dCygıdZ a iJKVtĈNt"#,+mlc|!ɀ/}Ub(R532]'[c jPmK4dYQOǀ`EK|mu iyk¨#/+J>({ I'uFJ04' cFJ zEӧ C2zǏ3\1l)+>vla~Nq^/e:I^gϞKE,ܹs^}.NPJJ)Θ_Pu"P[Q$ƶTUMhexb8bЋ@!| rrItDiDϪS@U7X9jP[5_[:1 ڂ Ǔ9B 9#A蘘~yyV,^uˠÞ{{YyvxEAk U?3- &hI`BVC*%ɨGݵG cܹ,aOFܽu^yً]?%2.X EIUUXcXV}ɝ̈́~LԛD~O 7y뭷~%#Ɛ=..̟=akk$II!6kOJ~+,Ysu]_+)(Z;G'Xq|s+~q݃򐞳a(a@)סm\ 3I"O!-A=}_HJk)ÁuXmXZQEWs-/i ?u0$$!my}xJO0ΰ\e4" t8ٔ"<$A(I"2VB؏>*9붣j'#a>> n(B&Dq@SW8FkT 6O)KXf% SZ6fޮ V=,أ,K^qcڶsP=eeŘfͼZVєj}@!Pcs2H )GXCY}IׂvVa ݻGZ,5|w])[[[Xr~~oO݋c{?k_|ӟ3IgϞ!_e>3_^zhp;ԅ;5!_ocz[H }[K4L67f<7v0Z8::a9yciZ^='LM5,(6Q$achb#h~HהuWcFZK6 C]Ueh0$[f<{%Iuvokbzß>,6'^>?wV.oe6sg\>|6e2 lmm Vx/ {\. I#; 0|zz%E~j$޸C_'i^zPzC!="MSn on** =M{|^GݺVĕ<Ji[gi׽|`8 IDATײU_ub(\v)˒`@7v" +_'TzUJpj;}-]GӶ׃X?r68x " a =8׃_øh4t Hľjϱ)JCŲZqXR ƴllN(, tR#j\趣\f\radDI$~|8^ߢ(hK_"|ߡkA C\R[y×>8lggm TOCU\1.P~`0`{#x:_S~2H~2H"IbuT,_:Yȋ۷>$*5hA'q-^l.l5>&K# 滴BPij$"t]j%AjdYd2m[...׿ӧOF|'˲ý{?ϳg8<:FJd2O$N֘/fL67vԓ|orvvj I6uiJ\Tʫ\N(/|b4N٢KȲkI<-\'M = ʒ $umLCVttr]*,:HH#lKޔ $  C=n@ڙد^RJhЭfO׽a/RIV"DPQaLKG B"M@HSwllQq~"[|a-6uK>h:gI& ^@YugK/xtP Pv5^~ǗW ?@KtRli4!YVxDw=!ۛ;Q8>jx闙ϗf̗p1JV C*[45MɳEр8Y2..m]*<&L}^Z4sВnz)O4L&vnvX|{Rjpam8 c͍ 4"e.o~tƃW^ >jJߛ.+BkF\]Kӵk mPݟwgf4]+RJ)VٵLEy_o(: a15-.S7hM;W|îulnJbMPrp' `PR(Kss ]iw0xF%Y INC[qJSy˜ E>AеI'ij\K`tGՖ'+`lEHS7{I%w](ڦ&ϓݼAt:CƜZ-fF# Ð/~om C}ڙ??? }]& g'kw#_29ô<+y+3ܹoӧ/|]ܽwMptrJxIYWaTv5mޢګ`%HZLM60rR)&{7s&"Z)gGLʲ:붏Bzή *%-YKJXH׊.o J $v-v^#ЧCZU6 hR1X:q Hhk eI)CTҸUGiԧ @WٶKѴ%mZz!m k|m UUu1. *7x9==*ѴMѓ)[[;ܹuMODعA?!e*+$oFK1o`ur "F]c, M)s?US!Í1B8c$tM&b:XGYN|8-^# ez|󛜜YHJ*t5Gu6*$-PJziԛ;0zki+ $Iڵsgm"6RurLSZV҅]4RH0`pb!C|xDJ!0rYXtא-r)Y1Hg#Z_]KckkM4"Rf9MYV5*/HˆF[NHcwMDILnZJO(ڢBJg{:::B=ZkJG!O3:X y,KRm(=&/FDA|1S׵;ºp(#o1@ip0P*dd?RY4ˆ%k] ucKX-4Mh4 _eDORܰlʵ$wopmi8xPK`5{p7J(IҐxdc^A)Klqxtzf wRWοXo, .c mR[Oy@HP0v{@2 iIW=9ł.0 {.!wygϞ''ZͲ%QNϧOUSVbҐ#.6DIEK/#dkHVˌrIv$QPq^Q`5nPeD=(&a̠7{/=457o}gI}I^:" cZBWtrGJtgiѠ$* %µ5Y麿k0 [b+(S>nAxn3Mz:= 8q @ ASӀUVBeNW U[Cg4B)d$ˋmCvV+67V[.u)XZe}$4M4NW9A Bp|\00oo}0L '~̝{=_OՓlޠk..FSڎ\w-gN'B9s& Pèkܐ--*麎*W֤UCUd9QjA\!nn0nPB`<#CJ)RvZW!mW"Um$i;y5,9O> _<$[n񳟿p__2mrttӧOx1l[ %If~xW7gs^}U.gUgFo~s!,q9[Oݘ\%Y,VH “( a5B[Kg}ȡ5TU>)B]!^ͯʫOX  Ut+~MUѴ9sp]ʝ|Yj-hJ5fHiWcɺ(P$x1(|Eh+Qʡ5:6A۸1l6c9uSbE1gۊԋ9]Θf5"0tƥ,H;#S:!HcfZqvѱXL9:~ouJfy;!{Un4/|ESi-:;ƘEIYԄnG` FcۿjU?X9Q3?9om2a4d4`̈Bkٽȁslq bd#R&iKD/`aabE: ISvTC]qsA [KZ~ON W_͛7]bA)Ҧ]3"~cV`P#RҶn+w兼nV Aʀz4 Ð~?e4{ĿHԝoeiZg0T!+).օstuX;XWCSkD2-'gTS!,AUN^_p؅Xdْ,;$~T PxIPz.Pi'|IOk%Yg RE4qe}|6sN?b{< m5]׃W8 yQ`&}goE^0[UoR%iP%)I+5}tun_9uQ9 ďڶ om7o1A 0"ZSx^@UV@7(+V*rU9%=aTslNvWcb6%MgOѣG]~w/|RL)jKo yO,4FXuByXsyz7b2 k7AGƔe!uWpduAѧ{ꖰaލpb\2!c<:}B HxL8S)u]a>cΎOBVZk0d{ŹTPd?BMj[ѾϺz[7*$BlJJa=AokiQdXcmM|V NUu,5N^.7c.l'}p77!ƧSmȪ"N<,GFQ6-A$`-"=A{4H07H׭k %ic(b0qI* (t)ZBS籪 ܌1ƬA?$3g@IlnP=;,[nvsNOO4ԗx*aUUeaW zCҰlŽ)@rsw4r]n٣׏m <zbڶ"I,YUS^(wqʐK6MGYOsNN%?{(q-Wd奻Ye8%MS,]j.n(7^_M>=bUrgxd0L9{7w@tI2 }7|y!ow2 FEŪhz8֓NFuY>׋XKDwx^Kgw(SU-Yy9>^K?KÓOISۄWR)6opk_GG<|'LSфj[Q m5@:4BJGZvN~K,ZHuE; (Ua@vTlLENYXaG.p37xŊ(iV%77>Q%zYSe6wPX^{%s.Hˆ~PM8LBV)7oIXQ.3_G|b<ˊtJ("njzB)E7)*QT%A5}"MQRg t]L-,'Kɭ͘(Ï@ Iΐ/wuHc+Ôx4[]%V[,wy}أ.3iQJيݣ30[.cFuNvFհZdYAQb eQ\.ɲcz,3g|bΈdUXUDVFw!_ؾ0`}   a[jmI-"YdU3+̘g|N)j@ DFF}R훍ʲ0I놢h,я$?:~5&,KWjOQV+dc[Yjsxsys>>Č_ QӴyc-.i D)?ٳ'|vN',}9f4+F<][Ѻ/4u)򊶩bNK ݐ%)eYVhՐf1G ) b9/nZVILR m8:?Ɉ /3~G!< uMSӴ5H7Bjaƾ 8/^^ Lkx 6M[ST Q҂-@k/NZ94Qn^cƌP-BJ^r6DitC6GLl7hP{꒪\gb }e'W|O- oE^DDD^HZݶdEbbssG9XCY( C,cմHe>J)}f9^t;NG~ qxG|ST%{-k_ek{80G윍Q%wq\&E\Hm"?EkK  ;޾QÌ f&KS], MɄl!|a2 I IDAT3|ŖcxkLXRX, uqʌ$̓b<( $<#L NkaKx7&Cp,ʜm8&cO<ׅ+j~"ul,Yk95eZpl&}%<c̽vG, WZmKSő9XeI-̽MTHՒ  .p2 M&I@)> }{]'?⭽} ? ,njO_ NpuvθeksLSV,2Hĺ&[&m[ꦡ)r=,%Sy <\de :y=6;C)LJwtˆ4ٚʑk4bSֈnח{UUMQTe%MSqS2']66vr1"f6q.wY̦:| ^drDԍd,I2,Kc$Ҍ‡CMSyB+rR,IRUEgR]Ҵ:v6̍f">ԍVh61]cyᣟs^zޞ*]bZ56 =|m+Vˌ~or5O㘓3fk]źpd!Z YT"EK)qm !4;۷X,䴜\()z}6}C.&l'ByْUV꒲NόF\צlZʢ1X )n`! Kjmz*"e_x𹺞1_`{)/y'˓%e .UbӂeR(sj=iܯRk6i޿|) m("L!$dEQj\Ѷ@%$aވJ Mqq%-wn$S4oAE[uk)CUNp]y VJo_oP/ iZ`Ihuz=NDF˗/vV+ ٲ 47KՒN'ıQc)rM>_PcGx;D@Qw=xϓdIycq1 9 \ 0o Kx"-8zͯ}m~@4i]:coo|y.yэ:8ȯR|j㘦p[{jIQض1Heί(˒[~R6~Fk`\,G}Ddžն7NsjczC|j(3!6ѴTIFǵql$#@ URMpӬu|eKP*ҼY$&oA[UXC6"i-QL&)<~ Ѵ,ּ_Q]skzB֒S֕)K4Y\yqr>Q1 \xEwc}mI [,R\>0vPהƓ6v9>N)I(Z*uwWHo[ҡq\)mC(H-+xNxLSVkbXHs_j$,$'eiFa.IRURIꦦjZVt\8cm8{!D.&g%I, Ӑ.JTbٚn:},k;_패u?m,K IVt}<UT0n H9JI@ȊhDPUYTUmyzEfWW'pî(2BI)\FQ2AZAtӅSlb<0^r}w;#ebق(Qe9 |aÖC'|ay ɋ9 NV5E^3$k@418b&T?)p(JR嬑-hPhڰؠ.fK+㑶;aclM _n)>:*mskVؖj\OEK@(u)9H%J:\)tBfD~5UQtm-/cNN)NΝ;t=z>ŌmDcNORAK/>AibeFCR>k_y|9uDш焭&ha{52_zih-<9R%l>Msqz«/`=cslnA(Ido}~ a{3q(LQL4PRv:C"q웱q7a1Ʉ~݂8KpFL\è&L,9B0$MSߜ%9`9u Y\ cE}PU2FQ2!^޺ѭƱ,:aUjbجmЭ9kHM6H]Y`+ % G,ISVq"89=&KsڶuAlfk5iQAL&Hfī۶vҤ[O#ڈ۔1EQ*^5X*G62(V2oû n높 AGM nikMu˱P]oS1(.[(`\pgew7*k0+DZ6ZO>"X>#ӴeEkJҋ MJ`nі,}s-لҒ"+}z>;=K%DJ11R9GxG[[(%  F{Ʉǟ3 {D>E]l{llX{Adϸ8:CQwzbluT9]^|seEd[|k_e7fggI9>=3ܛcQ?mc!MK W2rM]Ӽ6866hnuǜ]o.qLdn.&eiht:qp%MS@S7&' C(2N$InjM4 %mm2t-$UQҢLSDYBYBim!iSMQVEL5RTE+t[c+QU\QX MUS5w|jEQ5kCo4"y}>x. !UUy E-(I u A(quo",Et;PWY5q ڎEAoH}Y}YH-AI&j|_eCll Ð27@)YD~IQ$񜲬h[mVC9œ4uz(JC9uk^|Ť`1! *D+xNUm8f>5,˘ͦ7,OϗI\,`wo"o9;=4lmm0 8xɳ/.ݻGrBfԕ5؎br}RʘiP q@| ~`lP)dkzgR8YI]ig̈c֭ɩ,MA\X@RB)Sp,,ˆ0@hr Yj\ 3AXˆn% t6uf3 Z֞RwesM'͏Hc B関Y`[IRyڛЛ6H mkVj 9bm:m8ք-[t]% MN0ۍ^U|S5iSkEr"sKedYF]WhzQ}0c<7zŌW/8|hՅ,aJ%hi=E|MSU,>=yg^OR(r[lm`/K5v`g!*p9KPNDHfk2+^=S X~H4bMÏ:4|SWL'o~ē?`ggg .(~YǾY.DQdBe9|KI` ( Ct(HᐋS]oL5Da?lWm2]Km$Iе!bq/')|i)mMMM7-U[@ںbӴE1;8dc:^(mhEd{7(8 -ŗUݗ0۔ Q%^l.I 3~QRX46Z#B5M ~fJr:!̳B\ףn,kFia;TllD]EUī8^(XN Ig?p )h7=7d<"Ic| c8tL^e:Pmg(oxdFh{@ј(PŲS,۸km{؀cSJs_go^0ڌXt:}.^۶ۿ/E96ՒGR S qY,&p <yFU{oszr3P5ڸp5{o-8(RBX>79y߼;;askxCwP׿~Yc/Hbnm3)W1qmYlkQp<"<$MS|ϥ)J9G!a'"+ $!@S*tph)-I Ij6)J~_m{xz.Y,gs`? |4eA$˘,NF*ڤ#~1rƐ \!HKcFqzRmoY}YHoVL/ft6R.CG$YL+^ Ǡ fr`~5Jml|k(u3@Uj*ym@>ߢ6vyGܿϰ\\_uݥP ]Hp99ݲV?xG~%;}4UNX2W5!N6m2=_6S1ibYrCb1'2TENM,FVah.*Iˊ0 e+~m$1%HeѪJ@u%h{D]5aGX- Vɛ A+%izFQ( [!i6 ̮ \ѡNWX <ؼA]\\\-lDŽ= \8ONtskp\%)%ʒF)ȧJC.Nˌ᠃ іR#1mS^2&G3:kv6v9>>JcI g4X ac\R&9{;. Edqj^[o.uPV)x[<'(GG7,Ž{wQVNkDYlm W3ue[ċ9;;;t^|qLJM&d`v/xkmkQI\R9rɟ=!UQ:6p|Qd_S~G1SdP|RG_ql*W|{< >õ-FN4s @K#˲3fAܚ{ϟ?çhg: ÿo|5IJ<"'_|)EUspplxtO_gFkJYf5Z ʲ(*, DM%)*]TUUiDZ7:U5UJ6vt;e iVehMZ|$c݈X VT mo`Xqۜqgwx:Ŋ~!l*c00sӕ9En:fcLF es3|=47/RJ"#IZC3YcM]>EKJdIVMKQ(kB 6@x ]KND tVԍ L!p-aa[B@UIIh%fOY$+ 庄7H[lFht_Gǯ_տbkw& Qk69 ,s|?}("##GQ1O<5EY>y}hm^G$7g3c/93}}.8/4keYVxӉ~g~.=Ƕ5T[|͍2!OSV%nx3Çy{;[a^ BѶqqq>IZc?~.wpȽ{\]\.=rl6u,K{dije!JZ,Y,,]p@QS)q+NNN淾m+>^gr"鰵i&vE`{gϾI_zclnI4.Bm] dznjEm(4Tde5RXRc[>W16#bI7f kk<-CNɒ )Uݐ%curqqm:%m>Is}f%R\L. E׏#p7F=*5Ժ1k5%I$mFտ a^omr&O- J"7^n-UYSf|,EV戸3Z[le8~s-\@BS4rBۥM:riѵ)|צ/(%VXRAh*Η[wٺ{1K=~4b4hU6݃v0ǟq~g4$],'1iR3 G/,%ZZ*Cs@BBHӝRDKzT&K.'NQ1TeA]Ę%{av#:%22򲤩kVq|0CjPVռ!iږ~h4@ْ|q lۆR< eE0q}qvݛKY Cf j5=&MSsmub UQUZu(Zk NS@6M}3}}H|2tצiLú |]vYWUŀNshoyl>TA*is59ݽ1 Vjŝ8;;eXbKE ]\{oVrbrEHe220wl*|n¶\1 eg铗}w?3]Xl\TBDQK9u֦0LYh-( \kSUev^X@Y5MHJōؾ(Jn| CYVdYh/dii |ϥ+ A$2K xuQ[ u4kP.+V墢HMSQ75M[)h܁Ó ln9rbɋ1G ;C:CA^/ Gܽh"pv:grVEˆ?~{چp1nq3Sԡ) ,C5wޥ\.F|8%pMzO>g8r||!ݣFt]ۡ횢h*#ϱ, e;7/w7EY&Lf47&/ײn@ȞiȲl%.ȋԐl{uvQJbY$〮3 ʒ4߫Z8 1{UAõ}$,X٧,#zǏqqq/WXJXma"f+چ^ǧFXWחTMI]a{.N7舃۷X$[ "c4F>ٜS.ɒ4My#8`OD>> ـ ]"!%Rg{k'""Nlo4* Y'#x2Zs-~wiE!R{r0ՂhWc.. dS:6@܎Sep^ `Ryf/z= 쮯IӔ( T F2, NY366S7/7`1L:JEksuuE58̳"5*҅5Yth85iPJ#'{UAal"hwlD88(SV*v#I%WW\G:Gԕ&X +Uy.ay`AUj*%>9wmڎk W䫜ي˦Q)Zd&Og-Z6hŬb9t;>Q;D ۣJ6 2#U.y}r|4]%}*s=> *ujek X*QeMċ%kҢ>.g Oٜ'<6}D{~)*V "rS2R\#E P &Wv+SfS쾺Fғ@Z Cyy2`L0FQi %Jwy??%"^8{wnc:rpp@ɟ UUoc RKM.))I(X,I9ᐫ1/Fk\t)#|"@8ZS9rV6؄a#hZ-C)_fUA]唩D+& ;Y+˒"wJZW ~h-:wc'=JtmnLf ./O;]z5^mVLÇ6,! [;L sqRMݻ [#6}N_rttp\.mʉw[3M9G:Y{0 iȲK1[[p^X,9<:Y}}ã\^sr|͏x1{NAa97GTUW9.qk .Ժ6;)8m),&k|͌q{C*˒A+{bRMVeFk=hmORB8n\.\*-*KI&7Pk-('H#޺*Gzklp8hm &+Ҥ,yAule֕}!V,q]683,jrdIFǶȤNV֕`C*k;>3do q]|sC|B+*WEVh#0ʂd #9::섴-ѵDOI]d24MY4.Q@Ω%Vf!*f3  \' d`ڡ 5odwoPAXd (/?b<1 r*JCQ&e fcR8CVK4jaC ^ڥw) wM.sB `:_1$ HAUDQ{O/~,$E-t9|)榭<9Ekds^_S^/jPM܄XJi{Ue n^CYT<+IҒXRU$0:amZ4WwvH3E``3> %Vq"1$yF\_\.VDZ jbgg(HnmmRyюT&sQP߀2MOW$IBE5~@0 a9mww\)h*6uϖKwk2_PUNǯ BIZ\͙'k ;0 1MF?hc )%`l i1N@<$ih\֬)Ft7q Ø%#z2lݢqËS|?d2^?őW)\p lPo#Ye̦V<顕cْfsmuY6ReֆGb=Ua.VяV"#_hOsttD+m |HaƕF:g1oYs~yOHӔݭZxjro""޽{fgggqCp?c?~#={F֭-w5O~ϸF1{[Qk (&*6WWsƄjlHyjS!@PU9"qmSCV˄"C|A$~ M?ٌ|# wMohM(X{ӡK޻_j;"U4[BS 8"MluVAH$EYqppLT%O|Ek2:f$mNu^!čò@k;BOU.ZsY.+lhd(7`66<}ueLEU)Ovzt{ - b8=9c'pd025RJ\Gj6Zk4Óvח'%*@C@Z )48,AשTF"\R%.Cҥ"Rs- * UI;hm@c2d_{8:>F='" whcQe%+AQPVzQ,&kA@у]*N983ꇌ6"GMPu>,S*AH &Y2c"̙g~`l%BV9[pO页(uxa5kPŽ˦Es^}m.M?1BUp0M[OYryv赻$wyQcol7pO~{{{V+ #c{k?e4szz>WWWkw^?ﳳJgM9#F[״$BbI%"{0zkAy??έ}jU2N3^+V G4ٜdBŰ?`6b>2u٢JdN;\/xѐK n'/qg6(fggggV1zh-!0<+jqvzTyֈ(;׿X_ K5ڥ*69?.Oߧlz,-𣈲*OY.m'yףs6č:6)@/lpש@w0~3v;[W`uDžZedyN] jj E3VkQƅnMTdyZuy&X4* Um(+EQ+ hO]MenU)QPDItY2ݶð! fڠj( (JY+ҁYڌtJ5\/O??7m ьn7ވ2cXR 2Ƴ' Dz' w{}"_})QrTuRvn:K}j8{"&I}* y\.YLglnn\ܹKkvs㈾{MzQ軉BЋ";4U~Mggf9L󈃐Vt ;]^4M`u6׏s$p=:qׯG]Lƪ XqzHcf%*ǑZyf$i[^.*mg'Ky Ah#0.)=z}D BSAۏ$".QDAH+qM?{]& #??H@U1$=tE; 0(Osɳ7٣0Nyp>Nvn\mkUㅁe^ٜ՘$MS|ηAiܿ?yw$B H#X.\\L^OQ 2|apv|BU\^^g#pw˯/?Ck}\z ysk]szzʪ۷Z`,8xs6ߣ Fu$n6xCՌ"or엔U8u4VJS6&ijm#lNcܐ`LEYIuҨZ vqQE}&waC* ;-ںk <q<# b]RX-RoHJ\Q׊8,[CBt,{@8jy6S ǂUc뺴.e"A:C1QIh(]Zlۍc (T% s AH0ʺ /0!Pj"/4YJ4E6@429AP4UAY_B($UM'PP(k kdB\q< Bcq4n%bJ改4犟 .|aw?6gq{n8 /<xγ㒛3hI]xxux 9W'zۣ-ήY̗Ad뺴f9OqvrNVj :q666P0FYvz]6ŏMyMAs(]ץȫfkY !ł<Ͽz8pYqLZeY3"o&<7((ll$I1(HVᒫ+\@؟}\]prrh%[Ғ$Ixwlݼ9u7.gGlnh|o C]('0 P tY}nݭmŒdVWĝ^m_! |rx|ȓ'Oxw?3/_ǜ ag{7z;<)ps FJisr=|÷8<-Z s$MIp}9f7k![-YR~7 EZRd8_ݵm/HW:׺Pi$:cle˲Cq1L! 7zҝ].Ŝ^p0@)ӧ2gog8z;-_>h\\^f{kDɊNj\ǔy&ݶ}_+NOe{{ vh%nc4#..Yy*f$IF$,K\/ՒP% 2?dos5ˌc'ﳹfkk$;B)EۥՎhw[UF <ϡB2!s6661Fu俭,2ڀ:jn +MeAE528njtZI0fX.(b<0)\6L8c,u|dIJJ0q<&`\Ϙ\ ~jePy h _iy$IAY GZ֢*&'5dnq08loDhLM;[wJXԕ,`4ea7C5EIV+q.Aࡕh4~.tVG8JU_0*c"vz`'^ER#46.AX6C-@{%+(kCZB5Yd2s.27JdtZ퀪@H7n3PBѕ@UG! HǀuHDQQ “TUr*ހ"[R5mt`cwfɘ㓯8|i5! b|+Ւ#NmܻߐN ?p}}9pGܹ}w[7W1wOYƗVgDH{|[n1^7x (3˸ڐ,VQ7 I{DQ@9~?}<><Z[@(.JvFl: qwnW7X-uYVgPEETWd%Jq}g>X,տi,9RQgcjCٿ>-'/W0-loAF,+={)!'._]F-SVϳf q:o6ua >nvǎsWZsm~#4FbfAT<<Za\fiaʾP3 ZŲ,}[8\2 ;nR2 S!&gfAzs666,_^Wh$Wb,30 "N (o]׌c6kCRa#qfkzd6jբΣGf3'9U\f=:.ɛ8t:^țo-T y^9>2nǓ/~K68??ŋgHek{z m_%k5 -! <>CbߵƺU1 Yƌ67Aqծ IDATHLV/>*߻ttP%{;;\O)_=yy[|O< UݻwA+ܻǯ~+NO1ZImk3K[at}thM;o!~K&+:e4f2rtt۬ፍ X.\\12c۹y9Y2C-s=?~^}ZQS"6PZ~V)i3θlX@ ҷL3R(mk; <doP8!Cnqy12EKik4BvvG__x~3ک [ll@.$VeTܔ44`NgQ 0ʢZ7X=Lq< Wf7/5M#\7uy!u%HӒ0M}&|?@ HY.Y:Hil29ҠU3J~Khʲ@m y"+@znEn4VP"F6xA "kHE 2r<82dAf9~\/ wA"]: H!~oT D p=N/FB(2Zx!!A,۳5F hˢ{$pۍCTX)V7"ՌW3nSVYbhBPo?dv'$ldgg}h1Fe^?#d/ ~Oy?oggw ՜$YBV>@kM3)J~GC_-/B!DR!BH"ܓգ>}7p{ț^KWn۵N~MpS*֗k睏Xwr-}Ip{%/?[6oqS*_9b}_xԀ7,n*v?xop{/+z MsM\r՗}nyɊ:^.qcՎ'\$.~r`fzW=k1jWEĈo7LލrH>Y7<[_/>'+nޏ9poE.1K^~}_r-^oҿ<{/~+j_->~Wߟ;>7s oxޏ?r|gp.û?'G߽߾~a?E~_nZ*qqۍWˣOv\|M2?;}Lgx81⣿VOp-^w-rH0}/}/8OO/sv湄QTǼ﹍Ma+~-mŻ/iq+ J ?(_x^Jv ^c\]|]oß;~e?K^/?ƸXjq񽛮{ɓ~?_}/_~V#..f7: e=z8d]>/c)g罄ss~Y*բ2geu g+狖nx۫#R|?s_b徺?MtM/<lv:y/{ϼ^_-.n*~OW8ԧO}m-q7?xYjZH\qq;v\Dž],!䓵TfS^t1/|rn[o ^{ɯ_Ugvr{.aao|oyKx/7sgw_x-ಯ}}?GP)Ć-^PCDG%.~q⟯SLJM7 '/,G+.ZK\烟^w$.jdj(N?|n_t5u#p3^%闾ß~⽏y#kW6F4 ̧=A}W /z=6nM<hg{Qy+^Gc~3'H#~r59/^y~W_#ğ3H\b-`C;%. Վŋ3ӓ|{>':"$azwvyqG|'x݋﹍ [=}>gr~$q|g>7\^M^w~Ug}wނ19wzt^o.ZvpO}ՇFop-2 sG~?/jW׾$v}+Z~n>jWZH\qq5|#??Ć-+#dНmV eyp`Cl>4~>x{;~{nyzxM[O]n[|}og~v5k-OO+/y/;3 dVN=>/rҩgr {vqֹß[o^yÎ7}o|~>ϋ^ڕێ˾<>xzOr\v|k}sӟ͇>7+?qqc:/_~V#.^ z,ב㭳HB!DI%wz;T!q!q!$.ą8^H"C&BBH\ q6B!BI!B!B!DR!BH")B!$B!BI!B!B!DR!BH")B!$B!BI!B!B!DR!BH")B!$B!BI!B!B!DR!BH")B!$B!BI!B!B!DR!BH")B!$B!BI!B!B!DR!BH")B!$B!BI!B!B!DR!BH")B!$B!BI!B!B!DR!BI$B!$B!BI!B!B!DR!BI$B!$B!BI!B!B!DR!BI$B!$B!BI!B!B!DR!BI$B!$B!MOc>wȫ|B!$<&_?+|B!~tdjG?a~s{z9s>yۓDXB!$y{>#y|?dGsB!q;G~8~uU^ϫ;v\w{sG?'+K/?Z>y;λo>_^h !DR}?%.#>|?~h;c|k\׾BqGa^{'ox!wuczO~r7_}C:u~ZBB!$?llk.{Exb>/f:6lvXtM1cQJc-ʼ_ϻ+5y%.$.8"S?!~6?{>'|_/:_}oW;̧?_{s,o5fB㐌Hx?7o]J~?|jե}s}ﴖvٜ~y"K\H\!$?+~)qo^ q!q !9;!d>$ ~ЭM!tF#yǻ>C_Y#)B!$B!?:'f5#v| VŬswX= c gh OM$!ObU,I򗿄J\!i [\vٷ8Ո5#{>K3fxE=#MS¨@N4)<עCՏ=.@?B!wصV;ps <<U,YL =l8$VR C<[b0O= ^L9"=3@qczjfA}t?p VXȲZ1tXki/Vvc {*NٺfCi7VSE o'iZ@gBxWti$ RjJ|E$%|ȓ= CM O(I nW)u\:N9_YQ"kgVG(Jww+|B!$? +=nyt;vyѹ;Wܖ+0Y 4Vg A)M1T ~3\{W" VkYg%bs%:ͭ)F?lQZFYŮJCivŔxs=;9!^ BH"Zq!K1EUűv obR/8DGM=xB!$<˫;ljP5\`ۗN=:ʡLMSr+^<E>4e,z(&22PTҞ?;ݔ"(=RgYbCUǨVtNk-Z4'"dY%wzZ+|_c\c-8kAy8@ٕQd-B!㑤p35qNnVk=@O)*6cll j%$'3FكS,1P-)GJ.W[,1{hb ՘9`l,/x3?B`qN=uYyZLPx&|'m9J`afk)nZ}mρKpy3BM٠R)E78gZ&=X\Bl-A9$&v BED~ۻV)1Dd[n\.wyysEIleـh G9IK%[G1֑d9ݸ1%PNA!iS)G2ӁSp \ʺk_F!v84Qlr_{x%)Yu'_Lsۭ774nwσ\q79bTѦO! BJeh,sݵWpu|rm7g-Z[4RL1z Q) =< X1`sJY ݟβ8z݄<)v';dr%1ERF}rq^|1ߐ6ʫ\^"K(R*VQYF(T#8Pi{ rʠAS{ԦjoQH!Hf OC(Px _S|JS`O9Q@߻=nz|,5vVdI{98O|:߿oJݻٷoJ 똜=wF9ҤKđCX.GfnZU 8cq&&GQT7}ܥ FЮ+p_b#s,EDx^E]y#^Y)qPՎ0X(fye6۬6L6j~P'I[4[s `\!w>~{Qas`lvwq _YDsQ #)IBI$0k׮Z-4_N& F}{h4(W+(h48pŹi8{ezz*CCC4[ Q*uS636lH>d L$^9dž ,v;K~z$A ۢ)?pD! ʝ.rnvfffHS2hs<=ϣhp};ٱcwp572<2ċ_r6mСC~ǎ;.[nejjy&&ƙm,..lS$1ryeuT*a)Z!(a2f4;Tu$.T ^XcLSO̍7ؚult`(\ыsRG3HssssZ~P_$ Y拂Z1+p9{)#ɣB!qoz ܷs'zٙ0lr=v835]֭[9sXZZ~ݻ֭袋׾ ^ h0P<,fhNtQꜴdg;1bPy2 (&8#hTp!Q*HӘ t]|#Nj5r2=}!L&m\X5L@ * 9?h1FXVR$^ Ѷ<'Q*Fk *cQY-HiTmrh_{# o;q֢Q80Y^lZ٭DtG=𛋘RL !$'fM7 9S$_j{^(NyZBN9vKKqa:͛i6y8bv1Jb<'ſ׎āA:ck;=ljqz\J%bXk)&W,7X$I5`#Zt<|.6Ld`F`Ӧ Zڭn)i0f6.5KaFpyFjro /'G ,$<*H)h(ɤBH"yۻSog݌h4v 3==F{weuYXkyK_6mꫯfvv믿[Onca,..nuPyjLjYl,`]Jwy ;]:XJQD)#I٢Vڭ.J#dY&<f SLqjt8묳.'".aJ~bb{cǎ59nبB]bQ0ܚ}<ϧݞJA \iA099wh"Q`|KBHH9i=QGH,+jLb8^Sx@TRP-G288H-vQ 8@X -ng)33{W|^۶mŋ^"Ϯ]t:AQolljJejjo.guqң6$I"(g|8i..Ph-.ͤq$퐤]Z%6l^G:@QX!,vnb]Ƙ~BS*{jV+S*"2C79餓[dxxf(?dY J%/,9bXK1C+DӤ('܊Sk5X36WU!$<1(S]]PE X(֌ERvڦ^cddfMgSz\30grrrLZnr9"MfO8sJghULڤYF'{g|JAUINh[ia,+SJ (y* IyLMM>Rdd\/MS|?M&k8gО%^S "P)ZY:ERT+RnG4b:u\ap d1 &Qق` 9 EeP\H~tStjv|۶󊗿rJ?z sXSv/̑16NnZ&$7=J墰\iH_]aG)rĖ-83寪BH"ybVY+ʣhr3(g <@kժ&Zc Y:Ht{m&qNaD% k}O?8ij1;T"T+ga222WU!$0BψJNk{fNa@j^FR,%Ӕ45Stqΐ破n&hE^;]4M t:4s=4MŦ kszOq|ɧCiftfqZS*((h6 K%*r)dA8@As^JN)lݲ͛nwTˆ$mi%(CBnc; DQjSZMVeMS1g8h.s2PU-Uжz-902-99o6lذ> [1I!ú,ZHboҔ=bue9=-q!8R+ ۶@U,G[]o֒ O?e>"^ IRe &`4:6 `]cXDuicz mq 9?#~7~?}Bsmݚb]CuqA7 ٻ;e{ּWD6A0GTA?&N-ƮѺ%ɠV YIx?;{lI=Y:u}N%ۻekX<~s5G 7x+))ӝ!'Xc zM8]Lce~5J );і?&IsuR5yLU]뀒1u(zB琡.DVP5"r3 ~M\OkWcF u"?Y3NX.mKuv@:U+X\wS1yƒ#bՊ7\9agk`8e0$="|٬$(;9xk^zFg? |dYuYIق &]jR^?ݰ׭[7=y Ogc#7ew7ݿ, [kdtT >(MhB"F IDATͺZC)X ;0_o' g8GgÆ 6B+|"Xi B)[!>}Ɠ/ˁ <)4"tuu-n6x)-A!c o)ݭ $|Q ji됑0m{m~e8~nFbs=$IX7(|Tr]1 hڊժGԱXjijή7Mew{bI:Y.3DHeXUB<.ȳӧ|/}Ou-(( fyh̚ۊ]$$#gI1T|/5qg.ވׯOzyܥ ۓ>Y2eUѪჇS? TZgH=n޹nVIDt<Ba$JmsWݰaÆj0ٽ% "hB0;ـsN4$Ir8ޣ ՊI7SUx II]0>B`Ġ'eb~™c䙡*/ް^h8owSsHpFc JIӜ۷2;尿$r&c~MՔ15> bk{LUhFcNgX{6ޥ^kGkK-ZYB&)ʖޡ|12%sD;nܘ=G)u]nذaFHܺu)"Xfg'<>#^~IƁ}J6dھj!!\VgBJDJ$Yٹ!MƁ1xX벍`=ѸRRqml[ם`4:lci Y"ET) A9)^ѮŊbix'W8[QE%誮כ; 6l?ytք ;w}Ho_0^/c^w?*AV AeȣFD!4ʘHZghлqrrWOnA@!hNN9;o"z;ĩ4튭bMUUW]OF 5g'ؿ>I*)ƴ3ٹ#"d4")?f9[F}g5:8TBhۊ-imj!d18(XWL3ƹ`gRU%7]uÆ 6BA0^|8,qc{cL_c>?ǻ[z s <~^)c,2`C1m۠9Aa:(xtER^{\-4룥( >}J/K3Ti2SY`'5eu , "i\VbET(ӣNs2&  }d^PŋW,KÂSVANwZ8=yM]D* 4H1GX13c$*!a(9&n*NaZI( rI] z8*$`~~!aÆ !A MP 4%BZ yšɸϝ;u^kڨ;17SR4Jr29G5t$)˚q (dLk*R*vH)/'QH!ýQ!KK)_hCE4Mγ5fZCER EYH%uqml-8Y5t)ޢ%l#lCEDưcy^}BΎȳ:|~B[j:C`"ƴ ݜW8RE+NώAZ,̋s*yqiCk>htn#z,מPf,p{|NHC|v5\$:,ҸnذaFH~5no_VZvBk$Ӗ{h mAd(.K[Ht" {\ ޼-,~:)vx';Xҁ}p4xwyɛ',g?㿍"i`أmkv[F#fM\R5UV4M)Y\ H%;cf${xHSO:oZpHp.P-jEMkk-+K;lOcnyڶ䘳ZPA$xR@d떪>9֚$uqbxID)h->ߎRl6lذ_E?Izh;!%i"-WŊXO/X+3_sـ,BDb2 DK]+08S"uXpkh31uò>ÅI ~r!O8_x銲P1}l;2ȑRC-Z$'I%햋 cV4e1b-tN8[5 6lWc۟yⲇQpo菆(8;:mJYsr|H152[LbʚlC8 D!D[G kOU:C s !AhPx̵0> o4ە_<'V4MQl(#9B[y` 4bw{@>ȱv/^5%yMdI>q-VZ{FIRK֫s^B۶0ZsP /1!R(g4mL;Gb`1s`xskJ6ZPTH! !YI|EQOH}tf]P.}[͎81 %inذaFH~58::D궗uŊX+ #V3ʲD)p'BDbKSU!6"eTg[TXk捣 <5]rB 42|Dg1纪mxeůJ1ƀ!.m[i:Lt7\#f=oHSh|VpptB"U8 %䃔K+ |1p Is6{ g@_w(J8:4A΀C##ϓGQ.pt-G38c{g 6T.BF1;T뚦ZeCBjꂪQCˈ$}Zc-ǤQ0e+@&4 zNI{pМV4eb4p$QW ' ť૦e>_B0!R)JH%MA:h@z> O,O3rz{TeŲ"JH6mUHb"C"lsWݰaÆjY.lmmՉʲ/Z.-r꺦:g>w[8cQRr4c4`+,aUgɄQ#+uY.Kf.Y8X+.EƼ*0AzÂF"eh1ٞZ-0U bɲ2RZX2;'C"AGNPE Z8+p"#X%ݰNh)E 9tO]$͌5;J_ VGd&ߺɧNu`w߸ rz@E?ʲpq㗼~a?իnOQsҁDa2T Lv}AtkڣEQ!blY.״ePxA,ƫ90cz٘7Qp >]Rݡ"}]~;}J7lذaFHxǰ#c xa9N{fS$8Ff19Bp*Yλ/A7Rz)G09qq^ $g^駟rxx΄tx)fs_GxD''Yb(D*JXg3n?c{{W/NN&BksR=;.>~qAa75{v]<$gO?)LQ3)Y:i>|rzCytk(C118Qx,ˈt|mcl#H6!#t =xA4eD&\' g7lb5B'BgOȿ]6lذaFH8^[@7p xG+`\\\ Ijz]eFތ@.x}'YJogkƋ/BJt:%"nݺC^~M1Cd;7/ۼ_czc P%^LF}Z` dcNϖ`Lc,uISw6?Qh-Y7wv]9EG9?}y_6Qg}?΄’%'$V1;Diċ|w>Fɔ o,mue0;-J)Mm1msjk(RDQRgcq4d];tBgfŪx 5ϛ&|'L;a[ uֻ눺(Q_{C$f4qeP qÇ|W3N+BQZMST2%qU{RT54D4^RY(ZϪ>eQΠ$Y|Knq]ȇSvࡧ'L}9&h glm,S<~/vc^|[P1ym޼>iF,W$QL[(-Jiu@)y/CwU5.NyMpK)C54e 9 2m)0ݿb6! 6lwwwKSml(%s:Z+NҚS7 ЉH$.NAe! -Wy8mʲĶeDloo3YC*E zʪv\>pjfZ(6ph+ K2#қFbΛ7'ģ]ɍwv]ܹ3V5Q!3`xW) h ^`]ES  Qjsoc*`tO~SEխ+QNbOUx#V) K%C "h*ґ)RHjΨS Y.3VYN$voqpmo,%|Y0 '$7"rÆ 6B+ijer&cRcUB5R @s IDAT@N\UHKJH¥u%sZaZ hGGI7!Kqꉵ$UFGA'}E( N&V\h̎^ir*]Bnc+5zpрpt^RG2 yiO`w{HrV~wr],gK>ƴdđ$KǼx}ʪxQl>h:Zf?gŌ $p6/=ʲ{(E,*4QJ"$D1(-H3-McʓFq$9Ҝ!2̦ve5j3K=\N  eF՟>\\߰aÆ !O_tC1q_WW.W8WBk Ja]l`81'__GxC'L yRW%.N"ucD|'?>l ׶Hi@ɍ>쓏$8@S⁤!x^!:z=~h7]6 K)Eή)y3Hsq153((ʆ Snh5-*0n?=j숋s~>zr͊۷x!}9R֫|I%83DžV,N`4PZ_ gw>Xspb=h5(OIS<>\K1yHnذaÆ_9^(.T7EiJeiz}DQD R˪y RbU.W$ـ_cҬت=snky`g^RkLEO0A"#@T| (MGkaے7oԭgQclS h!"DzmAF8$ *!#X+ib4u1?9f)%*C=x[7w)뒻wa|qlQzn9::"xb G~}˿#R(&S!I#&wΏ qpaE秤Ɉ {[ F b=a0R5.1Ĺ"Ȑcl %Zw7{9rÆ 6B+bq[b8v'Q$/ZklD5q/Gėf(xEm A|`XkN߼}1|۷/Sm Zb(+ʪ6-,-~gk>yu-(K. !($GưX6( Dq]_V1B(S F! iR|4xgE] <~|!;zNǜpttA9ZD&Ȁu-^Ӵ+1/hʊ-|)Nxg8?;WoYVeM29>: IUxԧ[*nܜRsb˚%SvG8+0aA" ShksdCOSxϨwd vEC2DqaH/[=R^[`nyFnذaFH~(䕘Ǝc,<|R(Rı&յ$J3aY߸òh=o7o^{92*5eY.,Ų"NO>[?7aS^X [W` LK6uEЙR6HP2DbY,f /<|$IStHcJlH)hڊuiMá8$KϞK?ڇQZ)vvٲ?eZR)U^0XQW'M/Y{GYs}R}-jW Kd%2(fi״@S4#JKk/ h^/=ڰaÆ*QV/+!yn䵰R^ >ĖH(-?Mjcj9<='Nt.?/WWG?kRK"Y궡jZbZuPژVl]9]XoC/Ia;O9_PK"ڊBBZ55S\Mh*춰3b]褨w75j$1!ͫiL$ !x04"[_ $!Kr!zgܿ? {,kTq| J޽ϧ}Dxs|tTvNQx4A,k֫\6A]JX-ޯ(@ =ѧ\4,,8Z7EDۿm~/m'ܰaÆzDŽ@UUǷ95 / f/'??shV|bA$%8I =qיߍiQ f5<,ʆ՜W|snhp_vigP/% T xvHu@ي@@k*L/_C΃rH;.I 04(i =ey7H R 6JsX0 Fߢ*\~uBE$ip. ("/^$F`LDL{֡U,ZI|kI*\qidԕEf¶z@I 2]PVr義κ 6lذ@4_H^=z˕D'.}]vtvZH޸h*h`_}ƒ[4Uᚒ~zcLׯ#@\[IiRb! 5jɠ7|i;wXƔK]zz [-H% T:B!ygEZh9ٓ$Yz3rϬ`  E@j /@2ӋfA2DIQ "48͙ {饶5%2^=xgds ؼ=w`c٬;K\wJ8DD~baalcٺE~H>aSX&MC-Vn׭0<SWu@)ɰuO>OjL酻v 0=DSFJͫ'Cbus'ӝɲ]D%| a 鹞ɜhh$Jlr(fCjdJ!G>TO㘪!BJR4a08Swyg)p ѰϏ>ȥ;6Xwo񃷿;E!4 *w|wL<2#0- BD8%L4~j< LnJt{'t:gE?şNH)TmF8i1'YLP 鐘BB)Y8MFOXZZ"U1^splð޿GիsZJ,eaq,Z&Jz2׮_W;w?Qk0,(j4JBBQ ð 9L>SLon߾2=&\<.\Ïʛ_g /@QŴv  F}6 Fu!G'|_b4qhAtj ryM4Z8(1ޘAzm۲h bԦ0$Z D!QbY+ F%hvw\|LӔ D%JDdaۦiRqq׶<П ,VhԼO*Uٚ mdsG~6,ap||B$~|K_^G!v,,vכh%0yp6h[uq]>#zLsh0$c&ciL I$ Je#g_v"9yUڭ6woA"7iܹs -2op;&M\&vpDOuY^^&wÏkgaa;{(d ֯?6=4Q]d*[V0&o1$f<=Y>Zӣ})LbJ&0LTJY[ 0$R%xa뺄aߙ?)ai-ʲv%JDq#O!xuk[Z~i?3Ӥ 6X]csse*]:#dbba8NZJ^g4uשT|_hR =}zQ/" 8XSyuYY`k"f $I ''',..ri''GսBUʔS0M̔lI~Mj"9/'_ V7*/od0xi: KHCstt"q$[PW\aiiwﲵ;V.n^_goKTZ`*S=8$L6h:<]PPuHCNE,/.0 |˄ J!eV",TKYZZ"T*$ϔ$1:IOR,QDH>C}@Y'M"440 S@c;.Zh")M$IBEHKUj:;{ V}_6Q,^^ŢqqeYԽ .ѽIh4s_|[ܽ{!Km(Ej RkMbfa|Tyޙo}|/~?KR=>tw~3~Ng? ј++8RpVHEVVQZ.zҒ $KtiԖry[w!Kk,zXnhseZEB#… AGRJiYi; 25Q~ԤBFӔZp(a@5 !f3U,m(QDI$Dqen3U.aAxFǫ!?R0b"@]hA¬ I´-V7֘N}RR0$Q3 AkÐ!U4[U*~"% CS(eqqwOBЬyx1;e{(,2r0MϽ_OF<w']W_y0!G|+_w~*W^w~Dsk/`J1 8PZr%oy\zp`iU 991KNO4mG}x0R˴VIq;_ P6t*&,wHBpm,>!}i\nAXqR뺘]8؟%J(3Z^Ę4)1FSMbl8T\73(t"1L*p]V16h/ITDD&cVW֙Y\E%)7]fcsTDi$~ HzR ?{\p˰i4 }(t$I:rM*Y㸈xu$5;P@>=]F1쏘N \vxBEܸvr=^}s>RJNεAp82{퍬W?h4{89:{xa[T Xf˗6Ul#&:U\E^CJgW0ca!ZR˳ EIuj1UL0l㡵Өٱd3襔$vhq\~(QDI$ GY[P1 Y.^bJ(S NF0; ϩyi",@w<Ķ]^F_~ZLX\lk+L4jU\ۡly5߿iJ܊ T`Jp8tHëS B]ʧ̏|VBw_XZYcoo5*53@!jCv—f8J%, 40$M@6q u9jfAkYCG $IS )g(o%J(!%z=: C&Ia2IiLcZh09:4|2IHDJ&U @'JB#(&kkx^zI1?}}):e^s.҈988:զao.cSld}y scw+&Mrm$Df(iznϓNF >*S, ܼs(Li_y;wnq㥗_}^<8<>!"\{2 6Q0$S֗O(qSuH! V% 2B#d~1:E(yQL5Jid[e`QZD%|e>G[k0T:+Ni$DqkMk7"И@%I* zv_*_ڛDJwOu.89۟& C]z1ժBǥi=hG%Xt*i1LHNq 粶2 B]yirr0-s}O $ C44M\#"|ߧ?8ŋDQD$$q:3Qt1,.?H0Mˉ!ǧ=F O>xՌ.,,ps''l)rAYccJ&, qJA& *-OeBKl)^3yֺ XG[gUXZmJ(Q$ 8X #A^o$ GGG6Ɋ}D1sBz=6 ׮+/hOB_PqID=;M/O[79Rx@%Hfzn1O)s#DG(QDH>i6Pqm˲ })qi!i`Z" KH .غr~/>bk> IDAT X"e Bc|e?#n˵q Xz$IрKҘ4e>^`6d!nnE?R/0ִ?ddd ðHER޽{\vwnW/s-M+iv*`S;Jcfa:S3gZhy fȮmgy>ۼ (c?< ʞC%J(34Mp2.IH%Rda ; +yLp8Rq@M*ШD5vYآ1"jߣlpiy)~?ǨSDJn YY%>hD &*5L)2R!$R&LSƓ1ǭ)+lJgqDd0 A f%%: /dՅ ClfqqPFߟ(fǃY;-![eiI"MS*Klmmqt׿|}Wi+T*Pz /_6L(Afa ˠb*WjmU")K( 2 ^'>w9(D%J"L@hp, ׶^2m!DF**+i[uE7MS3\Y^Rk$XLM6'|r|$IhZ,/%!ZXZ\ia.Z͋;D60!$ ˜8LBD(LY̓;3Y(QF\ϓ.o| *#(g,[`ELFCq64qLvVZ&QXi0OIӛLQ½X yL2:?H*iZ3yl츶mpiJ M9XS),l(QDI$")zĔԦJ$Ii4L̐d}c9NLyC}JZ\|Z{noPq22p=&ox&"6A$Ɍ8EMlnnQC"-3#iJ%d 3BupYu.{~BljqrrR"=zk)Wu8c"I@JIJ3Om>m;_OOF>?smK"YD%|f(Mv+aID ?+!*p\vgDLA2L1FcӬ,AH s6ÇYi I8vE_ҥK.G{C8dy)$ISm2sgq?2LNLd2i6%Z 4m[6aUJi0͌omSTP*GIJ8( sS @4:3^󹙚YdԘs*֚x\lT*\|7p]~:;F#Leaa k z1)~5_H϶?A2+ff%J(Qg4Ð p\fMFJyloo% eDZ4EMiB^'3gݻj-!lc? Tk.'GCV;OO1 ?GL&SB?" b(!R  m4Q%* Q#e#&7x^0mtmTⶳtb'~mk(IaFZBB[oЀJi~w0^lopiL,qvi0!BA RF#`(Q$ t:Lda$i@a[x^i$Rj鄣] {ỈDqF,2ܪMB֐$Y u22#i2 REqnr6ibvFFtrn&y wmd+O|'Ɍc1R{M,ÈׯcY/{I&>)899"M"$ǴYPC$;L$Ȣ(NEƘa 4}`8(bm,r2iae?gfn?1mr~rHOXhITR}YIJd:FZh?f#8}0W~l]S&)?嗿ՙ^ǂSL=9r(~Ԛ\:n*XyZ_%J(䳀jx<7np NOOv)A`YV8fԙ,.u@d2$ ''G Zhsgec szBjf4SVu4 fI&)0M Cd!ЉU^W˶Th<'Qk@k'2"[2d" _fyU7ǼۚqRLKblεި1 ,gr:j5p$- ØuTկ~[w>e8rppHG?}s1S;,$/_&cq]NCV8(33T ~mg%p0pT'qaHDeh4Kg'b?8ƫ8dSH),ȍ!Rه8.nA6u`0 qf8OyKAh`&n7 7 370d1Nш h%N "z5.l$yYp8V c&=3jhZXL&>ݿt4ʵF.D*iUUCꛍ6GGG4M 0f# UgH%,QDH>SNTM ѣGXv~_'xaxnK^Wz4`3{LY tQGT}֏dS"#rn\_N/ssXL2Ѣ(MDT fn47n0 wB1nZ a)%ZOnEceejM MhZ<~8W rmܽEUϦV< `p=![[y!.7o}__/X$ К$P833J!TVҌ*(4 zcIk5 $c?MS{D%J", @8~@jY^^VpU!;;;Hc Af$L_N)4Z<CюFUꬌj۲HRe'zlݬ2#R"YTtV5fa8U<炐adYt,\L!f##NE2?}LSlnnQÏsBL|Rܿɘ8 0 gL'.^Z~zh^a)Nq=u"'yomJJdt~{%J(3Vd9`!ۏviۅK4Ð`L'x HO)lqOO'&dM2 % L0bcQ(* ia;&f]!LYY [TqS4;lllR~qÇV<5Ek8::f:"u9::)v1ܿ)' Z|7o2 GtֳzQ0,}5+++AZa ,//1 LS"%bGw$ID{BtS8qST43-<eV9|y^A$ SZKywP(ʟE#xz G~B?K/q/<;;;tOz6B3!QɄJř>O+Wpm z,--qEL;^۩j|~>v4Y]]lSR( ffώA>;p0BPV (=>hj4q\$ zv'&(QDH>ըz.aPqm,S&QEa0S\""JVRbn,bd\-}vUFHV "U B͑LNg!Z  cҌJQdQ ҄4 c29#R3paۙSubvӊ+W۷b&F)/_f}}jܹsvw #%NNNXE C)7n DQ@?lIdwg`emRo67[i"a{siԪsr|bg+W `8RT XV+~+S';m&c6/l6q]dR>Gg%J(Qg DQ뺴Z-1gd1 g#|?r7m3fY,O接PNsή7!Jʞ TFUz>HZ3?f1Bz8?#,1 S|hdq* {{{{ZSpL1+mf1A>3+S{\m?h4ܽ{8HVcyy5:_W}VER\֭[TUǤiɈNŋ[o|v\opy"_iPkK+ˀ[vx˗t]?-A137XU/LruL|2Qvmod~Q%J(30 =0j\zpz"u"/ \iI'$1qR(T_APbNx"[XOeϓ5̼"s1Ca*$ aF0z=]2O5#Ɠ>Km._$IµkOOyݽ,Bz.v4hԫ--0#Nc~_4$g\X; j >1KKK<4fmm 4>.]? b6\|8q*hשB8ٜ0 jU)z^C7!++ #I5$RL6JED%J"sŚ7,g=E,b|URżh-'mRifFqPx)+q#^nXXxGVV1d=ܽ{dx*9bY*2,RkYNR,iPf$8S[iL"?`p(Oqq8Jt {essCt:0ŋ ZmF//x@ܾ s/E{{{loo37iʍ8VWMhݧ^k*.]bDq888Zrzz ,..a8Yd̴{vjZƧfpxdğ"e\w/%J(3\i'xI37D.d8WJRj 7Gs9\˲by6d~yқ&Ǜ'qbU(uY^^R0 L&z)Vjahz!apbYkkk|Ǽ= IDATܼ E1_"%[-|M^}e0b4P8giiFI>ސ>|FAz<>>lh4L&~m}]t:k8S \?\l{F:%S%I*mgʴ$IRIZD%|6+yDrZ"(Ju[wHƔ_r$*=G,Lr52^dey;_ Ey|`NǡZ4M))O+|g<.o6XŋyΙMrytt⭷;,u<vW_}``0`듪 ض͵kX]]% Clnn~0C$4Y^^-:Lmgx<&cvfP,lu,' 򬍢͗Q!%J(3gph\8{J\IZ~(?G~'Oօ(u]\=j'ﭜwg?Ϋ9Y/uid2aooP_/4a2PJNK.ŕ+W t:/~ {<~tzkܸq }60%Buz.`H iw];wx9zizx,bi5:6;;;f/Y.!.\ OVU* c|hD:[acfqܕDD%J"L0d gzs'`4#9r;ts'\t'{(s0}dM3s|Oe(yo7 CJs+Ύm!ь@ڳR|1 /yevMݦhn=Yodq!$KE: }^~9K\]ZΝ;?f}m%vJMWΕ:M 5AʸV~ի ,].[{=ͲsNRhNH;x YfU3X'_% fmd;e>**Wett79֭[ Glذd2I&uH1<<>.媲*Z g*7Z}*E-o)˔Jiiw8a0-K^-OAxt:߻l^(4DձX@ .eA7jHF2gdYrVZNîљI裏rM7H2<3d~eWseQVyvffi4ڵE.#8]nX,_>oz'H9r}qבbN޾%2tH4DnYɩӽ(S$I&.*Tz΋^b000 Q*bԄDB GD"d\AC\@ 6 " *C4޼j;SJUJ)G-%Hd PzUyqiowNT.gYwӻ,^ynt ^޽{ku8c#xof:$6@vX2p_Bgg'`ZN 2w}A~(Ǐĉj59c'xq.V^5W^EP`tt ؿ?زe |k_el޼ި('TCp8L.P(Fljx<ի YMI<#/0vcZzj.qɓ  'b[ժ7e8)R)e~S%^jrL2t_*:lKޖ,hjti4®N7X@"؎CH(L4quȴJ\߫;vpIn%H$bnvvvTj*Z;ҙiF~"jmOb͚5DQט`r|`0H$!Nɶm۸+T*;v1B+ruĉrF۶]QPT*#SF&LhR$<]Qf7( bH(F[ܚ^GK^aO7c^wD *S TK`22RΡRF2IRuTK oTԭ7w\)pZȆCۨVSɿ{Bttt099I(btvv6K%@:Ѱ5lƲjQM&_,cddaDHx²,nVbXs{0v+nrHI&''mL&CRqBP*6/#kSh$B;B>_t DtH$zNXdrrEd`ACX,K)*7l1JS*jX-s\>=oe9C"Ƕiˌ*GnfyC7;.efo:3̲wT>wJSTF|"&*ҔB\gl';wdժUzʩ|E/v8b`0DȲhTAbӠRٙŶr9j+A*"Ptw-n@$pd2T5wK.avIRyU mUS RtPY(D4v}gõ Gvlw  9ZUk^qfe48ʸSƘcSDz%jh6z{F$8usczCuuzu*N'Tf*%V d"A\ $/u&B@gg'8zX4dL$oJaM3  Ǚ P(8pxիW39'aY|[rkwwwkn\W@ Urjg22e{:::Je6DBf;6cǛ} cnyTJAĐ}2*UB\v_^cUT } 8F3  L2NVfjqC!,,M  G vжXXgͮ#9<fӖٕ>5 婄D"ӒGijޛH QWFL[qfd2RV9qq,@bH*M>рJElmX! !ۮ3<|Ž=]qDjpI>OPT# rٻ%7O>gb"GTmٽ{7]tŋI$ͥQЕv)/ d2F|eYDm !y"岻Nd*-P\v#tU r晜ݭNx<XA vt:gAV6Hpj+e7@TmT~rUcscTeWF5FME%hTc `8zjD[{ bH 4?RƠ2fA*ck Aޔ*HPK`h4ޤ^KO*WVH7|g*ݲ,׿S+e.T\,l X l4  N/aSMBn>F۶Il߾\.͛ٲe /\.JfTy,s[E$g֮]K&!LXz5qfȉ 7@g```JiUL~d- 9[Az/Eo2*MN]H8x(ZH$RFJtVglBep㫔>ފ4cccKez<.Dž666lz\7kvf" RD$f~mCBJUX(Z˹[X~}ϲTrċŢ;Pm ;f۶M-9<<̲˙NvMɓB!z{{&!i^GT~xL&E bH3(ū0zs%UZ^TFe|>流.r9׈T7F;xi7Uǻ ʀTתTP4Su֪W]lV EQcb@.\RSKvz&^3io v+WK_T ~VMp)H$:u's H&tuu~zvE,1vttL&huuPH$YPߖeu+ d\AC)CLD*5 ՒRbbW,Q^L2Ry*L.+cU3T~ŧzfBH6]lVPEjP(Dr]''rl&noNO1L&]Xjj¡q[pk+_Z`cj5jp JǎchhTiTr9J'N`6{]^էW*7[=eTAC$In *el8*~x_jk*eJhoeةͬV33F)^?Gu~ ermRqwU-*{pi#wh8u:ZbИ(oO%e{](T& CUԩSb129^D( n(jJ.#ˑd852L(;ŢET*:ɞwjkSTXx1TR!UN5A) w|qR pR =ojRQ`ÔJ%2RbHPp OS^ބ 2^5T-UB!7GJ JiEpx*5P3 7dDQ7k,Ԥv *b|n!+@\!79N1߬RQRiZԽSՓ*E((>Nd6%J166榐R}Dոfpp\.Gww74ٰad;|>)7TR!JUrh4cdd-) Cvo޶}2zЎ%*  ,H.yEZo޳߀4     WO6pwt[Mw?߱-$xi7g}7tb_g;[[uk#M<ܮYӌ:>xpskVիvR~<ẵZiqOl{X+/\&̯B~|+r>l []~osװ򻷑ύKAĐ<[lL`ŗ?#Or_s.7|Q7|8Ol{:FNS;#Or[7g2c{*y|Kۿv {?'y7&yo_?~! !)ؗkG\||#_0>6̺ƈW7rݧėy{^x~‘(7yiڷ x'[?e7|.F.r_}o|dLv#Cǧ}Oid~|+G{Ê5O=ms{]FG,[z vp_7Rj*PZ}{.c٪Kު'6`ws{]ɲ,[=a5=~_x.>Շkxɗ?!m5Y/>$Cr'}o`0$BA8k,t?}r۴Ϟcx(E_˝|cu-u r1=wׯ_}Nf٪ ڷ[k{7oZixJn}6 <_gBW_]~-`K^{/|}!5Wpx6 ^\  ϒwC.=zƳ;~ħ>{;wVѸz=/։?qtoW_lt}Fs;n W^3Al٧~Ć_wn-?R  Bӕmf)`r>\oGrO?_/q&FK殏}%hӻ'~7#N{xiQwOn{ñxk3_uUٶ7 CG oz ~;kBa?y5{_?|{wd͆gk6:OmesgdŚ xj#|$71ʍo? #/A^!Ze)(  d[I.Y /_  1$2b/As AAĐAAaC2`   q^ud2ik IDATX& ‚#mW!& ‚c!8:C3\?& ‚?3\ZpL3 r؈p,遂  \ I:y<҂AA GAACRAACRAACRAACRAAĐAAĐAA egg|.Z˻8̿g;^f-a:?cF~͵OG fV85Z]_;uhl_Lwu`#a_E=alG}t9YO3;1VYLq[/|87X>̯?o1q1ܳV^6Ŷ:1u1~_,gW~yҺW!"  `LEOЙ̀u YՆe2SZ)ChKGAc&ar<]v0QGt+Y33ӝ=4: >Swƫ,T]TY{t(,Ͷky'_l;cتۇf2977nc:ۙ*货nѵ7t UQߪ>v(CRIAAeH/:3fٷTRh}uf;3;4|f~mGXcz t]wm/k̚i:jV폦r sh>U/Ltɹ[cd҆~ cMr['Ŧ7Dg[Cc{O3o5ctm4OU84n_pAA$iP3siwj(:~*ά]SWIUؿN[Цzz*_kuh>(M:Ϟ}:[jMպ`ꃭj- HwEvuu2gϧl>1蓺+&ɻ]'Ӊ*ozY[Lvu0MM*g'1y/}+g߉  3I|}usor*U`u(h/DSN2]iԧ$ǢnԹsC]Uh'cA+>`9yG9Ѝ06`bs誈:φN5y_g07>!)  `אԍҙifQ~>VlZrI17St,Y|]uiM'Eי([QlDS&J5mhj];k7ln( ~Ol>u=`ęvphg[`0.'N"4 >6ݬ1ϪDm  m13׎L>akp|mۺ3B 3kURL|7LfԦ:}I+ٗα^t&e$58~f(nVy0~ȤLaAAXpd]o為&ɌR5r/u,s6::aKz&Ut MU_vЁ<ڭ=v*?3[GYۏO׋Hv=@jj;9uw禕:f5VcX৞_aZݜ:|}aY3L%获h[wl0Jr\,,lrSҢZ:I۶e" IumFcݬٚyR g~*UTS`CJ2*7v7ө_d(P3vm1TLIu0˛Qt>_TnGQ12w=ϸN5QtqMW=LrsW:6-_V}Meo ~g_\MV?ht[H7i.X>q:k'aѯ̊MZuUWhSPHɭ{e?sym:M+ïoQh( gd5]y`xO1^mRoLMgLo B?Gx0Eo贳/nvgh.  3km(1*>3Koƭ3C1MDFSA7ޯZaF~jE 3q_״O (:3Sɘ0z/gkr| 8]UG89aCawZbcgcEvQ*'Rβ5N;f^=uZB$ge@0n;_V9n4cӮ9osSoh64>}4Bo<7M~Ym^н:|{25zd')>ѽt_|^jm sŋ`a8NsN9wИG1cϳOre':/gźM#eDv[^xyF[ ء}+מqmݽtV)K$- xHt=?DSvA?DɃ辣ZmkZdnh|ꎕ:548jje}~j&^n>Q| sEϊ ?_b-P좞i/Y`(Lע>,Zu} ,[M0d`*FNCӷ Xѡ4 c0$=o)``") %9HV_Zʢngי1(3Z fg~jmU)tgK:JSĴIMp.:ȦjNko4|u9Uh,@ ϒ:k@,tV9Yvb'sVʳ!un E϶iU0S.M+oF/'_)?l/J:ULUh?dvOUj UxҍyMM6pg{ ?WX"T7=up3% I pN1WԶ KGsv-tG' $&w&g;lҙ!궋nY]6ɕ~3Fp`^ooi T5[XS)/#M5⮏dXɔS*L)3}9Db9QHL*Ϧ& nlr~hy[\G7D=t_};i\p VEءY~Ǐp?vg{8Ql~w>Cg8 @R& G῎V7Uw=|$uV럦o6dRWTGB;Y=~ЎO[OQBL>$ N_TI[UڭftLQ0Tf;}_0tX;uJc<;ՑɴS?~tvӥ<+2:Jqɤ?NTډs,'󩎣0#h۱?tVE;4s8?:?D7'H pn59yI4lTKv8Kء>IRn&$``*n)s7ϥ BAD˂5jnd|"yuJ;cיk7-|ftf:Rޛԭ4dڟځI0̨fj0lkq:gmHuҫZ*2p7̚|cqt'\r FStnasךyFo`VMAsSgݘh"-;6,BRN ?3k3#sHNN5?^"@)5j.2*@A(rTPP9 +㢢"I9 ]<1{'  rTPP9 *@A(rTPP9 *@Ѱ^ =*Y뫿c7+R`-~flbT G9F@~:[|x]UG}=#ү_Oq+7ܸ|/gS3\jO=#ګgn,uhּuXpD +1nܵlZ6C9+_Jt>3Aj9_F>erl^9/ o5gH;e9GM_o:L=?WRz0?uw~Vak[K޳bg"GJc>H.Ho岁D]]k/%*$5j.2*@A(U#p(rTPP9 *@A(t #{B=)$&***҄(rTPP9 *@A(rTPP9 *@A(rTPP9 *@A(rTPP9 *@јww7ww-#?Ӄgc%>dCszf_hׯ﫯B@_h"ZҴҤ_/{Hϔ|/]TqƍS_kժehh_^^9}EEśo٪Uˎ;̞֍74YVV`.^-gpܹk}~S=۾}B^fIIɻөSVZN>\eQ#[n",;}- u֭[^HH-N}Ms^yk!Y5xJv:KKKy ?wJ5i$U˱i).]$O6{ٳBTUUdBB<&))I5dȐ۷XVk@Y՜Xon4n4ٳnV}Wo=[+΅ M:cEEE !bcc{Z޽{vy]׿YTT{s'OB[Ns~/;znnnGZ4jʕtwwϜ_jW\˗/%%%^OKEE:wSfcccV!/̊+vک{{s!'|ZPp̙ߟ#X~k\͛gJK&ӑUMz3wLߌ7pc/(8' ֭[kڗfl%B/ܹgH%/恫,X0""iڴiB۷o/^8<^1jSr{РAB'NHӅ '@# tdUֱu&oH*(--5jcӦM7Ç;vL-Ь5k&5,,,M>>>r[|M'W~%$5gˊ{Q.]z?oQZmTV5Ͱޭ3!~3&ר `FͲxbgg['\sժ=*q!mŹҭ By˗/{Z0\{z~kժ믿g'&޽^m\utdU :7KX6߄Y.\bŊg}yo5CF7`r{۶mB2:tBh&wǎBv5P0aaaB={vk/8!֭ϟz(S]:w7\gݳX[[{B=zĨjқa[gBfLQk<èQO}Bdddpp4ysn޼yEBgyV%=2~۷oym-Z(x:(=GzhƏ/q׮]%%%;v_{{{!ľ}>9<))IszڰaUBu.((Bh"##֕RUUՓO׌GR ֚ӧʚWaСzרVǏ'=_5K`KcbblbxV5׮7zNo |6քͯٗβf͚S_ӊs]PH!DllQs5{VZcooߦM9s>(kk7L6Ʀm۶gv:m],Ydcc#=(kN2y掎Q+W~'pppk L8ãYf˗/˖-3djƌ~~~ƍ[dYf~74|FeU :7cX6f_;3>uo5|hH}իW>}~~~Æ̜9Օ|h{rTPP9 *@A(rTPP9 *@A(rTPP9 *@A(rTPP9 *@A(rTPP9 *@A(rTPP9 *@A(rЀݤr&M2a&uM(Nx@P9xmܸW7n0d)xrsT/b˖AA? Æ  jժ>}z„Z hݺo^\\͛7d֭ٲeK֭qOk>_ǛNVZZ:w}^ZzYk׮AA~/ z82CǏ/HNN!A!_.**ؼy+W.˓ >|ǎwܑ0a,ϟ' ۾}qѢ~yYfiEYXX(x՚EКL9?^S]]~Q1kF5|ȐAAN66Nx(EEE !bcc{AddSNb㯿ڷoSKh蘐N|ri,Y"hݺ [B{!?}j޼yW䋊k-?^u۷oöm1-N)v !ZDGGKݻKLpXjGj[joZ 7DDDH WWWQZZ{FŬ{Q8͢:-ٳyeee:&kٲMǎ!*<yR9hB y-rQ]jܾ}[j\vMjܴFIjXZZ_5*fYUU֐[nIf͚i-Ǵ8"=[%#GX;?xh+ï8/=DHHXS4~jV֚^*5k...RC(/_s/.*WWKtsjZQ1WTThmӳY[Rҩ{"T={5Mr48p@j=zDjtQjtUj:tPjwܩdF)S>xq``/y˖Z ]ns=׬Y3a͚Xi;^T&^WK* ܵm\kk-~ 4QjuvY$'':~x!Drr ;q}]pQQ10ĭ[mڴII9(1_;f\sࡪ*ggoܴ#.˖mZ;)/HvHW xݺѣ "FeEUQQ!X\~O?FٳSAs̒q322.\tvm6cF:xІ ?S9&"Q9@ϭ\b„43xQ--Ren㩿9[PYYV|rx~}P=@TЄ-Y$$$dÆ7nܰ۷ߌ3ڴiӨ .">>^mTWWTK.ٓ1)t; IDATy%VMJ G$Cp֬Y+VEEE۾}իWqMKo@^^NyyJhaaxl"… tN)J`MMJ ƍf͚UXX(*omۤvqqmSI۶AOg:]VVc2;;{߶mE@S_ r /d4i8pضm{Ν˛bZ~JwttMC|% %%%R#::޾Iɉ <+*)**FCBCC݂3f[ttp:ujѹs3.]Ts/𼿿_PPiӊQn?׮]{:{{{EGGgR GK֭qOk111q !!Z?Z۵i/G?ղe@'N8r$EsX$'':~x!Drr j,_|akkw̙3ҨۗiӺlԩ .vڀi.w}B*ŋi:ukWdcPPS5x'/} ӳPc[Ǎ{BsСCӧO٭_{H!DllQsqjРAB!ÇQ k7)??ҕ?g/ӄϟϴ斔ܡC!DZZW_};kk>Ґ˿|/BQQ`|}}O>5o޼6o޼ ?;w\!DEE… ֮]+ :utӧsFYVVWB !:$j pGGǞ={ !v%8po)}ܳg֒Ν`+Iw0|ihhӇ~$ 1̚V@@GΝZΝ;N''W^yU~)!mܱcG777$?lmm{챭[۷q!!Āw*{B}⭷ڽ;.///++c4ķo߮u!n,5W~Dڐ ]Ү_oFZnܸ!5kРBŋ !uֺu뼼O>Y\UU%O#h֬իWg vyB/k u0*rÃngoܸA:aāvMs''ׯ !_Vx"XngϞB޽{ !֯_/ &{AqѱcG 7JRcÆiBEOnn#GZ ϟ' 5)#ttt999W\66Cw ݻڵk|0G%=h„{7==>>^2tի0*%5|K.՜CzΝ;##jԩjNckkk[4H!S-=ӳ]vB![TT<{Om۶-Z5m۶׼2q/111NNN...'N\nT60͒%Kckk|c1o{mڴ 7o_B ?/ѣ7o, <ܸrTPP9 *@A(rTPP9 *@A(rTPP9 *@A(HHOOqFII (''';LJWWW +)))))9=,--I ^T*++YZZ٫{dhJKK v}_ʊ??L]k֬IOOk֬ٔ)SIcٲeii')&xC2 ϟoeeK/S62~~//\@Btr!D|~ZݫWo___<|zU]]O6tr!DZZk׮xXEDD!HnT!(..Bxyy a%TFBr! VvvvB2RrTPP9 *@A0̙3fΜaS)))A1{f_]9.3ߎ&hR"oɒO/\IM=9L_%s 5k&BXXXnݺɣΝ;'3{ \p,Y@E۠Gv}j<;pKOO?w|N!,!DddTrrRV*^@տ?pZ!"Oq\X|+۷o?iZS^^m۶Ǐ;f Z=pO6oޜR7p7޾K.11å%ĉzXYYk"""F3{[_Hw&Po 9u{2l#C(_Cz#̈`~6?9s̙3ee=&P+V,OOOW-,,Zw߭Ȑdeii9d믿:}hΝyJܥ~i㍿g7nBTTT۷n!Uh2pՎ;o&OV\\|@|uuرOkOK;)%->>>//oڴ5C2kx\sɵ|՗yyyǔRy礦7}g :uԏ?pTvbbBϞɎ=2aĐ?C~~~Ro=zQwppx饗\]v?99?k]dN+ !ƌV߲e˱cǴ=}ԟ99dzg=MyoӼynRoyyyNN&Lhݺufffly礦7kwؾo߾ѢEM~9tбc4MK1cu5t#GRrss{m333֬Yp ̄Q{_Rucǎ[&]>NKKW#GTUUEDtUTI,_o_;Pw/2v+r{ws !2۷Bo߾-33s'zz8E>E6Q砺9Ի@6dZ?(_,̈WQQ!իג%K4699E1q-[8y+3fM>}׮LMǎBtbkk#-{'"##:u;c =qBbb*4SjK,ݻ/Ø.]5vQ1acbbt/ÇK1bisRȑ#|1y͕cRZ5u>lذ;L5J`uЌғ7RRiwiݺMI{LBtmUC&{[a=Vz`<{{{&{{r!ٳxٴnFb`̆P sSd{0= 3jZLz <0<<<1116vMj* .O@@K!=)BP`jС}S{)R)i݌:7fccm9\)27Ё¼gL{fD0mYYYEEEwJcGČ NNN&MnݺQ&<\~_G&i[?k5ͧ:;;˗77 )uEuƍoVzZ...ZI5$z,]۵.DŽ9I~c} ͕p_j>VZk uy#Cf֭G DFF{]]]bbbQQF-_o_;Vۋ wdff !Y:hٌ'8NzOMf9.lx.=?EF@a3{fD0cYYYaa# GkkQF 6,==---=##dݺu3fBؔ}\Ew05ږJCn޼!pppԜ֭[򕑛7ojnn.>LXE=wr|||ZlKk^(-䬙4:B=1p^ppp,)yM7778MZ+ᆳƛXUU%Uvc c~׮]7m%%%ϿK.Bv%%%]z5%%EREFF|}UZAo/2j+r1Ƕhwʪr *//r劵YSd:ugf\a@G:Pi= x`-[ڞ{1!DVB%瞛5kҥpoo!Dzz`jBdd(ᥥ !ZL} 99z0a)R6i#F.--tל,++[n7Ё\{{I Iܻwώۯ]V]]}ڵ]^h)3g8th#BOOOil׮]6Z^^~M~9sƲe_;PztSSxxb˖-ii'Nn۶U9ٯQ^^o߾MeH.?5Wg*L)Y999}4\zlӦ_N:UQQuVG?seUډ[nVy&ӺVttr^/-[mYk(++;ֻ=210c`ޗȿ"((H t7::vfo;X{ws%Ґ={v_\4ͨsP=c6@ sjYӊ#tةG ۷o߾}K?B 0`kv=z8~ӧX{{ßk͚5+..^tG5*fԻwcǎ}7mCs2OOϯJfH.p5Wg*L)BCCV^zjg;u ?okI \Yڵ#)򪣢ǩ۵='|rxDCxxD\ܮ;wܹS#/^4`̓i{?88ƍ͛7}E{{{|}l0yG1s !Ē%KuLs% <(^plOc 9l8 4soz,rgܸAA풒~p­[}||GKddJe˕ {_uȑk׮9::4˫ƍEw05TW_}mGq㆛[׮Zɓccdee9;;w޽ LX ;EȑuTWW{yyׯhO?"5]ddСCkjB0ʪŤIwؑ|Ν;;Vttr^/Jz嗷l;w*|Bm횆ZUU\YY4nxy0mTȨ={vKo|+++cؙEo;ڵRE{)>uv3TϘ͸@m.97Ё\{/%'':~x!Drr #W_~Y !B x^^^o5-\TGU˯؃)5j.9AtDDͥK6n /<">\G𠋌 {{{xH{͠*g^={"0j IDAT܏?IQ9 Vh?h߾}鶶'Nu񊊊FWWאx\]U^ݽ{WIIL&5ԲVj6m2Sjp{mnWT`̬bّh{ơhCKo4VuƬʺ\xW_iŋ===_vv#~~پ}[NNL& >}[[{ƚ~`ls7,F?]E:bϟv*Se#WJR ;uzt:.IJJڵkٳgϞ=T6LxBV Bzȑ˖-,e WWW ם4#B3 򝜜M)W_;//Owa;;ӧXݻw?~\Z޿{JJJYYѣGe2ٌ35wܙ*\^^sK---V\\,MLL,--[l$ 0ҲaRejjRH S455o^ZСuuuI-- PO>իWG6)}5Zk3^hᛛ?Sn!B̌sCԑbx1: 2U[٠ WW⼼#Gj)Gis+/_믿=#5DRRRYY/H6cg#F9r(((껉oυ Cs,衩#}sy{{:p`:xtNw!7ztVsa!…FV333ov1sZffBOH]S bcc5\vW_۷~ǎΎ>JssjZܕױkkV׏?pСlG ^|}~ݡGnK/ׯ޽{RRRNZpQHHHIIM'8qDqq#<2th`QQ͛ RǍf|M0`SYZZ`!CmٲE?Yj3{)RRWUUyzz>c7nT(r2;*U?::ZV'''߿?;;[g'tssOLLLJ:a0GoY/F>˚@m/m?9;YpAnbbC_8X:%2Ul *{Tvqqqrr onnb̘gQ€o=cAA#JJ7o|9ͦ+bٙQFngM:6577?_,ZXIgkԑ⸸8Ý#RGN։WfԬۇ+9466 !d2\.?~u[һBE0@&/ZXI|||晙K"͛ V^^~kCBh=w___Lv=/י^ٳg7̝;_ZSLBdg>)U?88D.@1?<ٺi{{1cfΜّ@ 矅@@>(>k֬]㸸8){O9s;99M4IQW3[F>˚@m/mFg͚$ݜf;!-hV̮lh(i͛7ݵǧ ܎a"ݎȷio+++N:p@˲5zhyF;UG+;pJWQ+7ՄmݺS'OdޚVUU !>:U]]rA6ٚ}:uZ1}O?Ǚ3gX]]]YY)%3ؚ/J7>mhР҂NnBf71b&,8H+%#G !Ο7#>NJJ4v8P{).\ \+# QGzC_4Vʔ#?UY;;q:t=),,\rYijF!4Z2cegFW^WTTtсN}hW_R{uF4Ƨ=ΦY(=RX4hq/Rɻ)-k}W˚gYYeƍmF,-{!-hᮢϲƎ;|pzO2L)y{9AI0H ٴYhE,83[jVML~$7m(.\#M衩#}TKX#WJwz;~bY^lGzc0ă>8sL"77W߱cNJ+2LT6G5~ػwOffJ@0P7jYS,k26 7;::_v횻U:~4VpWgYe]\\BBBO>1`GfRn8;юfȬa"ALiLm۾|ՑlM<4I;j꫃WJwz;~bY^0WԅΕ맑dcƄ-X_B\p^Z#g!.]8L1lp!Ğ=]e?ksrryyU!{KS6eLѿ!DYYv￿Vx?fGEE74(5lLm2Of 7[tZ!;FceJHή3fB*((,9#cgF^Žбcv0[MwNe^\}Y|tNuر 0sA&irrRҹG}r劔Rؘ%|s0""BwӧOTK.ݻg67Ge !4>VVVtbۼ}!fu S*bQz.U;۷̟?6db&0h _}uJTSwܓp" aaGNLLLLLB?I&mܸqm۷oӬ6m{Tvqq_K󮃃ìY[ WW#Mյ?ӧ;zyy_ѣG[qqq'j 2$66@VTTTTZZZYYgWoS_=Ar !Gsτ_~GJkN*-nܸqƍUQq.88gt3dZ2F I-v0ix a4V~!-@ݳcf777!!!c>EyO>K3mV0"KJJߎ'''T*Jo@:_6==uD{h _}uJTSwܓ 3f̸CCC~xQ?:p@޽{8pɒ;;ԩS===\\\ß~1L棚T !ή.K. d?]e˞4i״iӗ-{piR)[[ۧz*..ٹ\.8p/BrS] {)d2ٳ>ثWaÆ=q899988 4'ZMF>˚@m/m666K,>^.kQT۷oɑdaaaӧppph3>##5""bڴizcjP[h}͛YYW^uuu2ej{i?ڱcɓ'rѣgΌo37[71JuSU*UHHhBBB[ĉZ;.&&F?/<$$d'4+|ɨQ4+7lؠP.Y444ԬV3?C//W_}FŋhnzªU޽D&EFFΚuhZ=hVwn@`J&9sZ0Å-0w}wA˺䤖愄yɾ3BJTVV| ?Q_|B[#˗-[=vlXKKK/kkkKK˖-[Eܹ355UBrrryys=3}F#gI/333: IIIv$8{ٳgʆ*88YP\~IZy)!Dn`b}|''`>ooC9R>/O!=zkO>իWG6m)h *=6j]p7^{h{miiٰa}ii,55UTޅ;;3;*;7!::ZV'''߿?;;[gH QRRysΥ7^' B0ۻ۷o}ݺ|ĉb''Gyd͛7Eӝ~ͷMIffRȽyA믕j !DLLZM\\\kjj57o,..vww}m=g\??''I& !LFXиV&Q{yC 0cƄ͜9`sUVVVUU !jNPrP!DUU~>UUB>j #%1[#G !ΟHH̥ZuB >\+l[>}j)2ۻ[={o߾NBL>Oq̙VWWWVVJɬjm7nܡC?C ! nk[K B7n{m^Żn޽[QQ)$4tԮ] W^/J 'sqq,GbnnT*5B'bD:UhTFn4b7nh3œ9sr~J˓FdA>&&fΊɓ)<<<Pgd !j퉍we2tumf@Gᆳ#b׮]B888&ɔJ;6:|f\.ohhwssūBGG',=OU[quФT3Eںш9::_v횻: {~gΜP*;vXbEFDDݻ'33_RI6lXzzK233mmm###jqqq =}Tv/ IDATvvtttnnǀjk-k:55ƵJ`uݼE0w@s3 !~̎nh 5g!|}}sBH+XBLrsB~ڊ[B;W^M%999BY[71??_q9W`VdcƄ-X_B\p C.]$=0pbذB={v_v-((H3thV3@zvkFFzvvv]]ݘ1M)-k:55ƵJ`unNɵ {B455|Wz{)**jll<}/j>}ZR]ti=+Wذa}{BF ssϨT3BdRYUU}ƿۼ}yg"...;dYY?OM!CƶIXX#J?]//?Ӽ=zѣ2u!**Z۸ŭ0iҤ7n߾mmwMf Ϡ WW kAi\Bndžݳcf777/)-k:55ƵJHuݼ퉊JKK+++y׾ݶe g?0hР{B$''k'7o^TTL&sqqo&-]DCcvvvO=۩Szzzٹ?r 0o߾vvv Ѕ˖==i$www;;;//iӦ/[&3fw}?<_?ҥKCCCe2g||ŏ}1uYdSd2YdddBBB[!22jGлw.Yt̘0CB[[iW{d !DXXx^:j =F)-k:55ƵJ`unP{SO=G.8Ǘ!r]eddd话?"##.loBUIT޻k׾o8ɓ'+^Z!{h]]ݚ5WVѲ;|2KDEE !njq"CN81l0 nܸppbҤL'yT*UB¼pLv…ݻw[(] 3p/^@zSZ>}:COLOOO:]3p<ӆxyy]r%00?͙37==-44tʔ't^{ ƍ7n<@7 O333Br!J"@OT*0f!pwwB/.\ 1s!Dpp"='BTB# aB1aD[[۴*7l=v؉)'Ng4iҼy 9^,Xx%''枩ijj",@ׯȑ&L! ^G@OH9999999999999999999999999999999999999999&wϬ,[ŋK,݁49qw?~ݻx73Cs  0h{?۷4h`@YIhO2%))I;K/_8G-Nu444^:44{„{6nܨ_ͤ??߇gb]|>˗/⋚TWW`hddd话?"##E3Z][[g-K__ߪ*MѣG:\.o3_?I'^{_~x^ccvL}9c !DCCɓ4z{{?b9h ??ci.^(viJ] JJ5}ӧOk+**4#EEE !njN䔚8Lq?͔Ǐ_nbРA'N 4H?!%%EJ666]իk׮|ҴAPPPzzzzzj.ܸq񔊊'xBZa4N.,,zwM)>si`Ĉ'N-%6f:>|zKZk6S_^V !z뭠ÇKV?Rںg?GGGggvHZطo; :t5k:Xk9r㫯&)))I˯xzz5ĺb]o~Ȑ!AAA0ZVN@/B`E666ҀZѬojjlmujcJ mfzBZGZ?>NZ駟Snܸ,''GZRi!**JZ`åt҄YV4Tg؎*ZZZrssΝ+S*0x^#X4} '''7nH  GZI҂f]+W{&0i2wuu;XkMvvvF'vE_ssΚׯTGe g5C|T ]ƗKZHNN^iA.]4...mf|׌W_]{.ZHKkeRZk=:S&ECgMe崊qlmmtKuW8ĎtG&ڎW^^֍́5 >\ZX={v_|ƍ˗/ɓYȑ#<""BZHI9.-?BL̯NBffN>HTp*N

d`ٳ(.?/￿̙3gϞ}75s&cf͚_+yJ ㏗/_~K)z92y3Z/͝JJJjj~xLː!Ck޸qCPw+uyҬ9{3g}wuW ---jZV_~R8Ïݗf$аtۇkZŋmy͟?_g̈́ !bbb}M3'());vlLIIb&N(YpO 2xƍ)?ܰ1111Jk- /t cƌB9s&<7o޴J'*|}}߿N&r|ǎ ,puuuqqY-[Jo988tAِq˖/ώ{_8tC~ww̘6Wl4&}5>##M<<~}pmmѰׯ !)eعsܹ ޼%xJsss>}pݹ{wG\;;&yȧWXX$ t;Hw:g;_ȨH!֭[^./oB7M4ʕ+G[ƍĂCp/˯O<}^^^ 7n4! <3Mݵk3@7nCd"fЍ=ȂK!… ׻ҊN=^!bclv}*칊&Z=,0pxj辺mLuօڵ3??իvvv0a+ܩ >>Cҋ2sVٙz<hKvMQQ=fMhh{`Ptϝ;=::Jzyŕ+W߿ߨQ+Vp~>/0pkkk5oyxK://_o=ۻtt']*&&hrNJJ}y2--mѢ!! 2eJRRN3gCh¬LwM &##Ck?ryYY\~0ԴÇT)gyf/_N#FB~٧ ӻwcǒ͛7OZ\:K {¢w}}ƵkVTT[zq5R۷KǏ{:yYޕ߿B&,,C2L;MmmK$1cƍ*v_潏?X;t(Z3OoݺU;\.߶m )**R3|j9Ң{yy 6L pwwҤ^x!C~=C߽{O||ˢEv)MXfݺus\.ӧ͛7-)Vzi3QVVVVVVVVVVVVVVVVVVVVVV*99YȽzj}}=)ggg#'LC@LЪo;vh]}}}}}}UU?|}͛a0WMMM_\;;qƏ;TCCŋR/<쳽zA._mٲEuuu}' 988aÆ3|uD0 !DUUURұ^z=SL=O>իWcǎUWWØ9")Z?>חh=߸q[ZZaB+ @O.P(r aBQWW'߿?z*邿P *J!S !J%0" IDATЊЊЊЊЊЊ +WXrouZ4}s0V/7׽zNWh2 orv܁ߦ_~7|7^߶m۝ܴwʕ+\V*[/_^r蔳=xMӑbw.ku+`Gy=U/Bt}:22vbӦEEEҲMg_PTVVjV!lllju~~~LLJ!z\R06S]<1A莁-++B,[t```KKܹ RM__?BQYYYYPP H/( qk@;D9cz^w;y555 ! fkk۫W}iOQYYYV lmmgΜiggWPPV5J)iA0~s'NԨjooq_mnn32\\\#""MpLTر#'gL3vvvv) F͛72^9eTM?;u񊊊FWWאx\ J}L6}  cq۟yM/Ç_diT:RBBBnbܘ/}nlnxM5ٔJ&9sZh绢wVJcz=h_H***nܸ1d77VTTHJw+DTe"e V9&|GH8qS4]a̘1Pu 1hK>fJo:CBB.}B2++?Ǘ5JrÆ E%Ksk;Q Xm֜*;vکyyٳg* '+P_|Bޭ=rpyyell̻W_;//O8r䰝*?+..^&&&-[ֶWJ;wLMM6\^^sC17O>իWG6ma1qLlJw:tP.999!aV3RВʖ/AHe }٧ҝm N)LFkfpgkiiٰa}ii,55UTZe4Otxzz544HBBÃJKK 򥙃ׯ_|ÊDFxhΥ2Ckvuqiynn)))eeR܎=*f̘i1S.i}nIĖ:vvvV(ׯ_wrrV>}J{F3sP_ )88vejV1wa!…FV333ov1effBOH]S bccs3ں+?:t(;;[;qDqq#<2th`QQ͛ RǍIvd…BBBJJ7mT^^Sl8_g6=GG'|=111)XFFFCmnMh3Q+ձcGs&DGGgggM=cAA#JJ7o|ܹ6dn1׮]{^MOٹ…  uyucskfpg,--utt\`!Cl1U_Qo8{\tɓ!{TU?n}māՂfķ &s`{D400 H};v;췥eDqYfI$7n(##= @"*++e?*{<}*%ĄqvBV 90w=ғ{3&&xo*,Y*HY\ٍ~#Vjn< 6vba̝;{g޼p"y‹^~~69܄>b:斖:W苈^YjXRi_tgla!]ks"n,0&v֬Ěl7np09bǛgsh5人:.44OHZUUU__q;A-(EujC 11{`&}5cO&~6æ5mDF ɂ"p?&ܽ{wXе&<'#zH`7M/ΚXӝoX6gs!^!"LV&UUU^ < K"*oQZЂeH`sh=5;Ň&ix>ctPز-(//[dJ1cFXXRHKK+--!vBV Wh~hOG{{5k֬XBT(JJsС[D"h4{3t0zzz<<<;]D좿Xoo/aww&pְ`V6Çg̘?k֬?qﮫtq[]=e]kst:`Fvbݘ7;kVbMH$tjzbݻre+DtVfoo? xԪ6\c meZ#;b-;و=VVVj(44L$涝Rffqa֬Y4\̕D$JGY~믿NDndRȍchL?"RS(Djkk\rx \5,؄²e˫O=:>>OÝz_ʕ뚚j" I0j``) Xֵ&<'6[v>57_7pݘ7;kAbQ_[I`דQpp[>>>ܙS"s-B144`PT,Sdp ن+aM?8Ue'C6# %0":vhwwL&O,q6g);;;;;;ϜٳtvDm۶MmTVVNDqqqD{Jն۶mݻLf7` Q^^BQjSNQLLb'NPTZ˧O""<^Ho)Y E=]ze``᫯b(8N<(냱UVY#<6RϵkMxN肂FVG}nlVzyw֬ i}ެMc(99RSSν9{줤$żw1*****JH/,؄/22('''''G7#d|'GL6yj UVT*6]ks{w,7Gn^ޝ5+7558ex[OmJD;v4Ina`oe.\`5OkJ"`RC![ Y 3]-mFHتW[в㟍Qd277.6͛70nh=̲f ;m,N,d_O=[F.ߴYTdoo/J7oΊa/Ҳe˼bcclyF33b6YL3JQ_NII˗fgoXLDmmm>>>o;M|۷k ٨l]u~!EÄ  T*UbbDR-mo޼y{{駟뭷E_~EDDDV֯7򗯟~~vޭT*6o/қdZ%ILLLjNNN Ũ^'yˠCoxfUQs;WQIǏ/--D-Zd)9''贴lBFȸ/Dڰh22U,;B~>L``؍;c;bA%  Ls?ԍ:h!cbAoioܰswwݻw5664T*3߉Qu#Gs?^vڵkMߢE v``/Z~lll?پMvo~{)nᎎ󃃺̵+?uuuQٳg/O8.**b_PPpf?v}V__~sN~~~ccSvv67xNgهV`hh/P*~̙_ΉXvX sr(,,ѱYV677KR___sWh8*^cLw~Cؒ1nկ-g*jĆիx" w4ǝȂ\*0wQS5"qgzG,$,Ʀ*, V 񘼱 IDAT+oO`=|lRi_ݸBD O|9ȑ##csFS\\7UUK,H$~~~f=֭[DCُ>>>w577-WЈ WWW"&̝;Z[[؛7[ڪ&05̳U7`1῏MN`0x>)pe޼y555l¢X+lc ƖX4 ՝={V*.Y;bxOp¯~+":003V]QYYED~皚Tr"""2\"|`U'yˠCox6AlX <朝H l΂BdAC o|Za(&owwIJJy,> )~!&XWN%qOn4Eމ3XR[\]]7o zށ͙;BWWמ={؄&p;E"ҟٙgԉݵZd?ɼ߽{w5؄9}x3 .ǻI+1jjj*OUhV&FF;alIEðݛ#H6l8;`^ơ7&%-JJ~LKKH$lscӣQbb7˖,YR)g̘T*JKKX߳~'gil3NAӇlRE->E3=V[noi;وtWȌQXPIV9!eO :dX[`[r`6I$Nww5۲_f͊+JBT===ںuA>mN{g>8cƌ(Yf}DkNfGdTA2E`.==ݞ6imh}Zv0wpDpvvX<ݝֲ:,+sݺmM8--"""!!APxzΘ5k+++ jl"В2H$˭ﮫt4r'-bf5 o4} oLT08,8Z#›+]"3" sGNh4VwG,$~0!2ևq<c7tR`` r迶Htt_u"u9s&555ܹm>x_?-[^} }}wl fFMD Iuu5z`L?"R; |}}@\rVmOD~1k; ^/V(cbbrss{zzxdڪN¢QVVZQQ#Xy!CJKKK***:::L,(3'BCÈر2̚3z*}[܁6 o4} oLV0kh+7oLw3f ޞw;bA%a6"X2 ix"S*FT0c@DDr{?ڶmkaaF/++'"cwjDtĉ7o=zGf7]ze``᫯vȂFVGĉ*J^|S\F`)5ZV9u$ ɼ*VRϵU[Qn1c۶mݽ{emg+` ܹD`Mhtt9x𠛛ۺub;h'1s̙9sfCC޽9dr=ݔ=uΜ9D$9/fcuuuUUU'O$"d;-i f=McA7oLw,'ո#V\ k'Ž&f+⦦ ݽcΝӧOQJJJNN<-|rm[c {laII۷>#˲eȢq9<<‚ Ğ9]~~~~~>[͛#q޵3ǨQn9LNNsoΞ=;))I`qqqe*jϞ@ lJb%%%UVVs+'"''+W׼m+`T*ikkJDh:يFAAVj;8*3F19Ct:s2ͭLJMwM''''Z#,L'(w6,ixˠCoxcZE'YDXP +]&G( FqDDDQQQccctt;!;bn%ihh` /e'5&s`%ɮRo&"!G,s<7mzV*:99K͛ 6A"oMO 75k]\\BBB{UVQAAex'/^ ʊH$^^^iii6=k,/xzzbooS_YK,H$r<33Vm-_|e˖yyyb77-[^ [7BVb^2_þv0MkVh:يFMM <eƨ7r'y/ʊD"v2M0„Ɉ(&&vǮZ`ڵ 'R~fq4e!71Rgi,(͕b#xyc4숹sܹqO 2Lwckz L Rw׭[GDeHBdgDcs/XGG{a̙o},"h4ÿS"|T4EVqϥK+;;^ʞAD<%%%}̞={J]CC_6lt>3G+V`Ko;V666V"ܺu̞7.^h/>w|"JIY@^6xY0ϟ?wbTrPXx!00=k`JÕ[充#&JMM-nڴ@g_|ym"00pl2yfU|juMM͖- |d+PRRt2''1/h/>?9s~zf,ǏR$sYUG[9 /e9!q+ݶm[W0x $ܑ ɂ !m$Pgg=/ց&dPTT{)w***,M0ULU;vX Ll woNUUU__F&n=zdSQV^s葖O0fR0ɍs%{1ajmm20v}҆ojj"̙3888!`T:nޜ8__)|111}"n Q4sL_ߙ̙3 (44T$M6m򌂢օ Md.\HV_x FM;_ ۼ9t:_ZZ|r}}}'OUUU ?7x߻V;-ⶶ!??v300޽{wsa}KKKD"ѢE,Yh:T]}):::-m%[С.]rppZ"ё{˺rҥ˸]0jFmQBH$bd;uTeeVMOؾ !髮/"""~ͽY^^|ϟϽ{nRysVdd$o "V{jD遠o# D.\J,˿xL5۸UUUU^pƍiii)=yUk*880Ft:ߟ 76ޜroyooo߾M___  hz[%Ax0lU(1 br`orݝ R~q̙_mggGD:O>ii8иp#Gs~ڵk׮i4}=>~z݈:zȅ 뼼<¦F"jgϞH$O>[칬Zvqppp׮ْwoll֟MeĎ[㫯Jb9X,NMM5!388{FcQQF17$&<//;1]XxAV{yy=s^^^999J"?[0x䍍7离\\YY]9tBQcʁ6IY:}Æ !!!q甉6n4k,Dq&"*//c TVVъisttK,h``@"888<;v|wիx"m%##=((;w_t\.wtt HKK#JKl#""5c;nDc Ćg̥Kb sCiJُJ޽{!!t_~~(%!݃aa ⒒BDƖ~wؙz/&"/_LDV vvv^f ,t͊7离\\\H$6mZLL]c1766YRƚ$ +&+Wdl>ZI!?-D>bV͛D4w\W11yyy@LLLqqTUU.YT"Yِ WWWٙt:QQsqs7y‰V;n .T6:blC3ƍOG1+$``еk|||*+(55O\SS#J[[[[ZZbBɽ7ϏAAAă#[ZZꖫWhHo"[nCf?A`~S7离˼yjjj؄E5sWV馷& fڪ&0 br`6vZcl6mn.{VKxn4C|A07 ,VTkWW͛BBB SnsӠӧO7"z;n ӡؐ`_v߬%&&>|lɒ*rƌaaaJ"--v3OG#Þab , [ψp朷'TUUܟ(Ѭ]'wZRƚ$ Ìepl#NJ$Ft09::vwwyz`.IC`oof͚+V( BR){zz:uVWoo/w񠻻:88xxx_]٢vM`xZP"y5+++ jDZRR^VV&ruuweȶևjjj1cFTTY>C"0m4NwQ Nh4(xc9opvvX<ݝ^pִ dV ٥& 9?)Jc EDryΉ޵3Ǩ(:99RSS-0{줤$+twwعs}/Ë7<-_\\8┫5/d277.6͛7:=0'Iuk_`<"##rrrrrr߿q9<<{laII۷>#+e˖^EEE1c͹!J}||ڤR-ⷸX@fu<`T*@||{g۶Dl09X,~ŗ-[%bcclyߟ[fӦg|r {{gee'\xcddOmryMJR'''{{{Tys񑕕)H6mzvD/xzzbooS_f 0&3sX,6e %_x[P$ɮRo&"kBb'XTELXbbb+ڵk$Ode&O0̿xx^&11)$$[j$+&>>yڴiaOpS*syxx 7GځZmmܹsl4Y7;;͛,Y!Hryff&O+--5|wݺuDTZZ/DvK4~ŭ#tttfΜۑd߾ٳgOڰaMM?3~!D~ 9[o;>NVKi SxbB``{x!<`lϙJMMErvvvyC'HL?h0!G< 9B }{վ|1{׮ꪲ^T*ko~{1߸O=, ~̙ŅX`g, Z`XP ,X`U- ,jXP ,X`T ,jX`AU ,V1/ldnymة ,Dk[*WxɷgyqaM]}?> 4;[*L[Εe]&Uy߾0& Z࿊X`T ,&g}viiiii駟y晻;N/yiii4WW駟_g}M{{,D7VO|{׾z-SO=u?\}kx7fDC>븀O.Z<0orZ}"Zo|_3};O~C'D<:;//=So{۞}o}[4+_|3UU=#Z9>}׿w~^JG?w]|??x Y,Zo|?{~>O~/~Ǐ'>/&#?ϾɟioeWg>k]u_G>|p?w܂u~c 3K_z?OϏ-o_??}ǟ{9~lMFp+=y|׿u=p.Dk!Z qw݇Gz+W>}}{"z{{ݣ:VP۞׻>z~{lhݟuKZ[*n:w\!_[[;zp}}}s=s/÷>JmL~я~ooo=3O~?O~Nh-DQUeoo~ȓ`Ji~}NЏ}cxݘ|[Z ,ɏ~w:uooyo|^}{-Dk!Zw֎ܾt{aYzkk'yZYY/]t)8~\};qyn}kZq#>~<xg^njh-Dn,{Ǟzǻ~qO=~:}}Cu~Gy8O>׿zΧ>湕):m5|$?_Bu7/ֿ˿}_}c7׾/#766ڶW2״ԩS?3?iX`!Zh>OoƷO|_W?>1{/!~~}#__y'?Os{M^J^ GCthw[zgt_̙3~+_}>yf=$h?hfD]´hAս/1f^ +ҥKOxʕ/~7yܹM~uD:};~ 9o˗/VHw>}M~uW qgoLoSG._7Q*(k/>ێ|[V# z'ki5h.}'RW3/=yAt'NF.^,-ee'+Jc]ӦIzݶ @Y/nvKYj!8P _1|-G8q B[o_rjݭ|c>tkܻr>7C'/ʥӉ7mM'㭭˻[1xB8)Y>65Z(!/)mPi# X$Զ^RD mt}BG`K/h7mYL-# ԍ79W ft_17P"%CQDJ+if,>(2ghUu iF.o(ɬ ?K{UfnBb7zYPhk9DZa=p+XkۺNڶQZ,˧{6HHy^b"e,31Fa7#W,4HigwGϿp٢SAؓO<|Wfҏ~PERfc" W,E@wpX̵ RDd!R1^Zkʲ@B}$Dd TJsBaI)% eS '@!P8 CbȒ4Di3g{b $HJCJ)F?TQc+"sj4xސ ^s|Z1ڶmF"uݰ(6u-b,Jಸyѐu uvdz+{|C6v B;U(mM6O .]y|#sIӽŦnn.:y,FhQq'OY+i#(eu9"0&JSzTo&ax3>O=N)'^HT|5kh8ptU&u=,1"+^`aD$)IbJ)8E1z(ϳegnS ̜ &YX '$ 1 }Y;vcT1Ah`aP3NB< (C<䩃 ҫս*Dvk(ႀ)%N [[;Ͻ ZyH)p״M޻,$L'͍ۡ*+̴ʣ+\uQt6cPdL5e(3]M7eW2co. ;, Tiyѓewɺ"#Kq$RIk.pAG[8B_eBSi~XhDЪu׎Gcyk ճ`TG^;vWZG8}]+<.N'#&@G} ,N,EڦfD(c!ϜuYDԶMjH̕0 ĺmXPH2PiPDEʹZXD!`VD )rB@(IXBڶș"3D9@k@"h Z>;\U=Arx~Ъ[n@U W9@B @*3EnW`TBIo:7*L,ܨ K9-z,@Sژ^YX]?^rMClڨ]i[N).n_>f;uR?'h;S^% SEZ佥 +!@$fJB!I,DZWW`Y|G\ҸSVz/?QbDNm[gVo0mu;껐@hA3H )1C'R)EcͲ̲{?lbM$FFV$ssSaA %+h2CLk_53[B@" )0Bm`VJ8y(pCxDJx$tp}l;Ę!gTRPf%~fQH-'˻}(IUJm5ڑPqgmUM'ۢO,Y1j1,ϋ}J!Y&J)$D*lo_*f.v../E' 瞟F$p>/;h۶@)ƌN{!EDJ+ 2 @,9"R)%F dnLI>d {KK13 RM=um DJ)1bkq@@Esmf玶mj:seFT(,IB SH# e4# ;FeThPHcF @!L)*HۺZtpƣ,Y}c!ᵻ:)pV A &;yѹ|ҚښP̚{ll:2Ze2͍'^8snw0But]%'{;^JJkWyoy5 x:ZΪn3ۋ~b;/_8kf;eV-u.jW6^/v}Фj"HIP(+ynSo8p~*P0G$RQ3z~-DSgުٰDɶm*{./wS !t*XkΌF;6#'H"hb:]}~g RJ8gVkT"*VȢ0b(#9&1V[gQ,J! @8($#3IR2KVnT A5vu53WUz.mhex ꦾt,[[QT,/++*Zjơ'psBL-_tzIÓ: /]x.r͝][][]DٴqYV7hXwjF tt%{;ۃݽz^]5SEgYbVǭn|6+s9%kA) |s "7Qް*+ `baf@!BJ,A R >FXQs]cOpy֕ hΜɝ˭ܱVŀI&"@Ĕ[{ hn Qq^57mb#)@\f5Q>Ź 9W\ (!B QDY29j8cQ$( AVP*32w m*-Ҿߔ*VOx xdʭĽ^Et)> Fcnԃ23NӅ3gqʪq2٨M{í$mgmiUY?$=7YY__twz"展NP)$*EMlfM3R+e7$fg23/Mڱ(6]riZyuCEY/5j;޳Y-yqFimPiXbLE)~ܺ ܳw`Ȃ%L!qd&ƐQ$lCۛXw'3? j{{_vJ` N4~:vb|)Ƙ8csUXDi,2m\bmAfa֨QR .w@S ¤5S3bAH"g2s5R☢gC" A" B"H y]Vv;6}3V~pu@w+G9?k%z_!u2nl+‹g/_WViնųd Luj8U=ۺNUJlm(4m]T'ƕeo{{{2Pqh&9Sb, m6{[r]6ϲ=MJ w]-9j\_Wz[:Fjcơ"eQ5Ҵg~ڕ5D5 ;{(O 6͝2c611 R+HRZ+"dNU+R2ʺtyiN62Ť'29"cJJ8ŶC޷MTP"@@l:(̤1:XRH=eH HOZw,xXJ^I:H;`!‚>E&8r*esa3ǎCPx L ,(? IDATwqxLMJFgv.Trc<ē?!IZ eDeY,u}\g2m-ʚ24-0uro8Kuy.~+IcH1ILRBT27/uW x"t >)!02CdVdDa$0 r"(M'\Lm, \xm9s#HNEJ$2AEķ#  >sIYkfEwr`TTͳ  *C6xBhd<Ij`a1,sY I% A)|t80Mh2kP(*ʳB[WAHhkW%jk-9gWZSO{;۽n(:e) 1%DT<:n~7:ݑw(xk "Owȼ}00Bhr楳[^wVXX_35mSWWhn]Pn8v[DVN yE^ݍK/06ǵuE^"`kd&Ӯ5!)LBAe646Zh&xi|QJ @rл`~!a$,Fkg0S[L!FQ@QCR7V:=Hk:n]MzʡE9ojй;}doBrhZa&!M s9kٌYAvNeDe뺪gM ~ &N!FhmV@b@dNهPvT Tl<Ԓˌ6Zb]7uK*RyFDHD2ٷo`n!@AO_x]X3r8)NuV+ /z]ä CUZ¤. 7ZWl:zݾBWvm4[{g^UwvG fyo dzx&hm 2Wtj4$rʕI$/Jel踬k^բ#T,UUۆ@)e3%"<԰/VM ((QXԱBl-cBV'N(ܤb'+E G0M'h&4;y⸳:veIU K„(MRF"qT( ,̀H>$D0Hd࡮Put+`aZ aHid73CL HMS¼TD1id*@8ѠRNQRs")ۙ_^\~VJ_Ag"#YKݕefܡìᴽ.jRt6T,>;W*iՆ{gՍs続UIXY^AcEDZ`2m 2y//+b":&:#m̵"yQʚyA=\ꊾ'>uƨR$)zKhW?cef}]Փmck2W(Ν{ޕΝg~ᵵբv'Y:Mƻ;@M(iI1",)p"m i'*HX:v2+:xo<ԡ5Vy%xhLAu@%ϵҊ`rNB&‡ 1*en? *θLLHZU:#"}*Smrƾ(W.J5DB.Ц`U%cLrڦm&!V+tnP|/VY;V9%E*s˫./n/zj"rqEGL4AF*.;TJiyOB Bs2֒"cbJ"Bt$C}f r! f.4r>6݋&IZ~Xj+n:{Ξ-~TZZS*++݉׳d22mb@ K"HJiEI$Nb15B-d]˻+yYaLC2& H!҆RJȭ-U 92PheE)@*J3kiCʠ P!ґć ٻkjho^#W3Z'jiH!BN4o딹鄷T`)Kk"'+7OM+Sdyv[r߸ƪF[gѺ0LIY Dژy36"JGHv:j2*(|;5whRY ޷ľLG;hgp%H 7C=裏zLR2=poȳٴ_e}+ER /N ^5PI x*9 0"Zk R&`wz;&,YHM!>VV)SJƑXPOfdb)J۝t6ƘQybInf%(K *FI( >'!oޫ2BQ^WıÖVJHTBH3㔻7jX܌$ :@Ks͉'xы+tyșoz1pj"(ʻJzm͋cN=x+ۻ[[;j z1gU e9V8qbFכ9 V@8G8#'x$p0ϼK;g'`ot Eq8BDcCfh0V!+KZ+)60YI i RĨmyN4/Jg'E:nUM:ɻF)BNf#ka!6Mc62x0L+wY-ˮY 00)1G1jO "{܎vYG*3"@Ux<^NUC;a u$ Qp[ V)DMBa4Q"2q' \wrʕ~R̕.ڧi}ؼ^Vt{aumcymLfFu\vy^6mNI{,/n%*0ȼQ!"<c67Wtiɭ*_@D^WJ=K/J5@j`7lo_lb"4u3IjVFv 1t4U1w( @8mC $)mE|杘:\Qڬ` "> ) ޒTv:j@!tCL!6!Α&1;B3iڪqVb4" Q;k2CAmLC!6(4"EbG_Ua+OrBnPtyy/HyB*GHMFH)x~YUe,PfLx]Qƶu!F$/&ͥ2+HtFWM)yĕnİ_YYuVV+ǻݾ.mAr"gcἫ?3ϛbt赜Gjno^խ7A8vA#6pl5^9 \Y ᤞ f͌}je7:M[͚"ԊRل&Bl4HbSXGQTԾo"# ЦjDFDQm;5MHO鄌#C t^t܁P}NhT^Oz n̓e0#&MHL)RJuVW%GTDKgը!DTaO&4v/' s!`=#6kS+]ZV6sȣQt$pUy?'DO fYzssQz#Uzn/'كjyV1ں•NC[Vtì NRTU*yX#@JP8sl&.vŐ6ԭQJ18k DȜN%QF)pz+˫ݢBm&E@atE̐”j:mKb5XGnB[f, 1Rd UVy+22.˭F@ÏB7/7۱#˲p̌JUIPKj%oFvC-ARUC >ɦs޻̝t2A&%]^݌fvl=)(ǪEXSpL^u+njFyJC?!B)2O2iw즡+0sHLi,j8:bb]c\ C&䢊N6k$oL @¹x˯CEф_.b4ÿT} $ H`.2A/|hfId Z6UnZUZ@ǫkvuӳcc c"G7(g7@Ua&$g&y5{ ] =KNv.-I|CkkBP,XChտ_vWe:۫݋^./m:^=~hN'>a4(xw{MŰ9=}QAJvѱQh_ZQC UuZ/r7$UU+V#EeI8V ];ŏE8)O iQ샋S2C;;NŅGj14))P-vX=r=y^E\avwnf. ݑ|*!"@),`RR. fYM6J_cX|O$zzQg.[L.Ώ*iLePЌR~ۮ?Y"jc"{D]!w3Z%<+"B5`3P1@NerD< )L&)OjޱwmN<z\r:PUAEݎ̤,bشrUk _RV4C*Y`*Id1ZTOb3/sh ǻ3f3w绋'r'h긬㲭vzkv :S>oH IDATXUv̐2Wb~\w7I Uo|Џ!\]m]4ěf#0JYL\-o.Uu{+T4z#{pBڹ0&+ynMlY韅@!T!{&jtzi]*[}TMy}:[,O&6e?Nc-VXVE;Lj{}j'J'>.51b9AhO~^NΪNSq̋vF]w!x!m%i$e!|=}S `{o>gt| F]ьQPJVɋ;9wbEq= :`C;XyA8]L[S:ZoVM]o3g[ZVMӾx²%r)y%OB_x[NDSJm2qJ}3#$0S)T TiAJΪ;-~G_uƔts\0r8Xw>~gaburzsZo]]˵J9L:֍L T  |tKg~<ɟ$Bc!H||[9_ϟ_uUWW/Dz=,*;ocլWS.TCߏj6#7B]&G`Z21VO׋2n.ϟ[^oKde,f>,bh!Db(o $L =jXwn ͚ 4(Ą cdxDL _].m;qQWIV=)4jQ "8"^$ETI-1=y33;is@{d:{0r+9$HETbawQ9gQuޯכJ`L%+ ]V u99]M<vB*Z ');]3+2~>Dg?> x~7p``ljղj=.6@x 0m14ki ]ph*3ZMvof}ҏiR23甎z;R*;GM$gG&%itl]JW2C>TnLUNuHB=Qv sv› ۜ&P LHUėF,n'Ά&˗ߦycMX4EǺZV~wR]´[B9(9p L F_zOa*)"L9w!`"&B&Q8`թsUU7g1vj*By8}dH.I) S}Uƅ66i9iTlą(jTR, dd櫰wĩBgD@eڧ/BzY"q:T]oς*K( YToc./~?S0@8}WU͐aVU>x1BsfRrIS_d%-h|bOa+_/* ru4e!xvcF@S-EwBBAn[CцgLi4oMfзoH4N1UNIJq~wmkQ({ى;=1}J}M Ī>8ȹY, s@^1~@|4 };Vchj4O7N2~7Ve?%lJDoRx^ݫ) *.y궑8t$h^RbBS0IΚ pܽ~x"a,"#Qs9$19ۑ71Nޕ0_$lOnf); 8f$q^\Z:z0xr_>nQճO(eZ#BiJʄ`Z.8lw2U|>婿)ދQ5d"B&놳/ *,,T/"`/_<6'=֕ !ݮh^nND 1%`Zo~'R&τ"0s2# $q뗇ݵ'UU#J)'gGij0yGs7J]_,؇nATŤ"13!Ĩ}S1W:vɲ//Wwy|e)EU74rҔLt\z]ls)%+f%. *B(RJɩ,U,|nh@u#0x U Dp~^T׭iګ!UOyb_Wu˾TsQHi7}\O0&,PX*HɳPUYfѹpYρO):~D,Q "im:LX6j|yT", D.*eꋒ8(lu"6j*cnt\I|ӴLMJ?AAo.88f^Tdբe,iC*7;O|g)!0B1{ L@#G")mS?^OݵL"i"g˓l;v嘋4Up ` L@a^n9чds4qC#5&`6jbf⛌ir4[~X5D Ѭ>VnC7[u7LS6rՀÔ`)7r5U|l]h ȡ#_Wf@ URd>%r3MܧDoe=炡/իCCpևlkx?QRHΑ1yR5FONJќr:]/h&_rIi2`@?xdT2m/Rwze{&bnziOb"* `oeaV~KU?d"2AXE@28u<Vu8^>V, cˋ4`9V\ 9Ѕ1865OYr?J쮮$IwjbE})jj&v31,0{ ʬ;'8yCVj#E C PaaʆnʠY,#plJN0 Sզu1b`ϑ(s "d`s/f|Ѐ'Onҗ?q/%J@AY?ǫ1Br0ҫY>b\Dd&)4붮#EdE?)M4]wY7lzo&ۦm3H4 /_<@ i (zbUr4u/Ou tw즱t\LEDUDI)_W u3y¦<0Id) D ]TDXdߴqHH.ĦۺYPOr[eruY9($ D)9GoDDfQbfGr1S@a.f}":'q2K{;lzfV^ˀiLyBc"|ewcrsђ2o,aXĶ; U^ai>{mNc}c 1F}p8=F&"jh9'C)8z ED)"}Esєe֡^oNNNXCuH1XB(~MG4#9hfM0Q$&fqR cî1Ɔw--,dϳ!}LYuz 432cЊ/ۿ~d\jn{.~ϲb5PrI}"&E} &%qDZ;}:y؄嗓j7LnENfWN30 9n;/r'qG28!c?|TyE5KSQ-%+K !"jON.bqyWߴM\,6bDj_fB䶩}]w԰(i_YfpXKoLLՐ N X܌}Xz5 mNB1ILe8[WKm*h tǽ2}ɥa0]/[fEr)*s{Y ٘aab5"%, "33f*jķ)~LƙHTE0( 3t\F4C4ӂ r3jEQ B˧/?l/|aUU[ya@U7Mbf8:c687o0WKCn\=zzκ-OCU-燿shjU'd. n#a|tUͿ[+ź0OBC0۩:3 4F$yԀ 퇹;{JH5ѡs3Py Pl$Ss~zݳ?O勧7UY/sMO7*|t&ZR 8,v:{؊iƱ+(AsRCrM}*;z4) z\5RNS[ Ec 9Irp<\_;YJT 9=_W/;һ 07\;f{[zmPq{Xcq}h(XЊ#d":b361#`-?Z)*ec+d?^^@7.e!u)ϲ,0dr̞]2o4_3 J.EŠd,5ެB.AGaG `94%BH 9\V=|F@hn'7X̀ơv=ݎݢֻ;Ղk"OmQo;_@~l:)~_כަ}xoy<\^trEf۫Quqm/ءT>Y-;x*&4`bAM m\7{5+Rtl"~̂a37[÷ ̐ơEȳľjņ)Ae[ jW'MHu[WLԜ2Lu fI` LlJSa'~L$Y9QSjck׻gI!Qѡ'ͣqs:4nln  t07ďƷ[\lߝ f`9qOŋq_=|xs^bI&zuywp8ǡ{Ţ JzњiU%yDJ)yթX) D92)E՘9f曪ٌ#_U%ߌ;@cF;f**%qR`G$*ƈnKjn|H7"T52uh!UswBܔrzro_|s*^=d!/_y///NOgϞ|mi!*DtiNL56TE PUnLJU31k̬7fӔw]x\9ÐҔ>lsOUiSʪ\x^bp qs]^}sݏ*T 1yGX_Z]f Tav ` nt-$ wD{߭Ag7ζw09,W~騒cdt|zQ^Ou˥.^_2(XRUu]團)\J0B)CK0<*m3J*)qZ 1so3D%b]ޫ #<7RDPTao"fa@q?;:;(yޯWٜ.HtWժ]@Z A31z2pTs!M;)]b̷-!-۲~*:Q|#f& X*6dbM嶯/_^^pjm<\~'MRr}u0l6"""L?>Dȥ 9q]{;t`8!!;ZErQDq=}.9٢]~x5R㘦i݃KɈG-A ^ w<[D1 nXS]'1;+ &Fc*R w8YZucrLljY4%TvqYW)X.qUrsN04N$R*L*)\p%PQCCqFUayf\τSp. fl`Ny:t*-kʬimX.)U'jA1+"P]*j11DeaJh7F/>3&' ej7H@ڜN *iYsi_L뮍umœׯ6CM qr9M3%uKÿiiRF T!44@f"RT1#i)$n89WWUMl 1?OkXz xeU7\yjRJ֒)Y 8  q-,ݬw?1.x槺z?Чn[/Ls8GڞhJS~o۶aLbPjOϳMxӧ?iBsķ ڛ5ɕibs%(TX$Ki4ff2ޤ1fc7b-Xr.gqw=Ht Dƽ~?z[!daAT0љ DaoO HJn9IjmFr~)S8=4<^^]eYJU sO`}"]mu} V[ e45D$/i\h|/2Tҭ7& z:Yf}?yzx6l 茊y׃[Y=ܾ/i?㫋awհUs)77Zk 1l֛aQ~U<x82 jM)MuUMB 5]I :7+9w} `GaYJqPy? .Y/WݧXmO<0..+b,9KRs$UEj;c]DwĎw{O@T+$(=){1BfpY8%Kg?wojc "x%v󜗔E͓0Z ViX(j^f-E$ 3K~A\4O4_ח}T7_%sZÐX]-{_A_'W~UT?hXђcPYmI3zf<1ZݪCpTmK->MSΙrD$"S..7xixYi:mV4inaЭi'4R!DBRiι>x;nZ4UO./_>f{Ia8tð+rYNx:Rp]LubO3Q6;swW&-;ދ,7e" pViJLZ-sQTc~z𐫥Rڋ?>\qW^qoeC HڔgID$ 9vZkJN("ЅrΥR~jWuV&?-Ybj6j#iQEG^R.U04\P<0VNB\^]=}ɽhy|W>l{yL|O/٬%|ح=,"jIyT"造DnfOm+ՒO[rb+*?,y[<&99!@_L@`fEY]l6qYR?߼:<ܟ|<}쪗\_/i6tjF=UI͉UwWc' u)}JvK~k_fkj鸻 ,Cl.[R73 ]zzf7e%guN˱a&VOat-C'ABݐ8vu|sqM!PoI}IscMW?%PO{ڹKNq1bT;f@x:ݺNWi:ۮ\a/O']wfDyx%q(a"p˼ޕLCdp08_^fN 65g"R`F& 1`귿q\Pdw<甦y!u 3i.yI>-Wz5}<=wxS]ܥ ߑEDߕ*Dd9auS LDC}fV/^|t]}՟^g=O{ }SNjb.v/!SDZU]4z. glHMU$𜳻Z=`m g!`1ڜ6T@urϹ@ͺ]nСiĆd@D]h8_=|ݱ]8<ލiZC7͙v\Vu7\T=KwXշk=6 o[qے K-<k>.=-o|9ofڇ.CjML y}j R, WF$iar.n}7 NUW5SLKN3P|N8aUeys{x_"z+;"L8`hՁNN}MRop#Wف3ېFt8O8/OwKkT:B P-nPf>a*~?z?8n_~5}Y/}:$گ *AZM℄t>(a i `"cu|rO gfMq`ւ[$ftAL\@UCBgwrf^]\m/7bܮ (9Ts:|O(4icB7/In%t2#{nwEۍ{[' KX{'oY x_QyGe ੖qϯ7^}@2poi"#˨%2O9QkUv  &bJ-n6[U .fsAc])+%fB,Y'}>Nj;Mk5v߼?է?Ͽ؟_|ǟŶbqLnE"=T׿xzeFPBxx%ԿNӳ˒<Ц9Djj-s CŜ34MD8!XlzUkt>it* "u]DujʹZe=T'DBin^֗|^bV,b''.";:ݿs:^P}.]^^l:/34q:4c$ȢXKɹ )Ui.( E/b+U@S0u^TDku7@d R\ <@UMq\\^^<{_?o?bYN7! 1_\_ sZiNv3 Fi ~iHC'BN!&zʏ8ҝ5gA| x-V Yd]K*sJ͢@֢`(DH mkQЊ%xy:l?j=^}t:"x.5@ͪ݌ CesBѼ1CB0"Z[Kӿ~ c&3&(qk*™"h҇[Iph;Sk"$Bfpw-Uk)ex wۋyܱR$$z{qY`2䳖S^)НRk+\pK31*@Dꛐ&'1{|;|@oKܷiY/৥S_x08*:AgbAmvZB??_<<ޖne|XCd v8tCחnwҤ]u]?%[!aDa!ZRU_LUTkєU|ܮRrY2P* ne?7i{ ͵nֱ̨2Mx[^Q!93(VՠV̂n"D̡fgTQ4 P͵R02Gu'WyjV(  u}Xժ[JBK.ZrAXT9 )i:<<޽~_ˋ/W>v> Pkpa2SD14/"Bd3_e `uHLgT㿴_&CdFp+ԖVCto@y3&%Dh:lDn4O|CtXC5y\_Ů/~qqM3kf~rŻۻZ=AK5C Pv]GDڧ6^REghe4={o[ora꩝VC̔?e< Q1~4W[Z>fy>Ljf}~g"4j1jԪssRSG U}YڒDLu*M(J]TyaA@95@'D2+5/f{?V~`q|O{!BSɵ117땙-n;LAV/Os֟캮)u)KUk-YB" A cHEB|HWKRJ Sγ sMt>LP͍kBuWً>*Fc'ܚOB@zkAH;i?_iCgr<,sdv RL9kN/f|q{:$:y:lx?~tϿz1aq:n<')̮1s DM}!b9O4HbWU=g5$j?ulH8bs5t=73Bo[vl_kEUXX`QUF?2WS֚q9LbCt^_Rׯ@$rѯ@ą+'gdU<sڿK}=e3;#o@wS'@abiq+!+د/ w7W_VMKҍO}UP54tuq/ZcCw1>h?Z5T=K#\\DCč^kkS13Y)" ncXn]^}$,99Ҥ Z_\z۵bUZ%6xce=tTk)XVeLpqq `5L@h gT\K.JnFV^G q톣@;P@ IDATIZ,/Z,iyYm~Z"@hZS:oo^~O/j enJ^MrZY纤-m3>nZ I$aֹЂzHDD΄$v]aNE~rsyQY VTkU+ՌZE5sYNj58!9BY4BрmIފw vK;.o0֊Ιwq <a\6o)_ 6m;#̶WCC\u*v϶j1Wpw+ZSa aqtuQð{x<ؿ_ų 1OG}}'K.q7-I5hrulf˲LߍVu\ O9UNHBD@w,6϶@6fEEnvl5* ßiN@^RهWW&aZa7>_n^NPe@-y)54Sɩbj-C]|ZK)j̴n<,)j`HdMx!"nr %%6J!wA?ϖogg\ڬZ̍ZV=aVݪ*B!i)%fR9rYǿw]q : e?lna??7w Hao>S asq ЪR m#aq"w+"!b[U3?/,밋"i{w7x84oCNeZaz΀e9޾*y:2jRJqK)]ץ~$IV !<4povgڬؒy!^K4TAJ.~fVS 7˔u%#cZa'Rp<OqTdaּeIpDJO5  5O-UZ}2k.!jf\*73# 3I,;!fNKEftCcj-q o%.R?y>y)V 0JY? ً1=le1HPxLtvy=QwRCqoie*lTvmmhZꇵȺԓ<vk)?,Oh%֌C`^b^o?w/yzqiiTqrέ p圫ֶ %#@jnܪy2k?J l,S*Vo KfvN{|_BR!9*Df)"Bf"yiR &uk4sj҄cD07()[A6 L 160(͆ 6}q϶LMK囯_yƁ?"`yuvKiOpMcNKTKb/൝ud&jyViQcU%T96kHzZ Zk- DpVI jr4/xőX$,yqwZjBB L dm,5f$  JR*HHKFtDwabXLOU똖npUa"Z]1miO|IE[^Nt<Is&eKҒЅ7ӜRB_;n}'sT$?NK]$iMv5y*5UaC ;^y C]Wr>K^fEHiD ]9C.n_D!3?F,*6!`hjJ%e秒Ǎb?YXk'6[4RAi&BZ ي.u\TRRE s** jǽYE4r~NJg9QW/>w]FjK1ȠVy^C߯V+ )0Y!û|Q޿{fVg[CoDݡ=ȍlj15^嫯y@-Voݾ?淿9o_o 4>NL nHLԎvrZ B7 ÐZ7"-˚Sj`GpHmg `!d"R7=k-K3P٬ABò8K+UYOZrq*6Q2еgBu']/R%/%څ2R Hݛu|rM+5G\0״FoHk !p΢b]4wU KZZplK%U]"A TD^*ou˵b-%/)9"I10 D.*Zk P:1aZU&!jJ/9-K"95af_=./]aX]]]aIᘞ?[&nwɉ0[f%y'bR"[X*wo烾ϫa5;*:(A-L;dl-_=Zy?جßӁp0Ϟa8i><>ޗ2i_Q5ULZ˔ǔ*p6ffZVw!KN}%Y^BF=rDA!jF,jVJղЛ2K ȷ+@m^{"P+iN-)sղC !4ɛؗKJ"Q^J\R.ufvLK9g%@!Zʜ*IĨC)52-zv״L^ noo^{U1` "(HX5 3#XZŮkyԜsm ӹd&%B7cDP)k{{oHD}߇[ 16WwJ·swlD-3; ʎ˲,SA$L&1 CNEZmԌHABZJJI)aS|j-9`!b nonOc^}p8ta{;BVi{l/.îVͻ'S'B$U {#06跅KPApOECD"y"R Cs~v,˒q*,"Dy4;^oLT}m+X}ZDNnfXD-s0"&9FΧt:HT4zd.tCg*n!CLlbR493xg˧~wyi?aNkּ0a~>p9s!əO35cI PylTv>,B5D%95!0pB8DuH Ddt45p#whuQ )ԎRKdD֧9C˟:L +ϫϣj˄` 62N膈׊^I/j-D*V7Y3`)fj.e\5j9a)u4t:94Hhd]̹ifLֱ#SDB@4VX׫a#E"Hꮈ^JșѴj͆}1ik)%HDb-U3{JǪUPZf;v]_>>>.7UքVjNrFM?%2vE2Ϡ*AKEhjdhj ݶ9'B@GP'!!D C.Pu ˄%Cܽ63[ (00 kKnE?²bV=1e3fM^y<, 2lQ܍ŐFQvWM؟d͠6T[! 3:V( \E[ !ֶ\BhwK` B4tcW!t"QBrb6![m1gsVZ άּ]gC!9y2ёܪZ4R:M͛yܪ֜4Ym+LDBDB#c?t|ĊZi]"E }ZRTuvcCJY5-˲,<-s`Ab%{Si>omPE[F VHPq<"yۚK9~zi^HshM[쪕Ỳ0Ħ^֥r}<=ae]6J*\^ fVД!b%Ue%Vdmpi2"P W:P sz^ BR-ҝ FC\y?hjcx ,D_~<.~A*o_~Y OV!f,L;;uw obje` 7**\ -L <"%w w+8G鈅p@@DwO7ND8`ֺ޶nڊB,Ds!x$&8 f¼c~^^7tttaD&gF&]m'NU} m3u3_uX<Pw0S3#z2sut} L@ye^$ۏe48}M  m[c "UPU RTD65.0<8u5}@D /_,> * <h{?t_K9+1gDz "0 w`|iRRjpw*r)T9 l(]8aaXa\ T01 '1+7>O!pJyc,R|t߾o/r7|UR P50IU`S $3MH6p#0e%o1D"< m1MiZ,Ȝ64|Sd*~~_J.,̕T"aɗ@gx7&q?"۾.~v7>f)10\!8M1$1bXhD>OTgw3 `)Ko,t2j⤌uٶÇ~ޮ|>||ZzWb.VJ辮KFEZ82!%o&+(#`!yPu_ve}B}wYG3w|`d_TUC!pNY>ZM[ڠ~R {d?zoPI{2^^~׺~ :lax*벭UMcƸ-z:#1ӹ.` P{C@F.E2-B6^auA!Doqd@ ͯo~²H]v:/Ew=^^O?m. B+vp.Eԥp]f'VA${`7& IDAT]5W+iKDXgg%L\/WuGlcxTIabDdYRf,n뉘RZ,sJ[hFH ' naE{9"zoRzy{~@OC]Wbcu?on_nvnnȸ->~>:_/۫` >ochDZ;u!}vZd+hRE F!@."g8[>m/r"hv{//_ `TAQ̻\YDUHҘ{WZw^NQ} 7$R$ T꒿w 8sY4w<נp"!GBL{h;c[;Q Cd#Q !Αyvb$!=x]jW^}jjyn`f:hqy}}6  "y]3Uu !D6Run/N}O'"Ekmp{k'!kYu4"#Bi~Wzh,Ot&"ULH[Da @їӶ>=EXbDJo(\.ovt3pc?^/o뺱p<2T"3g`}qc1*(iIt^}]YDb9pS/_a@T5ᇿ?;_.?~'8>OZJZǧ>=1"l4 Z0vP*1N>pq1+a#PX' ُ?^~/?}}ov{~y.1yMuD81P^} ,޶%"4rWf*RK),벮 ,Ѕϵ~߿~}vZץ>SD,5{\;CuHfBD-bt Lq}Gz1suׯ?[oet ]ww_օt}_a|ZʲT??=׫gElҶ P?/׷ן~i[N+#Da.o/O!l\^^}|:/z>붰/矾^.7>/ ן>]m]_<_a,^_['" W9LUxHP"= J5SU DD撴 ^_ {{D{" E,<f|TCcQHGm(+#0=o`f(le#@$F7Y; RPtm߯> \!3C(eDah!c፷G'YkGn&DeY> 8dQIHV.HZ ]n8FVB;/?_r6a6@jfc[O}~2L~ݯzE"ãܹG O@D#?aJNDu '# 5)sxީpw7' ʧ n&!(l&02n@ Ή;Rj'pګM5*400SCA~z!4 DW<|Sy8N#!M?ZKYa^,krTU Eʶm><ooZueW$*a\3C`֧c!I+ŠY*mAfOY|j}?zcokW#nFHj(=pKZF`0"*"cX6uhV^< $v6FS,CT@ܵYoݒ\na˺>OEDFnNT3OɋRݮo/coB| Bt1e*vFֲ 5}hMU= ˏ_~\;кJ).kZ`Y|fj8q#"*,`$0'wߏ>wA0إ3T V4< "}#Q)NEX M I:u"m90DKa@euc19P!zR1 KED.e[,CFkqݮmFfA5(LHn:F3%$fzyVL"dD`"$p]o+3?====&5ҷ3tQoHD?1ܮ3˙"m{-jzoљtޖN7U˲Zj-ϟ޾?~*R޻{c11$I;zg OE{HZѾK]+"~Mc~BS]>C)bq<<('=Ʊߧf" x0ab ^B6c0gfy Rʲe"bmZ> 㸼]>UO_JB't^tZ"/_G"eRd =}z*KɑӺl;sIkvu=c猻GG; ÚA>F؏]6& UL`nwGrNDf{fz)d/eW?Bskߞ?1{`"59DDl*/KM(~;P=]rIX)g}/^z+E@TJ%oQoM;{SQ{q8"Hs?Z_}g -eYz@M1 'F}ذ$1#4Ͷ@jN HȀ8URhY9PD9LG=Ba <ގ.n|p&7403ke]*rtd`@*^Zo۩fАᇇl2U5묄5>`9\R,R eYFщQ+3>Bg= SwGc $Jkp'AXJk{ wG C6ZU{_X.E(gFHn#Dt+a0BTbba!i 1yˌTokuefw;#u]f[1 ]m1F)Hq0"b*$" XZF.[oo?^%)w7-cND[/R%~RW!ܯa- "MIIq`RKNJZn6y),$RzOW:0,CTY?pY}UJ-F3S4Gp @st(rC9""5o>t) HjnY8eR-Y;1ܧay=Tu icX")& \"7.Niϻ6 꽏1F̬ &"yYׯ_!JZXq7Tc1<314ԙ"'8c-+/˚ߡ>ziQO?=} ch_.Z^o[1FDZZ -KE `R@/7\)ipfyTX 1ߊx S0b4XDXnMܚPø,$uECT{̱|]:T1 C$27Cc"Crd  i1<J*Laڇnqh[)ח_7- U"ꀁ`(2"ALRov}iq]@yeY ??vߌ1܍G41L,"=7s-Eࡆ5%T07\\.ip*I^PĀ0&E=<]`-_v}Fġv'RK T $`oe^pj@HDd y\tҸ{V`ܢ~Ms ("Tq= zkq9֫̍LLTK9"A)`RroOWHnw|7eYGGY!Ex]ҜoRޝ ,˲!tœ=^.6T =E "Ms X83"toi3(c)UeYֵ֊bRÄ9Ge,g3eqN%$WQϦyCHLn.oo_7kv;2 S!R@dcC2 7}oT./Tk-R=b͆1n]C%U9< DFf7̑+,Ĕh4O`3elY:sJmYWC0w5w112O^ת~1RD,vbO1D;Σ ,9ns+1\*R;%sѐ/f6 <%S>MYӄ%t&-2n6뚬x_R1*"SQsanc7DE)ҕ~0k<DtZ%e"9}%d+ %NKLz)?2"K]`ZY"u $]kQfA$EZ*@0aLȉ`tG} u#(,W뺚6p.‚ >B1ܡ$?1"-?86Pݻ\z7BP0BD@՞]PdJpOYrD0paEpqwD˗/ooolu]ӇMj%cJ6ظdMb(#Ma6,U?NB…D8Ŭ7G @t" 2'{),lnĂa)BiCL! 3> 9E1 i:##rHGtNQpTt*!`;n4wˣJn!%$ "E8",14wSjLi\M}GÄԲ"s]s7XmYs)KbM XJ}zeI>ܯH ͨlyfY"&0ghHFf`rݬ_ >}'KvptZ+ ;fH\91كiq֭K]Rjl,Ln Lɴ%'=-e@aaz8.~iw @-- i@=@=Caրjޏ3' ]-Ӈm9 ەKY1-K_5!R="wIGU!m"&m|2/1MYqV7Cr.LDMTYJs(Jqk:bdBn{62w‘͓MpRɁQ?4HA6<̳٣ofUlkY<ӿst AUav1cJFP3x}Fzκ#U&@fa,͒c 37Rʺn歵pX@jJD9)(1z5iJoRH;+XHN|>7L.ha>&}:rSqFµ7n98ݧv H6FF,Z}E 2uFYʿN9Fpw5N0婏MC7qK?mnFm4ӎI  $K!5.#!,3ځ2>|.R;/RBIiZoL$L!Nrqp*rq,LD$~P!l#GspGR8y2nadϘRRG$mr\i!1Lc3p(1| `;(GYR%g{VGx b͆j9=WFT%^XCwqwwpjhgU@cnj6#>3GP\r:o=RZwGDܶ"L/ߺ)DH敔WᏎaٱ44rl}.C\Q={tg).}.na[rG["@D eFw琺]c%"ṗ>̇@LF` T/?]t}fͬ rE|+Iǀ2H='Tws Ibsnۄ|:mO_1r(Zbj98LrJcXo# L,=ϴuϟ?/B&x۶m[[RJ)u/U1\sДɶqQ Qppp#R"ϖtݍƱʹQpg , ЍH#,kR9HP2 yB0 0pIsG?C C0, c`PFG EPXD "uk붨hz  zow,¼.ti~}cug@] IDAToAˉy X #BD":f`yp#a;9]KsP5]jdZ:z;<5"IJlۼ %Q˕9ʺVDs"e@itT)Ȃ1YC>sz2 LL֌PĻ!232Cma|>_.Gx .i,ET:N۶P̬f"|:n7+s}lΌz:m50Z2@CniϑOFd7Scsk3 Qc뺝 毟`ضE̴vwEt=|…c@F@~9q,ņR/(Bn&!h(ZsrVW_^/N1666 '(U"F&wg"[!c\/~:bGm?@L \vZk֚:2:"yLڇnH7 x~V  BӋ`fCveR2Z "B~mz@Sz61GIW)jAK2h !ƘH넠< 8 p$G(lBz~KOP2sډBfwSKn="RHI֙#rAUYO=d@-eYeYjaA*""ni+ryYU~׷t^֒VR5i,Xs[+  03!B0IXeOmgND@hG#F<葡 j7:b ,?;or IC9_ Tj0)[RĄ\KA-Z_/0KE!<NBfbāuiC$`LS ~C\.^jMu^k)[4wgG_jUN}RlȽU+!i'e-TjGN<0sn}JHADDPhˉ;/D1eǡc"rZOCm?jC -J3}JV0u7teN`."PiH孵=0gq\T(H%CP\""ú&nC0(=… c"RJ ޚGjoۆAaRN+ExZBю̻i{?\qza,KYB}J\ޏ/E "ԋ>0~4w98~޾]]6iy0CHUrf3q s Bļ'bL(\lE+:r=","!)tm?h)\X8)~~=qcwX1,M-K=mR̎v.]HF% "b$fw!"N9C-`ȤC:SQ򀰒*_Jv!K-c@0 Sfx{d7=Rd9gh\$Ayf OO~YId̷uCK q3fZN,I7`0뻹'ODyC _dy"&.MlLx1O')(UC8EY8.whj嫳t1Krm qP4Iےе|in6Tk<}2We2.q{Dj0mff"t.>^^^gB&c,u]7ff}?>ʥT=gTkM6\(,{@&޶UIOt%I5´ayL sΘCV>,P^ 7$"Dn!;# E"4XsHC 7F4!p֎{M;3a\$Ưq(@HAj8XV}+,iDn+#I)}A8L\BHn Ir&㝳vqk DB (! Rc⺡s)Z(B R*IRb,3N"ssAR@tӝKӔ$Kjz0 W'<z2> If9%Ib3OP;#Z`pQPR0CYD *POLq\D%1)rd%%(xuIJ! LIsI)XAN_IJ^@pe`g=#H;3PCD)VgniU2J IΎSj1DĵPkjs%:yT Ϸ1Nu9gi N!$WJaTZKqlgykgHZKg{RX f3I9ϴVJK*A,ʵr9J24Upaͩ g@ ヷ;\e< Љ p!##t{@A0b70ei `VVBH! X5#ka C` 4AMH偑y! (prFNQH}}bC{ozW'rPˮU '4B2DX5O1(KSՏ cB쐺2 *quHbylPIP{Zg)!0d3"TQJ|P U`! 42 @ 8C|yF5JT ⋮L`J6ᝳ>EI޹-s*lGE([̔FHWqU+gXH]M ,I^JQux`h?j&WU(trD`sH}a̙Vkdddtt Is)\j%u\^^Bk`ٲ67V*3!Of ܰ>SS; yg!0 .%Lr)yjN3#XbZ9dBTeklK%wࣦ2$B` 8X:GsDWJ+`:Rx*jn GrLhe|9BhI^E;E[JE*N|dd4MDKseYc4;KB?u<"  !,)D,J9c ιf  Kc%#G)t`-T2P* a*Ro ELH !4cN!CIJ*gRPɫʸAZ[KqQS2QUqBUt#ڈRB%FM4pdCRvAhMEfkcC46H3b_&R QƖEQyN.?D];/u]wxvt{K-Dom%6KMZg魅3:t}Nr;DЉRxi䲪eū EB tE{8})$%zw &$c p@h`H GA? BE0pk A<,5`KbDl)КZ3%OLijh%DK4s5Ɔ3;˺WJH"`Di-M zf^Q EA:v&=NԔc0n5A6 G}n*<qX_XsƤqц7,1b`BitxO#$j%)Rr 4Xxsh"IM!RBkMj4ʺʰu&x@):j1$ )cZ:5CuO1^6cNR$J)MAv$IZVevna[sd]ZԹ'$wFg: 7[kԌ36Oc U!0bD%bywRʈ&{198H 6$ Iv[p%yCoi 3``-W 8 hn+0q&AdG{O9C i CT{jL+%˲Gy $Id;\qR(AՕYKloyǘ2PgCUD {RWbKEU UZ#>Z$Ρ c 5јuEz[RPzTIJE﹕H7>z"#RmE݊b-v|RTJRa6cl'gMYCNz}z-yجH 1gOꏲ͛-|9Y/I)5(@!xmCpxf B0t, BuPOO*Ft:eYeYkZ+͠vp 2 υ:;%u`,4iyC"Gp`(xU: b>x,HC砪%i8ER!h)\ɀq8W $Zz)7w` JXj y%c)\(%)}j0.> X!x-c7)U8P?WkM(a`ZH| E$a`P}Bw|/4wlJ+@#B\L QJy)˲,}t$7%8';)Z\CF`(-Ӣ6(uTUٹueiyM4դ,B2vpJjE!la[r<.`=ݣ0V7 Ԩ|EyR$תdzB~X-QeUUHY%P 5TI%sTBJ*Ec5ƪ3ޅ ^YiBڹm-EףRBQPrhE{'l}E['IlC=ZEcBޠ)wKh&;e$󜼟1{%FReQR # ?Z+P\Jx7[I IDAT+x0":Hy1gؾ4w!T4R˜J)`@dHHy4#1",q0qڈYcpVzps, 5)7P GZ+;92gR $Oi1EMӴʒD Im1!<'!hXBRZ+bl\JN,FB2A*J1|BpDp` <19c, )N Yϊ-9]`l]:!ũ44RJ)%8p^ WRU `@T s7H0Xm)"A4T^)R=X"q4v$j@@k؂ݪz`jkzz[&{c 1A[:ȵlSni5z]jePzDpV;)h,D)M+[n4j`BPi%D)o;\HQ>`ts\U!ZhgG+K"$7Zӏ3)U<C+9ea&[Z+RBs.6SNNč!R$-ߖe)B"dfՌ!Rr$4-˲R$x@be WZ&@2mek~]FHCɓ7Y9DwjHΩY3PED\?X [gI;ؒuѧi&1q)`RdfL8I fZ{4D.R"I tgCLSBP C!Z 4uj8'Rd`]e%'bYZ!DWWjMϋv@LKdVtGF7 LHəbV&` hXgJ8/(>xV1'YXklG@|CR,Il& Ti r6H_ ,"QF\ $Lj%$!v;/˲+K,I5c|hّPŹxZ¦?綞UICE>f)$U)AP!`˛i*PGfA׈-pJ'\U%!DgP59c̺qPJZ)%3&DVTSj_R҆k0p)%AN P+|0UETXW#"gh!Mu&[cL!mTww1.ϭfY|hʨiEmWW"dڒRMk굢TDppb:IB,˔T·,KcBZIOoW5FR urT AXDz,4bY  t"&+` g R$+M99г鿨99 SN"zJcCk2bƅJdIA Dԉ p΂bA e{sM@ޘr&ǮQ+ !e^b`i6CY [kaa&DQLh!&Sؖi BvutEQ]cƘw:K iIID]P4Mtu ֫v$jRDH*h$\0R t쭡PB ZqVGNִ1EګUE-J@y*-2Hɔι!IN^%v;tJDpyqJ`#Hq*GbaeYI-[Qp;@ $&Ź)z)Y̲VBC>!V#CPCdCe1RRhZI1=E,RJ]oDZDڢ U`oIі-qZe-MQ2#O\D>8kS{ 'Zs˼pR}%EERfB$z(D ^IK#G4Hʲ щдnJ jx殺6zy|GHcQ1"P-ZBB+8ZK͇<;yډ5eBeVvuuiiQ8x[+ M˲n;GsZOoj:G!bGKp>x-V 8A}pKdYʒ Q9:g7RJ%} FZu/ tAOHwUE<0}qKJJ5}C%^X^P$Aجߤ+[mu%eUj:VPDLǤ"j` 1 !4Id',PZGQqKv2 Ѥ̣gՎK1 $1MWJȰw8id nj[k2cCO%@wxJV Pׇ˾Z$M)ؒ^(HDJvV{GDc\YT\@Z*2Yk;s[P]581RF@"ϢxGg.2E$Os}Y$'Ƽ R(˲<xBpkJ ;OW91UhΘCww79 `ȉ, :gGF6gY$tZiCYUQVPϟW8f?0B& $j'F@H4Co5XDlc5XJ˨[؞8Evm\T$+jcǀ?h#tByWzE 1(P Z;tx~ =O='2:objȹT250d8eUX:H %ȟ3TMFљs*(tBjmei "W)瑽+_8鷌;*a} HRZBpksVk1dX0`i#߮9/h4MҔ֡C𝼝d)MS8l3*: q+&*1l;RjD9\DJř$-A` s:0ИH')w:M댁ƙ![5cLĴFDhi8x֩{$HF~Z 9cn>x!0`JKig^ZF&fHU-ѽD)]9]DR$¬5$Qi֋6E+ 'q,*2NBBI&VJYS;dYק*Z5!1&MeI* a\:8U,Δ㜯*׸nTJ|Jk:ʼԕjh 8@+P<?Bgk"]F 4\1*8$ ,ˀV42c$ZwK)(qj׷vZhۅ7o}뤭͛͛7_z .6mٳ8oEO}NNzs9+W |#?̙3O?VΝ^{yk֬۲N:Ehݺu~}Cp}ݷ <}SNYbŶ^qʔ)BW_}K)S?K.ޢ{y.쨣>}~w '|[—C;XZm?\ʪ8U/O<o韖-[裏?}/_|y=7M˗/y?O,[>c=>n?;k֬s9Wlxx]z3蠃.]J??&GHir&~g9cqGbŊm=[___ߋknFr#[>pNX>z,GP;g}w8ոv\k[ǏzBƍz=#zj^;#x /|衇9K.[o[0o4}{k׮{SN9駟ӧO駷9t~-X`~߼lٲO7o '2f'|'xf̘f͚(N;qcj+W<_Ts#Ϙ:uKd郃;nŊ_oOSOg? Eve{vP,i֓w::(˲KfYvUW\l١J>v$yo.7;1k嗟s9tnsc9桇#>7BUZ_tpW+To׾չ/m_~KM6|_O;{zk:gϞ}?{o(YEDcs+;\tz7ͦMN8ooWխc\s5\CβϦE3S._|ժU/2u,/^|G\}3ft:CCCcs3gnAcs;:/}KUW]|;쳷B4)ଳz|~xݺu_()5kɓ/_^9{>u2nqm?>\,{;߹dɒ7}??oov}I"iCoZMw}wܶ3?W]uU}7ޯ^zΜ9wuk_ځyW\qO~?~O=S/jg}gcR=ַP3<3CUZ:~|HӴNR~WJ`1s17x㷾>)SO=|3E]D#䁁SO='s -_/{?Onk1eʔ뮻n͚5s?%K>O͛7oxxxɒ%GqDӹ+$z,o?ॗ^z}}+_o\ryu]|f[[ŋ?s1+gYv_y+{zz4e١mo{[}w|PJguy睷?|饗s=EQ̞=bKw:E~Ƿewߥ^s~a]r% ,5k|]bEQsu]~/W;\}_X`W^yv[mm%K\?8̝;Ї>4>jƵ:~VXc֬+7Xcs=]x_|.5ոVS6XcMXc5քk&T5XcMjeDIDATkBUc5Xk&T5Xc5kBUc5خbbݴ|!kc;i1jkL]k U6(c fnlȲ|Æ'h\k֮R17nl4ϧoܨ1/ǜ ָp]%*+^M\KD[}t(c4/Izf̐cd<kquʇjE3gPƗ|͚Gxgֹs>u;찃'o~8:z?3<ܛ$͛Ww];h!f ոVZMڎ=fY=}sժ :jj^D7yF^C=pʔ?\kּ5iƵz96A!{yFF:T hY?۞wgJNf&aȎI)Kӿ[J)Ƶj[K)\]I%ss|@㇊+I=noZ5)S:uƟ{j\kgUӴc/*t+{.nj=y>qΘ1Kw7vZk5j#gyX2~dϮ.8:Z;syg%c% <_ 7TB\Ip}מ:wn.ƵjB՟gϞuս.gFF ߾3S>ljW? q'.#ޛ1Gum?.8}}Z#e9O7nwa,c\7ָ_D]t|Ι_O{[εkظO7KcN6'~?S&wr==0w6G]30pʜ9ش== nӟV?쉳g?200W=vˣg&ʿjppċkUt7li_U<Ȳ)Sk}ƵƇk*`Dմ'M|"kU|WOfdԩE5mm:KZj\k/ƍ&앵ČSkڵBUc5.]6Xc5۽mkW;RkMrZkBjhhs%l4\)rҤI 45\kW);缷qW9l{k[Zõvc^ZtM7/}j1*h8_45>\6XcC@8uӦMvqԩ'x{ _<իW?cZ#8g}[nٸqcww 'PSXrRj֬Yoy[h2::_g>'|r^/N;úu뺺:׽umݶfc=7Μ9Ƶ&kB$0,͛-^w޹pB؊+6lpǞ|I,[lVkҥ'O~{{衇._|``R^s5rC9d֭Bx׻޵p§~zժU ,`mu9o6nxI'p Iw}'O| 78/^h"~v!iK5 gyfeZ׾v{y͞={󁁁 6 .Z(MӮ;b@4MiwN'xMozӤI<|ttA3gL#GG}NR~SLy̥q Z|'/_sYk9B8熇FL21} ^{{Y<@Jǿc=&O<22200W]ugZ 7ָքu,Xxg7%| M4P?餓>{[t5DR /U hqZܰaZu|OO>[pOOavgo^jU}ܴi_t$ۼysoo/c7mXj\k|,}U0{y|zz}٧뮻;CSO=oׯn:<000pO GyG?hѢ_Yf}7t_} Xx-ܲdɒO};׽׿u뺻O=ӧ駟~뭷^}I&}xckMdU/:|ƵvP+^ sM}ƵƇk22Z_F+kv[˜8ָp][HZVC:NaU5[.M4톪TJ&|ZvDk\XCXc5klȫ75k(֌kk(L[_XӸZPҥKzn>Qi\kvXկ~wܱbŊ(4Dq7nƍ俱Ƶq4M-Ztm͙3祽*\{O<ڵkCЕW^ypB+GR YreOOϻFqƵj J?nGDk=00@x(7[4:4k5&4袋_Aƌ}j^zڴiF;sŊ38瞻{M6iҤ{?k|׿?70eʔ˗ϝ;̞={ƍ˖-뮻֭[wQGzܟƵzElW!,_AOsε@Lkd ۭ]kW UV+0<<0 7aDkjd ƿ52kjd (Usvkk 5XkƚPXc5k U5XcMjƚPXc5քk&T5XcMjkBUc5քk&T5Xc5kBUc5XkƚPXc5k U5XcMjvU5IENDB`zoph-v0.9.19/docs/img/zoph-ssl-config.png000066400000000000000000001352471415176210700202340ustar00rootroot00000000000000PNG  IHDR bJ pHYs+tIME* iTXtCommentCreated with GIMPd.e IDATxw\GgqI"kIObKhb}% TK]6?N1yߓ9fggfg?w.]zJnZ(DDDDDvS\V5 [DDDD5@TsQEDDDDB3 { Hr}Ĺ,K $""" /#ѱj%~jmHDDDocYF7y ^D j5<={q B(6ȟ ms |鼱ƺۻ$_‘{6$9} 婈_6~#%ަj*YV7=__[e[.>% vzrZ v czz v bz'"""Wݼ|{ƹ#+6 t[pv4iҔ{w|>Yǡئ""""5<[(@~< @Zئ""""!5pVqb""""~QEDDDDyZHJrp&qE\ _Ut8[&<B7ҍMb-j""""l9%! ȟBj_~7>S'^."""gAKfg-S欑4#.%"""'C4B ~s bR."""WfX b{[| *""": ?F)/޴Pd*:%-W0&.$JS2h$GY9y/B*s[;`Ӂ/j×; dq Bqˎ<Byno6<y>v\eM?ްIS!ܸu?v#.\P+r?w cCߦHNxඝ} oHnܺbsێ lni9~:EiS 1e#']JI$җF)L!tALbrQJa)#UrzRAEg"PE8Qr9T!rJ!(2(==J(Fmוj/rJO!+##Cc#C ( E((R*)Hs(7iJH)-O2PR 0JvQˬJ&# N J pJ)$TPr}JqH("Ig?煜) zBAIA߾W*)!@P[Q$)4B$J!o/w_ORvwr9lKpt @ k/-EY؏OL̂|aaCBB. a[d2P O`M[w!V 2P; uZ$@IImGu!jPBB{:AvxJO"r}_a[_HN4;}ETiqs_w/MHZ6|ŚG9TV|]jj E}a= Qm y#Nq_]ƭ#GNvRS[c;aєT:Pjl뙺/ۉg89{>*0+oN6oo\@I%f, :9HG0l8B>{ۓ&pJJc ( ${oLe{?vTfѣ&;JHLf{bٰ"W q [If8 zlЭ;Cy]rw~C2T*ն7U6!qC$Ťe}#๦"$.=ALwرD{ϣoAa1a};5=3[g'$̘>?va=}tu}O0my89iܸ)>#'!2Ys;54"OVܼGN$wܸ)|ǵB˜f]Je6al0Y…vl)=3Q~Xqctяe٩Zgk׏?GoOZ-$1MFkacƜ%&Lr Ξ!tRGO5nܔCF,gdֵY˩}oF#;xS.-+/*.=~:0#+ǽoaw?3jr nںcة=ie>xHXdԭf&VztjjjڰhR8bt=8' 6ozI'ںjMm頑V._ @jb``XjldtS {f>~7{ t{O;p̌쨻LzIBQ0 [NVt] 'aE%_o:}D@ZFք1#{wÆ, IE%}e|b+K7|yw}S&r=*AfVQq {/l޹%$<95[چ¢>/g_=|Ư?>ebuM+7~ڲ~)B'L뇍FVV,`EUw+};6߸pnoef\V5?gKK֬í:<-lҲoű'iLgXhEn^ѓTX\}?fѼY ޶qˏ{dhNz>=3;~ `XYV,}{٢*]ƺ`Ό翻2>]br[blbقW(-HHJ)N:aeer東#' 4՟?}2ՠ[4*zMյ6NOVee >yւCGO>v[DrjFOމ<[X&**;;z̙1e̅3-;E3kE++ӻWBR꾃GB~}]~sѷk>ѽRfXHՌ)n\W?wK{͘2iU3/3rX1q GT5;yc֢NVY9 df@yӾ[eZF&tͨ&'ӖIi=S3)AAzs.]88gLyÚ,@Jt$ b/lԤб6}][['0jAn+-\|U7!hٱu\߱Eť%e~^C{g!׮8'a'<{ulegO#^ w?/_eZ=_8wƞ9lA}zzץBHʥ_~ .Wgnn4ǞxrE!{{t?B)Zѭݢy8۱P}y~Ѐ!:(GfϜʱ` qӽo}CSYdeajbuor{HW;~Cbw`q3?wrwlG}'X O,EIvy# =1˳_GNY2vg;wW,9y|^~]?`a˟~N.vjsةg:{][akcبc}+K @/!B BͅGȺev\7358v҅s=׹[.\xlg| eܻa'p@aldص[;$<=>|O81{]zy8F8\0IKN9u2˳g>^V38{c~}8ޭuڪ7Ծ[iKn K8g@_gOhnYбӴVl9sf 0H8sqٴ7K%iAe{8tpةUo/GCXC~=98(Ch~=0Zx5n˪pB__!ӗJXe[7A/+F[M75 :y/Yl2!6`תzCٱg401.`ߪbߘH`blo?}3BPBl8 V1۵1pğu>G!o2/qhhqחi:h q<5=sW_oE^UN066j5jZ֮BTs_H<__Pk4'$H$ԹF =wZ·cFݸt&: 5 AϪ\^^.|h4c CD䵢Uw4 bNdz)Aݺ"V8aXrj;=q<;'_7o}o㖟Rӳjf] "jݷc˖wJE·n_m,M q\|űݻwΜ9We(.f)"$)CGN6j#'Fŧ+?O|ՕU5<(w̔7ne>Z=?@t !RkVGLͳ IDAT\K;0}vl>UJrɓ' /|rkkk`` Xծ^ŋر#66V"""Omܠ---$%+t?5 ԓɤR$ ?{'�s>cWkuoLxg"iڦf;}80ozϓSz%m#GOJI,X 9bĈ 6444T/~z[[[??׫Tu-_T鯾jmۆED^3x#<!W.7xOhBص 0k++)gcm}?226t-_qԽ90lO{<a` mg]~B$RG!}'-[fooHLL B($$aḻD"cH$'N0aBvv֑#GŮ/"xegmβlSs:Bꗖ0--ͪohP'՟uvpE.B<Ͽ|̬7vms'N>_jҙ$6~q\Uu (37s?ʕ+k֬YjСClBի};; 􈋋300k< Ə~cdmS RaDH5S' NdzEnn|9xmK"Fc?y=?{ͅc@$aVuU#"",]T?nnbS=""""5=-gðu -bS+}u+"A!|`boy<* 7]\Tǒ3 wTkh5D^K  >ET)6q"7٬"""""-PTBI%O v0ݿ;+ө^1V.+E07˰jy{/@A/ٞ'G8˙JC>Hxd_^['H|^7s{/&Y+AFˬsa0UIQ(H%+hOM8ν|U518!KOYM-ꪺFa|j>u -AIyE·wť>"pE)* ج H-htjNA؃Iy:Uk芚ABZc‰9lOIBL.(.9ʝ䬢s"b2'MյAt\Ar}7B! J~T\ 0,A8/n{B|ѩ! b;<*nH-А{'hJ+"jacSB V9A7Sr *8kh~gԜAu}#!EⷢS˅sb3b31 ,jLzTˇ$ج,!A18a,L+Dd$g݌L^3V4E$5 9DeEf8=poS\#A[P1|ӆ=[v]\QLL neN $pj_l{Ffyv?z!B&Lc)GrHng٣ % K/#8qe@TlK# ~ZW[s\zv,D|}νTQ|z~YeTB%U=@7;[%By -0ϣ]G, ̍\ 1x|WBu/<|AdLZZ>qǰ_΅zxrqzj@?!.Ep|r;(̍M27z/{ 9$rqRBZh@phΣAFf;.vt2bw_\ObfrO(w.Iq  pbmجMyTU=m+`o{=ou6kUkZR˫"fB\̍\z9qT@iXu쒒Y̊``?.ߴ67lu}SBzAqym؃Lc?CgK !8I( %1C܄t {q/!$'hRgN<ʪW=Ȁx:޷f:[3 xu}3r=X-2ʝ:Ʈ)#Yng9GU;DRTk$Z$p. 8VUq|Nf_R!6׍aNF:Yd]'8wc0@@CBL>" Hz01Rö@_p%I(ZX^?sQyUo0>PͰ .hiVgJ)rgϝGi]:cX}y qy4G x~6Pq\xbfRFxFnIZnISjо|&Ѹ*mlV=cϧC9\Fit1sf7;=ov ! |ݞ F >h㰓AΖfnN `9@C~>!XNJ'SoR)c= N!@I3`ԡcg:fepՂ?cY~{O74jX^zX`^2D삊/ uNp[0,Z0|ǽ\?jk~sǂ+fPEhhҌ[qI MVo"ϞSPRhZC4ZF64cge~RnS=.;-X0`t_դ+Bo]M k2$~EtG$Y"G4b !َygX091-@1 r/$rk31 pa$TI@Dr֕\ȟ",;iPߟ9 9K(BYQO8!hc,Ǔ$0,@*y|-ɯtr<!ͰB4 g9N8a&T6 CcG;T 'pa9Y,ǑΰI,lk)Ej[C pmKP$?U$}sĺ%8^w ,"@-+"UjML#O_xx$6RZKwf[?_"A|). }}L_.m) 4A'v1g9-lbǡ<>Sgr~:.5ZоN-[ciH-*mjQ^KkzU,PaqVE$jڤ ;R1YK,K &|UXVܢO!DeajLN~9vt IvVAD~V~YPxRnQ=?1AC3%uAIuia9HllV o'# & pB80^BA!H*}j.(3T8QQ%#J+@{Ptl#!,LXTlVTl⪛Qz?ȸ]\^[!7 FwTlVTlcqi@Ș !d t6!%?gMH+iklP鹥B#qCQXkhRt0@衦uPDG.DH(E%חJ$dpdNͫk; ݎJ!pnFAI%ui9%.=~>}G7CJBv%4޽O7KcW6,]3y%ij1Ihjt;I4a6?,wA Y_\T9=q9Us-4aî7ILfknd/>8=SsJ+(~j:5dH^788z(-ùa ! jefhb8o{fؤ¡{a`dփ׺ؘ(drӶgNDw,@giSW h#Afg% %0vױfVFgcYT/B) 3#ĥ~ 8#Affa- Gv{▉hGAI}2 5ypO)XͱBHQɈ~?Bh/V㯥F26nߥò}eMB_gW+c us[*ӓXR$09U7هf9箺TZS1A. z.75Rtp)%޺Us?1')Xid_]hxf? 4;obFADl$.6f7ӥ[؃ zREycNjM_&17V ~PduF#3rKRsKUㇹ !a=Irw#~yE?Z>k3wpjkirVllr^SϡAK< X6ӯ **Y0CD^=<=fk! **inUF3,ϷG@HN_7$$xJaYuS}r L I4J%f%Q^Ր_R5_xT\ի['B9mCI4Òr:}nD1Ԟg99yOP\8g9kI%$Ͱ8@܄$.L8OJڢp# \Ff(5ڤBK3#KFM#d."DŽ؟wM‚$nqtK$ɰ1PBnZ7FC5.㱽ܮn صu!!qŠ Jk3?Z:^7-=A;K*!- u0]y-|q垚Bq6OnQs!gdx99SlsLa:J9x2kA2Jp-[Jt\qqY,Ggj.R )`J% pP$[LCBv2RkSs繸UQkFoW{>]744tV~sd>HJ(BA)0x>8F_& QKzJ(XNf1r4~"NJO38Ar|pD*!%$!Oi4Fkѧ7({&9.R(,1QQYTB¯$+B; ӵ]'gs& \w<:%#뎙wLoTw. aX~InMJ[] }bI-JH]~ǒD(mz^bB@1,č_'DO!<|.ҪyѸ+!qՍu-s& Ri記, ZIJ*>]:~R<{41Rp/חr/] -0p%Nv|Ɠ'Ԣp3vdE1)y]M 㗢8}fCj, j#*jVQpbTd\S7G(zߔHH'{n=t`s-4apwǢ㳦`[ͪclLr]_OB|!+=ާkJ;fudJ YT44>Z2.5$:.[K3xu0q4HOB,-꺦L8pwcg>柯zQ:vt,a&M3<:l@F5t3?tO[߱ ٻOܤRC3ow'z;h`ݭr-L -:鲉FJaNi5yE{Oޮmd bBSz[ z} N^YQPSނ1́!@wzq+֓Rɲ n<[i-+"l CX0^8u(, ®vfV2ǥꝄϖO~w}C/خ#9p~N]M /N>\49#}6' Bez9ZqQVUv?H)PR iej8}Ww\GgvwT)REPDED41FM'hb{6qw[~y$3/_p~w;3ߝ|5[=55Z*%"dav&S y"g&qvV0G[djnYӡ\* X6;cO/n|v9ðb!='1bQ~O3Ɲb}On 0?9%\˹eb^?ۭKjǎ_4%ګ}K{̉"`v'D=fy!̄aּgnܛl樗[xqyCbLТ'[ޥL_}yډY?pJA{r42BhDWeM`Cgn4I4~rs'r큨AZUuM;ս~'V"*䒸A.]Z*⊆ 7 x BwO,5tԚ6^+o5NuiUc;I܊kPD篧R2ѕⱑFO!jhLc,Co`򊫃ܳ +4zYJ*WB],8"k%cGQ@g&wviV py(Eե_!VBZURXV&}.΃)l^ZcopNuznBJsF\X+*jJ*]vKs>{e..w{yI;\4UY_U]``cpؓ2 մuJ x<Əohp5Nog5kr#tp>C̀!Ңgcu:lZ 7|</gwqsrU3hk. b!  G x%B<0fu{z|8tP!UPloY^ac,ntu8HSdSJ3K (quJUf~@(0,lois;KW 7GKKIV|R!JWGpܲVWG+M~&ʆFrx˘it5$/W!@L)K) K?! f"-L3P$$\4SD{.IqCH07W/{^ʸ)r.!vVJ$Z"}<"=*4Zc gܬ>!$\- s~fcL-K .|V sRj64wxp%[a/Ab@"LdsYxGnQU]S۔i4vdB.2jf 0`k~5 =Bf1:w0 t(BhkN@΂ i_Ϗ> 1=.ֵh AдP@X=ܝ{oBsY!|_1]r'M"f& #Vm;a@=aؿ}\ì\{ͧ{x΍pjʢxDAz/kP'/O;148nl^.Z>&ޢ@~٬|KV9[qf0M-&rBW++s{;N Y2}3n׫3HbįqRg`J߻41MmΤ7ݛuoyzF6USk ;oh{+% -}ǮdYvєh o[O'#4ASkB{9}[̘jۚ[U/[Vݸ q~? 0*껭EBAgWO co/ܪڰ79uz|_dY!_j™ uwEÞSj.' S4:X ~}߼3Kզ#Q&quYj0l~Iͤ!n_fO_ܪ:~>2qu7b/'#3UHCW'_ :aTbaYuCi#)!^*m^qP{c2Tnvvuo [ڻFji lby_Qx" LQQ|BI#;E VuikDz:INgYV,0{:ade֎.~lˮrvZPG[OG"`NpV81*wP';56zqhcf (lj? ^Z4D!Y0*RZQcKJ^K&,`h"KkFGV5($Vf  *[;)r7p@ɥb;k0߶.EC< *9dmj~H(Z8gb% 7)B&}of&@o@o򚦔Ԣ nV\DL>ںtfl -?O1h@Q[ޡ>]}*%[HS.D,ii*h8})GHS-* E\ѭ3XWyuys`XΫas*f٫yBj i!I¨FA8X0,`cҮ]z!Ԫ2ؚSm!Io_ASkG;rNvm]7'3,˱ ;_c!`f"kT[+v| kh;J*d TBh`Ɩ~=:6Ba985t@O^B!s1,{jvbDNQUk(Iu7mLoTa@@m!2A:>}Ɖ Y{\~jƨ"{sv /ACS{vQeaY"zLB amO8r,jjt5gY֬aSO\ܤ~>ᦟ<;-,ۘⅈ8nu&uPGMO njU)咾É&7 Mu)f_9B@,M~ޮ*{^Pi>[.$IN niﲵ46v^J/2X!W &_ɵ0WOԥF p}8SYI~]LYh *ͯ'8Xk2;qjCG:/8.fu⨠nm_Q;,716Kcz\q.× gznBJZtϫ*0Ws7s",6|K.6ƶKiE}Je Je *)V/}3elac"7ַ&W7{5F;Vg'VnL$+t#y7@"&~~gSȀN vcF orwjgRMGqR" 0"tBBDf~EL7loa"P$_1jH( p)T)4Wд@& iT" p5&òFKB-ȽY=k0Pޥd-2*B"axK7-̔RwLRR ' i9$ªYAYM}[EjL).0* @[;'; M\rH!K%ƉM"}<.tu" Bhkޭgl,M-i9ea|uR#T!( n9EUV&]RQAIH(<jo tBL2I^IXDIsz,#5B`"6uzH\[7O.Qdgaj"2{bo!DۘbZ"ʥ""Z"|RqDE6O!47gO (^d!! WR)8vQPP\KlPZ  4Z)Q '.d5u0[UA%,uWr̍vHrEQ\.(`YnєhZ{$9C*uMlhuR|-LzDE GȤ|T"$`eojn3| W^0oR6}Ҕª o;jvIj۞7TJǡIr$bZ@s&O)]B'/dU׷J4ˡ'{eJY3)[KDHyIEe݆svB,Ouu?Q]l~RDS{V6e`9-Wf¨ 7G;N L]c0y4ErZDTEMs Nkٲ/|秊hgx·[co 锁aܝ:CzSxb60qe' ̺gẊ[?{u:W9__:;i#=C̄aGI!;+y"b\Qfd/h~&. ܤ{+Ӥ!?gY)B{"ƒ~I04Kl,LF ~9[<β#y [gp3]:)nȓӢL;Z,H *j;FNԱ&R‰ IDAT {__ℯ7)j=ܗ@7?|V^? domVQhJmkG߀/.(@U]sPb9 ՌsU7G \:+Tq1>>dW?v>uXY^5ڞ%^?:|7}鬘[@.`X(X:+D! u_:;өK]әec_Zlg1}|+\-hZJ//wCi#_Y~ϙƖv_c|[ۻa=?=cˋ7}blpAYɋ9jAޮHT3L*^b+faSr}Ys7o} +3_6yw/=~AgR.HCBv)@{Cs͊z>QgW >:Ɩv,9;Xw*|"ayR}SG~IMjN-݈o)N+1" pMمd- !YcYƔe9H6ðe3soVBqH167KʘHl-M 98 =N@E!fHXrC=b̂ʨP/1L6fxtzCMc[߄D R;znsBD.=/۵]"!mީY|x}Uu-.dohe}Iچvkzz*%PA>]҆掔BcZn4pXѳ$@onI2WF2M0=MdJi]cەgѥrݸ7K%w'ޝYkZf@|= vblpx|< zvblZ{A;pىnR! kocVQt.%¤ޒqpى1:B&v(j,lx6ʚEƴwSIg&3l8G[²&ks%@g;2hסS'z`Kݡ ,` dT7!P BruU2hφl"$,j 4srqD48Rjf-UQfG\*efAO!+s%E%  !D 5M-]V>;,ȃw3NR - +F i*RZ\&4Y!CxI=IBLBP!2B8B8FviJ4:$ɾyM|$B=Z+M2+EHzZ Ybi.0;ZX*n䔆3QHf pz\q#4ݘԭ͵Rw'kW~/,-7KMzHϫ!^Â=2*cCJ) [+%\-a2 *AN׳Jݝ!A>n׳JMdvf!3 boiLg0MGA8ښ_*p4?=r;S E9ؚ{ٙ($, pih omgG7yC  hiS}g&U`#!i߅Hޕ,<7r {s p {c\}ƆoGa< ,y:7l+"aT/~cP¨)Y+sp_{sqC00 {s `o`01 9<^iABI+A0 @(Ûˤo`0%n74j6<߾uqs y$)H 4IQZZ, 1I%jU'ĉe1 c+O̊}[{~htɔYq>|fAd^5[&O1 STOv@@H,62!ftuW<ꀅ-BBH"`ٱAH0qp]}`w3g郻  #7<@o_@hd\xt|N徥5)g=W6.85cs,C|O~ fYo EiOZO']N&`Ion8oS}MӖ?e>Rwu[;Zۚۚ6]+& &$؛c0\̻#o}-($P  $X`" P%Avح"@q8l`0  )T.(n{U׭{?F `0'x!`o`0Hn\`0 K<p@ H_ʪӛCס`0GNW7`01 q֛B9(0 !oz󢲺Ki2`q>@=y]vBX\Rvzjfdn3_ 3;B]?4.HWHsK[|냕o׫_RugπU5}ՏBhYI#7\Kk8z 7|Ū{\*No!~ʛO|j-o!&&G@yEU]}c@ȈؘO̮(+Y\R;?eO#'N3iڌ[w6,6(48<9v_CFLϟ054b -lٹ'<<&hHtgJ5;x艍Mkj zuEttt3[vӾ1!Qv/ ^W6*z中?o =2~=ƎdӶ~XN K8}>MӐ21kĤ%e{N6$ɾQS22R0fv쁔O9>;7!546A-.8БM`)B($b1GON$ O?JՕWP!OߡcNGH$a0BС#]<$tdPȈZ<-3бcACzxOgL ڽ'>|}ڽ)㧓#HMϤ(rHD3^ "0op?yܤY$I 738nEcN:sP$>*ax,|>%"jlPȈ+׿׷y%e強 5fLҵt@zfN5_~_xb'FOqKxDܑg"b',\4dxExihl6wFw2sA| 1I3#G'rǷւ,^H:uSS1a1[w.Z!i|ph40:ZYKonkްpތbg͛Xff=+;7Bk;vr- ~-}~U*^W^yϏ:~Қ7?`vʙC&9wt" 7?ÆmcF=jS󝽂kU q!DɤR?X[iU]Ҩm4aܱ_wssȷsaRW]}8V{NUlT@RϿ93;tvddqZ8<53+a\S 疔 q Cw]?ok?AQSST"ifMkG@Zʩ > n4RHޔaVé5)p+|$H~ڹI_jnfeݪ)T [v~tqA 9 jXz&N;B8a\U( ,@߯>`5 8r›K͙=m=5́:g^d;6!qG:oXSϾb0XfH.Nnڋq#M&H"|xF HD"״U˰I.}aK+2_[Zlڶk/\~g?P Rdw_*-}|DGK/4 Kuzз_}VtvWOpbwzaDDk/, hʧ ZD5_odUk7:`!ѡJJ@p|\ !@dYPW~?ե '%rww z?_޹@j s3F#(FKqM-S%M'H ݦ&&FCӴRh4@`&@&MHm)s475F=S,65Qq¨1SN aQqɒEs CHEU5MHGK $< B~(TJH%[Pc0 ۓ#P"5FZ@^| oaۅK\?|sUnܴ]@Qשm۾ęsB?nMfΛ5m”յuϾvА3gr袹3Y@p#-!t~܃GO&N?o{ھ~Xi`9%ͪaw3cʤ k׬-'`ͺ!ڋ_{qyQqqVS_o0ze[0g{omwZFvVN^KkGR?J@\AyKohr x ,^x~gt xp¥E546qiOL>Im]úMX [pGヨ[P)&:K˕: p`8cb!dei~(/@Q䏛wjL*TĝJ>WYUcBZ@fdE iV@"=/hܼc+MMmmv/#]<&6tzOa0:y (?|wo<<`H$e$Ef{pF{O͏f00M^~' kjttU(v6&  3&}~ pvtv#-(pPi׮jjj3O Nz돫(\Y0n}rr 2BiǤq>^ }s'/mֶsw ;q\HX, ۲cI &JSav yav-eeO?9~qa~O-nvq5A/]YX8.|Hoi+, †[uMk/,Xx99Ϙ96mw5mӋ߼C~=o/]x/?mtkAHL IDATœW_XRZ>5)ATrw_PH%~!,4Xե~ﭗ9 6o)W?KVcFqjÆ#$E v֖-ёE*8;9`,-m?  |h7$I:XYZ;B.:i¼YS߰u0{;[_o/ l}z]+s~H@حۚ(v<-ō2b&#;'707ڻ_ tz7>- ssAXh0 8w]mHf|;D(!Dq twsyvS=TW߰i7@ B8g}<8t<+'o_ #d2ؑ3h @(48&Ie\IakcODH`$ ~O{~1< !0gƔ5?nvrrpvt ׃xY;[똨~ޱSgn[qo8)+ϯqMw~J%06VV]Ga><|heUΫEYyD"r-uɂ9ff-*"  ˕J]k@ig$:TlߜN-v$I)푺^/ 94AF;"nB.|MQ[|[B3/WVמ:ǀa9ݷw>\ZDT*He,"Mߩh_WS@"g^$i0aW~ߘЦkOorǡЕ4I~L]lp, !`򃭃`0 ?ޟcsp,٥0xs GyCC+B8}9͞y\ZJsr2  SlVbgA˃zsò11 a]9;b0 9`7`0  `/A i a;<$pj l`oi ǦxHk^d~~{)+!#93P`[@Q,:+Xw'1>x #^~W_Xj7o !CF ۵^\nMf`0]jMv~!g~<%>oAh:]+?;!\Fe|PZiV((Ͼnim @`0^(Z rZ*5Zz "I5?UW~f=jcO))+o]0߁@@].I`3bZz GP{s_~MkDBoCmٹG(AR96iL`% H^}#H, s qGRI3>9t_]9[ BB*Svuu%ExþB!:r̩3$1 3ey2.-^2gV|j@ 8fMsܶ]Ey[gc󱓿j-J}%@HC3K7[ oCBDZqq:}ύ  |`#h89dRX/4TrܱO:zg4^4qX pvt@LFzYA 0_>x84C>qGN,}rk˲\Fɤn.Δ0cᲈPB.01H;B"{HX]S8{]}cRt^Oʪj^oÖ-Ru~#xnb>O<uS?8_C=6w}Li!-:!?B|\LxhH~ЭcW_435yg^P(%Wn%ǭ__ieAjPᡕ:UݳKq uگS\}% h!-i|a0!r7=7y|Pqn:sZ""MG Fgf!Bkv<炂k'2K·+S J^ST|rxge`00C{fe]Hcx+ao!wt闰0 "#-X'@`S`0cL y`7`0 \jm¦aZs3ONuuUuu[UooUyI.Z oSMM/rIʑ$VFQiNP(*0 V8:Fa~+D9F&5J]ע(Eu?c}SgRx`̺ߥ64[#,@p)9.:s_ͳJN5S;mJutbZaWTSAQan<=;uܐq1x]<{ܢcω Q!?bX2dx㥑ao>bXB0BB2=p+x&Z8ĉu:@-hL1ںIa~Q}apmc2f?N5g}<#'8AQ DSz. M 5gR% rd@˲ 0(rӠ 2ώcn50Jr޷wClI?zXcn6kwn}}‰=׭: G8Xc~&è! g7mdRmfP@cs(I!Kfbx3f02L;{~rg9 ^8c̼iB}O/]9~s 1(eP,nɲ,;]Ҝ)#x5zgzv%yqL) Y&$Mvsɨ@,2'eYyXEEG|sx NJ4oxeU~͛vZ^͟vK SC?ؚmи\G}݋|0X0=6"D^cb&%i¨k60cFp Jߣhl`5z-BHU]xXqٶBBq.fIyp!!P{}d#pDEb6v~ա.) .9ux;[XB95<:䋽Ks8w6 ,^9^DyڸJ !lM<^pcE/c՟V(-54>Gsk} I|!Kx@ O,ڟy?%Ijw,$䱻ݚՍ,nhf5o[4飭Z(I>Fݻ}uFGK+w!d2.:* ~;p,{۟}fȄ\F@xt#Hes8Hdc%8PRq,˂(axEcDsD Ad!P$! r,0X%Q9q8sgDp ,s B IXiQ$3KœQ\K8Y\=wU"1F<2q虗由{p>ޖD`.evL2,2XdQSH&Dl0X῾kaY%eŒ$ŷr 2&'_%]ςcx>}I1FNse fY3/ik՟9h$I>AT}+#&{%D1)!2! I3[ "#HNJ%+M{dYro=g>k/g&,9 2P yft^^t| O޿-EQ/v=EQSrqQ$"Ϛ4\%q+f$$gO~@3-wYxA xEIϗBR90Ac?Qr)OubQN|9N.:Dhү/A 硑3eďQI| JyCWΧ_Rc[帡#3uͦ7ſ>Ks6TS %YږĴBk!t,2+Tf)ݙZDU%44wȄ̙<Sqt2=6Gckg|J~CE2(J.g0;ZڻYz2NV*$USm=U!JruC[bja:t8D=q݁j϶'ۗZPQxѝ/}[]rPc2($(E3Kܫ  ! g#dBJO5v5 j֜mSVH=Qv,am_&ٖΤ"r+6'e)K̝2<<KcY&=<#B鄪[ҚGX8Y]WUUߒRPvl)9tMdĄTH'#Z:R ,VH.;UauU'2+in*'vr`3Q:-.f),d#"ܒ f_gfwfTL闇C|]'7h`w#CWo<kp )GEB 6лÃ|}c'+~ދfY9!l L#%FGֺ~ދfY`OHmISYygN4߭{=,c*n7DQM0LQmݝ6m\](e04~[K6.fwi.ATٶ'=n oufkw.'_4:ڷPiN:/v֤,,>btvQMzϨfը]2̮ÄYQy[ڻ !FG 6wٟStNLV/6fP`cKgGwonIΔe F(;f{5ΝPЦHs x.[o0ýy5-,2r}vublG_ 5BFG~_oBy=>s J$\1%9 -9e!aBp˜qNInBEgcѠ>a!Y\Yr% 05XJN5TV7dTX:譝<&33~C8}P#@,kN7W5eWVw Fvj\gGrLS[06E?082m YI"5wNq1 !@ݎeDEm 7%Y 0]= b2!ʌDV-˱7{_HT{(}X,˘cϟCE)=iRsƋe2Lx3zl}]V۰~C1oIP^5[q:\Y=g\[\=gXp8`z!x{Owz`4ۙ2  g^I#͟z/5PEucc~F%GØA3& Cj[ں~@@$x9m?0C"̫7%teNAS?y~C3wDYm_[00SF _9XnqXpȠ5NvYm<@gϡu]V.r8rY[ںj[EIs7>0A5z, X=ū7xwT7*V3'}{}rMV@\$07CRsSc b/Jyz.Eo]7t(ʢ$ayQjԂ$*#hg ܗx YJta\ =]geqN/jZ(oR,(a¨>)+ڴ\`Vr | u^rYi1+DKgۙl2hhk\ zBYJs,zE&.@J \N)ʵ՛S(  BҜBP(TS( Js BҜBP(B*@e~lL9ݳOzP4:8?ӪFhVzP~Jiw$X߄2zK=!xj*$3`//!ޢ#uZram=osLA%}}( ce Q~({#ƈ#bZZ/Y!XXRށEWǷFOXvOUeIGR}Q*yK+*ӎ5<繬'@VK[BYD9ۈx{7ήne74d<Ϸ=V+ݟ`LyoCz` 4bŒ#GӎdN Zr #cٷVpqѲ{xWUq[{)sNMGS4O_T2tȨ7K9%/ƭ/~^7FzSJk#sH{㥿[.^r~OYiۢP(?3()p{$IpeݟL'I;k!uu5{冊ӤV?GKg);|'V6!Aj:rpĢe 1,FI3<4BQ66 htlb{|¡o_+ }5}di~q+0rS3uaQ(Ȓ$$]~-m ޳cC Bpfyl{+I:bc&CaCIxg Uߠ}dx{x=}:p+|~kugW7YM&k/?b:rPR/qG}ҐQf?JJ%JD>)]QY/!k@s0Iv*3maA~mv!:9'O9/<5-eټi.";+'+0jzVhZ\g?\/>jU@w!Y{݂ %v9$I;>r,X.Bm.I%E% zztg= r刢bҡDϝ>zC)}~_PKx?\ I9G-~(,O8V`'?/#ͯQܶ~5 S_]EZJїSi~ .Z hP(  B&Wq@Q\h5Pi~f~m&-Vۃiw~*ͯbmS}|@(('*]%ǗzB2Oc\(*ͯiJϜ.DpRϱgY!V `IJtSeMDQ{^kGB/6`z9=w@GgϾZuVͫTfU# Z$k>OkT:ײۻ͈ O9Q&I;ܚVRWշX{O6OzcFbXOmب޾%s@&dx !b UIw4 6ݩ56+k4oۺFY&}k}gw9;{~-AEGW0ިH5hvKCK]-M):;,2a𴸘A!7Rs>o}߭-aAб5KYqDESGh|rVO޿pݎoq̋laYDGsEQ%ywRg3.$% A\஛82!afM3(0gQǧ/5m3}ݶpw q8`_-m+1̲Lx}5Ba[玏t!Y17ǧx:jOݿ\C:UO|5Ѡml7moV, 3qݎ#O?onVy@ g\^||g{_"hn:cF_4^RF26hkND(ၾ2-<%7xe3_:/Ym>wsetFields($request_vars); $obj->update(); $action = "display"; } else if ($_action == "new") { $obj->setFields($request_vars); $action = "insert"; } else if ($_action == "insert") { $obj->setFields($request_vars); $obj->insert(); $action = "display"; } else if ($_action == "delete") { $action = "confirm"; } else if ($_action == "confirm") { $obj->delete(); $_action = "new"; $action = "insert"; // in case redirect doesn't work breadcrumb::eat(); $crumb = breadcrumb::getLast(); if ($crumb instanceof breadcrumb) { $url=$crumb->getURL(); } else { $url = $redirect; } redirect($url, "Redirect"); } else { $action = "display"; } ?> zoph-v0.9.19/php/admin.php000066400000000000000000000020701415176210700153530ustar00rootroot00000000000000isAdmin()) { redirect("zoph.php"); } $title=translate("Adminpage"); $adminpages=admin::getArray(); $tpl=new template("admin", array( "title" => $title, "adminpages" => $adminpages )); echo $tpl; ?> zoph-v0.9.19/php/album.php000066400000000000000000000036211415176210700153660ustar00rootroot00000000000000canEditOrganizers()) { redirect("zoph.php"); } $album=$controller->getObject(); switch ($controller->getView()) { case "confirm": $view=new album\view\confirm($request, $album); $title=translate("delete album"); break; case "insert": case "update": $view=new album\view\update($request, $album); break; case "redirect": $view=new album\view\redirect($request, $album); $view->setRedirect($controller->redirect); $title = translate("Redirect"); echo $view->view(); end; break; case "notfound": $view=new album\view\notfound($request); break; case "display": default: # display is a redirect to albums.php $title = translate("Display album"); $view=new album\view\display($request, $album); echo $view->view(); end; break; } $title = $view->getTitle(); require_once "header.inc.php"; echo $view->view(); require_once "footer.inc.php"; ?> zoph-v0.9.19/php/albums.php000066400000000000000000000100021415176210700155400ustar00rootroot00000000000000prefs->get("view"); } $_autothumb=getvar("_autothumb"); if (empty($_autothumb)) { $_autothumb=$user->prefs->get("autothumb"); } $parent_album_id = getvar("parent_album_id"); if (!$parent_album_id) { $album = album::getRoot(); } else { $album = new album($parent_album_id); } try { $selection=new selection($_SESSION, array( "coverphoto" => "album.php?_action=coverphoto&album_id=" . $album->getId() . "&coverphoto=", "return" => "_return=albums.php&_qs=parent_album_id=" . $album->getId() )); } catch (photoNoSelectionException $e) { $selection=null; } $pagenum = getvar("_pageset_page"); $album->lookup(); $obj=&$album; $ancestors = $album->getAncestors(); $title = $album->get("parent_album_id") ? $album->get("album") : translate("Albums"); $ancLinks=array(); if ($ancestors) { while ($parent = array_pop($ancestors)) { $ancLinks[$parent->getName()] = $parent->getURL(); } } require_once "header.inc.php"; try { $pageset=$album->getPageset(); $page=$album->getPage($request_vars, $pagenum); $showOrig=$album->showOrig($pagenum); } catch (pageException $e) { $showOrig=true; $page=null; } $tpl=new template("organizer", array( "page" => $page, "pageTop" => $album->showPageOnTop(), "pageBottom" => $album->showPageOnBottom(), "showMain" => $showOrig, "title" => $title, "ancLinks" => $ancLinks, "selection" => $selection, "coverphoto" => $album->displayCoverPhoto(), "description" => $album->get("album_description"), "view" => $_view, "view_name" => "Album view", "view_hidden" => null, "autothumb" => $_autothumb )); $actionlinks=array(); if ($user->canEditOrganizers()) { $actionlinks=array( translate("edit") => "album.php?_action=edit&album_id=" . (int) $album->getId(), translate("new") => "album.php?_action=new&parent_album_id=" . (int) $album->getId(), translate("delete") => "album.php?_action=delete&album_id=" . (int) $album->getId() ); if ($album->get("coverphoto")) { $actionlinks["unset coverphoto"]="album.php?_action=unsetcoverphoto&album_id=" . (int) $album->getId(); } } $tpl->addActionlinks($actionlinks); $sortorder = $album->get("sortorder"); $sort = $sortorder ? "&_order=" . $sortorder : ""; $tpl->addBlock(new block("photoCount", array( "tpc" => $album->getTotalPhotoCount(), "totalUrl" => "photos.php?album_id=" . $album->getBranchIds() . $sort, "pc" => $album->getPhotoCount(), "url" => "photos.php?album_id=" . $album->getId() . $sort ))); $order = $user->prefs->get("child_sortorder"); $children = $album->getChildren($order); if ($children) { $tpl->addBlock(new block("view_" . $_view, array( "id" => $_view . "view", "items" => $children, "autothumb" => $_autothumb, "topnode" => true, "links" => array( translate("view photos") => "photos.php?album_id=" ) ))); } echo $tpl; require_once "footer.inc.php"; ?> zoph-v0.9.19/php/auth.inc.php000066400000000000000000000032501415176210700157750ustar00rootroot00000000000000start(); $request=request::create(); $auth = new web($session, $request); if ($request["_action"] == "logout") { $auth->logout(); } } else { $auth = new cli(); } $user = $auth->getUser(); $lang = $auth->getLang(); if ($user instanceof user) { user::setCurrent($user); $_action = $request["_action"]; } if ($auth->getRedirect()) { redirect($auth->getRedirect()); } ?> zoph-v0.9.19/php/autoload.inc.php000066400000000000000000000034731415176210700166530ustar00rootroot00000000000000isAdmin()) { redirect("zoph.php"); } $ctrl = new controller(request::create()); $view = $ctrl->getView(); $headers = $view->getHeaders(); if (is_array($headers)) { foreach ($headers as $header) { header($header); } echo $view->view(); } else { $title = $view->getTitle(); require_once("header.inc.php"); echo $view->view(); require_once("footer.inc.php"); } zoph-v0.9.19/php/breadcrumbs.inc.php000066400000000000000000000024771415176210700173370ustar00rootroot00000000000000prefs->get("show_breadcrumbs")) { breadcrumb::init(); // can probably be removed if (!empty($tpl_title)) { $title=$tpl_title; } if (!isset($_action)) { $_action=""; } breadcrumb::create($title, $_action); $_clear_crumbs = getvar("_clear_crumbs"); $_crumb = getvar("_crumb"); if ($_clear_crumbs) { breadcrumb::eat(0); } else if ($_crumb) { breadcrumb::eat($_crumb); } echo breadcrumb::display(); } ?> zoph-v0.9.19/php/calendar.php000066400000000000000000000034101415176210700160330ustar00rootroot00000000000000setSearchField($search_field); if ($year && $month) { $date=new Time($year . "-" . $month . "-01"); } else if ($date) { list($year, $month, $day) = explode("-", $date); $date=new Time($year . "-" . $month . "-01"); } else { $date=new Time("first day of this month"); } $title=$date->format("F Y"); $header=translate("calendar"); $calendar=$cal->getMonthView($date); // size of thumbnail + 40 for size of box + 10 margin + 10 padding * 7 for 7 days + 60 for padding of ul $width = ((THUMB_SIZE + 40 + 10 + 10) * 7) + 60; $tpl=new template("calendar", array( "title" => $title, "header" => $header, "extrastyle" => "body { width: " . $width . "px; }" )); $tpl->addBlock($calendar); echo $tpl; zoph-v0.9.19/php/categories.php000066400000000000000000000104211415176210700164070ustar00rootroot00000000000000prefs->get("view"); } $_autothumb=getvar("_autothumb"); if (empty($_autothumb)) { $_autothumb=$user->prefs->get("autothumb"); } $parent_category_id = getvar("parent_category_id"); if (!$parent_category_id) { $category = category::getRoot(); } else { $category = new category($parent_category_id); } try { $selection=new selection($_SESSION, array( "coverphoto" => "category.php?_action=coverphoto&category_id=" . $category->getId() . "&coverphoto=", "return" => "_return=categories.php&_qs=parent_category_id=" . $category->getId() )); } catch (photoNoSelectionException $e) { $selection=null; } $pagenum = getvar("_pageset_page"); $category->lookup(); $obj=&$category; $ancestors = $category->getAncestors(); $photoCount = $category->getPhotoCount(); $totalPhotoCount = $category->getTotalPhotoCount(); $title = $category->get("parent_category_id") ? $category->get("category") : translate("Categories"); $ancLinks=array(); if ($ancestors) { while ($parent = array_pop($ancestors)) { $ancLinks[$parent->getName()] = $parent->getURL(); } } require_once "header.inc.php"; try { $pageset=$category->getPageset(); $page=$category->getPage($request_vars, $pagenum); $showOrig=$category->showOrig($pagenum); } catch (pageException $e) { $showOrig=true; $page=null; } $tpl=new template("organizer", array( "page" => $page, "pageTop" => $category->showPageOnTop(), "pageBottom" => $category->showPageOnBottom(), "showMain" => $showOrig, "title" => $title, "ancLinks" => $ancLinks, "selection" => $selection, "coverphoto" => $category->displayCoverPhoto(), "description" => $category->get("category_description"), "view" => $_view, "view_name" => "Category view", "view_hidden" => null, "autothumb" => $_autothumb )); $actionlinks=array(); if ($user->canEditOrganizers()) { $actionlinks=array( translate("edit") => "category.php?_action=edit&category_id=" . (int) $category->getId(), translate("new") => "category.php?_action=new&parent_category_id=" . (int) $category->getId(), translate("delete") => "category.php?_action=delete&category_id=" . (int) $category->getId() ); if ($category->get("coverphoto")) { $actionlinks["unset coverphoto"]="category.php?_action=unsetcoverphoto&category_id=" . (int) $category->getId(); } } $tpl->addActionlinks($actionlinks); $sortorder = $category->get("sortorder"); $sort = $sortorder ? "&_order=" . $sortorder : ""; $tpl->addBlock(new block("photoCount", array( "tpc" => $category->getTotalPhotoCount(), "totalUrl" => "photos.php?category_id=" . $category->getBranchIds() . $sort, "pc" => $category->getPhotoCount(), "url" => "photos.php?category_id=" . $category->getId() . $sort ))); $order = $user->prefs->get("child_sortorder"); $children = $category->getChildren($order); if ($children) { $tpl->addBlock(new block("view_" . $_view, array( "id" => $_view . "view", "items" => $children, "autothumb" => $_autothumb, "topnode" => true, "links" => array( translate("view photos") => "photos.php?category_id=" ) ))); } echo $tpl; require_once "footer.inc.php"; ?> zoph-v0.9.19/php/category.php000066400000000000000000000036201415176210700161020ustar00rootroot00000000000000canEditOrganizers()) { redirect("zoph.php"); } $category=$controller->getObject(); switch ($controller->getView()) { case "confirm": $view=new category\view\confirm($request, $category); $title=translate("delete category"); break; case "insert": case "update": $view=new category\view\update($request, $category); break; case "redirect": $view=new category\view\redirect($request, $category); $view->setRedirect($controller->redirect); $title = translate("Redirect"); echo $view->view(); end; break; case "notfound": $view=new person\view\notfound($request); break; case "display": default: $title = translate("Display category"); $view=new category\view\display($request, $category); echo $view->view(); end; break; } $title = $view->getTitle(); require_once "header.inc.php"; echo $view->view(); require_once "footer.inc.php"; ?> zoph-v0.9.19/php/circle.php000066400000000000000000000121411415176210700155240ustar00rootroot00000000000000canEditOrganizers()) { $_action = "display"; } if (!$user->canBrowsePeople()) { redirect("zoph.php"); } $circleId = (int) getvar("circle_id"); $circle = new circle($circleId); $obj = &$circle; $redirect = "people.php"; require_once "actions.inc.php"; if ($_action=="update") { if (((int) getvar("_member") > 0)) { $circle->addMember(new person((int) getvar("_member"))); } if (is_array(getvar("_removeMember"))) { foreach (getvar("_removeMember") as $personId) { $circle->removeMember(new person((int) $personId)); } } $title = e($circle->getName()); $action = "update"; } else if ($_action != "new") { $circle->lookup(); if (!$circle->isVisible()) { redirect("people.php"); } $title = e($circle->getName()); } else { $title = translate("New circle"); } if ($circle->isHidden() && !$user->canSeeHiddenCircles()) { redirect("people.php"); } try { $selection=new selection($_SESSION, array( "coverphoto" => "circle.php?_action=update&circle_id=" . $circle->getId() . "&coverphoto=", "return" => "_return=circle.php&_qs=circle_id=" . $circle->getId() )); } catch (photoNoSelectionException $e) { $selection=null; } require_once "header.inc.php"; if ($action == "display") { $actionlinks=array(); if ($user->isAdmin()) { $actionlinks=array( translate("edit") => "circle.php?_action=edit&circle_id=" . $circle->getId(), translate("delete") => "circle.php?_action=delete&circle_id=" . $circle->getId(), translate("new") => "circle.php?_action=new" ); if ($circle->get("coverphoto")) { $actionlinks[translate("unset coverphoto")]= "circle.php?_action=update&circle_id=" . $circle->getId() . "&coverphoto=NULL"; } } $tpl=new template("display", array( "title" => $title, "actionlinks" => $actionlinks, "mainActionlinks" => null, "obj" => $circle, "selection" => $selection, "pageTop" => null, "pageBottom" => null, "page" => null, "showMain" => true )); if ($user->canSeePeopleDetails()) { $tpl->addBlock(new block("definitionlist", array( "class" => "display circle", "dl" => $circle->getDisplayArray() ))); } } else if ($action == "confirm") { $actionlinks=array( translate("delete") => "circle.php?_action=confirm&circle_id=" . $circle->getId(), translate("cancel") => "circle.php?_action=display&circle_id=" . $circle->getId(), ); $tpl=new template("confirm", array( "title" => translate("delete circle"), "actionlinks" => null, "mainActionlinks" => $actionlinks, "obj" => $circle )); } else { if ($circle->getId() != 0) { $returnURL = "circle.php?circle_id=" . $circle->getId(); } else { $returnURL = "people.php"; } $actionlinks=array( translate("return") => $returnURL, translate("new") => "circle.php?_action=new" ); $tpl=new template("edit", array( "title" => $title, "actionlinks" => $actionlinks, "mainActionlinks" => null, "obj" => $circle )); $form=new form("form", array( "formAction" => "circle.php", "onsubmit" => null, "action" => $action, "submit" => translate("submit", 0) )); $form->addInputHidden("circle_id", $circle->getId()); $form->addInputText("circle_name", $circle->getName(), translate("Name"), "", 32); $form->addTextArea("description", $circle->get("description"), translate("Description"), 40, 4); $form->addInputCheckbox("hidden", $circle->isHidden(), translate("Hide in overviews")); $curMembers=$circle->getMembers(); $members=new block("members", array( "members" => $curMembers, "group" => $circle )); $form->addBlock($members); $tpl->addBlock($form); } echo $tpl; ?> zoph-v0.9.19/php/classes/000077500000000000000000000000001415176210700152105ustar00rootroot00000000000000zoph-v0.9.19/php/classes/Time.inc.php000066400000000000000000000054501415176210700173730ustar00rootroot00000000000000today=empty($datetime); try { if ($tz instanceof TimeZone && TimeZone::validate($tz->getName())) { parent::__construct($datetime,$tz); } else { parent::__construct($datetime); } } catch (Exception $e){ echo "Invalid time
"; log::msg("

" . $e->getMessage() . "
", log::DEBUG, log::GENERAL); } } /** * Get only the formatted date portion of the datetime * @return string date */ public function getDate() { return $this->format("Y-m-d"); } /** * Get only the formatted date portion of the datetime * formatted according to the settings in the configuration * @return string date */ public function getFormatted() { return $this->format(conf::get("date.format")); } /** * Get a link to the date * @param string field to search for instead of the default 'date' * @return template/block link */ public function getLink($field = "date") { if (!$this->today) { return new block("link", array( "link" => $this->getFormatted(), "href" => "calendar.php?date=" . $this->getDate() . "&search_field=" . $field, "target" => "" )); } } } ?> zoph-v0.9.19/php/classes/TimeZone.inc.php000066400000000000000000000110411415176210700202200ustar00rootroot00000000000000createElement("zones"); $zones=static::listIdentifiers(); array_unshift($zones, " "); $len=strlen($search); foreach ($zones as $id => $tz) { $tzshort=strtolower(substr($tz,0,$len)); if (strtolower($search)==$tzshort) { $newchild=$xml->createElement("tz"); $key=$xml->createElement("key"); $title=$xml->createElement("title"); $key->appendChild($xml->createTextNode($id)); $title->appendChild($xml->createTextNode($tz)); $newchild->appendChild($key); $newchild->appendChild($title); $rootnode->appendChild($newchild); } } $xml->appendChild($rootnode); return $xml; } /** * Get array to build html select box * @return array zones */ public static function getSelectArray() { $zones=static::listIdentifiers(); array_unshift($zones, ""); return $zones; } /** * Get array of timezones with timezone names as key * @return array zones with names as key */ public static function getTzArray() { $zones=static::getSelectArray(); $zones=array_values($zones); $zones=array_combine($zones, $zones); return $zones; } /** * Get Key from timezone name * @param string timezone * @return string key */ public static function getKey($tz) { return array_search($tz,static::getSelectArray()); } /** * Create Pulldown menu for timezone selection * @param string name for the html document * @param string current value * @return \template\block pulldown */ public static function createPulldown($name, $value=null) { return template::createPulldown("timezone_id", static::getKey($value), static::getSelectArray()); } /** * Validate a timezone name * @param string Timezone name * @return bool */ public static function validate($tz) { // Checks if $tz contains a valid timezone string $tzones=static::listIdentifiers(); return array_search($tz, $tzones); } /** * Guess timezone based on lat & lon * Uses the geonames project * @param float latitude * @param float longitude * @return string timezone */ public static function guess($lat, $lon) { if (class_exists("XMLReader")) { $failed=false; $xml=new XMLReader(); @$xml->open("http://api.geonames.org/timezone?username=zoph&lat=" . $lat . "&lng=" . $lon) or $failed=true; if (!$failed) { while ($xml->read()) { if ($xml->name=="timezoneId") { $xml->read(); return $xml->value; } } } else { $error=error_get_last(); log::msg("Could not connect to Geonames site: " . $error["message"], log::ERROR, log::GENERAL); } return null; } } } ?> zoph-v0.9.19/php/classes/admin.inc.php000066400000000000000000000051601415176210700175630ustar00rootroot00000000000000.png, no path) */ public function __construct($name, $desc, $url, $icon) { $this->name=$name; $this->url=$url; $this->desc=$desc; $this->icon=template::getImage("icons/" . $icon); } /** * Get an array of all entries in the admin page */ public static function getArray() { if (empty(static::$pages)) { static::createArray(); } return static::$pages; } /** * Fill the static array containing the entries for the admin page */ private static function createArray() { static::$pages=array( new admin("users", "create or modify user accounts", "users.php", "users.png"), new admin("groups", "create or modify user groups", "groups.php", "groups.png"), new admin("pages", "create or modify zoph pages", "pages.php", "pages.png"), new admin("pagesets", "create or modify pagesets", "pagesets.php", "pagesets.png"), new admin("tracks", "create or modify GPS tracks", "tracks.php", "tracks.png"), new admin("backup", "download a backup of the database", "backup.php", "backup.png"), new admin("config", "modify configuration items", "config.php", "configure.png"), new admin("default preferences", "change default preferences for new users", "prefs.php?user_id=-1", "prefs.png") ); } } zoph-v0.9.19/php/classes/album.inc.php000066400000000000000000000506301415176210700175750ustar00rootroot00000000000000getId(); if (!is_numeric($id)) { die("album_id must be numeric"); } if (!$id) { return; } $qry=new select(array("a" => "albums")); $distinct=true; $qry->addFields(array("*"), $distinct); $where=new clause("a.album_id=:albumid"); $qry->addParam(new param(":albumid", (int) $this->getId(), PDO::PARAM_INT)); if (!$user->canSeeAllPhotos()) { $qry->join(array("gp" => "group_permissions"), "a.album_id=gp.album_id") ->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $clause=new clause("gu.user_id=:userid"); $where->addAnd($clause); $qry->addParam(new param(":userid", (int) $user->getId(), PDO::PARAM_INT)); if ($user->canEditOrganizers()) { $subqry=new select(array("a" => "albums")); $distinct=true; $subqry->addFields(array("*"), $distinct); $subwhere=new clause("album_id=:subalbum_id"); $subqry->addParam(new param(":subalbum_id", (int) $this->getId(), PDO::PARAM_INT)); $subwhere->addAnd(new clause("a.createdby=:ownerid")); $subqry->addParam(new param(":ownerid", (int) $user->getId(), PDO::PARAM_INT)); $subqry->where($subwhere); $qry->union($subqry); } } $qry->where($where); return $this->lookupFromSQL($qry); } /** * Insert a new album in the db */ public function insert() { parent::insert(); $parentId=$this->get("parent_album_id"); $qry=new select(array("gp" => "group_permissions")); $qry->addParam(new param(":albumId", (int) $parentId, PDO::PARAM_INT)); $where=new clause("album_id=:albumId"); $where->addAnd(new clause("subalbums=1")); $qry->where($where); $perms=permissions::getRecordsFromQuery($qry); foreach ($perms as $perm) { $perm->set("album_id", $this->getId()); $perm->insert(); } } /** * Add a photo to this album * @param photo Photo to add */ public function addPhoto(photo $photo) { $user=user::getCurrent(); if ($this->isWritableBy($user)) { $qry=new insert(array("photo_albums")); $qry->addParam(new param(":photo_id", (int) $photo->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":album_id", (int) $this->getId(), PDO::PARAM_INT)); $qry->execute(); } } /** * Remove a photo from this album * @param photo Photo to remove */ public function removePhoto(photo $photo) { $user=user::getCurrent(); if ($this->isWritableBy($user)) { $qry=new delete("photo_albums"); $where=new clause("photo_id=:photo_id"); $where->addAnd(new clause("album_id=:album_id")); $qry->where($where); $qry->addParams(array( new param(":photo_id", (int) $photo->getId(), PDO::PARAM_INT), new param(":album_id", (int) $this->getId(), PDO::PARAM_INT) )); $qry->execute(); } } /** * Delete this album */ public function delete() { parent::delete(array("photo_albums", "group_permissions")); $users = user::getRecords("user_id", array("lightbox_id" => $this->get("album_id"))); if ($users) { foreach ($users as $user) { $user->lookup(); $user->setFields(array("lightbox_id" => null)); $user->update(); } } } /** * Get the name of this album */ public function getName() { return $this->get("album"); } /** * Get the subalbums of this album * @param string optional order * @return array of albums */ public function getChildren($order=null) { $user=user::getCurrent(); $qry=new select(array("a" => "albums")); $qry->addFields(array("*", "name"=>"album")); $where=new clause("parent_album_id=:album_id"); $qry->addGroupBy("a.album_id"); $qry->addParam(new param(":album_id", (int) $this->getId(), PDO::PARAM_INT)); $qry=selectHelper::addOrderToQuery($qry, $order); if ($order!="name") { $qry->addOrder("name"); } if (!$user->canSeeAllPhotos()) { $qry->join(array("gp" => "group_permissions"), "a.album_id=gp.album_id") ->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $where->addAnd(new clause("gu.user_id=:userid")); $qry->addParam(new param(":userid", (int) $user->getId(), PDO::PARAM_INT)); if ($user->canEditOrganizers()) { $subqry=new select(array("a" => "albums")); $subqry->addFields(array("*", "name"=>"album")); $subwhere=new clause("parent_album_id=:subalbum_id"); $subqry->addParam(new param(":subalbum_id", (int) $this->getId(), PDO::PARAM_INT)); $subwhere->addAnd(new clause("a.createdby=:ownerid")); $subqry->addParam(new param(":ownerid", (int) $user->getId(), PDO::PARAM_INT)); $subqry->where($subwhere); $qry->union($subqry); } } $qry->where($where); $this->children=static::getRecordsFromQuery($qry); return $this->children; } /** * Get details (statistics) about this album from db * @return array Array with statistics * @todo this function is almost equal to category::getDetails() they should be merged */ public function getDetails() { $user=user::getCurrent(); $qry=new select(array("pa" => "photo_albums")); $qry->addFunction(array( "count" => "COUNT(DISTINCT p.photo_id)", "oldest" => "MIN(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "newest" => "MAX(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "first" => "MIN(p.timestamp)", "last" => "MAX(p.timestamp)", "lowest" => "ROUND(MIN(ar.rating),1)", "highest" => "ROUND(MAX(ar.rating),1)", "average" => "ROUND(AVG(ar.rating),2)")); $qry->join(array("p" => "photos"), "pa.photo_id = p.photo_id") ->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id"); $qry->addGroupBy("pa.album_id"); $where=new clause("pa.album_id=:albid"); $qry->addParam(new param(":albid", $this->getId(), PDO::PARAM_INT)); if (!$user->canSeeAllPhotos()) { $qry->join(array("gp" => "group_permissions"), "pa.album_id=gp.album_id") ->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $where->addAnd(new clause("gu.user_id=:userid")); $qry->addParam(new param(":userid", (int) $user->getId(), PDO::PARAM_INT)); } $qry->where($where); $result=db::query($qry); if ($result) { return $result->fetch(PDO::FETCH_ASSOC); } else { return null; } } /** * Turn the array from @see getDetails() into XML * @param array Don't fetch details, but use the given array */ public function getDetailsXML(array $details=null) { if (!isset($details)) { $details=$this->getDetails(); } $details["title"]=translate("In this album:", false); return parent::getDetailsXML($details); } /** * Get count of photos in this album * @todo This function is very similar to album::getPhotoCount, should be merged */ public function getPhotoCount() { if ($this->photoCount) { return $this->photoCount; } $qry=new select(array("pa" => "photo_albums")); $qry->join(array("p" => "photos"), "pa.photo_id = p.photo_id"); $qry->addFunction(array("count" => "count(distinct pa.photo_id)")); $where=new clause("pa.album_id = :alb_id"); $qry->addParam(new param(":alb_id", $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $count=$qry->getCount(); $this->photoCount=$count; return $count; } /** * Return the amount of photos in this album and it's children */ public function getTotalPhotoCount() { // Without the lookup, parent_album_id is not available! $this->lookup(); $qry=new select(array("pa" => "photo_albums")); $qry->addFunction(array("count" => "COUNT(DISTINCT pa.photo_id)")); $id_list=null; $this->getBranchIdArray($id_list); $ids=new param(":alb_id", $id_list, PDO::PARAM_INT); $qry->addParam($ids); $where=clause::InClause("pa.album_id", $ids); $qry=selectHelper::expandQueryForUser($qry); $qry->where($where); return $qry->getCount(); } /** * Get the photos in this album * Does NOT check user permissions! */ public function getPhotos() { $qry=new select(array("pa" => "photo_albums")); $qry->addFields(array("photo_id" => "pa.photo_id")); $qry->where(new clause("pa.album_id = :alb_id")); $qry->addParam(new param(":alb_id", $this->getId(), PDO::PARAM_INT)); return photo::getRecordsFromQuery($qry); } /** * Get array of fields/values to create an edit form * @return array fields/values */ public function getEditArray() { if ($this->isRoot()) { $parent=array ( translate("parent album"), translate("Albums")); } else { $parent=array ( translate("parent album"), static::createPulldown("parent_album_id", $this->get("parent_album_id"))); } return array( "album" => array( translate("album name"), create_text_input("album", $this->get("album"),40,64)), "parent_album_id" => $parent, "album_description" => array( translate("album description"), create_text_input("album_description", $this->get("album_description"), 40, 128)), "pageset" => array( translate("pageset"), template::createPulldown("pageset", $this->get("pageset"), template::createSelectArray(pageset::getRecords("title"), array("title"), true))), "sortname" => array( translate("sort name"), create_text_input("sortname", $this->get("sortname"))), "sortorder" => array( translate("album sort order"), template::createPhotoFieldPulldown("sortorder", $this->get("sortorder"))) ); } /** * Create an array describing permissions for all groups * for display or edit * @param bool Return array of groups instead of array of permissions * @return array permissions */ public function getPermissionArray($getGroup=false) { $groups = group::getAll(); $perms=array(); foreach ($groups as $group) { $permissions = $group->getGroupPermissions($this); if ($permissions) { if ($getGroup) { $perms[]=$group; } else { $perms[]=$permissions; } } } return $perms; } /** * Can the user write to this album? * @param user user * @return bool */ public function isWritableBy(user $user) { return $user->isAdmin() || $user->getAlbumPermissions($this)->get("writable"); } /** * Get a link to this album * @return string link to this album * @todo returns HTML, should be phased out in favour of getURL() */ public function getLink() { if ($this->get("parent_album_id")) { $name = $this->get("album"); } else { $name = "Albums"; } return "getURL() . "\">$name"; } /** * Get coverphoto for this album. * @param string how to select a coverphoto: oldest, newest, first, last, random, highest * @param bool choose autocover from this album AND children * @return photo coverphoto * @todo This function is almost equal to category::getAutoCover(), should be merged */ public function getAutoCover($autocover=null, $children=false) { $coverphoto=$this->getCoverphoto(); if ($coverphoto instanceof photo) { return $coverphoto; } $qry=new select(array("p" => "photos")); $qry->addFunction(array("photo_id" => "DISTINCT ar.photo_id")); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id") ->join(array("pa" => "photo_albums"), "pa.photo_id = ar.photo_id"); if ($children) { $ids=new param(":ids",$this->getBranchIdArray(), PDO::PARAM_INT); $qry->addParam($ids); $where=clause::InClause("pa.album_id", $ids); } else { $where=new clause("pa.album_id=:id"); $qry->addParam(new param(":id", $this->getId(), PDO::PARAM_INT)); } $qry = selectHelper::expandQueryForUser($qry); $qry=selectHelper::getAutoCoverOrder($qry, $autocover); $qry->where($where); $coverphotos=photo::getRecordsFromQuery($qry); $coverphoto=array_shift($coverphotos); if ($coverphoto instanceof photo) { $coverphoto->lookup(); return $coverphoto; } else if (!$children) { // No photos found in this album... let's look again, but now // also in subalbum... return $this->getAutoCover($autocover, true); } } /** * Lookup album by name * @param string name * @param bool do a "LIKE" comparison instead of "equals" * @todo This function is almost equal to category::getByName(), should be merged */ public static function getByName($name, $like=false) { if (empty($name)) { return false; } $qry=new select(array("a" => "albums")); $qry->addFields(array("album_id")); if ($like) { $qry->where(new clause("lower(album) LIKE :name")); $qry->addParam(new param(":name", "%" . strtolower($name) . "%", PDO::PARAM_STR)); } else { $qry->where(new clause("lower(album)=:name")); $qry->addParam(new param(":name", strtolower($name), PDO::PARAM_STR)); } return static::getRecordsFromQuery($qry); } /** * Get Top N albums */ public static function getTopN() { $user=user::getCurrent(); $qry=new select(array("a" => "albums")); $qry->addFields(array("album_id", "album")); $qry->addFunction(array("count" => "count(distinct pa.photo_id)")); $qry->join(array("pa" => "photo_albums"), "pa.album_id=a.album_id"); $qry->addGroupBy("a.album_id"); $qry->addOrder("count DESC")->addOrder("a.album"); $qry->addLimit((int) $user->prefs->get("reports_top_n")); if (!$user->canSeeAllPhotos()) { $qry->join(array("gp" => "group_permissions"), "a.album_id=gp.album_id") ->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $qry->where(new clause("gu.user_id=:userid")); $qry->addParam(new param(":userid", (int) $user->getId(), PDO::PARAM_INT)); } return parent::getTopNfromSQL($qry); } /** * Get autocomplete preference for albums for the current user */ public static function getAutocompPref() { $user=user::getCurrent(); return ($user->prefs->get("autocomp_albums") && conf::get("interface.autocomplete")); } /** * Return all albums * @return array array of albums */ public static function getAll() { $user=user::getCurrent(); $qry=new select(array("a" => "albums")); $qry->addFields(array("album_id"), true); $qry->addFields(array("album")); $qry->addOrder("album"); if (!$user->canSeeAllPhotos()) { $qry->join(array("gp" => "group_permissions"), "gp.album_id = a.album_id"); $qry->join(array("gu" => "groups_users"), "gp.group_id = gu.group_id"); $qry->where(new clause("gu.user_id=:userid")); $qry->addParam(new param(":userid", $user->getId(), PDO::PARAM_INT)); } return static::getRecordsFromQuery($qry); } /** * Get albums newer than a certain date * @param user get albums for this user * @param string date * @return array array of albums */ public static function getNewer(user $user, $date) { $qry=new select(array("a" => "albums")); $qry->addFields(array("album_id"), true); $qry->join(array("gp" => "group_permissions"), "a.album_id=gp.album_id") ->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $where=new clause("user_id=:userid"); $where->addAnd(new clause("gp.changedate>:changedate")); $qry->addParams(array( new param(":userid", $user->getId(), PDO::PARAM_INT), new param(":changedate", $date, PDO::PARAM_STR))); $qry->where($where) ->addOrder("a.album_id"); return static::getRecordsFromQuery($qry); } /** * Get number of albums for the currently logged on user * @return int count */ public static function getCount() { $user=user::getCurrent(); $qry=new select(array("a" => "albums")); $qry->addFunction(array("count" => "COUNT(DISTINCT a.album_id)")); if (!$user->canSeeAllPhotos()) { $qry->join(array("gp" => "group_permissions"), "a.album_id=gp.album_id") ->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $where=new clause("user_id=:userid"); $qry->addParam(new param(":userid", $user->getId(), PDO::PARAM_INT)); $qry->where($where); } return $qry->getCount(); } } ?> zoph-v0.9.19/php/classes/album/000077500000000000000000000000001415176210700163105ustar00rootroot00000000000000zoph-v0.9.19/php/classes/album/controller.inc.php000066400000000000000000000127051415176210700217610ustar00rootroot00000000000000_action=="new" || $request->_action=="insert") { $album=new album(); if (isset($this->request["parent_album_id"])) { $album->set("parent_album_id", $this->request["parent_album_id"]); } $this->setObject($album); $this->doAction(); } else { try { $album=$this->getAlbumFromRequest(); } catch (albumNotAccessibleSecurityException $e) { log::msg($e->getMessage(), log::WARN, log::SECURITY); $album=null; } if ($album instanceof album) { $this->setObject($album); $this->doAction(); } else { $this->view = "notfound"; } } } /** * Get the album based on the query in the request * @throws albumNotAccessibleSecurityException */ private function getAlbumFromRequest() { $user=user::getCurrent(); if (isset($this->request["album_id"])) { $album = new album($this->request["album_id"]); if (!$album->lookup()) { $album=null; } } else { $album=album::getRoot(); } if ($user->isAdmin() || $user->getAlbumPermissions($album)) { return $album; } throw new albumNotAccessibleSecurityException( "Security Exception: album " . $album->getId() . " is not accessible for user " . $user->getName() . " (" . $user->getId() . ")" ); } /** * Do action 'confirm' */ public function actionConfirm() { if (user::getCurrent()->canDeletePhotos()) { parent::actionConfirm(); } else { $this->view="display"; } } /** * Do action 'delete' */ public function actionDelete() { if (user::getCurrent()->canDeletePhotos()) { parent::actionDelete(); } else { $this->view="display"; } } /** * Do action 'display' */ public function actionDisplay() { $user = user::getCurrent(); $this->view="display"; } /** * Do action 'edit' */ public function actionEdit() { $user = user::getCurrent(); if ($this->object->isWritableBy($user)) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'new' */ public function actionInsert() { parent::actionInsert(); $this->redirect="album.php?_action=edit&album_id=" . $this->object->getId(); $this->view="redirect"; } /** * Do action 'new' */ public function actionNew() { $user = user::getCurrent(); if ($user->canEditOrganizers()) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'update' */ public function actionUpdate() { $user=user::getCurrent(); if ($this->object->isWritableBy($user)) { $this->object->setFields($this->request->getRequestVars()); $this->object->update(); redirect("album.php?action=edit&album_id=" . $this->object->getId(), "Update done"); } $this->view="display"; } public function actionCoverphoto() { $user=user::getCurrent(); if ($this->object->isWritableBy($user) && isset($this->request["coverphoto"])) { $this->object->set("coverphoto", (int) $this->request["coverphoto"]); $this->object->update(); } $this->view="redirect"; } public function actionUnsetcoverphoto() { $user=user::getCurrent(); if ($this->object->isWritableBy($user)) { $this->object->set("coverphoto", null); $this->object->update(); } $this->view="redirect"; } } zoph-v0.9.19/php/classes/album/view/000077500000000000000000000000001415176210700172625ustar00rootroot00000000000000zoph-v0.9.19/php/classes/album/view/confirm.inc.php000066400000000000000000000031561415176210700222050ustar00rootroot00000000000000 "album.php?_action=confirm&album_id=" . $this->album->getId(), translate("cancel") => "albums.php?parent_album_id=" . $this->album->getId() ); } /** * Output view */ public function view() { return new template("confirm", array( "title" => translate("delete album"), "actionlinks" => $this->getActionlinks(), "mainActionlinks" => null, "obj" => $this->album, )); } } zoph-v0.9.19/php/classes/album/view/display.inc.php000066400000000000000000000024741415176210700222170ustar00rootroot00000000000000setRedirect("albums.php?parent_album_id=" . $album->getId()); } } zoph-v0.9.19/php/classes/album/view/notfound.inc.php000066400000000000000000000030111415176210700223720ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); } /** * Output view */ public function view() { return new template("notFound", array( "title" => $this->getTitle(), "msg" => translate("Album not found") )); } /** * Get the title for this view */ public function getTitle() { return translate("Album not found"); } } zoph-v0.9.19/php/classes/album/view/redirect.inc.php000066400000000000000000000031271415176210700223470ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->album=$album; } /** * Output view */ public function view() { redirect($this->redirect, "Redirect"); } /** * Set the page to redirect to * @param string redirect target */ public function setRedirect($redirect) { $this->redirect=$redirect; } } zoph-v0.9.19/php/classes/album/view/update.inc.php000066400000000000000000000061171415176210700220320ustar00rootroot00000000000000request["parent_album_id"] != 0) { $returnURL = "albums.php?parent_album_id=" . $this->request["parent_album_id"]; } else if ($this->album->getId() != 0) { $returnURL = "album.php?album_id=" . $this->album->getId(); } else { $returnURL = "albums.php"; } if ($this->request["_action"] == "new") { return array( translate("return") => $returnURL ); } else { return array( translate("return") => $returnURL, translate("new") => "album.php?_action=new&parent_album_id=" . $this->album->getId(), translate("delete") => "album.php?_action=delete&album_id=" . $this->album->getId() ); } } /** * Output the view */ public function view() { $user = user::getCurrent(); $actionlinks=$this->getActionlinks(); if ($this->request["_action"] == "new") { $action = "insert"; $permissions=new block("message", array( "class" => "info", "text" => translate("After this album has been created, groups can be given access to it.") )); } else if ($this->request["_action"] == "edit") { $action = "update"; $editPermissions=new editPermissions($this->album); $permissions=$editPermissions->view(); } else { // Safety net. This should not happen. $action = $this->request["_action"]; $permissions=""; } $tpl = new template("editAlbum", array( "actionlinks" => $actionlinks, "action" => $action, "album" => $this->album, "title" => $this->getTitle(), "user" => $user, "permissions" => $permissions )); return $tpl; } } zoph-v0.9.19/php/classes/album/view/view.inc.php000066400000000000000000000034341415176210700215210ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->album=$album; } /** * Get actionlinks */ abstract protected function getActionlinks(); /** * Output view */ abstract public function view(); /** * Get the title for this view */ public function getTitle() { return translate($this->request["_action"] . " album"); } } zoph-v0.9.19/php/classes/anonymousUser.inc.php000066400000000000000000000131211415176210700213560ustar00rootroot00000000000000prefs=new prefs(); } /** * Return a bogus id */ public function getId() { return 0; } /** * Fake lookup */ public function lookup() { return false; } /** * Fake update */ public function update() { return false; } /** * Return a bogus person id */ public function lookupPerson() { return false; } /** * Anonymous users never have a lightbox */ public function getLightbox() { return false; } /** * Fake preferences lookup */ public function lookupPrefs() { return false; } /** * Anonymous user is never admin */ public function isAdmin() { return false; } /** * Anonymous user can never view all photos */ public function canSeeAllPhotos() { return false; } /** * Anonymous user can never edit organizers * @return bool user can add, edit and delete albums, categories, places and people */ public function canEditOrganizers() { return false; } /** * Anonymous users are never allowed to delete photos * @return bool user can delete photos */ public function canDeletePhotos() { return false; } /** * Anonymous user can never browse people * @return bool user can see the list of people that are in photos this user can see */ public function canBrowsePeople() { return false; } /** * Anonymous user can never see people details * @return bool user can see details of people */ public function canSeePeopleDetails() { return false; } /** * Anonymous user can never browse places * @return bool user can see the list of places where photos this user can see were taken */ public function canBrowsePlaces() { return false; } /** * Anonymous user can never browse tracks * @return bool user can see tracks */ public function canBrowseTracks() { return false; } /** * Anonymous user can never see details of places * @return bool user can see details of places */ public function canSeePlaceDetails() { return false; } /** * Anonymous users don't get notified. */ function getLastNotify() { return 0; } /** * No link for anonymous users. */ function getLink() { return false; } /** * No URL for anonymous users. */ function getURL() { return false; } /** * Return a standard name * at this moment this is used nowhere... */ function getName() { return("Anonymous User"); } /** * No groups for user */ function getGroups() { return 0; } /** * Get albums user can see * Anonymous user has no albums permissions * always return null * @param album unused, only for compatibility with @see user object */ function getAlbumPermissions(album $album) { return null; } /** * Get permissions for specific photo. * No permissions for anonymous user * @param photo unused, only for compatibility with @see user object */ function getPhotoPermissions(photo $photo) { return new permissions(0,0); } /** * Get array for display * Anonymous user doesn't get displayed, so return empty array. */ function getDisplayArray() { return array(); } /** * At this moment, anonynmous users only get photos * and no text, so no need load any language strings * @param bool Force loading - unused, only for compatibility with @see user object */ function loadLanguage($force = 0) { return null; } } zoph-v0.9.19/php/classes/auth/000077500000000000000000000000001415176210700161515ustar00rootroot00000000000000zoph-v0.9.19/php/classes/auth/auth.inc.php000066400000000000000000000017771415176210700204070ustar00rootroot00000000000000lookup(); $user->lookupPerson(); $user->lookupPrefs(); $user->prefs->load(); $lang=$user->loadLanguage(); $this->user=$user; $this->lang=$lang; } public function getUser() { return $this->user; } public function getLang() { return $this->lang; } public function getRedirect() { return; } } ?> zoph-v0.9.19/php/classes/auth/validator.inc.php000066400000000000000000000044151415176210700214230ustar00rootroot00000000000000username = $username; $this->password = $password; } /** * Validate a user. */ public function validate() { // No username or password are given, and a default user is defined // let's login as that... if (!$this->username && !$this->password && conf::get("interface.user.default")) { $user = new user(conf::get("interface.user.default")); $user->lookup(); return $user; } else { try { $user=user::getByName($this->username); $hash=$user->get("password"); if (password_verify($this->password, $hash)) { return $user; } } catch (\userException $e) { /* We are not giving any feedback on why there was a failure because this might give an adversary more info than we want to give away */ } } } /** * Hash password with PHP's password hash library * currently using blowfish hashing * @param string password * @return string password hash */ public static function hashPassword($password) { return password_hash($password, PASSWORD_BCRYPT); } } ?> zoph-v0.9.19/php/classes/auth/web.inc.php000066400000000000000000000175511415176210700202200ustar00rootroot00000000000000session=$session; $this->request=$request; $migrations=false; $this->redirect="logon.php"; $this->lang = new language(conf::get("interface.language")); if (isset($this->session['user'])) { $this->user = $this->session['user']; } if (empty($this->user)) { $this->checkRemoteUser(); $migrations=$this->checkRequireMigrations(); } if ($this->checkAnonymousUser()) { return; } if (!empty($this->user)) { if ($migrations) { $this->redirect="upgrade.php"; } else if (!empty($this->request["redirect"])) { $this->setRedirect(); } } else if ($migrations) { $this->setRedirectLogon("error=ADMINONLY&"); } else if (!empty($this->request["uname"])) { /* A username was given, but by now, no succesful logon has happened so, we'll redirect, with error set to PWDFAIL. This will show an error on the login screen. There is no indication what the exact problem was (unknown user, wrong password, etc.) as this might give an adversary more information than we want to give away */ $this->setRedirectLogon("error=PWDFAIL&"); } else { $this->setRedirectLogon(); } } /** * Check for remote user authentication */ private function checkRemoteUser() { if (conf::get("interface.user.remote") && $this->request->getServerVar("REMOTE_USER")) { $username = $this->request->getServerVar("REMOTE_USER"); try { $user = user::getByName($username); $this->user = $user; } catch (userNotFoundException $e) { $this->loginUser(); } } else { $this->loginUser(); } } /** * Check if migrations are needed */ private function checkRequireMigrations() { if ((!$this->user instanceof anonymousUser) && $this->user instanceof user && $this->checkMigrations()) { if (!$this->user->isAdmin()) { $this->user=null; unset($this->session["user"]); } return true; } return false; } /** * Check for anonymous user authentication */ private function checkAnonymousUser() { if ($this->user instanceof anonymousUser) { if (!defined("IMAGE_PHP")) { $this->user=null; } else { user::setCurrent($this->user); } $this->redirect=""; return true; } else if ($this->user instanceof user) { $this->loadUser(); return false; } } /** * Get URL to redirect to * string|null URL or null when no redirect required */ public function getRedirect() { return $this->redirect; } /** * Get language * @return language Translation object */ public function getLang() { return $this->lang; } /** * Get logged on user * @return null|user logged on user or null if nobody succesfully authenticated */ public function getUser() { return $this->user; } /** * Logout * @codeCoverageIgnore */ public function logout() { $this->session->logout(); $this->redirect="logon.php"; } /** * Logon user * Authenticate user or perform login for an anonymous user */ private function loginUser() { $hash=$this->request["hash"]; if (defined("IMAGE_PHP") && conf::get("share.enable") && !empty($hash)) { $user = new anonymousUser(); } else if (defined("IMAGE_PHP") && defined("IMAGE_BG")) { $user = new anonymousUser(); } else { $uname = $this->request["uname"]; $pword = $this->request["pword"]; if (!empty($uname)) { $validator = new validator($uname, $pword); $user = $validator->validate(); } } if (isset($user)) { $this->user = $user; if (!($user instanceof anonymousUser)) { $this->session['user'] = $user; } } } /** * Load user * User has been authenticated and data for this user is loaded */ private function loadUser() { $user=$this->user; $user->lookup(); $user->lookupPerson(); $user->lookupPrefs(); $user->prefs->load(); $this->lang=$user->loadLanguage(true); // Update Last Login Fields $user->set("lastlogin", "now()"); $user->set("lastip", $this->request->getServerVar("REMOTE_ADDR")); $user->update(); $user->lookup(); $this->user=$user; $this->redirect=""; } /** * Redirect to Logon screen * @param string error code to pass */ private function setRedirectLogon($error = null) { $thisPage=urlencode(preg_replace("/^\//", "", $this->request->getServerVar("REQUEST_URI"))); if (strstr($thisPage, "service%2F")) { $page = "../logon.php"; } else { $page = "logon.php"; } $this->redirect=$page . "?" . $error . "redirect=" . $thisPage; } /** * Redirect to Zoph page * Redirect to a Zoph page, making sure you are not tricked into opening a link * that would damage your database, for example deleting a photo by a url pointing * you to the "confirm" action. Just to be extra sure, any action, except "search" is * replaced by "display". */ private function setRedirect() { $redirect="/" . urldecode($this->request["redirect"]); $this->redirect=preg_replace("/action=(?!search).[^&]+/", "action=display", $redirect); } /** * Check if there are any migrations to do * if this is the case, only admin users can log on * @return bool are there any migrations? */ private function checkMigrations() { return (new migrations())->check(conf::get("zoph.version")); } } ?> zoph-v0.9.19/php/classes/backup/000077500000000000000000000000001415176210700164555ustar00rootroot00000000000000zoph-v0.9.19/php/classes/backup/controller.inc.php000066400000000000000000000036471415176210700221330ustar00rootroot00000000000000doAction(); } /** * Do action 'display' */ public function actionDisplay() { $this->view=new view\display($this->request); } /** * Do action 'backup' * Create a backup */ public function actionBackup() { try { $backup = new backup($this->request["rootpwd"] ?? null); $this->view=new view\backup($this->request, $backup->execute()); } catch (\Exception $e) { $this->view=new view\error($this->request, $e->getMessage()); } } } zoph-v0.9.19/php/classes/backup/view/000077500000000000000000000000001415176210700174275ustar00rootroot00000000000000zoph-v0.9.19/php/classes/backup/view/backup.inc.php000066400000000000000000000037311415176210700221610ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->backup = $backup; } public function getHeaders() { $size = strlen($this->backup); $filename = "backup.sql.gz"; return array( "Content-Description: File Transfer", "Content-Type: application/gzip", "Content-Disposition: attachment; filename=\"" . $filename . "\"", "Content-Transfer-Encoding: binary", "Expires: 0", "Cache-Control: no-cache", "Content-Length: " . $size ); } /** * Output view */ public function view() { return $this->backup; } /** * Get the title for this view */ public function getTitle() { return translate("Create backup"); } } zoph-v0.9.19/php/classes/backup/view/display.inc.php000066400000000000000000000054521415176210700223630ustar00rootroot00000000000000request=$request; } /** * Get headers - always null, so default headers will be used * @return null */ public function getHeaders() { return null; } /** * Get view * @return template view */ public function view() { if (empty($this->request->getServerVar("HTTPS"))) { $warning = new block("message", array( "class" => "warning", "title" => translate("Warning! Unencrypted connection!"), "text" => translate("You are connected via http, it is not recommended to send your root password over a non-encrypted connection!") )); } $form=new form("form", array( "class" => "backup", "warning" => $warning ?? "", "formAction" => "backup.php", "onsubmit" => null, "action" => "backup", "submit" => translate("start backup"), )); $form->addInputPassword("rootpwd", translate("root password"), translate("leave empty to perform backup with normal Zoph database user"), 32, 16); $tpl=new template("main-nh", array( "title" => $this->getTitle(), )); $tpl->addBlocks(array($form)); if (isset($this->request["_return"])) { $tpl->addActionlinks(array( translate("return") => $this->request["_return"] )); } return $tpl; } /** * Get title * @return string title */ public function getTitle() { return translate("Create backup"); } } zoph-v0.9.19/php/classes/backup/view/error.inc.php000066400000000000000000000036301415176210700220430ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->msg = $msg; } public function getHeaders() { return null; } /** * Output view */ public function view() { $tpl = new template("main-nh", array( "title" => $this->getTitle() )); $tpl->addBlock(new block("message", array( "class" => "error", "title" => translate("Error occured during backup"), "text" => $this->msg ))); $tpl->addActionlinks(array( translate("return") => "backup.php" )); return $tpl; } /** * Get the title for this view */ public function getTitle() { return translate("Error"); } } zoph-v0.9.19/php/classes/breadcrumb.inc.php000066400000000000000000000130121415176210700205740ustar00rootroot00000000000000title=$title; $this->url=$url; } /** * Create a crumb * Crumbs are the path a user followed through Zoph's web GUI and can be * used to easily go back to an earlier visited page * only add a crumb if a title was set and if there is either no * action or a safe action ("edit", "delete", etc would be unsafe) * @param string title * @param string action (display, edit, delete, etc.) * @todo calls $_SERVER directly */ public static function create($title, $action) { $user=user::getCurrent(); $url=htmlentities($_SERVER["REQUEST_URI"]); $page=array_reverse(explode("/", $_SERVER['PHP_SELF'])); $page=$page[0]; $allowed = ((in_array($action, self::$actions, true)) || ($user->prefs->get("auto_edit") && $page == "photo.php" && $action == "edit")); $numCrumbs = count(static::$crumbs); if (isset($title) && $numCrumbs < 100 && $allowed && ($numCrumbs == 0 || (!strpos($url, "_crumb=")))) { // if title is the same remove last and add new if ($numCrumbs > 0 && static::getLast()->getTitle()==$title) { static::eat(); } else { $numCrumbs++; } self::add(new self($title, self::updateURL($url, $numCrumbs))); } } private static function updateURL(string $url, int $num) { $question = strpos($url, "?"); if ($question > 0) { $url = substr($url, 0, $question) . "?_crumb=$num&" . substr($url, $question + 1); } else { $url .= "?_crumb=$num"; } return $url; } public static function add(breadcrumb $crumb) { static::$crumbs[] = $crumb; } /** * Get the title of the breadcrumb * @return string title */ public function getTitle() { return $this->title; } /** * Get the URL of the breadcrumb * @return string url */ public function getURL() { return $this->url; } /** * This function reads the crumbs from the session, and makes sure it is updated */ public static function init() { if (isset($_SESSION["crumbs"])) { static::$crumbs=$_SESSION["crumbs"]; } $_SESSION["crumbs"]=&static::$crumbs; } /** * construct the link for clearing the crumbs (the 'x' on the right) */ public static function getClearURL() { if ($_POST) { $clear_url=$_SERVER["PHP_SELF"] . "?" . getvar("_qs"); } else { $clear_url = htmlentities($_SERVER["REQUEST_URI"]); } if (strpos($clear_url, "clear_crumbs") == 0) { if (strpos($clear_url, "?") > 0) { $clear_url .= "&"; } else { $clear_url .= "?"; } $clear_url .= "_clear_crumbs=1"; } return $clear_url; } /** * Eat a crumb * A crumb is 'eaten' when a user clicks on the link * it means that the crumbs at the end are removed up to the place * where the user went back to * @param int number of crumbs up to which to eat */ public static function eat($num = -1) { if (count(static::$crumbs) > 0) { if ($num < 0) { $num = count(static::$crumbs) - 1; } static::$crumbs = array_slice(static::$crumbs, 0, $num); } } /** * Get the last crumb */ public static function getLast() { if (count(static::$crumbs) > 0) { return end(static::$crumbs); } } public static function display() { $user=user::getCurrent(); $max_crumbs=$user->prefs->get("num_breadcrumbs"); if (($num_crumbs = count(static::$crumbs)) > $max_crumbs) { $crumbs=array_slice(static::$crumbs, $num_crumbs - $max_crumbs); $class="firstdots"; } else { $crumbs=static::$crumbs; $class=""; } $tpl=new block("breadcrumbs", array( "crumbs" => $crumbs, "class" => $class, "clearURL" => static::getClearURL() )); return $tpl; } } ?> zoph-v0.9.19/php/classes/calendar.inc.php000066400000000000000000000170041415176210700202440ustar00rootroot00000000000000searchField=$search; } /** * Get the array of strings used to label the days of the week. This array contains seven * elements, one for each day of the week. The first entry in this array represents Sunday. */ public function getDayNames() { return $this->dayNames; } /** * Set the array of strings used to label the days of the week. This array must contain seven * elements, one for each day of the week. The first entry in this array represents Sunday. */ public function setDayNames($names) { $this->dayNames = $names; } /** * Gets the start day of the week. This is the day that appears in the first column * of the calendar. Sunday = 0. */ public function getStartDay() { return $this->startDay; } /** * Sets the start day of the week. This is the day that appears in the first column * of the calendar. Sunday = 0. */ public function setStartDay($day) { $this->startDay = $day; } /** * Gets the start month of the year. This is the month that appears first in the year * view. January = 1. */ public function getStartMonth() { return $this->startMonth; } /** * Sets the start month of the year. This is the month that appears first in the year * view. January = 1. */ public function setStartMonth($month) { $this->startMonth = $month; } /** * Return the URL to link to in order to display a calendar for a given month/year. */ public static function getCalendarLink(Time $date, $searchField="date") { $month=$date->format("m"); $year=$date->format("Y"); return "calendar.php?month=" . $month . "&year=" . $year . "&search_field=" . $searchField; } /** * Return the URL to link to for a given date. */ public function getDateLink(Time $date) { if ($date > new Time) { return; } if ($this->searchField == "timestamp") { // since timestamps have hms, we have to do // timestamp >= today and timestamp < tomorrow // Or we could trim the date within Mysql: // substring(timestamp, 0, 8) = today $today = $date->format("Ymd000000"); $date_tomorrow = clone $date; $date_tomorrow->add(new DateInterval("P1D")); $tomorrow = $date_tomorrow->format("Ymd000000"); $qs = rawurlencode("timestamp#1") . "=" . "$today&" . rawurlencode("_timestamp-op#1") . "=" . rawurlencode(">=") . "&" . rawurlencode("timestamp#2") . "=" . "$tomorrow&" . rawurlencode("_timestamp-op#2") . "=" . rawurlencode("<"); } else { $today=$date->format("Y-m-d"); $qs = "date=$today"; } return "photos.php?$qs"; } /** * Return the HTML for a specified month * @todo Day names are hardcoded and not localized */ public function getMonthView(Time $date) { $date->setTime(0, 0, 0); $prev_date=clone $date; $prev = $prev_date->sub(new DateInterval("P1M")); $next_date=clone $date; $next = $next_date->add(new DateInterval("P1M")); $daysInMonth=$date->format("t"); $firstDay=$date->format("w"); $today=new Time(); $today->setTime(0,0,0); $header=$date->format("F Y"); $days=array(); $titles=array("S", "M", "T", "W", "T", "F", "S"); for ($i=0; $i < $firstDay; $i++) { $days[]=array( "date" => "", "link" => "", "class" => "calendar", "photo" => null, "photocount" => 0 ); } for ($day=1; $day<=$daysInMonth; $day++) { $classes="calendar day"; $photos=photos::createFromVars(array("date" => $date->format("Y-m-d"))); if ($photos instanceof photos && sizeof($photos) > 0) { $classes .= " photos"; $link = $this->getDateLink($date); $cover = $photos->random()->pop()->getImagetag(THUMB_PREFIX); $count = sizeof($photos); } else { $link = ""; $cover = null; $count = 0; } if ($date == $today) { $classes .= " today"; } $days[]=array( "date" => $day, "link" => $link, "class" => $classes, "photo" => $cover, "photocount" => $count ); $date->add(new dateInterval("P1D")); } $tpl=new block("calendar", array( "prev" => $this->getCalendarLink($prev), "next" => $this->getCalendarLink($next), "header" => $header, "titles" => $titles, "days" => $days )); return $tpl; } } ?> zoph-v0.9.19/php/classes/category.inc.php000066400000000000000000000404011415176210700203050ustar00rootroot00000000000000addParam(new param(":photo_id", $photo->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":category_id", $this->getId(), PDO::PARAM_INT)); $qry->execute(); } /** * Remove a photo from this category * @param photo Photo to remove * @todo Permissions are currently not checked, this should be done before calling this function */ public function removePhoto(photo $photo) { $qry=new delete(array("photo_categories")); $where=new clause("photo_id=:photoid"); $where->addAnd(new clause("category_id=:catid")); $qry->where($where); $qry->addParams(array( new param(":photoid", $photo->getId(), PDO::PARAM_INT), new param(":catid", $this->getId(), PDO::PARAM_INT) )); $qry->execute(); } /** * Can user edit this category * No specific user rights, so we simply return whether * this user can edit categories or not. * @param user user * @return bool can edit */ public function isWritableBy(user $user) { return $user->canEditOrganizers(); } /** * Delete category */ public function delete() { parent::delete(array("photo_categories")); } /** * Get the name of this category * @todo can be moved into zophTable? */ public function getName() { return $this->get("category"); } public static function getAll() { if (static::$categoryCache) { return static::$categoryCache; } $qry=new select(array("c" => "categories")); $qry->addFields(array("*", "name" => "category")); $user=user::getCurrent(); if (!$user->canSeeAllPhotos()) { $userQry=new select(array("c" => "categories")); $userQry->addFields(array("category_id", "parent_category_id")); $userQry = selectHelper::expandQueryForUser($userQry); if ($user->canEditOrganizers()) { $subqry=new select(array("c" => "categories")); $subqry->addFields(array("category_id", "parent_category_id")); $subqry->where(new clause("c.createdby=:ownerid")); $subqry->addParam(new param(":ownerid", (int) $user->getId(), PDO::PARAM_INT)); $userQry->union($subqry); } $categories=static::getRecordsFromQuery($userQry); $ids=static::getAllAncestors($categories); if (sizeof($ids)==0) { return array(); } $ids=new param(":catid", array_values($ids), PDO::PARAM_INT); $qry->addParam($ids); $qry->where(clause::InClause("c.category_id", $ids)); } static::$categoryCache=static::getRecordsFromQuery($qry); return static::$categoryCache; } /** * Get sub-categories * @param string order */ public function getChildren($order="name") { if (!in_array($order, array("name", "sortname", "oldest", "newest", "first", "last", "lowest", "highest", "average", "random"))) { $order="name"; } $qry=new select(array("c" => "categories")); $qry->addFields(array("*", "name" => "category")); $categories=static::getAll(); $catIds=array(); foreach ($categories as $category) { $catIds[]=$category->getId(); } if (sizeof($catIds)==0) { return array(); } $ids=new param(":catid", $catIds, PDO::PARAM_INT); $qry->addParam($ids); $where=clause::InClause("c.category_id", $ids); $parent=new clause("parent_category_id=:parentid"); $qry->addParam(new param(":parentid", (int) $this->getId(), PDO::PARAM_INT)); $qry->addGroupBy("c.category_id"); $qry=selectHelper::addOrderToQuery($qry, $order); if ($order!="name") { $qry->addOrder("name"); } $where->addAnd($parent); $qry->where($where); $this->children=static::getRecordsFromQuery($qry); return $this->children; } /** * Get count of photos in this category * @todo This function is very similar to album::getPhotoCount, should be merged */ public function getPhotoCount() { if ($this->photoCount) { return $this->photoCount; } $qry=new select(array("pc" => "photo_categories")); $qry->addFunction(array("count" => "count(distinct pc.photo_id)")); $where=new clause("category_id = :cat_id"); $qry->addParam(new param(":cat_id", $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $count=$qry->getCount(); $this->photoCount=$count; return $count; } /** * Get count of photos for this category and all subcategories */ public function getTotalPhotoCount() { $where=null; if ($this->photoTotalCount) { return $this->photoTotalCount; } $qry=new select(array("pc" => "photo_categories")); $qry->join(array("p" => "photos"), "pc.photo_id = p.photo_id"); $qry->addFunction(array("count" => "count(distinct pc.photo_id)")); $qry = selectHelper::expandQueryForUser($qry); if ($this->get("parent_category_id")) { $id_list=null; $this->getBranchIdArray($id_list); $ids=new param(":cat_id", $id_list, PDO::PARAM_INT); $qry->addParam($ids); $catids=clause::InClause("category_id", $ids); if ($where instanceof clause) { $where->addAnd($catids); } else { $where=$catids; } } if ($where instanceof clause) { $qry->where($where); } $count=$qry->getCount(); $this->photoTotalCount=$count; return $count; } /** * Get array that can be used to create an edit form * @todo Returns HTML, move into template */ public function getEditArray() { if ($this->isRoot()) { $parent=array( translate("parent category"), translate("Categories")); } else { $parent=array( translate("parent category"), static::createPulldown("parent_category_id", $this->get("parent_category_id")) ); } return array( "category" => array( translate("category name"), create_text_input("category", $this->get("category"),40,64)), "parent_category_id" => $parent, "category_description" => array( translate("category description"), create_text_input("category_description", $this->get("category_description"), 40, 128)), "pageset" => array( translate("pageset"), template::createPulldown("pageset", $this->get("pageset"), template::createSelectArray(pageset::getRecords("title"), array("title"), true))), "sortname" => array( translate("sort name"), create_text_input("sortname", $this->get("sortname"))), "sortorder" => array( translate("category sort order"), template::createPhotoFieldPulldown("sortorder", $this->get("sortorder"))) ); } /** * Create a link to this category * @todo returns HTML, needs to be replaced by getURL() */ public function getLink() { if ($this->get("parent_category_id")) { $name = $this->get("category"); } else { $name = translate("Categories"); } return "getURL() . "\">$name"; } /** * Get coverphoto for this category. * @param string how to select a coverphoto: oldest, newest, first, last, random, highest * @param bool choose autocover from this category AND children * @return photo coverphoto * @todo This function is almost equal to album::getAutoCover(), should be merged */ public function getAutoCover($autocover=null,$children=false) { $coverphoto=$this->getCoverphoto(); if ($coverphoto instanceof photo) { return $coverphoto; } $qry=new select(array("p" => "photos")); $qry->addFunction(array("photo_id" => "DISTINCT ar.photo_id")); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id") ->join(array("pc" => "photo_categories"), "pc.photo_id = ar.photo_id"); if ($children) { $ids=new param(":ids",$this->getBranchIdArray(), PDO::PARAM_INT); $qry->addParam($ids); $where=clause::InClause("pc.category_id", $ids); } else { $where=new clause("pc.category_id=:id"); $qry->addParam(new param(":id", $this->getId(), PDO::PARAM_INT)); } $qry = selectHelper::expandQueryForUser($qry); $qry=selectHelper::getAutoCoverOrder($qry, $autocover); $qry->where($where); $coverphotos=photo::getRecordsFromQuery($qry); $coverphoto=array_shift($coverphotos); if ($coverphoto instanceof photo) { $coverphoto->lookup(); return $coverphoto; } else if (!$children) { // No photos found in this cat... let's look again, but now // also in subcat... return $this->getAutoCover($autocover, true); } } /** * Get autocomplete preference for categories, for the current user */ public static function getAutocompPref() { $user=user::getCurrent(); return ($user->prefs->get("autocomp_categories") && conf::get("interface.autocomplete")); } /** * Get details (statistics) about this category from db * @return array Array with statistics * @todo This function is almost equal to album::getDetails() these should be merged */ public function getDetails() { $qry=new select(array("pc" => "photo_categories")); $qry->addFunction(array( "count" => "COUNT(DISTINCT p.photo_id)", "oldest" => "MIN(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "newest" => "MAX(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "first" => "MIN(p.timestamp)", "last" => "MAX(p.timestamp)", "lowest" => "ROUND(MIN(ar.rating),1)", "highest" => "ROUND(MAX(ar.rating),1)", "average" => "ROUND(AVG(ar.rating),2)")); $qry->join(array("p" => "photos"), "pc.photo_id = p.photo_id") ->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id"); $qry->addGroupBy("pc.category_id"); $where=new clause("pc.category_id=:catid"); $qry->addParam(new param(":catid", $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $result=db::query($qry); if ($result) { return $result->fetch(PDO::FETCH_ASSOC); } else { return null; } } /** * Turn the array from @see getDetails() into XML * @param array Don't fetch details, but use the given array */ public function getDetailsXML(array $details=null) { if (!isset($details)) { $details=$this->getDetails(); } $details["title"]=translate("In this category:", false); return parent::getDetailsXML($details); } /** * Lookup category by name * @param string name * @todo This function is almost equal to album::getByName() these should be merged */ public static function getByName($name, $like=false) { if (empty($name)) { return false; } $qry=new select(array("c" => "categories")); $qry->addFields(array("category_id")); if ($like) { $qry->where(new clause("lower(category) LIKE :name")); $qry->addParam(new param(":name", "%" . strtolower($name) . "%", PDO::PARAM_STR)); } else { $qry->where(new clause("lower(category)=:name")); $qry->addParam(new param(":name", strtolower($name), PDO::PARAM_STR)); } return static::getRecordsFromQuery($qry); } /** * Get Top N categories */ public static function getTopN() { $user=user::getCurrent(); $qry=new select(array("c" => "categories")); $qry->addFields(array("category_id", "category")); $qry->addFunction(array("count" => "count(distinct pc.photo_id)")); $qry->join(array("pc" => "photo_categories"), "pc.category_id=c.category_id"); $qry->addGroupBy("c.category_id"); $qry->addOrder("count DESC")->addOrder("c.category"); $qry->addLimit((int) $user->prefs->get("reports_top_n")); if (!$user->canSeeAllPhotos()) { $qry->join(array("p" => "photos"), "pc.photo_id=p.photo_id"); $qry = selectHelper::expandQueryForUser($qry); } return parent::getTopNfromSQL($qry); } /** * Get number of categories for the currently logged on user */ public static function getCountForUser() { $user=user::getCurrent(); if ($user->canSeeAllPhotos()) { return static::getCount(); } else { $qry=new select(array("pc" => "photo_categories")); $qry->addFunction(array("category_id" => "distinct pc.category_id")); $qry = selectHelper::expandQueryForUser($qry); $categories=static::getRecordsFromQuery($qry); $ids=static::getAllAncestors($categories); return count($ids); } } } ?> zoph-v0.9.19/php/classes/category/000077500000000000000000000000001415176210700170255ustar00rootroot00000000000000zoph-v0.9.19/php/classes/category/controller.inc.php000066400000000000000000000127551415176210700225030ustar00rootroot00000000000000_action=="new") { $category=new category(); if (isset($this->request["parent_category_id"])) { $category->set("parent_category_id", $this->request["parent_category_id"]); } $this->setObject($category); $this->doAction(); } else { try { $category=$this->getCategoryFromRequest(); } catch (categoryNotAccessibleSecurityException $e) { log::msg($e->getMessage(), log::WARN, log::SECURITY); $category=null; } if ($category instanceof category) { $this->setObject($category); $this->doAction(); } else { $this->view = "notfound"; } } } /** * Get the category based on the query in the request * @throws categoryNotAccessibleSecurityException */ private function getCategoryFromRequest() { $user=user::getCurrent(); if (isset($this->request["category_id"])) { $category = new category($this->request["category_id"]); $category->lookup(); } else { $category=category::getRoot(); } if ($user->isAdmin() || $user->getCategoryPermissions($category)) { return $category; } throw new categoryNotAccessibleSecurityException( "Security Exception: category " . $category->getId() . " is not accessible for user " . $user->getName() . " (" . $user->getId() . ")" ); } /** * Do action 'confirm' */ public function actionConfirm() { if (user::getCurrent()->canDeletePhotos()) { parent::actionConfirm(); } else { $this->view="display"; } } /** * Do action 'delete' */ public function actionDelete() { if (user::getCurrent()->canDeletePhotos()) { parent::actionDelete(); } else { $this->view="display"; } } /** * Do action 'display' */ public function actionDisplay() { $user = user::getCurrent(); $this->view="display"; } /** * Do action 'edit' */ public function actionEdit() { $user = user::getCurrent(); if ($this->object->isWritableBy($user)) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'new' */ public function actionInsert() { parent::actionInsert(); $this->redirect="category.php?_action=edit&category_id=" . $this->object->getId(); $this->view="redirect"; } /** * Do action 'new' */ public function actionNew() { $user = user::getCurrent(); if ($user->canEditOrganizers()) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'update' */ public function actionUpdate() { $user=user::getCurrent(); if ($this->object->isWritableBy($user)) { $this->object->setFields($this->request->getRequestVars()); $this->object->update(); redirect("category.php?action=edit&category_id=" . $this->object->getId(), "Update done"); } $this->view="display"; } public function actionCoverphoto() { $user=user::getCurrent(); if ($this->object->isWritableBy($user) && isset($this->request["coverphoto"])) { $this->object->set("coverphoto", (int) $this->request["coverphoto"]); $this->object->update(); } $this->view="redirect"; } public function actionUnsetcoverphoto() { $user=user::getCurrent(); if ($this->object->isWritableBy($user)) { $this->object->set("coverphoto", null); $this->object->update(); } $this->view="redirect"; } } zoph-v0.9.19/php/classes/category/view/000077500000000000000000000000001415176210700177775ustar00rootroot00000000000000zoph-v0.9.19/php/classes/category/view/confirm.inc.php000066400000000000000000000032231415176210700227150ustar00rootroot00000000000000 "category.php?_action=confirm&category_id=" . $this->category->getId(), translate("cancel") => "categories.php?parent_category_id=" . $this->category->getId() ); } /** * Output view */ public function view() { return new template("confirm", array( "title" => translate("delete category"), "actionlinks" => $this->getActionlinks(), "mainActionlinks" => null, "obj" => $this->category, )); } } zoph-v0.9.19/php/classes/category/view/display.inc.php000066400000000000000000000025451415176210700227330ustar00rootroot00000000000000setRedirect("categories.php?parent_category_id=" . $category->getId()); } } zoph-v0.9.19/php/classes/category/view/redirect.inc.php000066400000000000000000000031571415176210700230670ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->category=$category; } /** * Output view */ public function view() { redirect($this->redirect, "Redirect"); } /** * Set the page to redirect to * @param string redirect target */ public function setRedirect($redirect) { $this->redirect=$redirect; } } zoph-v0.9.19/php/classes/category/view/update.inc.php000066400000000000000000000054341415176210700225500ustar00rootroot00000000000000request["parent_category_id"] != 0) { $returnURL = "categories.php?parent_category_id=" . $this->request["parent_category_id"]; } else if ($this->category->getId() != 0) { $returnURL = "category.php?category_id=" . $this->category->getId(); } else { $returnURL = "categories.php"; } if ($this->request["_action"] == "new") { return array( translate("return") => $returnURL ); } else { return array( translate("return") => $returnURL, translate("new") => "category.php?_action=new&parent_category_id=" . $this->category->getId(), translate("delete") => "category.php?_action=delete&category_id=" . $this->category->getId() ); } } /** * Output the view */ public function view() { $user = user::getCurrent(); $actionlinks=$this->getActionlinks(); if ($this->request["_action"] == "new") { $action = "insert"; } else if ($this->request["_action"] == "edit") { $action = "update"; } else { // Safety net. This should not happen. $action = $this->request["_action"]; $permissions=""; } $tpl = new template("editCategory", array( "actionlinks" => $actionlinks, "action" => $action, "category" => $this->category, "title" => $this->getTitle(), "user" => $user, )); return $tpl; } } zoph-v0.9.19/php/classes/category/view/view.inc.php000066400000000000000000000034721415176210700222400ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->category=$category; } /** * Get actionlinks */ abstract protected function getActionlinks(); /** * Output view */ abstract public function view(); /** * Get the title for this view */ public function getTitle() { return translate($this->request["_action"] . " category"); } } zoph-v0.9.19/php/classes/circle.inc.php000066400000000000000000000303471415176210700177410ustar00rootroot00000000000000set("createdby", (int) user::getCurrent()->getId()); return parent::insert(); } /** * Update this object in the database */ public function update() { $this->set("hidden", (bool) $this->get("hidden") ? "1" : "0"); parent::update(); } /** * Get the name of this circle */ public function getName() { return $this->get("circle_name"); } /** * Get URL for this circle */ public function getURL() { return static::$url . $this->getId(); } /** * Get display array * Get an array of properties to display * @return array properties */ public function getDisplayArray() { $da=array( translate("circle") => $this->getName(), translate("description") => $this->get("description"), translate("members") => implode("
", $this->getMemberLinks()), ); if ($this->isHidden()) { $da[translate("hidden")]=translate("This circle is hidden in overviews"); } return $da; } /** * Returns whether or not this circle is hidden * @return bool hidden or not */ public function isHidden() { return (bool) $this->get("hidden"); } /** * Is this circle visible for this user? * Bear in mind that this is NOT the opposite of the isHidden() function above! * That function is about hiding otherwise visible circles, this function is * about checking access rights. Possibly the two concepts should be merged * at some point. */ public function isVisible() { $user=user::getCurrent(); return ((sizeof($this->getMembers())>0) || $this->isCreatedBy($user) || $user->isAdmin()); } /** * Has this circle been created by the given user? * @param user User to check * @return bool */ public function isCreatedBy(user $user) { $this->lookup(); return ((int) $this->get("createdby") === $user->getId()); } /** * Automatically select a coverphoto for this circle * It selects the coverphoto by FIRST getting the photos with the most people on it and * only then picking the oldest, newest, etc. * @param string how to select a coverphoto: oldest, newest, first, last, random, highest * @return photo coverphoto */ public function getAutoCover($autocover=null) { $coverphoto=$this->getCoverphoto(); if ($coverphoto instanceof photo) { return $coverphoto; } $people=new select(array("cp" => "circles_people")); $people->addFields(array("person_id")); $people->where(new clause("circle_id=:circleid")); $people->addParam(new param(":circleid", (int) $this->getId(), PDO::PARAM_INT)); $peopleIds=$people->toArray(); if (empty($peopleIds)) { return; } $param=new param(":personIds", (array) $peopleIds, PDO::PARAM_INT); $qry=new select(array("p" => "photos")); $qry->addFields(array( "photo_id" => "p.photo_id", "rating" => "ar.rating" )); $qry->addFunction(array("count" => "count(person_id)")); $qry->join(array("ppl" => "photo_people"), "p.photo_id=ppl.photo_id"); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id=ar.photo_id"); $qry->addOrder("count DESC"); $qry->addGroupBy("photo_id"); $qry->addLimit(1); $qry->addParam($param); $where=clause::InClause("ppl.person_id", $param); $qry=selectHelper::getAutoCoverOrder($qry, $autocover); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $coverphotos=photo::getRecordsFromQuery($qry); $coverphoto=array_shift($coverphotos); if ($coverphoto instanceof photo) { $coverphoto->lookup(); return $coverphoto; } } /** * Get details (statistics) about this circle from db * @return array Array with statistics * @todo this function is almost equal to the getDetails() function in other classes they should be merged */ public function getDetails() { $qry=new select(array("p" => "photos")); $qry->addFunction(array( "count" => "COUNT(DISTINCT p.photo_id)", "oldest" => "MIN(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "newest" => "MAX(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "first" => "MIN(p.timestamp)", "last" => "MAX(p.timestamp)", "lowest" => "ROUND(MIN(ar.rating),1)", "highest" => "ROUND(MAX(ar.rating),1)", "average" => "ROUND(AVG(ar.rating),2)")); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id"); $qry->join(array("pp" => "photo_people"), "p.photo_id = pp.photo_id"); $qry->join(array("cp" => "circles_people"), "cp.person_id = pp.person_id"); $qry->addGroupBy("cp.circle_id"); $where=new clause("cp.circle_id=:circleid"); $qry->addParam(new param(":circleid", $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $result=db::query($qry); if ($result) { return $result->fetch(PDO::FETCH_ASSOC); } else { return null; } } /** * Turn the array from @see getDetails() into XML * @param array Don't fetch details, but use the given array */ public function getDetailsXML(array $details=null) { if (!isset($details)) { $details=$this->getDetails(); } $details["title"]=translate("Photos of people in this circle:", false); return parent::getDetailsXML($details); } /** * Get the number of people in this circle * @return int count */ public function getPeopleCount() { return sizeof($this->getMembers()); } /** * Get members of this circle * @return array of people */ public function getMembers() { $qry=new select(array("cp" => "circles_people")); $qry->addFields(array("person_id")); $qry->where(new clause("circle_id=:circleid")); $qry->addParam(new param(":circleid", (int) $this->getId(), PDO::PARAM_INT)); if (!user::getCurrent()->canSeeAllPhotos()) { $allowed=person::getAllPeopleAndPhotographers(); $ids=array(); foreach ($allowed as $person) { $ids[]=$person->getId(); } $param=new param(":peopledIds", $ids, PDO::PARAM_INT); $qry->addParam($param); $qry->addClause(clause::InClause("person_id", $param), "AND"); } return person::getRecordsFromQuery($qry); } /** * Make getChildren an alias of getMembers() so tree view can work for circles * @return array of people */ public function getChildren() { return $this->getMembers(); } /** * Add a member to a circle * @param person Person to add */ public function addMember(person $person) { $qry=new insert(array("cp" => "circles_people")); $qry->addParams(array( new param(":circle_id", (int) $this->getId(), PDO::PARAM_INT), new param(":person_id", (int) $person->getId(), PDO::PARAM_INT) )); $qry->execute(); } /** * Remove a person from a circle * @param person Person to remove */ public function removeMember(person $person) { $qry=new delete(array("cp" => "circles_people")); $where=new clause("circle_id=:circleid"); $where->addAnd(new clause("person_id=:personid")); $qry->addParams(array( new param(":circleid", (int) $this->getId(), PDO::PARAM_INT), new param(":personid", $person->getId(), PDO::PARAM_INT) )); $qry->where($where); $qry->execute(); } /** * Get an array of people that are NOT a member of this circle * @return array of people */ public function getNonMembers() { $personIds=array(); $memberIds=array(); $people=person::getAll(); $members=$this->getMembers(); foreach ($people as $person) { $personIds[]=$person->getId(); } if ($members) { foreach ($members as $member) { $memberIds[]=$member->getId(); } $nonMemberIds=array_diff($personIds, $memberIds); } else { $nonMemberIds=$personIds; } $nonMembers=array(); foreach ($nonMemberIds as $id) { $nonMembers[]=new person($id); } return $nonMembers; } /** * Create a pulldown to add new members to this circle * @param string name for the pulldown field * @return template Pulldown */ public function getNewMemberPulldown($name) { $valueArray=array(); $newMembers=$this->getNonMembers(); $valueArray[0]=null; foreach ($newMembers as $nm) { $nm->lookup(); $valueArray[$nm->getId()]=$nm->getName(); } return template::createPulldown($name, null, $valueArray); } /** * Get links to all members of this group * @return array array of links */ public function getMemberLinks() { $links=array(); $members=$this->getMembers(); if ($members) { foreach ($members as $member) { $member->lookup(); $links[]=$member->getLink(); } } return $links; } /** * Get all circles * @param bool Whether or not to show hidden circles * @return array of circles */ public static function getAll($showHidden=false) { $rawCircles=static::getRecords("circle_name"); $user=user::getCurrent(); if (!$user->canSeeAllPhotos()) { $circles=array(); foreach ($rawCircles as $circle) { if ($circle->isVisible()) { $circles[]=$circle; } } $rawCircles=$circles; } if ($showHidden && ($user->canSeeHiddenCircles())) { $circles=$rawCircles; } else { $circles=array(); foreach ($rawCircles as $circle) { if (!$circle->isHidden()) { $circles[]=$circle; } } } return $circles; } } ?> zoph-v0.9.19/php/classes/cli/000077500000000000000000000000001415176210700157575ustar00rootroot00000000000000zoph-v0.9.19/php/classes/cli/arguments.inc.php000066400000000000000000000523271415176210700212560ustar00rootroot00000000000000arguments=$argv; if (count($this->arguments)===0) { $this->arguments[]="--help"; } $this->process(); $this->lookup(); } /** * Process the arguments * @todo This function contains a list of all arguments Zoph can understand * this really doesn't belong here and should be moved into a controller * part of the app. */ private function process() { $argv=$this->arguments; $args=&$this->processed; $args["albums"]=array(); $args["categories"]=array(); $args["files"]=array(); $args["people"]=array(); $args["photographer"]=array(); $args["location"]=array(); $args["instance"]=""; $args["fields"]=array(); $args["path"]=""; $args["dirpattern"]=""; /* For new albums, categories, places, people */ $parent=0; $args["palbum"]=array(); $args["pcat"]=array(); $args["pplace"]=array(); /* Used short arguments: A C D H I N P U V X a c d f g h i l n p r t u v w */ $size=sizeof($argv); for ($i=0; $i<$size; $i++) { switch($argv[$i]) { case "": break; case "--instance": case "-i": $args["instance"]=$argv[++$i]; break; case "--access": $this->argsAddAccess(explode(",",$argv[++$i])); break; case "--no-access": $this->argsRemoveAccess(explode(",",$argv[++$i])); break; case "--admin": $args["_admin"]=true; break; case "--no-admin": $args["_admin"]=false; break; case "--albums": case "--album": case "-a": $this->argsAlbums(explode(",",$argv[++$i]), $parent); $parent=0; break; case "--category": case "--categories": case "-c": $this->argsCategories(explode(",",$argv[++$i]), $parent); $parent=0; break; case "--config": case "-C": static::$command="config"; $args["_configitem"]=$argv[++$i]; if (isset($argv[$i+1])) { $args["_configvalue"]=$argv[++$i]; } else { $args["_configdefault"]=true; } break; case "--dumpconfig": case "--dump-config": static::$command="dumpconfig"; break; case "--fields": case "--field": case "-f": $args["fields"][]=$argv[++$i]; break; case "--getconfig": case "--get-config": case "-g": static::$command="getconfig"; $args["_getconfigitem"]=$argv[++$i]; break; case "--group": case "-G": $groups=explode(",",$argv[++$i]); foreach ($groups as $group) { $args["_groups"][]=trim($group); } break; case "--place": case "--location": case "-l": // Multiple locations are possible when using --new $locs=explode(",",$argv[++$i]); foreach ($locs as $loc) { $args["location"][]=trim($loc); if (isset($parent)) { $args["pplace"][]=trim($parent); } } $parent=0; break; case "--people": case "--persons": case "--person": case "-p": $people=explode(",",$argv[++$i]); foreach ($people as $person) { $args["people"][]=trim($person); } break; case "--photographer": case "-P": $args["photographer"]=$argv[++$i]; break; case "--parent": $parent=$argv[++$i]; break; case "--password": $args["_password"]=$argv[++$i]; break; case "--thumbs": case "-t": conf::set("import.cli.thumbs", true); break; case "--nothumbs": case "--no-thumbs": case "-n": conf::set("import.cli.thumbs", false); break; case "--exif": case "--EXIF": conf::set("import.cli.exif", true); break; case "--no-exif": case "--noEXIF": case "--noexif": case "--no-EXIF": conf::set("import.cli.exif", false); break; case "--size": conf::set("import.cli.size", true); break; case "--nosize": case "--no-size": conf::set("import.cli.size", false); break; case "--hash": conf::set("import.cli.hash", true); break; case "--no-hash": conf::set("import.cli.hash", false); break; case "--username": case "--user-name": $args["_username"]=$argv[++$i]; break; case "--userid": case "--user-id": $args["_userid"]=(int) $argv[++$i]; break; case "--update": case "-u": static::$command="update"; break; case "--import": case "-I": static::$command="import"; break; case "--new": case "-N": static::$command="new"; break; case "--delete": static::$command="delete"; break; case "--user": case "-U": switch ($argv[++$i]) { case "add": static::$command="adduser"; break; case "delete": static::$command="deleteuser"; break; case "show": static::$command="showuser"; break; case "update": static::$command="updateuser"; break; default: throw new \cliNoUserCommandException(translate("The option --user must be followed by 'add', 'delete', 'show' or 'update' to indicate what you would like to do with a user")); break; } case "--useIds": case "--useids": case "--use-ids": case "--useid": case "--use-id": conf::set("import.cli.useids", true); break; case "--copy": conf::set("import.cli.copy", true); break; case "--move": conf::set("import.cli.copy", false); break; case "-A": case "--autoadd": case "--auto-add": conf::set("import.cli.add.auto", true); break; case "-w": case "--add-always": case "--addalways": conf::set("import.cli.add.always", true); break; case "-r": case "--recursive": conf::set("import.cli.recursive", true); break; case "--dateddirs": case "--datedDirs": case "--dated": case "-d": conf::set("import.dated", true); break; case "--hierarchical": case "--hier": case "-H": conf::set("import.dated", true); conf::set("import.dated.hier", true); break; case "--no-dateddirs": case "--no-datedDirs": case "--no-dated": case "--nodateddirs": case "--nodatedDirs": case "--nodated": conf::set("import.dated", false); break; case "--no-hierarchical": case "--no-hier": case "--nohierarchical": case "--nohier": conf::set("import.dated.hier", false); break; case "-D": case "--path": $args["path"]=$argv[++$i]; break; case "--dirpattern": $args["dirpattern"]=$argv[++$i]; break; case "-V": case "--version": static::$command="version"; break; case "-h": case "--help": static::$command="help"; break; case "-X": case "--xmp": case "--XMP": $args["_xmp"] = true; break; case "-v": case "--verbose": $verbose=conf::get("import.cli.verbose"); conf::set("import.cli.verbose", ++$verbose); break; default: if (substr($argv[$i],0,1)=="-") { echo "unknown argument: " . $argv[$i] . "\n"; exit(1); } else { $args["files"][]=$argv[$i]; } break; } } if (isset($args["fields"])) { $newfields=array(); foreach ($args["fields"] as $f) { $field=explode("=", $f); $newfields[$field[0]]=$field[1]; } $args["fields"]=$newfields; } if (conf::get("import.cli.useids")==true && static::$command=="import") { static::$command="update"; } } private function argsAddAccess(array $access) { foreach ($access as $acc) { $this->processed["_add_access"][]=trim($acc); } } private function argsRemoveAccess(array $access) { foreach ($access as $acc) { $this->processed["_remove_access"][]=trim($acc); } } private function argsAlbums(array $albums, $parent=null) { foreach ($albums as $album) { $this->processed["albums"][]=trim($album); if (isset($parent)) { $this->processed["palbum"][]=trim($parent); } } } private function argsCategories(array $categories, $parent=null) { foreach ($categories as $cat) { $this->processed["categories"][]=trim($cat); if (isset($parent)) { $this->processed["pcat"][]=trim($parent); } } } /** * Looks up the given parameters in the database and gives back ids */ private function lookup() { $args=$this->processed; $vars=&$this->vars; foreach ($args as $type=>$arg) { if (empty($arg) || empty($type)) { continue; } log::msg($type . "\t->\t" . implode(",", (array) $arg), log::DEBUG, log::IMPORT); switch($type) { case "albums": $this->lookupAlbums($arg, $args["palbum"]); break; case "categories": $this->lookupCategories($arg, $args["pcat"]); break; case "people": $this->lookupPeople($arg); break; case "photographer": $this->lookupPhotographer($arg); break; case "location": $this->lookupLocations($arg, $args["pplace"]); break; case "path": $vars["_path"]=$arg; break; case "dirpattern": if (!preg_match("/^[aclpDP]+$/", $arg)) { throw new \cliIllegalDirpatternException("Illegal characters in " . "--dirpattern, allowed are: aclpDP"); } else { $vars["_dirpattern"]=$arg; } break; case "fields": foreach ($arg as $field=>$value) { $vars[$field]=$value; } break; case "_xmp": $vars["_xmp"] = true; break; case "_admin": $vars["_admin"] = (bool) $arg; case "_add_access": case "_remove_access": /** @todo: implement lookup of various access right */ $vars[$type]=$arg; break; case "_group": /** @todo: implement lookup of groups */ $vars["_group"]=$arg; break; case "_password": $vars["_password"]=$arg; break; case "_username": case "_userid": if (static::$command=="adduser" && $type == "_username") { $vars["_username"]=$arg; } else if ($type == "_username") { $user = user::getByName($arg); $vars["_userid"]=(int) $user->getId(); } else { $vars["_userid"]=(int) $arg; } break; case "_configitem": case "_configvalue": case "_configdefault": case "_getconfigitem": $vars[$type]=$arg; break; default: log::msg($type . "\t->\t" . implode(",", (array) $arg), log::ERROR, log::IMPORT); } } } /** * Returns the list of files */ public function getFiles() { return $this->processed["files"]; } /** * Returns an array of variables, with keys. */ public function getVars() { return $this->vars; } private function lookupAlbums(array $albums, $parents) { foreach ($albums as $name) { if (static::$command=="new" || (conf::get("import.cli.add.auto") && !album::getByName($name))) { $parent=array_shift($parents); // this is a string comparison because the trim() in process() changes // everything into a string... if ($parent==="0") { if (conf::get("import.cli.add.always")) { $parent_id=album::getRoot()->getId(); } else { throw new \cliNoParentException("No parent for album " . $name); } } else { $palbum=album::getByName($parent); if ($palbum) { $parent_id=$palbum[0]->getId(); } else { throw new \albumNotFoundException("Album not found: $parent"); } } $this->vars["_new_album"][]=array("parent" => $parent_id, "name" => $name); } else { $album=album::getByName($name); if ($album) { $this->vars["_album_id"][]=$album[0]->getId(); } else { throw new \albumNotFoundException("Album not found: $name"); } } } } private function lookupCategories(array $categories, $parents) { foreach ($categories as $name) { if (static::$command=="new" || (conf::get("import.cli.add.auto") && !category::getByName($name))) { $parent=array_shift($parents); // this is a string comparison because the trim() in process() changes // everything into a string... if ($parent==="0") { if (conf::get("import.cli.add.always")) { $parent_id=category::getRoot()->getId(); } else { throw new \cliNoParentException("No parent for category " . $name); } } else { $pcat=category::getByName($parent); if ($pcat) { $parent_id=$pcat[0]->getId(); } else { throw new \categoryNotFoundException("Category not found: $parent"); } } $this->vars["_new_cat"][]=array("parent" => $parent_id, "name" => $name); } else { $cat=category::getByName($name); if ($cat) { $this->vars["_category_id"][]=$cat[0]->getId(); } else { throw new \categoryNotFoundException("Category not found: $name"); } } } } private function lookupPeople(array $people) { foreach ($people as $name) { if (static::$command=="new" || (conf::get("import.cli.add.auto") && !person::getByName($name))) { $this->vars["_new_person"][]=$name; } else { $person=person::getByName($name); if ($person) { $person_id=$person[0]->getId(); $this->vars["_person_id"][]=$person_id; } else { throw new \personNotFoundException("Person not found: $name"); } } } } private function lookupPhotographer(string $name) { if (static::$command=="new" || (conf::get("import.cli.add.auto") && !person::getByName($name))) { $this->vars["_new_photographer"][]=$name; } else { $person=person::getByName($name); if ($person) { $person_id=$person[0]->getId(); $this->vars["photographer_id"]=$person_id; } else { throw new \personNotFoundException("Person not found: $name"); } } } private function lookupLocations(array $locations, $parents) { foreach ($locations as $name) { if (static::$command=="new" || (conf::get("import.cli.add.auto") && !place::getByName($name))) { $parent=array_shift($parents); // this is a string comparison because the trim() in process() changes // everything into a string... if ($parent==="0") { if (conf::get("import.cli.add.always")) { $parent_id=place::getRoot()->getId(); } else { throw new \cliNoParentException("No parent for location " . $name); } } else { $pplace=place::getByName($parent); if ($pplace) { $parent_id=$pplace[0]->getId(); } else { throw new \placeNotFoundException("Location not found: $parent"); } } $this->vars["_new_place"][]=array("parent" => $parent_id, "name" => $name); } else { $name=$locations[0]; $place=place::getByName($name); if ($place) { $place_id=$place[0]->getId(); $this->vars["location_id"]=$place_id; } else { throw new \placeNotFoundException("Location not found: $name"); } } } } } ?> zoph-v0.9.19/php/classes/cli/cli.inc.php000066400000000000000000000525121415176210700200140ustar00rootroot00000000000000user=$user; if (!$user->isAdmin()) { throw new \cliUserNotAdminException("CLI_USER must be an admin user"); } user::setCurrent($user); $user->prefs->load(); $user->loadLanguage(); $this->args=new arguments($args); } /** * Run the CLI */ public function run() { $this->processFiles(); switch(arguments::$command) { case "import": $this->doImport(); break; case "update": $this->doUpdate(); break; case "new": $this->addNew(); break; case "config": $this->doConfig(); break; case "getconfig": $this->doGetConfig(); break; case "dumpconfig": $this->doDumpConfig(); break; case "adduser": $this->doAddUser(); break; case "deleteuser": $this->doDeleteUser(); break; case "showuser": $this->doShowUser(); break; case "updateuser": $this->doUpdateUser(); break; case "version": static::showVersion(); break; case "help": static::showHelp(); break; default: throw new \cliUnknownErrorException("Unknown command, please file a bug"); } } /** * Check list of files */ private function processFiles() { $files=$this->args->getFiles(); foreach ($files as $filename) { try { if (arguments::$command=="import") { $file=new file($filename); $file->check(); $file->getMime(); if ($file->type=="directory" && conf::get("import.cli.recursive")) { $this->files=array_merge($this->files, file::getFromDir($file, true)); } else if ($file->type!="image") { throw new \importFileNotImportableException("$file is not an image\n"); } else { $this->files[]=$file; } } else { if (conf::get("import.cli.useids")) { $file=$filename; if (is_numeric($file)) { $this->photos[]=$this->lookupFileById($file); } else if (preg_match("/^[0-9]+-[0-9]+$/", $file)) { list($start, $end) = explode("-",$file); foreach (range($start, $end) as $id) { try { $this->photos[]=$this->lookupFileById($id); } catch (\importException $e) { echo $e->getMessage(); } } } else { throw new \importIdIsNotNumericException( "$file is not numeric, but --useids is set.\n"); } } else { $this->photos[]=$this->lookupFile($filename); } } } catch (\Exception $e) { echo $e->getMessage(); } } } /** * Looks up a photo by photo_id */ private function lookupFileById($id) { $photo=new photo((int) $id); $count=$photo->lookup(); if ($count==1) { return $photo; } else if ($count==0) { throw new \importFileNotFoundException("No photo with id $id was found\n"); } else { throw new \importMultipleMatchesException( "Multiple photos with id $id were found. This is probably a bug"); } } /** * Looks up a file by filename * @todo Maybe this should be moved into the file object? */ private function lookupFile($file) { $filename=basename($file); $path=dirname($file); if ($path==".") { // No path given //unset($path); $path="./"; } if (substr($path,0,2)=="./") { // Path relative to the current dir given, change into absolute path $path="/" . file::cleanupPath(getcwd() . "/" . $path); } if ($path[0]=="/") { // absolute path given $path="/" . file::cleanupPath($path) . "/"; // check if path is in conf::get("path.images") if (substr($path, 0, strlen(conf::get("path.images")))!=conf::get("path.images")) { throw new \importFileNotInPathException($path . "/" . $file ." is not in the images path (" . conf::get("path.images") . "), skipping.\n"); } else { $path=substr($path, strlen(conf::get("path.images"))); if ($path[0]=="/") { // conf::get("path.images") didn't end in '/', let's cut it off $path=substr($path, 1); } } } else { $path=file::cleanupPath($path); } $photos=photo::getByName($filename, $path); if (empty($photos)) { $photos=photo::getByName($filename); } if (empty($photos)) { throw new \importFileNotFoundException($file ." not found.\n"); } else if (sizeof($photos)==1) { return $photos[0]; } else { throw new \importMultipleMatchesException("Multiple files named " . $file ." found.\n"); } } /** * Process --import */ private function doImport() { $vars=$this->args->getVars(); if (conf::get("import.cli.add.auto")) { $vars=$this->addNew(); } if (is_array($this->files) && !empty($this->files)) { if (!isset($vars["_dirpattern"])) { $photos=array(); foreach (array_unique($this->files) as $file) { $photo=new photo(); $photo->file["orig"]=$file; if (isset($vars["_xmp"])) { $xmp = new xmpReader($file); $data=new xmpdecoder($xmp->getXMP()); foreach ($data->getSubjects() as $subject) { $categories = category::getByName($subject); $category = array_pop($categories); if ($category instanceof category) { if (!isset($photo->_category_id)) { $photo->_category_id=array(); } $photo->_category_id[]=$category->getId(); } } $rating = $data->getRating(); if ($rating) { $photo->_rate=$rating; } } $photos[]=$photo; } } else { $photos=$this->processDirpattern(); } \import\cli::photos($photos, $vars); } else { throw new \cliNoFilesException("Nothing to do, exiting"); } } /** * Process --update */ private function doUpdate() { if (is_array($this->photos) && !empty($this->photos)) { $total=sizeof($this->photos); $cur=0; foreach ($this->photos as $photo) { cliimport::progress($cur, $total); $cur++; $photo->lookup(); $photo->setFields($this->args->getVars()); $photo->update(); $photo->updateRelations($this->args->getVars(), "_id"); if (conf::get("import.cli.thumbs")===true) { $photo->thumbnail(true); } if (conf::get("import.cli.exif")===true) { $photo->updateEXIF(); } if (conf::get("import.cli.size")===true) { $photo->updateSize(); } if (conf::get("import.cli.hash")===true) { $photo->getHash(); } } } else { throw new \cliNoFilesException("Nothing to do, exiting"); } } /** * Add albums, categories, places, people that should be added because of --new or --autoadd * if $vars is given, */ public function addNew() { $vars=$this->args->getVars(); $newvars=array(); $return_vars=array(); foreach ($vars as $var=>$array) { switch($var) { case "_new_album": $newvars["_album_id"]=array(); foreach ($array as $new) { $album=new album(); $album->set("album", $new["name"]); $album->set("parent_album_id", (int) $new["parent"]); $album->insert(); $newvars["_album_id"][]=$album->getId(); } break; case "_new_cat": $newvars["_category_id"]=array(); foreach ($array as $new) { $cat=new category(); $cat->set("category", $new["name"]); $cat->set("parent_category_id", (int) $new["parent"]); $cat->insert(); $newvars["_category_id"][]=$cat->getId(); } break; case "_new_place": foreach ($array as $new) { $place=new place(); $place->set("title", $new["name"]); $place->set("parent_place_id", (int) $new["parent"]); $place->insert(); $newvars["location_id"]=$place->getId(); } break; case "_new_person": $newvars["_person_id"]=array(); foreach ($array as $new) { $person=new person(); $person->setName($new); $person->insert(); $newvars["_person_id"][]=$person->getId(); } break; case "_new_photographer": foreach ($array as $new) { $person=new person(); $person->setName($new); $person->insert(); $newvars["photographer_id"]=$person->getId(); } default: $return_vars[$var]=$array; } } foreach ($newvars as $name=>$array) { if (array_key_exists($name, $return_vars) && is_array($return_vars[$name])) { $return_vars[$name]=array_merge($return_vars[$name], $array); } $return_vars[$name]=$array; } return($return_vars); } /** * Process --config */ private function doConfig() { $vars=$this->args->getVars(); $name=$vars["_configitem"]; $default=isset($vars["_configdefault"]); $item=conf::getItemByName($name); if ($default) { $value=$item->getDefault(); } else { $value=$vars["_configvalue"]; } if (conf::get("import.cli.verbose") > 0) { echo "Setting config \"$name\" to \"$value\"" . ($default ? " (default)" : "") . "\n"; } $item->setValue($value); $item->update(); } /** * Process --getconfig */ private function doGetConfig() { $vars=$this->args->getVars(); $name=$vars["_getconfigitem"]; $item=conf::getItemByName($name); echo $item->displayValue() . "\n"; } /** * Process --dump-config */ private function doDumpConfig() { $conf=conf::getAll(); foreach ($conf as $item) { foreach ($item->getItems() as $citem) { echo $citem->getName() . ": " . $citem->displayValue() . "\n"; } } } /** * Process --user add */ private function doAddUser() { $vars=$this->args->getVars(); if (!isset($vars["_username"])) { throw new \cliNoUserException( sprintf(translate("No user (%s) specified"), "--username")); } if (!isset($vars["_password"])) { throw new \cliNoPasswordException( translate("No password specified")); } $user = new user(); $user->set("user_name", $vars["_username"]); $user->set("password", validator::hashPassword($vars["_password"])); if (isset($vars["_admin"]) && $vars["_admin"] === true) { $user->set("user_class", 0); } else { $user->set("user_class", 1); } $user->insert(); } /** * Process --user delete */ private function doDeleteUser() { $vars=$this->args->getVars(); if (!isset($vars["_userid"])) { throw new \cliNoUserException( sprintf(translate("No user (%s) specified"), "--username / --userid")); } } /** * Process --user show */ private function doShowUser() { $vars=$this->args->getVars(); if (!isset($vars["_userid"])) { throw new \cliNoUserException( sprintf(translate("No user (%s) specified"), "--username / --userid")); } $u = new user($vars["_userid"]); $u->lookup(); $da=$u->getDisplayArray(); $groups = array_map( function($g) { $g->lookup(); return $g->getName(); }, $u->getGroups() ); $tpl = new \template\template("cliShowUser", array( "userid" => $u->get("user_id"), "username" => $da["username"], "userclass" => $da["class"], "groups" => $groups, "lastlogin" => $da["last login"], "rights" => $u->getAccessRightsArray() )); echo $tpl . "\n\n"; } /** * Process --user update */ private function doUpdateUser() { $vars=$this->args->getVars(); if (!isset($vars["_userid"])) { throw new \cliNoUserException( sprintf(translate("No user (%s) specified"), "--username / --userid")); } } /** * Process the --dirpattern setting */ public function processDirpattern() { $vars=$this->args->getVars(); $patt=str_split($vars["_dirpattern"]); $cur=getcwd(); $curlen=strlen($cur); foreach ($this->files as $file) { if (substr($file, 0, $curlen) != $cur) { throw new \cliNotInCWDException("Sorry, --dirpattern can only be used when " . "importing files under the current dir. i.e. do not use absolute paths " . "or '../' when specifying --dirpattern."); } $filename=substr($file, $curlen + 1); $dirs=explode("/", $filename); array_pop($dirs); $photo=new photo(); $photo->file["orig"]=$file; $counter=0; foreach ($dirs as $dir) { if (isset($patt[$counter])) { switch($patt[$counter]) { case "a": // album $album=album::getByName($dir); if ($album[0] instanceof \album) { if (!is_array($photo->_album_id)) { $photo->_album_id=array(); } $photo->_album_id[]=$album[0]->getId(); } else { throw new \albumNotFoundException("Album not found: " . $dir); } break; case "c": // category $cat=category::getByName($dir); if ($cat[0] instanceof \category) { if (!is_array($photo->_category_id)) { $photo->_category_id=array(); } $photo->_category_id[]=$cat[0]->getId(); } else { throw new \categoryNotFoundException("Category not found: " . $dir); } break; case "l": // location $place=place::getByName($dir); if ($place[0] instanceof \place) { $photo->set("location_id", $place[0]->getId()); } else { throw new \placeNotFoundException("Place not found: " . $dir); } break; case "p": // person $person=person::getByName($dir); if ($person[0] instanceof \person) { if (!is_array($photo->_person_id)) { $photo->_person_id=array(); } $photo->_person_id[]=$person[0]->getId(); } else { throw new \personNotFoundException("Person not found: " . $dir); } break; case "D": // dir / path $path=$photo->_path; if (!empty($path)) { $path .= "/"; } $photo->_path=$path . $dir; break; case "P": // photographer $person=person::getByName($dir); if ($person[0] instanceof \person) { $photo->set("photographer_id", $person[0]->getId()); } else { throw new \personNotFoundException("Person not found: " . $dir); } break; default: // should never happen... throw new \cliUnknownErrorException("Unknown error"); } } $counter++; } $photos[]=$photo; } return $photos; } /** * Show help */ private static function showHelp() { echo "zoph " . VERSION . "\n"; echo << zoph-v0.9.19/php/classes/comment.inc.php000066400000000000000000000141451415176210700201400ustar00rootroot00000000000000toHTML(true); } /** * Insert a new comment into the db */ public function insert() { $this->set("comment_date", "now()"); $this->set("ipaddr", $_SERVER['REMOTE_ADDR']); parent::insert(); $this->lookup(); } /** * Update existing comment in the db */ public function update() { $this->set("timestamp", "now()"); parent::update(); $this->lookup(); } /** * Delete a comment from the db */ public function delete() { if (!$this->getId()) { return; } parent::delete(array("photo_comments")); } /** * Get array to display comment data * @return array display array */ public function getDisplayArray() { $user=user::getCurrent(); $date=$this->get("comment_date"); $changed=$this->get("timestamp"); $zophcode = new zophCodeParser($this->get("comment"), array("b", "i", "u")); $comment="
" . $zophcode . "
"; return array( translate("subject") => $this->get("subject"), translate("date") => $date, translate("user") => $this->getUserLink(), translate("IP address") => $user->isAdmin() ? $this->get("ipaddr") : "" . translate("only visible for admin users") . "", translate("comment") => $comment, translate("updated") => $changed ); } /** * Lookup user that created this comment and return a link */ private function getUserLink() { $user = new user($this->get("user_id")); $user->lookup(); $user->lookupPerson(); return $user->getLink() . " (" . $user->person->getLink() . ")"; } /** * Get the photo that this comment belongs to */ public function getPhoto() { if (!$this->getId()) { return; } $qry=new select(array("pcom" => "photo_comments")); $qry->addFields(array("photo_id")); $qry->where(new clause("comment_id=:commentId")); $qry->addParam(new param(":commentId", (int) $this->getId(), PDO::PARAM_INT)); $qry->addLimit(1); $result=photo::getRecordsFromQuery($qry); if (isset($result[0])) { $result[0]->lookup(); return $result[0]; } else { return null; } } /** * Add this comment to a photo * @param photo photo to add comment to */ public function addToPhoto(photo $photo) { $qry=new insert(array("photo_comments")); $qry->addParams(array( new param(":photo_id", (int) $photo->getId(), PDO::PARAM_INT), new param(":comment_id", (int) $this->getId(), PDO::PARAM_INT) )); $qry->execute(); } /** * Return whether the given user is the owner (creator) of this comment * @param user User to check * @return bool true: user is owner, false: user is not owner */ public function isOwner(user $user) { return ($user->getId()==$this->get("user_id")); } /** * Display this comment * @param bool Display a thumbnail of the photo this comment belongs to * @return block Template block */ public function toHTML($thumbnail=false) { $user=user::getCurrent(); $this->lookup(); $photo=$this->getPhoto(); $tplData=array( "subject" => $this->get("subject"), "commentdate" => $this->get("comment_date"), "userlink" => $this->getUserLink(), "zophcode" => new zophCodeParser($this->get("comment"), array("b", "i", "u")), "actionlinks" => null ); if ($user->isAdmin() || $this->isOwner($user)) { $tplData["actionlinks"]=array( translate("display") => "comment.php?_action=display&comment_id=" . $this->getId(), translate("edit") => "comment.php?_action=edit&comment_id=" . $this->getId(), translate("delete") => "comment.php?_action=delete&comment_id=" . $this->getId() ); } if ($thumbnail) { $tplData["thumbnail"]=$photo->getThumbnailLink(); } return new block("comment", $tplData); } public function getName() { return $this->get("subject"); } } ?> zoph-v0.9.19/php/classes/comment/000077500000000000000000000000001415176210700166525ustar00rootroot00000000000000zoph-v0.9.19/php/classes/comment/controller.inc.php000066400000000000000000000106211415176210700223160ustar00rootroot00000000000000canLeaveComments()) { throw new userInsufficientRightsSecurityException("User has no rights to leave comments"); } $comment_id = (int) $request["comment_id"]; $comment = new comment($comment_id); if ($comment_id !== 0) { $comment->lookup(); } $photo = $comment->getPhoto(); if (!$photo instanceof photo && $comment_id == 0) { $photo = new photo((int) $request["photo_id"]); $photo->lookup(); } $this->photo=$photo; if (!$user->getPhotoPermissions($photo) && !$user->canSeeAllPhotos()) { throw new photoNotAccessibleSecurityException("User has no access to this photo"); } $this->object=$comment; $this->doAction(); } /** * Do action 'confirm' */ public function actionConfirm() { $user = user::getCurrent(); if ($user->isAdmin() || ($user->canLeaveComments() && $this->object->isOwner($user))) { parent::actionConfirm(); $this->redirect="photo.php?photo_id=" . $this->photo->getId(); } else { $this->view="display"; } } /** * Do action 'delete' */ public function actionDelete() { $user = user::getCurrent(); if ($user->isAdmin() || ($user->canLeaveComments() && $this->object->isOwner($user))) { parent::actionDelete(); } else { $this->view="display"; } } /** * Do action 'edit' */ public function actionEdit() { $user = user::getCurrent(); if ($user->isAdmin() || ($user->canLeaveComments() && $this->object->isOwner($user))) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'update' */ public function actionUpdate() { $user=user::getCurrent(); if ($user->isAdmin() || ($user->canLeaveComments() && $this->object->isOwner($user))) { unset($this->request["photo_id"]); $this->object->setFields($this->request->getRequestVars()); $this->object->update(); } $this->view="display"; } /** * Do action 'insert' */ public function actionInsert() { $user = user::getCurrent(); if ($user->canLeaveComments()) { $comment = new comment(); $comment->set("user_id", user::getCurrent()->getId()); $this->setObject($comment); unset($this->request["photo_id"]); parent::actionInsert(); $comment->addToPhoto($this->photo); } $this->redirect="photo.php?photo_id=" . $this->photo->getId(); $this->view="redirect"; } } zoph-v0.9.19/php/classes/comment/view/000077500000000000000000000000001415176210700176245ustar00rootroot00000000000000zoph-v0.9.19/php/classes/comment/view/confirm.inc.php000066400000000000000000000045331415176210700225470ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $comment=new comment($request["comment_id"]); $comment->lookup(); $this->comment=$comment; } /** * Output view */ public function view() { $actionlinks=array( "confirm" => "comment.php?_action=confirm&comment_id=" . $this->comment->getId(), "cancel" => "comment.php?comment_id=" . $this->comment->getId() ); return new template("confirm", array( "title" => $this->getTitle(), "actionlinks" => null, "mainActionlinks" => $actionlinks, "obj" => $this->comment, )); } public function getTitle() { $subject = $this->comment->get("subject"); $commentUser = new user($this->comment->get("user_id")); $commentUser->lookup(); $name=$commentUser->get("user_name"); return sprintf(translate("Confirm deletion of comment '%s' by '%s'"), $subject, $name); } } zoph-v0.9.19/php/classes/comment/view/display.inc.php000066400000000000000000000050261415176210700225550ustar00rootroot00000000000000lookup(); $this->comment=$comment; } /** * Get action links * @return array action links */ protected function getActionlinks() { $user=user::getCurrent(); $actionlinks=array( "return" => "photo.php?photo_id=" . $this->comment->getPhoto()->getId() ); if ($user->isAdmin() || $this->comment->isOwner($user)) { $actionlinks = array_merge($actionlinks, array( "edit" => "comment.php?_action=edit&comment_id=" . $this->comment->getId(), "delete" => "comment.php?_action=delete&comment_id=" . $this->comment->getId(), )); } return $actionlinks; } /** * Output view */ public function view() { $user=user::getCurrent(); $tpl=new template("main", array( "title" => $this->getTitle(), )); $tpl->addActionlinks($this->getActionlinks()); $tpl->addBlock($this->comment->getPhoto()->getImageTag(MID_PREFIX)); $tpl->addBlock(new block("definitionlist", array( "class" => "display comment", "dl" => $this->comment->getDisplayArray() ))); return $tpl; } /** * Get the title for this view */ public function getTitle() { return $this->comment->get("subject"); } } zoph-v0.9.19/php/classes/comment/view/redirect.inc.php000066400000000000000000000030471415176210700227120ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); } /** * Output view */ public function view() { redirect($this->redirect); } /** * Set the page to redirect to * @param string redirect target */ public function setRedirect($redirect) { $this->redirect=$redirect; } public function getTitle() { return "Redirect"; } } zoph-v0.9.19/php/classes/comment/view/update.inc.php000066400000000000000000000073451415176210700224000ustar00rootroot00000000000000lookup(); $this->comment=$comment; $this->request=$request; $photo = $comment->getPhoto(); if (!$photo instanceof photo && $comment->getId() == 0) { $photo = new photo((int) $request["photo_id"]); $photo->lookup(); } $this->photo=$photo; } /** * Create the actionlinks for this page */ protected function getActionlinks() { return array( "return" => "photo.php?photo_id=" . $this->photo->getId(), "new" => "comment.php?_action=new" ); } /** * Output the view */ public function view() { $user = user::getCurrent(); $photo=$this->photo; if ($this->request["_action"] == "new") { $action = "insert"; } else if ($this->request["_action"] == "edit") { $action = "update"; } else { // Safety net. This should not happen. $action = $this->request["_action"]; } $tpl = new template("main", array( "title" => $this->getTitle(), "actionlinks" => $this->getActionlinks(), "mainActionlinks" => null, )); $tpl->addBlock($photo->getImageTag(MID_PREFIX)); $form = new form("form", array( "formAction" => "comment.php", "class" => "comment", "onsubmit" => null, "action" => $action, "submit" => translate($action, 0) )); $form->addInputHidden("comment_id", $this->comment->getId()); $form->addInputHidden("photo_id", $this->photo->getId()); $form->addInputText("subject", $this->comment->get("subject"), translate("subject")); $form->addTextarea("comment", $this->comment->get("comment"), translate("comment"), 80, 8); $tpl->addBlock($form); /*

*/ $tpl->addBlock(smiley::getOverview()); return $tpl; } /** * Get the title for this view */ public function getTitle() { if ($this->comment->getId()==0) { return translate("Add comment"); } else if (trim($this->comment->get("subject"))=="") { return translate("comment"); } else { return $this->comment->get("subject"); } } } zoph-v0.9.19/php/classes/conf/000077500000000000000000000000001415176210700161355ustar00rootroot00000000000000zoph-v0.9.19/php/classes/conf/collection.inc.php000066400000000000000000000025251415176210700215550ustar00rootroot00000000000000getName(); } parent::offsetSet($off, $value); } } zoph-v0.9.19/php/classes/conf/conf.inc.php000066400000000000000000000156531415176210700203550ustar00rootroot00000000000000get("conf_id"); } /** * Read configuration from database */ public static function loadFromDB() { confDefault::getConfig(); $qry=new select(array("co" => "conf")); $qry->addFields(array("conf_id", "value")); try { $result=db::query($qry); } catch (\PDOException $e) { log::msg("Cannot load configuration from database", log::FATAL, log::CONFIG | log::DB); } while ($row = $result->fetch(PDO::FETCH_NUM)) { $key=$row[0]; $value=$row[1]; try { $item=static::getItemByName($key); try { $item->setValue($value); if ($item->isDeprecated() && $value != $item->getDefault()) { static::$warnings[]="Deprecated configuration item " . $key . " is used!"; } } catch (\configurationException $e) { /* An illegal value is automatically set to the default */ log::msg($e->getMessage(), log::ERROR, log::CONF); } } catch (\configurationException $e) { /* An unknown item will automatically be deleted from the database, so we can remove items without leaving a mess */ log::msg($e->getMessage(), log::NOTIFY, log::CONF); $qry=new delete(array("co" => "conf")); $qry->where(new clause("conf_id=:confid")); $qry->addParam(new param(":confid", $key, PDO::PARAM_STR)); $qry->execute(); } } static::$loaded=true; } /** * Read configuration from submitted form * @param array of $_GET or $_POST variables */ public static function loadFromRequestVars(array $vars) { confDefault::getConfig(); foreach ($vars as $key=>$value) { if (substr($key,0,1) == "_") { if (substr($key,0,7) == "_reset_") { $key=substr(str_replace("_", ".", $key),7); $item=static::getItemByName($key); $item->delete(); } continue; } $key=str_replace("_", ".", $key); try { if (!isset($vars["_reset_" . $key])) { $item=static::getItemByName($key); $item->setValue($value); $item->update(); } } catch(\configurationException $e) { log::msg("Configuration cannot be updated: " . $e->getMessage(), log::ERROR, log::CONFIG); } } static::$loaded=true; } /** * Get a configuration item by name * @param string Name of item to return * @return \conf\item\item Configuration item * @throws \configurationException */ public static function getItemByName($name) { $nameArr=explode(".", $name); $group=array_shift($nameArr); if (isset(static::$groups[$group])) { $items=static::$groups[$group]->getItems(); if (isset($items[$name])) { return $items[$name]; } } throw new \configurationException("Unknown configuration item " . $name); } /** * Get the value of a configuration item * @param string Name of item to return * @return string Value of parameter */ public static function get($key) { if (!static::$loaded) { static::loadFromDB(); } $item=static::getItemByName($key); return $item->getValue(); } /** * Set the value of a configuration item * Does not store this value in the database as this is mainly * used for runtime-overriding a stored value. This function returns * the object so the calling function can do a $item->update() if * it should be stored in the db. * @param string Name of item to change * @param string Value to set * @return \conf\item\item the item that has been updated */ public static function set($key, $value) { $item=static::getItemByName($key); $item->setValue($value); return $item; } /** * Get all configuration items (in groups) * @return array Array of group objects */ public static function getAll() { if (!static::$loaded) { static::loadFromDB(); } return static::$groups; } /** * Create a new conf\group and add it to the list * @param collection collection to add as group * @param string name * @param string label * @param string description */ public static function addGroup(collection $collection, $name, $label, $desc = "", $hidden = false) { $group = new group($collection); $group->setName($name); $group->setLabel($label); $group->setDesc($desc); if ($hidden) { $group->setHidden(); } static::$groups[$name]=$group; return $group; } /** * Return warnings generated while loading configuration */ public static function getWarnings() { return static::$warnings; } } zoph-v0.9.19/php/classes/conf/confDefault.inc.php000066400000000000000000001237131415176210700216570ustar00rootroot00000000000000setName("zoph.version"); $zophVersion->setLabel("Zoph Version"); $zophVersion->setDesc("The current version of Zoph. You should not need to change this. " . "Changing this may lead to unexpected behaviour."); $zophVersion->setDefault("v00.09.19.000"); $zophVersion->setRegex("^v([[0-9]{2}\.){3}[0-9]{3}"); $zophVersion->setInternal(); $zoph[]=$zophVersion; conf::addGroup($zoph, "zoph", "Zoph general settings", "Settings that define the inner workings of Zoph", true); } /** * Get config collection for interface settings */ private static function getConfigInterface() { $interface = new collection(); $intTitle = new text(); $intTitle->setName("interface.title"); $intTitle->setLabel("Title"); $intTitle->setDesc("The title for the application. This is what appears " . "on the home page and in the browser's title bar."); $intTitle->setDefault("Zoph"); $intTitle->setRegex("^.*$"); $interface[]=$intTitle; $intWidth = new text(); $intWidth->setName("interface.width"); $intWidth->setLabel("Screen width"); $intWidth->setDesc("A number in pixels (\"px\") or percent (\"%\"), the latter " . "is a percentage of the user's browser window width."); $intWidth->setDefault("1000px"); $intWidth->setRegex("^[0-9]+(px|%)$"); $interface[]=$intWidth; $intTpl = new select(); $intTpl->setName("interface.template"); $intTpl->setLabel("Template"); $intTpl->setDesc("The template Zoph uses"); $intTpl->addOptions(template::getAll()); $intTpl->setDefault("default"); $interface[]=$intTpl; $intAutoc = new checkbox(); $intAutoc->setName("interface.autocomplete"); $intAutoc->setLabel("Autocomplete"); $intAutoc->setDesc("Use autocompletion for selection of albums, categories, " . "places and people instead of standard HTML selectboxes. Can be individually " . "switched off from user preferences."); $intAutoc->setDefault(true); $interface[]=$intAutoc; $intLang = new select(); $intLang->setName("interface.language"); $intLang->setLabel("Default language"); $intLang->setDesc("Set the language used when neither the user or the browser " . "specifies a preference"); $langs=language::getAll(); foreach ($langs as $iso => $lang) { $intLang->addOption($iso, $lang->name); } $intLang->setDefault("en"); $interface[]=$intLang; $intMaxDays = new number(); $intMaxDays->setName("interface.max.days"); $intMaxDays->setLabel("Maximum days"); $intMaxDays->setDesc("The maximum days Zoph displays in a dropdown box for 'photos " . "changed / made in the past ... days' on the search screen"); $intMaxDays->setDefault("30"); $intMaxDays->setRegex("^[1-9][0-9]{0,2}$"); $intMaxDays->setBounds(0, 365, 1); $interface[]=$intMaxDays; $intSortOrder = new select(); $intSortOrder->setName("interface.sort.order"); $intSortOrder->setLabel("Default sort order"); $intSortOrder->setDesc("Default sort order of photos"); $intSortOrder->addOptions(photo::getFields()); $intSortOrder->setDefault("date"); $interface[]=$intSortOrder; $intSortDir = new select(); $intSortDir->setName("interface.sort.dir"); $intSortDir->setLabel("Default sort direction"); $intSortDir->setDesc("Default sort order of photos, ascending or descending"); $intSortDir->addOption("asc", "Ascending"); $intSortDir->addOption("desc", "Descending"); $intSortDir->setDefault("asc"); $interface[]=$intSortDir; $intLogonBgAlbum = new select(); $intLogonBgAlbum->setName("interface.logon.background.album"); $intLogonBgAlbum->setLabel("Logon screen background album"); $intLogonBgAlbum->setDesc("Select an album from which a random photo is chosen as a " . "background for the logon screen"); $intLogonBgAlbum->addOptions(album::getSelectArray()); $intLogonBgAlbum->setOptionsTranslate(false); $intLogonBgAlbum->setDefault(null); $intLogonBgAlbum->requiresEnabled(new checkbox("share.enable")); $interface[]=$intLogonBgAlbum; $intCookieExpire = new select(); $intCookieExpire->setName("interface.cookie.expire"); $intCookieExpire->setLabel("Cookie Expiry Time"); $intCookieExpire->setDesc("Set the time after which a cookie will expire, that is, " . "when a user will need to re-login. \"session\" (default) means: until user " . "closes the browser"); $intCookieExpire->addOptions(array( 0 => "session", 3600 => "1 hour", 14400 => "4 hours", 28800 => "8 hours", 86400 => "1 day", 604800 => "1 week", 2592300 => "1 month" )); $intCookieExpire->setDefault(0); $interface[]=$intCookieExpire; $users=user::getAll(); $intUserDefault = new select(); $intUserDefault->setName("interface.user.default"); $intUserDefault->setLabel("Default user"); $intUserDefault->setDesc("Automatically log on as this user when not logged " . "on. Can be used to give people access without a username and password. " . "This user should be a non-admin user and should not have any change " . "permissions."); $intUserDefault->addOption(0, "Disabled"); foreach ($users as $usr) { if (!$usr->isAdmin()) { $intUserDefault->addOption($usr->getId(), $usr->getName()); } } $intUserDefault->setDefault(0); $interface[]=$intUserDefault; $intUserCli = new select(); $intUserCli->setName("interface.user.cli"); $intUserCli->setLabel("CLI user"); $intUserCli->setDesc("This is the Zoph user that is used when using the CLI " . "interface when interacting with Zoph. This user must be an admin user. " . "You can also set it to \"autodetect\", which means Zoph will lookup the " . "name of the Unix user starting the CLI client and tries to find that user's " . "name in the Zoph database."); $intUserCli->addOption("0", "Autodetect"); foreach ($users as $usr) { if ($usr->isAdmin()) { $intUserCli->addOption((string)$usr->getId(), $usr->getName()); } } $intUserCli->setDefault("1"); $interface[]=$intUserCli; $intUserRemote = new checkbox(); $intUserRemote->setName("interface.user.remote"); $intUserRemote->setLabel("Enable REMOTE_USER authentication"); $intUserRemote->setDesc("Authenticate using an external authenticator. " . "This could be used for a single sign-on or other external authentication " . "mechanism. This relies on the external authenticator to insert a \$_SERVER " . "variable 'REMOTE_USER'. If this is not present, Zoph will fall back to it's " . "own logon screen"); $intUserRemote->setDefault(false); $interface[]=$intUserRemote; conf::addGroup($interface, "interface", "Interface settings", "Settings that define how Zoph looks"); } /** * Get config collection for Path settings */ private static function getConfigPath() { $path = new collection(); $pathImages = new text(); $pathImages->setName("path.images"); $pathImages->setLabel("Images directory"); $pathImages->setDesc("Location of the images on the filesystem. Absolute path, " . "thus starting with a /"); $pathImages->setDefault("/data/images"); $pathImages->setRegex("^\/[A-Za-z0-9_.\/\-]+$"); $pathImages->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), and underscore (_). Must start with a /"); $pathImages->setRequired(); $path[]=$pathImages; $pathUpload = new text(); $pathUpload->setName("path.upload"); $pathUpload->setLabel("Upload dir"); $pathUpload->setDesc("Directory where uploaded files are stored and from where " . "files are imported in Zoph. This is a directory under the images directory " . "(above). For example, if the images directory is set to /data/images and " . "this is set to upload, photos will be uploaded to /data/images/upload."); $pathUpload->setDefault("upload"); $pathUpload->setRegex("^[A-Za-z0-9_]+[A-Za-z0-9_.\/]*$"); $pathUpload->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), and underscore (_). Can not start with a dot or a slash"); $path[]=$pathUpload; $pathTrash = new text(); $pathTrash->setName("path.trash"); $pathTrash->setLabel("Trash dir"); $pathTrash->setDesc("Directory where photos are moved when they are " . "deleted. If left blank, files will remain where they were. This is a directory " . "under the images directory (above). For example, if the images directory is set to " . "/data/images and this is set to trash, photos will be moved to /data/images/trash."); $pathTrash->setDefault(""); $pathTrash->setRegex("^[A-Za-z0-9_]*[A-Za-z0-9_.\/]*$"); $pathTrash->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), and underscore (_). Can not start with a dot or a slash"); $path[]=$pathTrash; $pathMagic = new text(); $pathMagic->setName("path.magic"); $pathMagic->setLabel("Magic file"); $pathMagic->setDesc("Zoph needs a MIME Magic file to be able to determine the " . "filetype of an uploaded file. This is an important security measure, since " . "it prevents users from uploading files other than images and archives. If " . "left empty, PHP will use the built-in Magic file, if for some reason this " . "does not work, you can specify the location of the MIME magic file. Where " . "this file is located, depends on your distribution, " . "/usr/share/misc/magic.mgc, /usr/share/misc/file/magic.mgc, " . "/usr/share/file/magic are often used."); $pathMagic->setDefault(""); $pathMagic->setRegex("^\/[A-Za-z0-9_.\/]+$"); $pathMagic->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), and underscore (_). Must start with a /. Can be " . "empty for PHP builtin magic file."); $path[]=$pathMagic; $pathUnzip = new text(); $pathUnzip->setName("path.unzip"); $pathUnzip->setLabel("Unzip command"); $pathUnzip->setDesc("The command to use to unzip zip files. Leave empty to " . "disable uploading .zip files. On most systems \"unzip\" will work."); $pathUnzip->setDefault(""); $pathUnzip->setRegex("^([A-Za-z0-9_.\/ -]+|)$"); $pathUnzip->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), underscore (_), dash (-) and space. Can be empty to disable"); $path[]=$pathUnzip; $pathUntar = new text(); $pathUntar->setName("path.untar"); $pathUntar->setLabel("Untar command"); $pathUntar->setDesc("The command to use to untar tar files. Leave empty to disable " . "uploading .tar files. On most systems \"tar xvf\" will work."); $pathUntar->setDefault(""); $pathUntar->setRegex("^([A-Za-z0-9_.\/ ]+|)$"); $pathUntar->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), underscore (_), dash (-) and space. Can be empty to disable"); $path[]=$pathUntar; $pathUngz = new text(); $pathUngz->setName("path.ungz"); $pathUngz->setLabel("Ungzip command"); $pathUngz->setDesc("The command to use to unzip gzip files. Leave empty to disable " . "uploading .gz files. On most systems \"gunzip\" will work."); $pathUngz->setDefault(""); $pathUngz->setRegex("^([A-Za-z0-9_.\/ ]+|)$"); $pathUngz->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), underscore (_), dash (-) and space. Can be empty to disable"); $path[]=$pathUngz; $pathUnbz = new text(); $pathUnbz->setName("path.unbz"); $pathUnbz->setLabel("Unbzip command"); $pathUnbz->setDesc("The command to use to unzip bzip files. Leave empty to disable " . "uploading .bz files. On most systems \"bunzip2\" will work."); $pathUnbz->setDefault(""); $pathUnbz->setRegex("^([A-Za-z0-9_.\/ ]+|)$"); $pathUnbz->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward " . "slash (/), dot (.), underscore (_), dash (-) and space. Can be empty to disable"); $path[]=$pathUnbz; conf::addGroup($path, "path", "Paths", "File and directory locations"); } /** * Get config collection for maps settings */ private static function getConfigMaps() { $maps = new collection(); $mapsProvider = new select(); $mapsProvider->setName("maps.provider"); $mapsProvider->setDesc("Enable or disable mapping support and choose the " . "mapping provider"); $mapsProvider->setLabel("Mapping provider"); $mapsProvider->addOption("", "Disabled"); $mapsProvider->addOption("mapbox", "Mapbox (OpenStreetMap)"); $mapsProvider->addOption("osm", "OpenStreetMap"); $mapsProvider->setDefault(""); $maps[]=$mapsProvider; $mapsMapBoxAPIKey = new text(); $mapsMapBoxAPIKey->setName("maps.mapbox.apikey"); $mapsMapBoxAPIKey->setLabel("Mapbox API key"); $mapsMapBoxAPIKey->setDesc("API key to use to access MapBox. The default is Zoph's API key, please do not use it in other projects. If you are setting up a high-volume site, please consider requesting your own key"); $mapsMapBoxAPIKey->setDefault("pk.eyJ1IjoiamVyb2Vucm5sIiwiYSI6ImNpdmh6dnlsazAwYWUydXBrbG50cHhlbmMifQ.0pSkJxO6ycD2Wg5GL4yYyw"); $mapsMapBoxAPIKey->setRegex("^[0-9a-zA-Z\.]+$"); $maps[]=$mapsMapBoxAPIKey; $mapsGeocode = new select(); $mapsGeocode->setName("maps.geocode"); $mapsGeocode->setLabel("Geocode provider"); $mapsGeocode->setDesc("With geocoding you can lookup the location of a " . "place from it's name. Here you can select the provider. Currently " . "the only one available is 'geonames'"); $mapsGeocode->addOption("", "Disabled"); $mapsGeocode->addOption("geonames", "GeoNames"); $mapsGeocode->setDefault(""); $maps[]=$mapsGeocode; conf::addGroup($maps, "maps", "Mapping support", "Add maps to Zoph using various different mapping providers."); } /** * Get config collection for import settings */ private static function getConfigImport() { $import = new collection(); $importEnable = new checkbox(); $importEnable->setName("import.enable"); $importEnable->setLabel("Import through webinterface"); $importEnable->setDesc("Use this option to enable or disable importing using " . "the webbrowser. With this option enabled, an admin user, or a user with " . "import rights, can import files placed in the import directory (below) " . "into Zoph. If you want users to be able to upload as well, you need to " . "enable uploading as well."); $importEnable->setDefault(false); $import[]=$importEnable; $importUpload = new checkbox(); $importUpload->setName("import.upload"); $importUpload->setLabel("Upload through webinterface"); $importUpload->setDesc("Use this option to enable or disable uploading files. " . "With this option enabled, an admin user, or a user with import rights, " . "can upload files to the server running Zoph, they will be placed in the " . "import directory (below). This option requires \"import through web " . "interface\" (above) enabled."); $importUpload->setDefault(false); $import[]=$importUpload; $importMaxupload = new number(); $importMaxupload->setName("import.maxupload"); $importMaxupload->setLabel("Maximum filesize"); $importMaxupload->setDesc("Maximum size of uploaded file in bytes. You might " . "also need to change upload_max_filesize, post_max_size and possibly" . "max_execution_time and max_input_time in php.ini."); $importMaxupload->setRegex("^[0-9]+$"); $importMaxupload->setDefault("10000000"); $importMaxupload->setBounds(0, 1000000000, 1); // max = 1GB $import[]=$importMaxupload; $importParallel = new number(); $importParallel->setName("import.parallel"); $importParallel->setLabel("Resize parallel"); $importParallel->setDesc("Photos will be resized to thumbnail and midsize " . "images during import, this setting determines how many resize actions run " . "in parallel. Can be set to any number. If you have a fast server with " . "multiple CPU's or cores, you can increase this for faster response on " . "the import page."); $importParallel->setRegex("^[0-9]+$"); $importParallel->setBounds(1, 99, 1); $importParallel->setDefault("1"); $import[]=$importParallel; $importRotate = new checkbox(); $importRotate->setName("import.rotate"); $importRotate->setLabel("Rotate images"); $importRotate->setDesc("Automatically rotate imported images, requires jhead"); $importRotate->setDefault(false); $import[]=$importRotate; $importResize = new select(); $importResize->setName("import.resize"); $importResize->setLabel("Resize method"); $importResize->setDesc("Determines how to resize an image during import. " . "Resize can be about 3 times faster than resample, but the resized image " . "has a lower quality."); $importResize->addOption("resize", "Resize (lower quality / low CPU / fast)"); $importResize->addOption("resample", "Resample (high quality / high CPU / slow)"); $importResize->setDefault("resample"); $import[]=$importResize; $importDated = new checkbox(); $importDated->setName("import.dated"); $importDated->setLabel("Dated dirs"); $importDated->setDesc("Automatically place photos in dated dirs " . "(\"2012.10.16/\") during import"); $importDated->setDefault(false); $import[]=$importDated; $importDatedHier = new checkbox(); $importDatedHier->setName("import.dated.hier"); $importDatedHier->setLabel("Hierarchical dated dirs"); $importDatedHier->setDesc("Automatically place photos in a dated directory " . "tree (\"2012/10/16/\") during import. Ignored unless \"Dated dirs\" is " . "also enabled"); $importDatedHier->setDefault(false); $import[]=$importDatedHier; /** * @todo This requires octdec to be run before using it so use * octdec(conf::get("import.filemode")) or you will get "funny" results */ $importFilemode = new select(); $importFilemode->setName("import.filemode"); $importFilemode->setLabel("File mode"); $importFilemode->setDesc("File mode for the files that are imported in Zoph. " . "Determines who can read or write the files. (RW: Read/Write, RO: Read Only)"); $importFilemode->addOptions(array( "0644" => "RW for user, RO for others (0644)", "0664" => "RW for user/collection, RO for others (0664)", "0666" => "RW for everyone (0666)", "0660" => "RW for user/collection, not readable for others (0660)", "0640" => "RW for user, RO for collection, not readable for others (0640)", "0600" => "RW for user, not readable for others (0600)" )); $importFilemode->setDefault("0644"); $import[]=$importFilemode; /** * @todo This requires octdec to be run before using it so use * octdec(conf::get("import.dirmode")) or you will get "funny" results */ $importDirmode = new select(); $importDirmode->setName("import.dirmode"); $importDirmode->setLabel("dir mode"); $importDirmode->setDesc("Mode for directories that are created by Zoph. " . "Determines who can read or write the files. (RW: Read/Write, RO: Read Only)"); $importDirmode->addOptions(array( "0755" => "RW for user, RO for others (0755)", "0775" => "RW for user/collection, RO for others (0775)", "0777" => "RW for everyone (0777)", "0770" => "RW for user/collection, not readable for others (0770)", "0750" => "RW for user, RO for collection, not readable for others (0750)", "0700" => "RW for user, not readable for others (0700)" )); $importDirmode->setDefault("0755"); $import[]=$importDirmode; $importCliVerbose=new number(); $importCliVerbose->setName("import.cli.verbose"); $importCliVerbose->setLabel("CLI verbose"); $importCliVerbose->setDesc("Set CLI verbosity, can be overriden with --verbose"); $importCliVerbose->setDefault("0"); $importCliVerbose->setBounds(1,99,1); $importCliVerbose->setInternal(); $import[]=$importCliVerbose; $importCliThumbs=new checkbox(); $importCliThumbs->setName("import.cli.thumbs"); $importCliThumbs->setLabel("CLI: generate thumbnails"); $importCliThumbs->setDesc("Generate thumbnails when importing via CLI. Can be " . "overridden with --thumbs (-t) and --no-thumbs (-n)."); $importCliThumbs->setDefault(true); $import[]=$importCliThumbs; $importCliExif=new checkbox(); $importCliExif->setName("import.cli.exif"); $importCliExif->setLabel("CLI: read EXIF data"); $importCliExif->setDesc("Read EXIF data when importing via CLI. The default " . "behaviour can be overridden with --exif and --no-exif."); $importCliExif->setDefault(true); $import[]=$importCliExif; $importCliSize=new checkbox(); $importCliSize->setName("import.cli.size"); $importCliSize->setLabel("CLI: size of image"); $importCliSize->setDesc("Update image dimensions in database when importing " . "via CLI. The default behaviour can be overridden with --size and --no-size."); $importCliSize->setDefault(true); $import[]=$importCliSize; $importCliHash=new checkbox(); $importCliHash->setName("import.cli.hash"); $importCliHash->setLabel("CLI: calculate hash"); $importCliHash->setDesc("Calculate a hash when importing or updating a photo " . "using the CLI. Can be overridden with --hash and --no-hash."); $importCliHash->setDefault(true); $import[]=$importCliHash; $importCliCopy=new checkbox(); $importCliCopy->setName("import.cli.copy"); $importCliCopy->setDefault(false); $importCliCopy->setLabel("CLI: copy on import"); $importCliCopy->setDesc("Make a copy of a photo that is imported using the " . "CLI. Can be overridden with --copy and --move."); $import[]=$importCliCopy; $importCliUseids=new checkbox(); $importCliUseids->setName("import.cli.useids"); $importCliUseids->setLabel("CLI: Use Ids"); $importCliUseids->setDesc("Use ids instead of filenames when referencing photos."); $importCliUseids->setDefault(false); $importCliUseids->setInternal(); $import[]=$importCliUseids; $importCliAddAuto=new checkbox(); $importCliAddAuto->setName("import.cli.add.auto"); $importCliAddAuto->setLabel("CLI: Auto add"); $importCliAddAuto->setDesc("Add non-existent albums, categories, places and " . "people, when a parent is defined."); $importCliAddAuto->setDefault(false); $importCliAddAuto->setInternal(); $import[]=$importCliAddAuto; $importCliAddAlways=new checkbox(); $importCliAddAlways->setName("import.cli.add.always"); $importCliAddAlways->setLabel("CLI: Auto add always"); $importCliAddAlways->setDesc("Add non-existent albums, categories, places " . "and people, regardsless of whether a parent is defined."); $importCliAddAlways->setDefault(false); $importCliAddAlways->setInternal(); $import[]=$importCliAddAlways; $importCliRecursive=new checkbox(); $importCliRecursive->setName("import.cli.recursive"); $importCliRecursive->setLabel("CLI: Recursive"); $importCliRecursive->setDesc("Recursively import directories when importing " . "using the CLI."); $importCliRecursive->setDefault(false); $importCliRecursive->setInternal(); $import[]=$importCliRecursive; conf::addGroup($import, "import", "Import", "Importing and uploading photos"); } /** * Get config collection for watermark settings */ private static function getConfigWatermark() { $watermark = new collection(); $watermarkEnable = new checkbox(); $watermarkEnable->setName("watermark.enable"); $watermarkEnable->setLabel("Enable Watermarking"); $watermarkEnable->setDesc("Watermarking only works if the watermark file below is set " . "to an existing GIF image. Please note that enabling this function uses a " . "rather large amount of memory on the webserver. PHP by default allows a " . "script to use a maximum of 8MB memory. You should probably increase this " . "by changing memory_limit in php.ini. A rough estimation of how much memory " . "it will use is 6 times the number of megapixels in your camera. For " . "example, if you have a 5 megapixel camera, change memory_limit in php.ini to 30M"); $watermarkEnable->setDefault(false); $watermark[]=$watermarkEnable; /** @todo: should allow .png too */ $watermarkFile = new text(); $watermarkFile->setName("watermark.file"); $watermarkFile->setLabel("Watermark file"); $watermarkFile->setDesc("If watermarking is used, this should be set to the name of the " . "file that will be used as the watermark. It should be a GIF file, for best " . "results, use contrasting colours and transparency. In the Contrib directory, " . "3 example files are included. The filename is relative to the image directory, " . "defined above."); $watermarkFile->setDefault(""); $watermarkFile->setRegex("(^$|^[A-Za-z0-9_]+[A-Za-z0-9_.\/]*\.gif$)"); $watermarkFile->setHint("Alphanumeric characters (A-Z, a-z and 0-9), forward slash (/), " . "dot (.), and underscore (_). Can not start with a dot or a slash"); $watermark[]=$watermarkFile; $watermarkPosX = new select(); $watermarkPosX->setName("watermark.pos.x"); $watermarkPosX->setLabel("Horizontal position"); $watermarkPosX->setDesc("Define where the watermark will be placed horizontally."); $watermarkPosX->addOptions(array( "left" => "Left", "center" => "Center", "right" => "Right" )); $watermarkPosX->setDefault("center"); $watermark[]=$watermarkPosX; $watermarkPosY = new select(); $watermarkPosY->setName("watermark.pos.y"); $watermarkPosY->setLabel("Vertical position"); $watermarkPosY->setDesc("Define where the watermark will be placed vertically."); $watermarkPosY->addOptions(array( "top" => "Top", "center" => "Center", "bottom" => "Bottom" )); $watermarkPosY->setDefault("center"); $watermark[]=$watermarkPosY; $watermarkTrans = new number(); $watermarkTrans->setName("watermark.transparency"); $watermarkTrans->setLabel("Watermark transparency"); $watermarkTrans->setDesc("Define the transparency of a watermark. 0: fully " . "transparent (invisible, don't use this, it's pointless and eats " . "up a lot of resources, better turn off the watermark feature " . "altogether) to 100: no transparency."); $watermarkTrans->setDefault("50"); $watermarkTrans->setRegex("^(100|[0-9]{1,2})$"); $watermarkTrans->setBounds(0, 100, 1); $watermark[]=$watermarkTrans; conf::addGroup($watermark, "watermark", "Watermarking", "Watermarking can display a (copyright) watermark over your full-size images."); } /** * Get config collection for rotation settings */ private static function getConfigRotate() { $rotate = new collection(); $rotateEnable = new checkbox(); $rotateEnable->setName("rotate.enable"); $rotateEnable->setLabel("Rotation"); $rotateEnable->setDesc("Allow users (admins or with write access) to rotate images"); $rotateEnable->setDefault(false); $rotate[]=$rotateEnable; $rotateCommand = new select(); $rotateCommand->setName("rotate.command"); $rotateCommand->setLabel("Rotate command"); $rotateCommand->setDesc("Determine which command is used to rotate the image. " . "This command must be available on your system. Convert is a lossy " . "rotate function, which means it will lower the image quality of your " . "photo. JPEGtran, on the other hand, only works on JPEG images, but " . "is lossless."); $rotateCommand->addOptions(array( "convert" => "convert", "jpegtran" => "jpegtran" )); $rotateCommand->setDefault("convert"); $rotate[]=$rotateCommand; $rotateBackup = new checkbox(); $rotateBackup->setName("rotate.backup"); $rotateBackup->setLabel("Backup"); $rotateBackup->setDesc("Keep a backup image when rotating an image."); $rotateBackup->setDefault(true); $rotate[]=$rotateBackup; $rotateBackupPrefix = new text(); $rotateBackupPrefix->setName("rotate.backup.prefix"); $rotateBackupPrefix->setLabel("Backup prefix"); $rotateBackupPrefix->setDesc("Prepend backup file for rotation backups with this."); $rotateBackupPrefix->setDefault("orig_"); $rotateBackupPrefix->setRegex("^[a-zA-Z0-9_\-]+$"); $rotateBackupPrefix->setRequired(); $rotate[]=$rotateBackupPrefix; conf::addGroup($rotate, "rotate", "Rotation", "Rotate images"); } /** * Get config collection for share settings */ private static function getConfigShare() { $share = new collection(); $shareURLoverride = new text(); $shareURLoverride->setName("share.url.override"); $shareURLoverride->setLabel("URL override"); $shareURLoverride->setDesc("In most cases, Zoph can determine the URL it is " . "reachable on. If the URL it automatically determines is incorrect, " . "you can override it here. Specify a full URL, including http or https."); $shareURLoverride->setRegex("^$|^http(s?):\/\/[a-zA-Z0-9\.\/~]+$"); $shareURLoverride->setDefault(""); $share[]=$shareURLoverride; $shareEnable = new checkbox(); $shareEnable->setName("share.enable"); $shareEnable->setLabel("Sharing"); $shareEnable->setDesc("Sometimes, you may wish to share an image in Zoph " . "without creating a user account for those who will be watching them. " . "For example, in order to post a link to an image on a forum or website. " . "When this option is enabled, you will see a 'share' tab next to a photo, " . "where you will find a few ways to share a photo, such as a url and a " . "HTML <img> tag. With this special url, it is possible to open a " . "photo without logging in to Zoph. You can determine per user whether " . "or not this user will see the tab and therefore the urls."); $shareEnable->setDefault(false); $share[]=$shareEnable; $shareSaltFull = new salt(); $shareSaltFull->setName("share.salt.full"); $shareSaltFull->setLabel("Salt for sharing full size images"); $shareSaltFull->setDesc("When using the sharing feature, Zoph uses a hash " . "to identify a photo. Because you do not want people who have access to " . "you full size photos (via Zoph or otherwise) to be able to generate " . "these hashes, you should give Zoph a secret salt so only authorized " . "users of your Zoph installation can generate them. The salt for full " . "size images (this one) must be different from the salt of mid size " . "images (below), because this allows Zoph to distinguish between them. " . "If a link to your Zoph installation is being abused (for example " . "because someone whom you mailed a link has published it on a forum), " . "you can modify the salt to make all hash-based links to your Zoph invalid."); $shareSaltFull->setDefault("Change this"); $shareSaltFull->setRequired(); $share[]=$shareSaltFull; $shareSaltMid = new salt(); $shareSaltMid->setName("share.salt.mid"); $shareSaltMid->setLabel("Salt for sharing mid size images"); $shareSaltMid->setDesc("The salt for mid size images (this one) must be " . "different from the salt of full images (above), because this allows " . "Zoph to distinguish between them. If a link to your Zoph installation " . "is being abused (for example because someone whom you mailed a link " . "has published it on a forum), you can modify the salt to make all " . "hash-based links to your Zoph invalid."); $shareSaltMid->setDefault("Modify this"); $shareSaltMid->setRequired(); $share[]=$shareSaltMid; conf::addGroup($share, "share", "Sharing", "Sharing photos with non-logged on users"); } /** * Get config collection for feature settings */ private static function getConfigFeature() { $feature = new collection(); $featureDownload = new checkbox(); $featureDownload->setName("feature.download"); $featureDownload->setLabel("Downloading"); $featureDownload->setDesc("With this feature you can use download a set of " . "photos (Albums, Categories, Places, People or a search result) in " . "one or more ZIP files. Important! The photos in the ZIP file will " . "NOT be watermarked. You must also grant each non-admin user you " . "want to give these rights permission by changing \"can download " . "zipfiles\" in the user's profile."); $featureDownload->setDefault(false); $feature[]=$featureDownload; $featureComments = new checkbox(); $featureComments->setName("feature.comments"); $featureComments->setLabel("Comments"); $featureComments->setDesc("Enable comments. Before a user can actually leave " . "comments, you should also give the user these rights through the edit " . "user screen."); $featureComments->setDefault(false); $feature[]=$featureComments; $featureMail = new checkbox(); $featureMail->setName("feature.mail"); $featureMail->setLabel("Mail photos"); $featureMail->setDesc("You can enable or disable the \"mail this photo feature\" " . "using this option. Since Zoph needs to convert the photo into Base64 " . "encoding for mail, it requires quite a large amount of memory if you " . "try to send full size images and you may need to adjust memory_limit " . "in php.ini, you should give it at least about 4 times the size of your " . "largest image."); $featureMail->setDefault(false); $feature[]=$featureMail; $featureMailBcc = new text(); $featureMailBcc->setName("feature.mail.bcc"); $featureMailBcc->setLabel("BCC address"); $featureMailBcc->setDesc("Automatically Blind Carbon Copy this mailaddress when " . "a mail from Zoph is sent"); $featureMailBcc->setDefault(""); // not sure how long the "new" TLD's are going to be, // 10 should be enough for most, feel free to report // a bug if your TLD is longer. $featureMailBcc->setRegex("^([0-9a-zA-Z_\-%\.]+@([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,10})?$"); $feature[]=$featureMailBcc; $featureRating = new checkbox(); $featureRating->setName("feature.rating"); $featureRating->setLabel("Photo rating"); $featureRating->setDesc("Allow users to rate photos. Before a non-admin user can " . "actually rate, you should also give the user these rights through the " . "edit user screen."); $featureRating->setDefault(true); $feature[]=$featureRating; conf::addGroup($feature, "feature", "Features", "Various features"); } /** * Get config collection for date settings */ private static function getConfigDate() { $date = new collection(); $dateTz = new select(); $dateTz->setName("date.tz"); $dateTz->setLabel("Timezone"); $dateTz->setDesc("This setting determines the timezone to which your camera " . "is set. Leave empty if you do not want to use this feature and always set " . "your camera to the local timezone"); $dateTz->addOptions(TimeZone::getTzArray()); $dateTz->setDefault(""); $date[]=$dateTz; $dateGuesstz = new checkbox(); $dateGuesstz->setName("date.guesstz"); $dateGuesstz->setLabel("Guess timezone"); $dateGuesstz->setDesc("If you have defined the precise location of a place " . "(using the mapping feature), Zoph can 'guess' the timezone based on this " . "location. It uses the Geonames project for this. This will, however, send " . "information to their webserver, do not enable this feature if you're not " . "comfortable with that."); $dateGuesstz->setDefault(false); $date[]=$dateGuesstz; $dateFormat = new text(); $dateFormat->setName("date.format"); $dateFormat->setLabel("Date format"); $dateFormat->setDesc("This determines how Zoph displays dates. You can use the " . "following characters: dDjlNSwzWFmMntLoYy (for explanation, see " . "http://php.net/manual/en/function.date.php) and /, space, -, (, ), :, \",\" and ."); $dateFormat->setDefault("d-m-Y"); $dateFormat->setRegex("^[dDjlNSwzWFmMntLoYy\/ \-():,.]+$"); $dateFormat->setRequired(); $date[]=$dateFormat; $dateTimeFormat = new text(); $dateTimeFormat->setName("date.timeformat"); $dateTimeFormat->setLabel("Time format"); $dateTimeFormat->setDesc("This determines how Zoph displays times. You can use the " . "following characters: aABgGhHisueIOPTZcrU (for explanation, see " . "http://php.net/manual/en/function.date.php) and /, space, -, (, ), :, \",\" and ."); $dateTimeFormat->setDefault("H:i:s T"); $dateTimeFormat->setRegex("^[aABgGhHisueIOPTZcrU\/ \-():,.]+$"); $dateTimeFormat->setRequired(); $date[]=$dateTimeFormat; conf::addGroup($date, "date", "Date and time", "Date and time related settings"); } } zoph-v0.9.19/php/classes/conf/group.inc.php000066400000000000000000000054521415176210700205600ustar00rootroot00000000000000collection=$collection; } /** * Set the name of the group * @param string Name */ public function setName($name) { $this->name=$name; } /** * Set the description of the group * @param string Description */ public function setDesc($desc) { $this->desc=$desc; } /** * Set the label of the group * @param string Label */ public function setLabel($label) { $this->label=$label; } /** * Get name * @return string Name */ public function getName() { return $this->name; } /** * Get description * @return string Description */ public function getDesc() { return $this->desc; } /** * Get label * @return string Label */ public function getLabel() { return $this->label; } public function getItems() { return $this->collection; } /** * Set this group to be hidden */ public function setHidden() { $this->hidden=true; } /** * Display group * @return block template block */ public function display() { if (!$this->hidden) { return new block("confGroup", array( "title" => translate($this->getLabel(), 0), "desc" => translate($this->getDesc(), 0), "items" => $this->getItems() )); } } } zoph-v0.9.19/php/classes/conf/item/000077500000000000000000000000001415176210700170735ustar00rootroot00000000000000zoph-v0.9.19/php/classes/conf/item/checkbox.inc.php000066400000000000000000000044271415176210700221510ustar00rootroot00000000000000getValue() ? "true": "false"); } /** * Display this option through template * @return block template block */ public function display() { if ($this->internal) { return; } $tpl=new block("confItemCheckbox", array( "label" => e(translate($this->getLabel(),0)), "name" => e($this->getName()), "checked" => $this->getValue() ? "checked" : "", "desc" => e(translate($this->getDesc(),0)), "hint" => e(translate($this->getHint(),0)), )); return $tpl; } } zoph-v0.9.19/php/classes/conf/item/item.inc.php000066400000000000000000000202611415176210700213130ustar00rootroot00000000000000set("conf_id", $id); } else { log::msg("Illegal configuration id", log::FATAL, log::VARS); } } /** * Update or insert configuration item * checks if item already exists in db * and updates it if it does or inserts * if it does not. */ final public function update() { if ($this->checkValue($this->get("value"))) { $qry=new select(array("co" => "conf")); $qry->addFunction(array("count" => "COUNT(conf_id)")); $qry->where(new clause("conf_id=:confid")); $qry->addParam(new param(":confid", $this->fields["conf_id"], PDO::PARAM_STR)); if ($qry->getCount() > 0) { parent::update(); } else { parent::insert(); } } } /** * Get name of item * @return string name */ final public function getName() { return $this->fields["conf_id"]; } /** * Get label for item * @return string label */ final public function getLabel() { return $this->label; } /** * Get description for item * @return string description */ final public function getDesc() { return $this->desc; } /** * Get value of item * if value is not set, get default * @return string value */ final public function getValue() { if (!isset($this->fields["value"]) || $this->fields["value"]===null || !$this->requirementsMet()) { return $this->getDefault(); } else { return $this->fields["value"]; } } /** * Get value of item for display * @return string value */ public function displayValue() { return $this->getValue(); } /** * Set value of item * @param string value * @throws configurationException */ public function setValue($value) { if ($this->checkValue($value)) { $this->fields["value"]=$value; } else { throw new \configurationException("Configuration value for " . $this->getName() . " is illegal"); } } /** * Get default value of item * @return string default value */ final public function getDefault() { return $this->default; } /** * Get hint for item * @return string hint */ final public function getHint() { return $this->hint; } /** * Set name (id) of item * @param string name */ final public function setName($name) { $this->fields["conf_id"]=$name; } /** * Set label for item * @param string label */ final public function setLabel($label) { $this->label=$label; } /** * Set label for item * @param string label */ final public function setDesc($desc) { $this->desc=$desc; } /** * Set hint for item * @param string hint */ final public function setHint($hint) { $this->hint=$hint; } /** * Set whether or not a field is required * @param bool */ final public function setRequired($req=true) { $this->required=(bool) $req; } /** * Set whether or not a field is deprecated * @param bool */ final public function setDeprecated($dep=true) { $this->deprecated=(bool) $dep; } /** * Get whether or not a field is deprecated * @param bool */ final public function isDeprecated() { return (bool) $this->deprecated; } /** * Set whether or not a field is internal * an internal field is not exposed in the webinterface * and (at this moment) not stored in the database, although this is not enforced * as there may be a future use-case where this will change. * @param bool */ final public function setInternal($int=true) { $this->internal=(bool) $int; } /** * Set default value for item * @param string default */ final public function setDefault($default) { $this->default=$default; } /** * This item requires another item to be enabled * @param checkbox configuration item checkbox that must be enabled to use this parameter */ final public function requiresEnabled(checkbox $item) { $this->requiresEnabled[]=$item; } /** * Are all requirements met? */ final protected function requirementsMet() { $met=true; foreach ($this->requiresEnabled as $req) { $req->lookup(); if ((bool) $req->getValue() === false) { $met=false; $this->unmet[$req->getName()]="enabled"; } } return $met; } /** * Return a template block to show the item is deprecated * @return block deprecation warning */ final protected function displayDeprecationWarning() { if ($this->isDeprecated()) { return new block("confDeprecated"); } } /** * Return a template block to show the unmet requirements for this * confItem. * @return block overview of unmet items */ final protected function displayUnmetRequirements() { if (!$this->requirementsMet()) { return new block("confUnmetRequirements", array( "unmet" => $this->unmet )); } } /** * Display the item */ abstract public function display(); /** * Check whether value is legal * @param string value */ abstract public function checkValue($value); } zoph-v0.9.19/php/classes/conf/item/number.inc.php000066400000000000000000000047501415176210700216520ustar00rootroot00000000000000internal) { return; } $tpl=new block("confItemNumber", array( "label" => e(translate($this->getLabel(),0)), "name" => e($this->getName()), "value" => e($this->getValue()), "desc" => e(translate($this->getDesc(),0)), "hint" => e(translate($this->getHint(),0)), "regex" => e($this->regex), "size" => (int) $this->size, "min" => (float) $this->min, "max" => (float) $this->max, "step" => (float) $this->step, "req" => ($this->required ? "required" : "") )); return $tpl; } public function checkValue($value) { if ($this->required && $value=="") { return false; } if ((isset($this->min) && ($value < $this->min)) || (isset($this->max) && ($value > $this->max)) || (isset($this->step) && ($value % $this->step !== 0))) { return false; } else if (isset($this->regex)) { return preg_match("/" . $this->regex ."/", $value); } else { return true; } } public function setBounds($min, $max, $step=1) { $this->min=$min; $this->max=$max; $this->step=$step; } } zoph-v0.9.19/php/classes/conf/item/salt.inc.php000066400000000000000000000034741415176210700213270ustar00rootroot00000000000000internal) { return; } $id=str_replace(".", "_", $this->getName()); $tpl=new block("confItemSalt", array( "label" => e(translate($this->getLabel(),0)), "name" => e($this->getName()), "id" => e($id), "value" => e($this->getValue()), "desc" => e(translate($this->getDesc(),0)), "hint" => e(translate($this->getHint(),0)), "regex" => e($this->regex), "size" => (int) $this->size, "req" => ($this->required ? "required" : "") )); return $tpl; } } zoph-v0.9.19/php/classes/conf/item/select.inc.php000066400000000000000000000061531415176210700216400ustar00rootroot00000000000000options[$key]=$desc; } /** * Add multiple options * @param array array of options */ public function addOptions(array $options) { foreach ($options as $key=>$desc) { $this->addOption($key, $desc); } } /** * Get array of options * @return array options */ public function getOptions() { return $this->options; } /** * Set whether or not the options must be translated * @param bool translate yes/no */ public function setOptionsTranslate($translate) { $this->translate=$translate; } /** * Check value * check if a specific value is legal for this option * @param string value * @return bool */ public function checkValue($value) { return array_key_exists($value, $this->options); } /** * Display this option through template * @return block template block */ public function display() { if ($this->internal) { return; } $params=array( "label" => e(translate($this->getLabel(), 0)), "name" => e($this->getName()), "value" => e($this->getValue()), "desc" => e(translate($this->getDesc(), 0)) . $this->displayUnmetRequirements() . $this->displayDeprecationWarning(), "hint" => e(translate($this->getHint(), 0)), "enabled" => (bool) $this->requirementsMet() ); if ($this->translate) { $params["options"] = translate($this->getOptions(), 0); } else { $params["options"] = $this->getOptions(); } $tpl=new block("confItemSelect", $params); return $tpl; } } zoph-v0.9.19/php/classes/conf/item/text.inc.php000066400000000000000000000043211415176210700213400ustar00rootroot00000000000000internal) { return; } $tpl=new block("confItemText", array( "label" => e(translate($this->getLabel(),0)), "name" => e($this->getName()), "value" => e($this->getValue()), "desc" => e(translate($this->getDesc(),0)) . $this->displayUnmetRequirements() . $this->displayDeprecationWarning(), "hint" => e(translate($this->getHint(), false)), "regex" => e($this->regex), "size" => (int) $this->size, "req" => ($this->required ? "required" : "") )); return $tpl; } public function setRegex($regex) { $this->regex=$regex; } public function checkValue($value) { if ($this->required && $value=="") { return false; } if (isset($this->regex)) { return preg_match("/" . $this->regex ."/", $value); } else { return true; } } public function setSize($size) { $this->size=(int) $size; } } zoph-v0.9.19/php/classes/db/000077500000000000000000000000001415176210700155755ustar00rootroot00000000000000zoph-v0.9.19/php/classes/db/alter.inc.php000066400000000000000000000102501415176210700201630ustar00rootroot00000000000000 ] for this query */ private $after=""; /** * Create INSERT query * @return string SQL query */ public function __toString() { $sql = "ALTER TABLE " . $this->table; if (empty($this->alteration)) { throw new dbAlterTableHasNoAlterationException(); } $sql .= " " . $this->alteration . $this->after; return $sql . ";"; } public function addColumn(column $column) { if (!empty($this->alteration)) { throw new dbAlterTableHasAlterationException(); } $this->alteration="ADD COLUMN " . (string) $column; return $this; } public function modifyColumn(column $column) { if (!empty($this->alteration)) { throw new dbAlterTableHasAlterationException(); } $this->alteration="MODIFY COLUMN " . (string) $column; return $this; } public function changeColumn(string $col, column $column) { if (!empty($this->alteration)) { throw new dbAlterTableHasAlterationException(); } $this->alteration="CHANGE COLUMN " . $col . " " . (string) $column; return $this; } public function dropColumn(string $col) { if (!empty($this->alteration)) { throw new dbAlterTableHasAlterationException(); } $this->alteration="DROP COLUMN " . $col; return $this; } public function first() { $this->after = " FIRST"; return $this; } public function after(string $col) { $this->after = " AFTER " . $col; return $this; } /** * Change MySQL/MariaDB Engine * @see @var $engines for supported engines * @param string engine * @return alter for concatenation of methods */ public function setEngine(string $engine) { if (in_array($engine, static::$engines)) { if (!empty($this->alteration)) { throw new dbAlterTableHasAlterationException(); } $this->alteration="ENGINE = \"" . $engine . "\""; } else { throw new dbAlterTableUnsupportedEngineException("Unknown database engine: " . $engine); } return $this; } } zoph-v0.9.19/php/classes/db/backup.inc.php000066400000000000000000000042261415176210700203270ustar00rootroot00000000000000host = escapeshellarg($db["host"]); $this->dbname = escapeshellarg($db["dbname"]); if ($rootpwd) { $this->user = escapeshellarg("root"); $this->pass = escapeshellarg($rootpwd); } else { $this->user = escapeshellarg($db["user"]); $this->pass = escapeshellarg($db["pass"]); } } public function execute() { $mysqldumpDescSpec = array( 1 => array("pipe", "w"), 2 => array("pipe", "w") ); $mysqldump = proc_open("mysqldump" . " -h " . $this->host . " -u " . $this->user . " -p" . $this->pass . " " . $this->dbname, $mysqldumpDescSpec, $mysqlPipes); $backup = stream_get_contents($mysqlPipes[1]); $mysqlError = stream_get_contents($mysqlPipes[2]); if (proc_close($mysqldump) !== 0) { throw new databaseBackupException("MySQL: " . $mysqlError); } return gzencode($backup, 9); } } zoph-v0.9.19/php/classes/db/clause.inc.php000066400000000000000000000066431415176210700203430ustar00rootroot00000000000000clause=$clause; } /** * Add a subclause with AND conjunction * @param clause subclause to be added */ public function addAnd(clause $clause) { $this->subclauses[]=array( "conj" => "AND", "subc" => $clause ); return $this; } /** * Add a subclause with OR conjunction * @param clause subclause to be added */ public function addOr(clause $clause) { $this->subclauses[]=array( "conj" => "OR", "subc" => $clause ); return $this; } /** * Add a subclause with NOT conjunction * @param clause subclause to be added */ public function addNot(clause $clause) { $this->subclauses[]=array( "conj" => "NOT", "subc" => $clause ); return $this; } /** * Create a WHERE ... IN (..., ..., ...) clause * @param string variable * @param param parameters * @return clause WHERE clause */ public static function InClause($var, param $param) { return new self($var . " IN (" . implode(", ", $param->getName()) . ")"); } /** * Create a WHERE ... IN (SELECT ...) clause * @param string variable * @param select subquery * @return clause WHERE clause */ public static function inSubQry($var, select $subqry) { return new self($var . " IN (" . rtrim($subqry, ";") . ")"); } /** * Create a WHERE ... NOT IN (..., ..., ...) clause * @param string variable * @param param parameters * @return clause WHERE clause */ public static function NotInClause($var, param $param) { return new self($var . " NOT IN (" . implode(", ", $param->getName()) . ")"); } /** * Build the clause * @return string clause */ public function __toString() { $sql="(" . $this->clause . ")"; if (is_array($this->subclauses)) { foreach ($this->subclauses as $subclause) { $conj=$subclause["conj"]; $subc=$subclause["subc"]; $sql.= " " . $conj . " (" . $subc . ")"; } } return $sql; } } zoph-v0.9.19/php/classes/db/column.inc.php000066400000000000000000000215441415176210700203610ustar00rootroot00000000000000name=$name; } /** * Set column to UNSIGNED */ public function unsigned() { $this->unsigned=true; return $this; } /** * Set column to AUTO_INCREMENT */ public function autoIncrement() { $this->autoIncrement=true; $this->setKey(); // Auto_increment fields must be key return $this; } /** * Set column to NOT NULL */ public function notNull() { $this->notNull=true; return $this; } /** * Is column to NOT NULL? * @return bool is NOT NULL */ public function isNotNull() { return($this->notNull); } /** * Set column as KEY */ public function setKey(string $keyname = null, int $length = 0) { $this->isKey=true; if ($keyname) { $this->keyName=$keyname; } else { $this->keyName=$this->name; } if ($length > 0) { $this->keyLength=$length; } return $this; } /** * Set column as PRIMARY KEY */ public function setPrimaryKey() { $this->primaryKey=true; return $this; } /** * Check if column is KEY * @return bool is KEY */ public function isKey() { return !$this->isPrimaryKey() && $this->isKey; } /** * Get column name * @return string name */ public function getName() { return $this->name; } /** * Get key name * @return string keyName */ public function getKeyName() { if (!$this->isKey) { throw new keyException($this->name . " is not a key"); } return $this->keyName; } /** * Get key length * @return int part of the field to use as key */ public function getKeyLength() { if (!$this->isKey) { throw new keyException($this->name . " is not a key"); } return $this->keyLength; } /** * Check if column is PRIMARY KEY * @return bool is PRIMARY KEY */ public function isPrimaryKey() { return $this->primaryKey; } /** * Set column DEFAULT value */ public function default(string $default) { $this->default = $this->addQuotes($default); return $this; } /** * Set column ON UPDATE value */ public function onUpdate(string $onUpdate) { $this->onUpdate = $this->addQuotes($onUpdate); return $this; } /** * VARCHAR type * @param size int length of field */ public function varchar(int $size) { $this->type=type::VARCHAR; $this->size=$size; return $this; } /** * CHAR type * @param size int length of field */ public function char(int $size) { $this->type=type::CHAR; $this->size=$size; return $this; } /** * TINYINT type * note that specifying the 'length' is not supported * this only works with 'ZEROFILL', which is not supported * (and it's not really the length) */ public function tinyint() { $this->type=type::TINYINT; return $this; } /** * SMALLINT type * note that specifying the 'length' is not supported * this only works with 'ZEROFILL', which is not supported * (and it's not really the length) */ public function smallint() { $this->type=type::SMALLINT; return $this; } /** * INT type * note that specifying the 'length' is not supported * this only works with 'ZEROFILL', which is not supported * (and it's not really the length) */ public function int() { $this->type=type::INT; return $this; } /** * FLOAT type * single precission float * @param int m total number of digits * @param int d number of digits after the comma * (and it's not really the length) */ public function float(int $m, int $d) { $this->type=type::FLOAT; $this->size = $m; $this->decimals = $d; return $this; } /** * TEXT type * note that specifying the length is not supported */ public function text() { $this->type=type::TEXT; return $this; } /** * BLOB type * note that specifying the length is not supported */ public function blob() { $this->type=type::BLOB; return $this; } /** * DATETIME type */ public function datetime() { $this->type=type::DATETIME; return $this; } /** * TIMESTAMP type */ public function timestamp() { $this->type=type::TIMESTAMP; return $this; } /** * ENUM type */ public function enum(...$values) { $this->type=type::ENUM; $this->values=$values; return $this; } /** * Build the clause * @return string clause */ public function __toString() { $sql = $this->name . " " . strtoupper($this->type); switch ($this->type) { case type::TINYINT: case type::SMALLINT: case type::INT: $sql .= $this->unsigned ? " UNSIGNED" : ""; $sql .= $this->autoIncrement ? " AUTO_INCREMENT" : ""; break; case type::FLOAT: $sql .= "(" . $this->size . "," . $this->decimals . ")"; $sql .= $this->unsigned ? " UNSIGNED" : ""; $sql .= $this->autoIncrement ? " AUTO_INCREMENT" : ""; break; case type::CHAR: case type::VARCHAR: $sql .= "(" . $this->size . ")"; break; case type::TEXT: case type::BLOB: case type::DATETIME: case type::TIMESTAMP: break; case type::ENUM: $sql .= "(\"" . implode("\", \"", $this->values) . "\")"; break; } $sql .= $this->notNull ? " NOT NULL" : ""; $sql .= $this->default ? " DEFAULT " . $this->default : ""; $sql .= $this->onUpdate ? " ON UPDATE " . $this->onUpdate : ""; return $sql; } /** * Add Quotes for ON UPDATE and DEFAULT * * This adds quotes to a string, except if they are in the 'noQuotes' array * this is used for SQL keywords as ON UPDATE or DEFAULT value * @param string to be quoted * @return string quoted string */ private function addQuotes(string $string) { if (!in_array($string, static::$noQuotes)) { $string = "\"" . $string . "\""; } return $string; } } zoph-v0.9.19/php/classes/db/create.inc.php000066400000000000000000000062561415176210700203320ustar00rootroot00000000000000name=$name; } public function addColumns(array $columns) { foreach ($columns as $column) { $this->addColumn($column); } } public function addColumn(column $column) { $this->columns[]=$column; } /** * Build the clause * @return string clause */ public function __toString() { $this->pKeys=array(); $this->keys=array(); $this->notNull=array(); $sql = "CREATE TABLE " . db::getPrefix() . $this->name . " (\n"; foreach ($this->columns as $column) { $sql .= " " . $column . ",\n"; if ($column->isKey()) { $this->keys[$column->getKeyName()]=$column; $this->keyLength[$column->getKeyName()]=$column->getKeyLength(); } if ($column->isPrimaryKey()) { $this->pKeys[]=$column->getName(); } if ($column->isNotNull()) { $this->notNull[]=$column; } } if (!empty($this->pKeys)) { $sql .= " PRIMARY KEY (" . implode(", ", $this->pKeys) . "),\n"; } if (!empty($this->keys)) { foreach ($this->keys as $name => $column) { if ($this->keyLength[$name] > 0) { $length = "(" . $this->keyLength[$name] . ")"; } else { $length = ""; } $sql .= " KEY " . $name . "(" . $column->getName() . $length . "),\n"; } } // Remove last comma $sql = substr($sql, 0, -2) . "\n"; $sql .= ") ENGINE=" . $this->engine; return $sql; } } zoph-v0.9.19/php/classes/db/db.inc.php000066400000000000000000000142231415176210700174450ustar00rootroot00000000000000 static::$dbhost, "dbname" => static::$dbname, "user" => static::$dbuser, "pass" => static::$dbpass, "prefix" => static::$dbprefix ); } /** * Get table prefix */ public static function getPrefix() { return static::$dbprefix; } /** * Connect to database * @param string PDO DSN * @codeCoverageIgnore - runs in test setup */ private static function connect($dsn=null) { if (!$dsn) { $dsn=static::getDSN(); } static::$connection=new PDO($dsn,static::$dbuser,static::$dbpass, array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' )); static::$connection->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); static::$connection->setAttribute( PDO::ATTR_EMULATE_PREPARES,false); } /** * Get the Data Source Name for the database connection * Currently hardcoded to MySQL, in the future this might change */ private static function getDSN() { $db="mysql"; return sprintf("%s:host=%s;dbname=%s", $db, static::$dbhost, static::$dbname); } /** * BEGIN a transaction */ public static function beginTransaction() { $db=static::getHandle(); $db->beginTransaction(); } /** * COMMIT a transaction */ public static function commit() { $db=static::getHandle(); $db->commit(); } /** * ROLLBACK transaction */ public static function rollback() { $db=static::getHandle(); $db->rollback(); } /** * Run a query * @param query Query to run */ public static function query(query $query) { $db=static::getHandle(); try { log::msg("SQL Query: " . (string) $query, log::DEBUG, log::SQL); $stmt=$db->prepare($query); foreach ($query->getParams() as $param) { if ($param instanceof param) { log::msg("Param: " . $param->getName() . ": " . $param->getValue(), log::DEBUG, log::SQL); $stmt->bindValue($param->getName(), $param->getValue(), $param->getType()); } } /** * Set LOG_SEVERITY to log::MOREDEBUG and LOG_SUBJECT to log::SQL in config.inc.php * to create a log of all SQL queries + execution times in /tmp */ if ((LOG_SEVERITY == log::TOFILE) && (LOG_SUBJECT & log::SQL)) { $start=$query->logToFile(""); } $stmt->execute(); if (isset($start)) { $time=microtime(true)-$start; file_put_contents("/tmp/zophdebug", ": " . $time . "\n", FILE_APPEND); } } catch (\PDOException $e) { $trace = $e->getTraceAsString(); $trace .= "\n" . $query->prettyPrint() . "\n"; $trace .= $e->getMessage() . "\n"; throw new exception("SQL failed\n" . $trace); } return $stmt; } /** * Execute an SQL query * This is meant to execute queries that cannot be handled via the query builder * it should not be used for SELECT, UPDATE, DELETE or INSERT queries, * these can be handled via their respective objects * @param string SQL */ public static function SQL($sql) { try { $db=static::getHandle(); $stmt=$db->prepare($sql); $stmt->execute(); } catch (\PDOException $e) { echo $e->getMessage() . "\n"; log::msg("SQL failed", log::FATAL, log::DB); } } } zoph-v0.9.19/php/classes/db/delete.inc.php000066400000000000000000000032741415176210700203260ustar00rootroot00000000000000table; if ($this->where instanceof clause) { $sql .= " WHERE " . $this->where; } else if (!$this->deleteAll) { throw new \databaseException("DELETE query without WHERE"); } return $sql . ";"; } } zoph-v0.9.19/php/classes/db/drop.inc.php000066400000000000000000000025151415176210700200250ustar00rootroot00000000000000name=$name; } /** * Build the clause * @return string clause */ public function __toString() { return "DROP TABLE " . db::getPrefix() . $this->name; } } zoph-v0.9.19/php/classes/db/exception.inc.php000066400000000000000000000016641415176210700210630ustar00rootroot00000000000000table; $fields=array(); $values=array(); foreach ($this->getParams() as $param) { $fields[]=substr($param->getName(),1); $values[]=$param->getName(); } foreach ($this->set as $name => $value) { $fields[]=$name; $values[]=$value; } $sql.=" (" . implode(", ", $fields) . ")"; $sql.=" VALUES(" . implode(", ", $values) . ") "; return $sql . ";"; } /** * Add a SET statement to this query * @param string name of the field * @param string value of the field */ public function addSet($name, $value) { $this->set[$name]=$value; } /** * Execute query and return new id * @return int ID */ public function execute() { parent::execute(); return db::getHandle()->lastInsertId(); } } zoph-v0.9.19/php/classes/db/keyException.inc.php000066400000000000000000000017501415176210700215300ustar00rootroot00000000000000name=array(); for ($n=0; $nname[]=$name . "_" . $n; } } else { $this->name=$name; } $this->value=$value; $this->type=$type; } /** * Get the name of the param * @return string name */ public function getName() { return $this->name; } /** * Get the value of the param * @return string value */ public function getValue() { return $this->value; } /** * Get the type of the param * @return int type */ public function getType() { return $this->type; } } zoph-v0.9.19/php/classes/db/query.inc.php000066400000000000000000000256001415176210700202260ustar00rootroot00000000000000alias=$alias; } $table=$tbl; $this->tables[$alias]=$tbl; } else { $this->tables[$table]=$table; } $table=db::getPrefix() . $table; $this->table=$table; } /** * Add one or more fields to a query * @param array list of fields [ "alias" => "field"] * @param bool Whether or not this is a DISTINCT query. * @return query */ public function addFields(array $fields, $distinct=false) { if (isset($this->alias)) { $table = $this->alias; } else if (isset($this->table)) { $table = $this->table; } else { $table = null; } foreach ($fields as $alias => $field) { if ($table && strpos($field, ".") === false) { $field=$table . "." . $field; } if ($distinct) { $field="DISTINCT " . $field; } if (!is_numeric($alias)) { $field .= " AS " . $alias; } $this->fields[]=$field; } } /** * Add one or more fields to a query that is calculated using an SQL function * @param array Array of functions [ "alias" => "function()"] */ public function addFunction(array $functions) { foreach ($functions as $alias => $function) { $this->fields[]=$function . " AS " . $alias; } } /** * Add a parameter for a prepared query * @param param parameter object */ public function addParam(param $param) { $this->params[]=$param; } /** * Add parameters for a prepared query * @param array parameters */ public function addParams(array $params) { foreach ($params as $param) { $this->addParam($param); } } /** * Get array of params */ public function getParams() { $params=array(); if (!is_array($this->params)) { return $params; } foreach ($this->params as $param) { if (!$param instanceof param) { continue; } $value=$param->getValue(); if (is_array($value)) { $value=array_values($value); $name=array_values($param->getName()); $type=$param->getType(); for ($n=0; $nwhere=$clause; return $this; } /** * Add a subclause to the WHERE, or set the clause as a WHERE if it is not yet set * @param clause clause to add * @param string AND|OR * @return query return the query to enable chaining */ public function addClause(clause $clause, $conj="AND") { if ($this->where instanceof clause) { if (strtoupper($conj) == "AND") { $this->where->addAnd($clause); } else if (strtoupper($conj) == "OR") { $this->where->addOr($clause); } else { throw new \databaseException("Unknown conjunction: " . e($conj)); } } else { $this->where($clause); } return $this; } /** * Add a HAVING clause to the query * @param clause HAVING clause * @return query return the query to enable chaining */ public function having(clause $hclause) { $this->having=$hclause; return $this; } /** * Add ORDER BY clause to query * @param string order to add * @example $qry->addOrder("name DESC"); * @return query return the query to enable chaining */ public function addOrder($order) { $this->order[]=$order; return $this; } /** * Get ORDER BY for query * @return string ORDER clause */ protected function getOrder() { $order=$this->order; if (is_array($order) && sizeof($order) > 0) { return " ORDER BY " . implode(", ", $order); } return ""; } /** * Add LIMIT clause to query * Be warned that count and offset are reversed compared to how they appear * in the query! * @param int count * @param int offset * @example $qry->addLimit(1,3); * @return query return the query to enable chaining */ public function addLimit($count, $offset=null) { $this->count=$count; $this->offset=$offset; return $this; } /** * Get LIMIT clause for query * @return string LIMIT clause */ protected function getLimit() { if (!is_null($this->offset)) { $limit=" LIMIT " . (int) $this->offset; if (is_null($this->count)) { $limit.= ", " . 999999999999; } else { $limit.=", " . (int) $this->count; } } else { if (!is_null($this->count)) { $limit=" LIMIT " . (int) $this->count; } else { $limit=""; } } return $limit; } /** * Check if a table is already included in this query * @param string query; */ public function hasTable($table) { return in_array($table, $this->tables); } /** * Execute a query */ public function execute() { return db::query($this); } /** * Add WHERE clause, by building it from a constraints array * @param array Constraints, conditions that the records must comply to * @param array Conjunctions, and/or * @param array Operators =, !=, >, <, >= or <= * @return query $this */ public function addWhereFromConstraints(array $constraints, $conj = "AND", $ops = null) { $where=null; foreach ($constraints as $name => $value) { $op = "="; if ($ops && !empty($ops["$name"])) { $op = $ops["$name"]; } $n = strpos($name, "#"); if ($n > 1) { $paramNumber=substr($name, $n + 1); $name = substr($name, 0, $n); $paramName=":" . $name . "_" . $paramNumber; } else { $paramName=":" . $name; } if ($value == "null" || $value == "''") { $value = null; } $clause=new clause($name . " " . $op . " " . $paramName); $this->addParam(new param($paramName, $value, PDO::PARAM_STR)); if ($where instanceof clause) { if ($conj == "AND") { $where->addAnd($clause); } else if ($conj == "OR") { $where->addOr($clause); } else { throw new \zophException("Illegal conjunction (" . e($conj) . ") should be AND or OR, please file a bug"); } } else { $where = $clause; } } if ($where instanceof clause) { $this->where($where); } return $this; } /** * Log the query to file, for debugging purposes * @param string Characters to be added at end of line * @param string Name of file to log the query to * @codeCoverageIgnore */ public function logToFile($eol="\n", $file="/tmp/zophdebug") { file_put_contents($file, $this->prettyPrint() . $eol, FILE_APPEND); return microtime(true); } /** * Format a query, including all parameters, for debugging purposes * @codeCoverageIgnore * @param bool Output with HTML */ public function prettyPrint($withHTML=false) { $sql=(string) $this; $allParams=$this->getParams(); // Here we sort the parameters by the length of their name, // longest first. // This is so we don't overwrite part of a parameter name // in case the first part of the name is the same $sort=function($a, $b) { return(strlen($b->getName()) - strlen($a->getName())); }; usort($allParams, $sort); foreach ($allParams as $param) { $value=$param->getValue(); if ($withHTML) { $value="" . $value . ""; } if ($param->getType() == PDO::PARAM_INT) { $sql=str_replace($param->getName(), $value, $sql); } else { $sql=str_replace($param->getName(), "\"" . $value . "\"", $sql); } } return $sql; } /** * The __toString() magic function creates the query to be fed to the db * each inheritance of this class will have to implement it. * @return string SQL query */ abstract public function __toString(); } zoph-v0.9.19/php/classes/db/select.inc.php000066400000000000000000000143651415176210700203460ustar00rootroot00000000000000subquery=$table; foreach ($tbl->getParams() as $param) { $this->addParam($param); } } else { parent::__construct($table); } } /** * Add a JOIN clause to the query * @param array table to join array of "alias" => "tablename" * or "alias" => select subquery * @param string ON clause * @param string join type * @return query return the query to enable chaining */ public function join(array $table, $on, $jointype="INNER") { if (!in_array($jointype, array("INNER", "LEFT", "RIGHT"))) { throw new \databaseException("Unknown JOIN type"); } $tbl=reset($table); $as=key($table); if ($tbl instanceof select) { // We are joining with a subquery $this->joins[]=$jointype . " JOIN (" . rtrim((string) $tbl, ";") . ") AS " . $as . " ON " . $on; } else { $table=$tbl . " AS " . $as; $this->tables[$as]=$tbl; $table=db::getPrefix() . $table; $this->joins[]=$jointype . " JOIN " . $table . " ON " . $on; } return $this; } /** * Add GROUP BY clause to query * @param string GRPUP BY to add * @return query return the query to enable chaining */ public function addGroupBy($group) { $this->groupby[]=$group; return $this; } /** * Get GROUP BY for query * @return string GROUP clause */ private function getGroupBy() { $groupby=$this->groupby; if (is_array($groupby) && sizeof($groupby) > 0) { return " GROUP BY " . implode(", ", $groupby); } return ""; } /** * Add a UNION clause to the query * @param select SELECT query to UNION with this one * @return query return the query to enable chaining */ public function union(select $qry) { $this->union[]=$qry; $this->addParams($qry->getParams()); return $this; } /** * Execute query */ public function execute() { return db::query($this); } /** * Create SELECT query * @return string SQL query */ public function __toString() { $sql = "SELECT "; if (is_array($this->fields)) { $sql.=implode(", ", $this->fields); } else { $sql.="*"; } if (isset($this->table)) { $sql .= " FROM " . $this->table; } else if (isset($this->subquery)) { if (is_array($this->subquery)) { $subqry = (string) reset($this->subquery); $alias = key($this->subquery); // We need to take off the ; $sql .= " FROM (" . rtrim($subqry, ";") . ") AS " . $alias; } else { // We need to take off the ; $sql .= " FROM (" . (string) rtrim($this->subquery. ";") . ")"; } } else { die("No from clause in query"); } if (isset($this->alias)) { $sql.=" AS " . $this->alias; } if (is_array($this->joins)) { $sql.=" " . implode(" ", $this->joins); } if ($this->where instanceof clause) { $sql .= " WHERE " . $this->where; } $groupby=trim($this->getGroupBy()); if (!empty($groupby)) { $sql .= " " . $groupby; } if ($this->having instanceof clause) { $sql .= " HAVING " . $this->having; } if (sizeof($this->union) > 0) { foreach ($this->union as $union) { // We need to take off the ; $sql .= " UNION (" . rtrim($union, ";") . ")"; } } $order=trim($this->getOrder()); if (!empty($order)) { $sql .= " " . $order; } $limit=trim($this->getLimit()); if (!empty($limit)) { $sql .= " " . $limit; } return $sql . ";"; } /** * Return the first column from the query as an array * This function should only be run on queries with a single column, * or it will make little sense * @return Array array of values */ public function toArray() { $stmt=$this->execute(); return $stmt->fetchAll(PDO::FETCH_COLUMN, 0); } /** * Executes a "SELECT COUNT(*) FROM ..." query and returns the counter * @return int count */ public function getCount() { try { $result = db::query($this); } catch (\PDOException $e) { log::msg("Unable to get count", log::FATAL, log::DB); } return $result->fetch(PDO::FETCH_BOTH)[0]; } } zoph-v0.9.19/php/classes/db/selectHelper.inc.php000066400000000000000000000160341415176210700215010ustar00rootroot00000000000000addFields(array("p.date", "p.time")); $qry=$query->addOrder("p.date")->addOrder("p.time")->addLimit(1); break; case "newest": $query->addFields(array("p.date", "p.time")); $qry=$query->addOrder("p.date DESC")->addOrder("p.time DESC")->addLimit(1); break; case "first": $query->addFields(array("p.timestamp")); $qry=$query->addOrder("p.timestamp")->addLimit(1); break; case "last": $query->addFields(array("p.timestamp")); $qry=$query->addOrder("p.timestamp DESC")->addLimit(1); break; case "random": $qry=$query->addOrder("rand()")->addLimit(1); break; case "highest": default: $query->addFields(array("ar.rating")); $qry=$query->addOrder("ar.rating DESC")->addLimit(1); break; } return $qry; } /** * Expand the query so that it is restricted it to the photos the (current) user can see * @param select SELECT query to be expanded * @param user user to expand the query for - if null, use the currently logged in user */ public static function expandQueryForUser(select $qry, user $user=null) { if (!$user) { $user=user::getCurrent(); } // The user is an admin, simply return the query and where clause unaltered if ($user->canSeeAllPhotos()) { return $qry; } if (!$qry->hasTable("photos")) { $qry=static::addPhotoTableToQuery($qry); } $subqry=new select(array("pu" => "view_photo_user")); $subqry->where(new clause("pu.user_id = :userid")); $qry->addParam(new param(":userid", $user->getId(), PDO::PARAM_INT)); $qry->join(array("spu" => $subqry), "p.photo_id = spu.photo_id"); return $qry; } /** * This function adds a relation table to the query, in order to make it possible to * JOIN with the photo table * @param select query * @return select modified query */ private static function addRelationTableToQuery(select $qry) { if ($qry->hasTable("albums") && !$qry->hasTable("photo_albums")) { $qry->join(array("pa" => "photo_albums"), "pa.album_id = a.album_id", "LEFT"); } else if ($qry->hasTable("categories") && !$qry->hasTable("photo_categories")) { $qry->join(array("pc" => "photo_categories"), "pc.category_id = c.category_id", "LEFT"); } else if ($qry->hasTable("people") && !$qry->hasTable("photo_people")) { $qry->join(array("pp" => "photo_people"), "pp.person_id = ppl.person_id", "LEFT"); } return $qry; } /** * This function tries to figure out how to JOIN the current query with the photo table * @param select query * @return select modified query */ private static function addPhotoTableToQuery(select $qry) { $qry=static::addRelationTableToQuery($qry); if ($qry->hasTable("photo_albums")) { $qry->join(array("p" => "photos"), "pa.photo_id = p.photo_id", "LEFT"); } else if ($qry->hasTable("photo_categories")) { $qry->join(array("p" => "photos"), "pc.photo_id = p.photo_id", "LEFT"); } else if ($qry->hasTable("photo_people")) { $qry->join(array("p" => "photos"), "pp.photo_id = p.photo_id", "LEFT"); } else if ($qry->hasTable("places")) { $qry->join(array("p" => "photos"), "p.location_id = pl.place_id", "LEFT"); } else { throw new \databaseException("JOIN failed"); } return $qry; } /** * Add modify query to ORDER BY a calculated field * @param select SQL query to modify * @param string [oldest|newest|first|last|lowest|highest|average|random] * @return query modified query */ public static function addOrderToQuery(select $qry, $order) { if (!$qry->hasTable("photos") && in_array($order, array("oldest", "newest", "first", "last", "lowest", "highest", "average"))) { $qry=static::addPhotoTableToQuery($qry); } if (!$qry->hasTable("view_photo_avg_rating") && in_array($order, array("lowest", "highest", "average"))) { $qry->join(array("ar" => "view_photo_avg_rating"), "ar.photo_id = p.photo_id"); } switch ($order) { case "oldest": $qry->addFunction(array("oldest" => "min(p.date)")); break; case "newest": $qry->addFunction(array("newest" => "max(p.date)")); break; case "first": $qry->addFunction(array("first" => "min(p.timestamp)")); break; case "last": $qry->addFunction(array("last" => "max(p.timestamp)")); break; case "lowest": $qry->addFunction(array("lowest" => "min(rating)")); break; case "highest": $qry->addFunction(array("highest" => "max(rating)")); break; case "average": $qry->addFunction(array("average" => "avg(rating)")); break; case "random": $qry->addFunction(array("random" => "rand()")); break; } if (!empty($order)) { $qry->addOrder($order); } return $qry; } } zoph-v0.9.19/php/classes/db/transaction.inc.php000066400000000000000000000030261415176210700214040ustar00rootroot00000000000000execute(); } catch (\Exception $e) { log::msg("Transaction failed, rollback.", log::ERROR, log::DB); log::msg($e->getMessage(), log::DEBUG, log::DB); $this->rollback(); throw $e; } } public function commit() { db::commit(); } public function rollback() { db::rollback(); } } zoph-v0.9.19/php/classes/db/type.inc.php000066400000000000000000000025011415176210700200350ustar00rootroot00000000000000set[]=$field . "=:" . $param; } /** * Add a field to be SET in UPDATE query, using a function to set it * @param string field=function() expression */ public function addSetFunction($function) { $this->set[]=$function; } /** * Get array of SET statements for this query * @return array SET statements */ public function getSet() { return $this->set; } /** * Create UPDATE query * @return string SQL query */ public function __toString() { $sql = "UPDATE " . $this->table . " SET "; if (is_array($this->set)) { $sql.=implode(", ", $this->set); } else { // throw new databaseException("UPDATE with no SET"); } if ($this->where instanceof clause) { $sql .= " WHERE " . $this->where; } else if (!$this->updateAll) { die("UPDATE query without WHERE"); } return $sql . ";"; } } zoph-v0.9.19/php/classes/file.inc.php000066400000000000000000000343251415176210700174170ustar00rootroot00000000000000name=basename($filename); $this->path=realpath(dirname($filename)); } /** * Whether or not this file is a symlink * @param bool whether or not this is a symlink */ public function isLink() { return is_link($this); } /** * Get filename with extension removed * @return string filename without ext */ public function getNameNoExt() { $fileinfo = pathinfo((string)$this); return $fileinfo['filename']; } /** * Returns the link destination. Contrary to the PHP readlink() function, * this function recurses through the links until it has located a real * file. So, in case a link points to a link, which points to a link, * which points to... I guess you got it. * Also, it will simply return a file object if the file is not a link. */ public function readlink() { if ($this->isLink()) { $file=new file(readlink($this)); return $file->readlink(); } else { return $this; } } /** * This function returns the name of a file, referenced by a directory * and an MD5 hash of the filename. */ public static function getFromMD5($dir, $md5) { $files=glob($dir . "/*"); foreach ($files as $file) { $f=realpath($file); log::msg($f . ": " . md5($f), log::DEBUG, log::IMPORT); if (md5($f) == $md5) { return new file($f); } } } /** * Returns full path + filename */ public function __toString() { return $this->getPath() . "/" . $this->getName(); } /** * Returns filename */ public function getName() { return $this->name; } /** * Returns full path */ public function getPath() { return $this->path; } /** * When a symlink is copied or moved, the name changes * this function returns the new name */ public function getDestName() { return $this->destName; } /** * This generates an MD5 for a filename, to uniquely identify a file * that is not (yet) in the database and therefore has no db key. */ public function getMD5() { return md5($this->path . "/" . $this->name); } /** * Deletes a file after doing some checks * @param bool Also delete related files, such as thumbnails * @param bool Do not delete the referenced file, only related files * @todo 'related' files really should be part of the photo object. * @see photo */ public function delete($thumbs=false, $thumbsOnly=false) { log::msg("Deleting " . $this, log::NOTIFY, log::IMPORT); if (!$thumbsOnly && file_exists($this)) { if (!is_dir($this) && is_writable($this)) { unlink($this); } else { log::msg(sprintf(translate("Could not delete %s."), $this), log::ERROR, log::IMPORT); return false; } } if ($thumbs) { $dir=dirname($this); $file=basename($this); $midname=$dir . "/" . MID_PREFIX . "/" . MID_PREFIX . "_" . $file; $thumbname=$dir . "/" . THUMB_PREFIX . "/" . THUMB_PREFIX . "_" . $file; $mid=new file($midname); $mid->delete(); $thumb=new file($thumbname); $thumb->delete(); $ignore=new file($this . ".zophignore"); $ignore->delete(); } } /** * Set the destination for copy or move operations; * @param string destination of the file */ public function setDestination($path) { $this->destPath="/" . file::cleanupPath($path) . "/"; $this->destName=basename($this->readlink()); } /** * Makes checks if a file can be found and read */ public function check() { if (!file_exists($this)) { throw new fileNotFoundException("File not found: $this\n"); } if (!is_readable($this)) { throw new fileNotReadableException("Cannot read file: $this\n"); } if (!conf::get("import.cli.copy") && !is_writable($this)) { throw new fileNotWritableException("Cannot move file: $this\n"); } } /** * Makes checks to see if a file can be copied */ public function checkCopy() { // First checks are the same... $this->check(); if (!is_writable($this->destPath)) { throw new fileDirNotWritableException("Directory not writable: " . $this->destPath); } if (file_exists($this->destPath . $this->destName)) { if ($this->backup) { $backupname=$this->destName; $counter=1; while (file_exists($this->destPath . $backupname)) { // Find the . in the filename $pos=strrpos($this->destName, ".") ?: strlen($this->destName); $backupname=substr($this->destName, 0, $pos) . "_" . $counter . substr($this->destName, $pos); $counter++; } rename($this->destPath . $this->destName, $this->destPath . $backupname); } else { throw new fileExistsException("File already exists: " . $this->destPath . $this->destName); } } return true; } /** * Makes checks if a file can be moved */ public function checkMove() { // First checks are the same... $this->checkCopy(); if (!is_writable($this)) { throw new fileNotWritableException("File is not writable: " . $this); } return true; } /** * Moves a file */ public function move() { $destPath=$this->destPath; $destName=$this->destName; $dest=$destPath . "/" . $destName; log::msg("Going to move $this to $dest", log::DEBUG, log::GENERAL); $this->checkMove(); if ($this->isLink()) { // in case of a link, we copy the link destination and delete the link $copy=$this->readlink(); $copy->setDestination($destPath); $newfile=$copy->copy(); unlink($this); return $newfile; } else { if (rename($this, $dest)) { return new file($dest); } else { throw new fileMoveFailedException("Could not move $this to $dest"); } } } /** * Copies a file */ public function copy() { $destPath=$this->destPath; $destName=$this->destName; $dest=$destPath . "/" . $destName; $this->checkCopy(); if (copy($this, $dest)) { return new file($dest); } else { throw new fileCopyFailedException("Could not copy $this to $dest"); } } /** * Changes the permissions for a file */ public function chmod($mode = null) { if ($mode===null) { $mode=octdec(conf::get("import.filemode")); } if (!chmod($this, $mode)) { log::msg("Could not change permissions for " . $this . "", log::ERROR, log::IMPORT); } } /** * Gets MIME type for this file */ public function getMime() { $fileinfo=new finfo(FILEINFO_MIME, conf::get("path.magic")); $mime=explode(";", $fileinfo->file($this->readlink())); log::msg("" . $this->readlink() . ": " . $mime[0], log::DEBUG, log::IMPORT); $this->setFiletype($mime[0]); return $mime[0]; } /** * Read first bytes from file * @param int number of bytes * @return string header of file */ private function read(int $bytes) { $fp = fopen($this, "rb"); $data=fread($fp, $bytes); fclose($fp); return $data; } /** * Gets type of file for this file */ private function setFiletype($mime) { switch ($mime) { case "image/jpeg": case "image/png": case "image/gif": $type="image"; break; case "application/x-bzip2": case "application/x-gzip": case "application/x-tar": case "application/gzip": case "application/zip": $type="archive"; break; case "text/xml": case "application/xml": case "text/plain": $type="xml"; break; case "directory": $type="directory"; break; default: $type=false; } if ($type == "xml") { $header=$this->read(100); if (strpos($header, "type=$type; return $type; } /** * Get files in a specific directory * * This function creates a list of files in a specific directory and * filters it on a given search string and filetypes. * @param string The dir to search * @param bool Whether or not to descent into directories * @param string Search string */ public static function getFromDir($dir, $recursive = false, $search=null) { $files = scandir($dir); $return = array(); foreach ($files as $filename) { if ($filename[0]!=".") { if (is_dir($dir . "/" . $filename)) { if ($recursive) { $return=array_merge($return, static::getFromDir($dir . "/" . $filename, true)); } } else if (is_null($search) || preg_match($search, $filename)) { $file=new file($dir . "/" . $filename); if (!file_exists($dir . "/" . $filename . ".zophignore")) { $file->getMime(); } else { $file->type = "ignore"; } if ($file->type) { $return[]=$file; } } } } return $return; } /** * Cleans up a path, by removing all double slashes, "/./", * leading and trailing slashes. */ public static function cleanupPath($path) { $search = array("/(\/+)/", "/(\/\.\/)/", "/(\/$)/", "/(^\/)/"); $replace = array("/", "/", "", ""); return preg_replace($search, $replace, $path); } /** * Create a directory * @param string directory to create * @return bool true when succesful * @throws fileDirCreationFailedException when creation fails */ private static function createDir($directory) { if (!file_exists($directory)) { if (@mkdir($directory, octdec(conf::get("import.dirmode")))) { if (!defined("CLI") || conf::get("import.cli.verbose")>=1) { log::msg(translate("Created directory") . ": $directory", log::NOTIFY, log::GENERAL); } return true; } else { throw new fileDirCreationFailedException( translate("Could not create directory") . ": $directory
\n"); } } } /** * Recursively create directory * checks if the parent dir of the dir to be created exists and if not so, tries to * create it first * @param string directory to create * @return bool true when succesful */ public static function createDirRecursive($directory) { $directory="/" . static::cleanupPath($directory); if (!file_exists(dirname($directory))) { static::createDirRecursive(dirname($directory)); } try { static::createDir($directory); } catch (fileDirCreationFailedException $e) { log::msg($e->getMessage(), log::FATAL, log::GENERAL); } } } ?> zoph-v0.9.19/php/classes/file/000077500000000000000000000000001415176210700161275ustar00rootroot00000000000000zoph-v0.9.19/php/classes/file/archive.inc.php000066400000000000000000000076401415176210700210400ustar00rootroot00000000000000filename=$filename; $this->type=$type; if ($this->checkZipSupport()) { $this->archive = new ZipArchive(); $this->tempfile = "/tmp/zoph_" . $user->get("user_id") . "_" . $this->filename ."_" . (int) $filenum . ".zip"; if (file_exists($this->tempfile)) { unlink($this->tempfile); } if ($this->archive->open($this->tempfile, ZipArchive::CREATE)!==true) { log::msg("Cannot create temporary ZIP archive, " . $this->tempfile, log::FATAL, log::GENERAL); } } else { } } /** * Set maximum size * Adding photos to the archive is stopped when the archive becomes too big * this is used to spread over multiple files * @param int maximum size in bytes */ public function setMaxSize($bytes) { $this->maxsize=(int) $bytes; } /** * Add photos to archive * @param photo\collection photos to add * @return int file number last added */ public function addPhotos(photoCollection $photos) { $zipsize=0; $file=0; foreach ($photos as $photo) { if ($data=@file_get_contents($photo->getFilePath())) { $size=strlen($data); $zipsize=$zipsize+$size; if ($this->maxsize>0 && $zipsize>=$this->maxsize) { break; } $file++; $this->archive->addFromString($photo->get("name"), $data); } else { echo sprintf(translate("Could not read %s."), $photo->getFilePath()) . "
\n"; } } if (!$this->archive->close()) { log::msg("ZIP file creation failed", log::FATAL, log::GENERAL); } return $file; } /** * Checks whether ZIP support is enabled in PHP * @return bool zip support enabled */ public function checkZipSupport() { return class_exists("ZipArchive") or log::msg(translate("You need to have ZIP support in PHP to download zip files"), log::FATAL, log::GENERAL); } } zoph-v0.9.19/php/classes/generic/000077500000000000000000000000001415176210700166245ustar00rootroot00000000000000zoph-v0.9.19/php/classes/generic/collection.inc.php000066400000000000000000000117201415176210700222410ustar00rootroot00000000000000items[$off]); } /** * Return item * For ArrayAccess interface * @param string offset * @return mixed value of the item */ public function offsetGet($off) { return $this->items[$off]; } /** * Add item * For ArrayAccess interface * @param string offset * @param string value */ public function offsetSet($off, $value) { if (!is_null($off)) { $this->items[$off]=$value; } else { $this->items[]=$value; } } /** * Unset item (remove) * For ArrayAccess interface * @param string offset */ public function offsetUnset($off) { unset($this->items[$off]); } /** * For IteratorAggregate interface * allow us to do foreach () on this object */ public function getIterator() { return new ArrayIterator($this->items); } /** * For Countable interface * return size of this collection */ public function count() { return count($this->items); } /** * Return a subset of this collection as a new collection * @param int start of subset * @param int size of subset */ public function subset($start, $count=null) { return static::createFromArray(array_slice($this->items, $start, $count, true)); } /** * Pop last element off the collection * @return mixed last object of the collection */ public function pop() { return array_pop($this->items); } /** * Shift first element off the collection * @return mixed first object of the collection */ public function shift() { return array_shift($this->items); } /** * Get random element(s) from the collection */ public function random($count = 1) { $count = min(sizeof($this), $count); $rndKeys=(array) array_rand($this->items, $count); $rndColl = new static(); foreach ($rndKeys as $key) { $rndColl[$key] = $this[$key]; } return $rndColl; } /** * Merge this collection with other collection(s) * @param collection to merge with [, collection to merge with [ , ... ]] * return collection */ public function merge(self ...$toMerge) { $merged=array(); array_unshift($toMerge, $this); foreach ($toMerge as $collection) { $merged=array_merge($merged, $collection->toArray()); } return static::createFromArray($merged); } /** * Renumber the items so that each item has it's key as * it's key in the array * @param callable alternate function to determine key (default ->getId() ) * @return collection */ public function renumber(callable $function=null) { // Default for callable can only be null if (!$function) { $function="getId"; } $return = new static; foreach ($this->items as $item) { $id = call_user_func(array($item, $function)); $return[$id]=$item; } return $return; } /** * Turn this collection into an array */ protected function toArray() { return $this->items; } /** * Create a new collection from an array * @param array Items to put in new collection */ public static function createFromArray(array $items, $withKeys = false) { $collection = new static(); if ($withKeys) { foreach ($items as $item) { $collection[$item->getId()]=$item; } } else { $collection->items = $items; } return $collection; } } zoph-v0.9.19/php/classes/generic/controller.inc.php000066400000000000000000000124031415176210700222700ustar00rootroot00000000000000request=$request; if (isset($this->request["_return"])) { $this->redirect=$this->request["_return"] . "?" . $this->request->getReturnQueryString(); } } /** * Set the object to operate on * @param zophTable object to operate on */ public function setObject(zophTable $obj) { $this->object=$obj; } /** * Do the action as set in the request * in the current mode of operation, no authorization checking is needed, * because currently, the authorization checking is done inside the actions * however, it would be nice to do some checking here as a first line of defense */ public function doAction() { $action=$this->request["_action"]; /** @todo This needs more authorization checking */ if (in_array($action, $this->actions)) { $function = "action" . ucwords($action); $this->$function(); } else { $this->actionDisplay(); } } /** * Action: edit * The edit action calls a view that will allow the user to update the * current object. */ protected function actionEdit() { $this->view = "update"; } /** * Action: update * The update action processes a form as generated after the "edit" action. * The subsequently called view displays the object. */ protected function actionUpdate() { $this->object->setFields($this->request->getRequestVars()); $this->object->update(); $this->view = "display"; } /** * Action: new * The new action calls a view that displays a form that allows the user * to create a new object. */ protected function actionNew() { $this->object->setFields($this->request->getRequestVars()); $this->view = "insert"; } /** * Action: insert * The insert action processes a form as generated after the "new" action. * The subsequently called view displays the object. */ protected function actionInsert() { $this->object->setFields($this->request->getRequestVars()); $this->object->insert(); $this->view = "display"; } /** * Action: delete * The delete action asks for confirmation of a delete of the current object */ protected function actionDelete() { $this->view = "confirm"; } /** * Action: confirm * The confirm action is called when the user confirms the delete * this deletes the object and then redirects the user back the the * last page he visited before the delete. */ protected function actionConfirm() { $this->object->delete(); breadcrumb::init(); breadcrumb::eat(); $crumb = breadcrumb::getLast(); if ($crumb instanceof breadcrumb) { $this->redirect=urldecode(html_entity_decode($crumb->getURL())); } $this->view = "redirect"; } /** * The display action displays the object */ protected function actionDisplay() { $this->view = "display"; } /** * get View * each of the actions dictate a subsequent view in the workflow, * the view can be called by this function * currently, it simply returns a name, in the future an action View object * may be returned. */ public function getView() { return $this->view; } /** * Get the object to operate on */ public function getObject() { return $this->object; } } zoph-v0.9.19/php/classes/generic/variable.inc.php000066400000000000000000000060351415176210700216760ustar00rootroot00000000000000value=$value; } /** * Get value */ public function __toString() { return (string) $this->value; } /** * Get value */ public function get() { return $this->value; } /** * This function will escape the user input and remove HTML tags */ public function input() { $var=$this->value; if ($var === "<" || $var === "<=" || $var === ">=" || $var === ">") { // Strip tags breaks some searches $value=$var; } else if (is_array($var)) { $value=array(); foreach ($var as $key => $arrayValue) { $keyVar=new variable($key); $valueVar=new variable($arrayValue); $value[$keyVar->input()]=$valueVar->input(); } } else { $value=strip_tags(html_entity_decode($var)); } return $value; } /** * Return escaped output * @param array|string value to be escaped */ public function escape($var=null) { if (!$var) { $var=$this->value; } if (is_array($var)) { $return=array(); foreach ($var as $key => $arrayValue) { $return[static::htmlEscape($key)]=static::htmlEscape($arrayValue); } } else { $return=static::htmlEscape($var); } return $return; } public static function htmlEscape($var) { $return=htmlspecialchars($var); /* Extra escape for a few chars that may cause troubles but are not escaped by htmlspecialchars. */ $return=str_replace(array("<", ">", "\"", "(", ")", "'", "[", "]", "{", "}", "~", "`"), array("<", ">", """, "(", ")", "'","[", "]", "{", "}", "~", "`"), $return); return $return; } } zoph-v0.9.19/php/classes/geo/000077500000000000000000000000001415176210700157625ustar00rootroot00000000000000zoph-v0.9.19/php/classes/geo/Exception.inc.php000066400000000000000000000017371415176210700212110ustar00rootroot00000000000000search = $search; $this->server = $server; $this->parse(); } /** * takes a string, which is probably an OpenSteetmap URL, and turn it into * a latitude and longitude * e.g. #map=9/10.4/-12.92 */ private function parseOpenstreetmap() { if (preg_match('/^https:\/\/www\.openstreetmap\.org\/#map=(\d+)\/([-]?[\d]*[.]?[\d]*)\/([-]?[\d]*[.]?[\d]*)/', $this->search, $matches)) { $this->zoom = $matches[1]; $this->lat = $matches[2]; $this->long = $matches[3]; } } private function parseOLC() { $digits = "23456789CFGHJMPQRVWX"; $zoomlevels = array(5, 9, 12, 14, 16, 17, 18); $olc = $this->search; $lat = 0; $lon = 0; $zoom = 0; if (preg_match('/^[2-9CFGHJMPQRVWX]{2,8}0*\+[2-9CFGHJMPQRVWX]{0,3}$/', $olc)) { $h = 400; $w = 400; $olc=str_replace("+", "", $olc); for ($i=0; $ilat = round($lat + ($h / 2) - 90, 7); $this->long = round($lon + ($w / 2)- 180, 7); $this->zoom = $zoomlevels[$zoom]; } } private function parseLocalURL() { if (preg_match("/(photo|image).php?.+photo_id=(\d+)/", $this->search, $matches)) { $obj = new photo($matches[2]); } else if (preg_match("/(place).php?.+place_id=(\d+)/", $this->search, $matches)) { $obj = new place($matches[2]); } else { return false; } $obj->lookup(); $this->lat=$obj->get("lat"); $this->long=$obj->get("lon"); $this->zoom=$obj->get("mapzoom"); } /** * Takes a string which contains a location in any of ways that a location * could be specified and sets lat and long appropiately (or to empty if * search does not contain a parseable location */ private function parse() { // error_log("locationLookup::parse() called with ".$search." **"); if (empty($this->search)) { $this->lat = "" ; $this->long = ""; // error_log("locationLookup was empty"); } else if (strcmp($this->search,',') == 0) { /* match empty except a single comma - excpt preg_match('/[\s]*[,][\s]',$search matched too easity */ // error_log("locationLookup - single comma"); $this->search = ""; } else if (preg_match('/([-]?[\d]*[.]?[\d]*)[,][\s]*([-]?[\d]*[.]?[\d]*)/', $this->search, $matches)) { $zoomlevels = array(4,7,10,12,14,15,16,17,1); /* match 1.456,-12.793 and similar */ // error_log ("locationLookup - a match was found"); $this->lat = $matches[1]; $this->long = $matches[2]; $lat = $this->lat * 1000000; $lon = $this->long * 1000000; $zoom = 8; while ($lat != 0 && $lon !=0) { $zoom--; $newlat = floor($lat / 10); $newlon = floor($lon / 10); if ($lat != $newlat * 10 || $lon != $newlon * 10) { break; } $lat = $newlat; $lon = $newlon; } $this->zoom=$zoomlevels[$zoom]; } else if (preg_match('/^https:\/\/www\.openstreetmap\.org\/.*/',$this->search)) { $this->parseOpenstreetmap(); } else if (preg_match('/^[2-9CFGHJMPQRVWX]{2,8}0*\ [2-9CFGHJMPQRVWX]{0,3}$/', $this->search)) { $this->search=str_replace(" ", "+", $this->search); $this->parseOLC(); } else if (preg_match("/^https?:\/\/" . $this->server . "\//", $this->search)) { // URL pointing to server $this->parseLocalURL(); } else { /* this is a case we cant handle yet */ error_log("locationLookup: unhandled case " . $this->search); } } /** * return the latitude from this location, or a null string if the location * does not contain a latitude * @return float latitude */ public function getLat() { return $this->lat; } /** * return the longitude from this location, or a null string if the location * does not contain a longitude * @return float longitude */ public function getLong() { return $this->long; } /** * return the zoom level from this location, or a null string if the location * does not contain a zoom level * @return int zoom */ public function getZoom() { return $this->zoom; } } ?> zoph-v0.9.19/php/classes/geo/map.inc.php000066400000000000000000000121721415176210700200230ustar00rootroot00000000000000map; } if (!array_key_exists("provider", $vars)) { $vars["provider"]=conf::get("maps.provider"); } parent::__construct($template, $vars); } /** * Add a marker to the map * @param marker marker to add */ public function addMarker(marker $marker) { $this->markers[]=$marker; } /** * Add multiple markers from objects * @param array Array of objects to get markers from */ public function addMarkers(array $objs) { foreach ($objs as $obj) { $marker=$obj->getMarker(); if ($marker instanceof marker) { $this->addMarker($marker); } } } /** * Get markers for this map * @return array array of markers for this map. * if multiple photos are taken in the same place, that place * is multiple times in the array, so this function removes doubles */ public function getMarkers() { return array_unique($this->markers, SORT_REGULAR); } /** * Checks whether this maps has markers * @return bool */ public function hasMarkers() { return !empty($this->markers); } /** * Add a track * @param track */ public function addTrack(track $track) { $this->tracks[]=$track; } /** * Get tracks * @return Array tracks */ public function getTracks() { return $this->tracks; } /** * Checks whether this maps has tracks * @return bool */ public function hasTracks() { return !empty($this->tracks); } /** * Set center and zoom * This sets the center point and zoom level for the map * @param float latitude * @param float longitude * @param int zoom level */ public function setCenterAndZoom($lat=0, $lon=0, $zoom=2) { $this->clat=(float) $lat; $this->clon=(float) $lon; $this->zoom=(int) $zoom; } /** * Set center and zoom from object * Can take a location object and determine center and zoom from there * it can also take a photo object to determine c&s. * If a photo object does not have c&z, it will see if the photo has * a location set, and determine it from there. * If a location does not have c&z, it can go up in the tree until * it find an ancestor with c&z set. * @param photo|place object to get location from * @todo mapable interface should be created */ public function setCenterAndZoomFromObj($obj) { $lat=$obj->get("lat"); $lon=$obj->get("lon"); $zoom=$obj->get("mapzoom"); if (!$lat && !$lon) { if ($obj instanceof photo && $obj->location instanceof place) { $this->setCenterAndZoomFromObj($obj->location); } else if ($obj instanceof place && (place::getRoot()->getId() != $obj->getId())) { $this->setCenterAndZoomFromObj($obj->getParent()); } } else { $this->setCenterAndZoom($lat ?: 0, $lon ?: 0, $zoom ?: 2); } } /** * Set whether or not this map can be changed * (used to add markers to a place or photo) * @param bool */ public function setEditable($edit=true) { $this->edit=(bool) $edit; } } zoph-v0.9.19/php/classes/geo/marker.inc.php000066400000000000000000000044751415176210700205360ustar00rootroot00000000000000lat=$lat; $this->lon=$lon; $this->icon=$icon; $this->title=$title; $this->quicklook=$quicklook; } /** * Get marker from object * @param photo|place Object to get marker from * @param string Icon to use * @return marker created marker. * @todo A "mapable" interface should be created to make sure * only certain objects can get passed to this function. */ public static function getFromObj($obj, $icon) { $lat=$obj->get("lat"); $lon=$obj->get("lon"); if ($lat && $lon) { $title=$obj->get("title"); $quicklook=$obj->getQuicklook(); return new self($lat, $lon, $icon, $title, $quicklook); } else { return null; } } } zoph-v0.9.19/php/classes/geo/point.inc.php000066400000000000000000000161111415176210700203740ustar00rootroot00000000000000xml($xmldata); $xml->read(); $point->set("lat", $xml->getAttribute("lat")); $point->set("lon", $xml->getAttribute("lon")); while ($xml->read()) { if ($xml->nodeType==XMLReader::ELEMENT) { switch ($xml->name) { case "name": $xml->read(); $name=$xml->value; $point->set("name", $name); break; case "ele": $xml->read(); $point->set("ele", $xml->value); break; case "speed": $xml->read(); $point->set("speed", $xml->value); break; case "time": $xml->read(); $datetime=strtotime($xml->value); $point->set("datetime", date("Y-m-d H:i:s", $datetime)); break; default: // unrecognized element, ignore break; } } } return $point; } /** * Create a query to find the next and previous points * this query will be expanded by the @see getNext() and @see getPrevious() methods * @return array (select, clause); */ private function getNextPrevQry() { $qry=new select(array("pt" => "point")); $where=new clause("track_id=:trackid"); $qry->addParams(array( new param(":trackid", (int) $this->get("track_id"), PDO::PARAM_INT), new param(":datetime", $this->get("datetime"), PDO::PARAM_STR) )); $qry->addLimit(1); return array($qry, $where); } /** * Get the next (in time) point from a track */ public function getNext() { list($qry, $where)=$this->getNextPrevQry(); $where->addAnd(new clause("datetime>:datetime")); $qry->where($where); $qry->addOrder("datetime"); $points=static::getRecordsFromQuery($qry); if (is_array($points) && sizeof($points) > 0) { return $points[0]; } else { return null; } } /** * Get the previous (in time) point from a track */ public function getPrev() { list($qry, $where)=$this->getNextPrevQry(); $where->addAnd(new clause("datetime<:datetime")); $qry->where($where); $qry->addOrder("datetime DESC"); $points=static::getRecordsFromQuery($qry); if (is_array($points) && sizeof($points) > 0) { return $points[0]; } else { return null; } } /** * Calculate the distance to another point * * @param point Point to calculate distance to * @param string "km" or "miles" * @return int distance */ private function getDistanceTo(point $p2, $entity="km") { $p1=$this; $lat1=$p1->get("lat"); $lon1=$p1->get("lon"); $lat2=$p2->get("lat"); $lon2=$p2->get("lon"); $distance=(6371 * acos( cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($lon2) - deg2rad($lon1)) + sin(deg2rad($lat1)) * sin(deg2rad($lat2)))); if ($entity=="miles") { $distance=$distance / 1.609344; } return $distance; } /** * Interpolate between points to find out the location on a * certain moment * * This is an approximate calculation * and could be very inaccurate if the distance between the * points is large, therefore you can give a max distance in km * * The longer time there is between 2 point the smaller the * chance is you actually travelled in a straight line between * the points, so you can also give a max time, in seconds. * @param point is the point where you are at t1 * @param point is the point where you are at t2 * @param int t3 is the time you want to calculate the position for * @param int maximum distance to to calculation for * @param string entity of distances ("km" or "miles") * @param int maximum time between two points * @return point this function will return where you are at t3 * @todo the "return false" should both be changed into an Exception */ public static function interpolate(point $p1, point $p2, $t3, $maxdist=null, $entity="km", $maxtime=null) { $t1 = strtotime($p1->get("datetime")); $t2 = strtotime($p2->get("datetime")); if ((!($t2 >= $t3 && $t3 >= $t1)) || ($maxtime && (abs($t1 - $t2) > $maxtime))) { return false; } if ($maxdist) { $dist=$p1->getDistanceTo($p2, $entity); if ($dist > $maxdist) { return false; } } $lat1=$p1->get("lat"); $lon1=$p1->get("lon"); $lat2=$p2->get("lat"); $lon2=$p2->get("lon"); // Calculate the deltas $dlat=$lat2-$lat1; $dlon=$lon2-$lon1; $dt=$t2-$t1; $dt3=$t3-$t1; $lat3=$lat1 + (($dlat/$dt) * $dt3); $lon3=$lon1 + (($dlon/$dt) * $dt3); $p3 = new point(); $p3->set("lat", $lat3); $p3->set("lon", $lon3); return $p3; } } ?> zoph-v0.9.19/php/classes/geo/track.inc.php000066400000000000000000000156371415176210700203630ustar00rootroot00000000000000updatePoints(); $this->insertPoints(); } /** * Lookup a track in the database. * * This will fill the object with the info already in the db */ public function lookup() { $result=parent::lookup(); $this->points=$this->getPoints(); return $result; } /** * Deletes a track * * Also deletes all point in the track * @see point */ public function delete() { if (!$this->getId()) { return; } parent::delete(); $qry=new delete(array("pt" => "point")); $qry->where(new clause("track_id=:trackid")); $qry->addParam(new param(":trackid", (int) $this->getId(), PDO::PARAM_INT)); $qry->execute(); } /** * Add a new point to a track * @param point point to add */ public function addPoint(point $point) { $point->set("track_id", $this->get("track_id")); $this->points[]=$point; } /** * This sets the track_id on all points in this track */ private function updatePoints() { foreach ($this->points as $point) { $point->set("track_id", $this->get("track_id")); } } /** * Insert points into database */ private function insertPoints() { foreach ($this->points as $point) { $point->insert(); } } /** * Read a GPX file and create track & point objects from there * @param string filename to read GPX from */ public static function getFromGPX($file) { $track = new track; if (!class_exists("XMLReader")) { throw new Exception("Class XMLReader not found"); } $xml=new XMLReader(); $xml->open($file); $track->set("name", substr($file, strrpos($file, "/") + 1, strrpos($file, "."))); $xml->read(); if ($xml->name != "gpx") { throw new gpxException($file . " is not a GPX file"); } else { $stack[]="gpx"; } while ($xml->read()) { if ($xml->nodeType==XMLReader::ELEMENT) { // Keep track of the current open tags if (!$xml->isEmptyElement) { $stack[]=$xml->name; } switch ($xml->name) { case "name": $current=$stack[count($stack) - 2]; if ($current=="gpx") { // only set the name if we're in $xml->read(); $track->set("name", $xml->value); } break; case "trkpt": // For now we are ignoring multiple tracks or segments // in the same file and we simply look at the points $xml_point=$xml->readOuterXML(); $point=point::readFromXML($xml_point); $track->addpoint($point); break; default: // not (yet?) supported break; } } else if ($xml->nodeType==XMLReader::END_ELEMENT) { $element=array_pop($stack); if ($element!=$xml->name) { throw new gpxException("GPX not well formed: expected <$element>, " . "found <$xml->name>"); } } } return $track; } /** * Get all points for this track * @return array Array of all points in this track. */ public function getPoints() { if (sizeof($this->points)==0) { $this->points=point::getRecords("datetime", array("track_id" => $this->get("track_id"))); } return $this->points; } /** * Get the first point from a track * @return point first point */ public function getFirstPoint() { $first=$this->getPoints()[0]; if (($first instanceof point)) { return $first; } else { return new point; } } /** * Get the last point from a track * @return point last point */ public function getLastPoint() { $points=$this->getPoints(); $last=array_pop($points); if (($last instanceof point)) { return $last; } else { return new point; } } /** * Get the number of points in a track * @return int count */ public function getPointCount() { return count($this->getPoints()); } /** * Get array that can be used to generate view for this track * @return array Display array */ public function getDisplayArray() { $first=$this->getFirstPoint(); $last=$this->getLastPoint(); $count=$this->getPointCount(); $return[translate("name")] = $this->get("name"); $return[translate("time of first point")] = $first->get("datetime") . " UTC"; $return[translate("time of last point")] = $last->get("datetime") . " UTC"; $return[translate("number of points")] = $count; return $return; } } ?> zoph-v0.9.19/php/classes/group.inc.php000066400000000000000000000160371415176210700176340ustar00rootroot00000000000000get("group_name"); } /** * Get permissions for a group * @param album Album to lookup permissions for * @return permissions Permissions object */ public function getGroupPermissions(album $album) { $gp = new permissions($this->getId(), $album->getId()); if ($gp->lookup()) { return $gp; } return null; } /** * Get albums associated with this permissions object * @return array of albums */ public function getAlbums() { $qry=new select(array("gp" => "group_permissions")); $qry->addFields(array("album_id")); $qry->where(new clause("group_id=:groupid")); $qry->addParam(new param(":groupid", (int) $this->getId(), PDO::PARAM_INT)); return album::getRecordsFromQuery($qry); } /** * Get display array * Get an array of properties to display * @return array properties */ public function getDisplayArray() { return array( translate("group") => $this->get("group_name"), translate("description") => $this->get("description"), translate("members") => implode("
", $this->getMemberLinks()) ); } /** * Create an array describing permissions for all groups * for display or edit * @param bool Return array of albums instead of array of permissions * @return array permissions */ public function getPermissionArray($getAlbum=false) { $albums = album::getSelectArray(); $perms=array(); foreach ($albums as $id => $name) { if (!$id || $id == 1) { continue; } $album=new album((int) $id); $permissions = $this->getGroupPermissions($album); if ($permissions) { if ($getAlbum) { $perms[]=$album; } else { $perms[]=$permissions; } } } return $perms; } /** * Get members of this group * @return array of users */ public function getMembers() { $qry=new select(array("gu" => "groups_users")); $qry->addFields(array("user_id")); $qry->where(new clause("group_id=:groupid")); $qry->addParam(new param(":groupid", (int) $this->getId(), PDO::PARAM_INT)); $members=user::getRecordsFromQuery($qry); $return=array(); foreach ($members as $member) { $member->lookup(); $return[]=$member; } return $return; } /** * Add a member to a group * @param user User to add */ public function addMember(user $user) { $qry=new insert(array("gu" => "groups_users")); $qry->addParams(array( new param(":group_id", (int) $this->getId(), PDO::PARAM_INT), new param(":user_id", (int) $user->getId(), PDO::PARAM_INT) )); $qry->execute(); } /** * Remove a member from a group * @param user User to remove */ public function removeMember(user $user) { $qry=new delete(array("gu" => "groups_users")); $where=new clause("group_id=:groupid"); $where->addAnd(new clause("user_id=:userid")); $qry->addParams(array( new param(":groupid", (int) $this->getId(), PDO::PARAM_INT), new param(":userid", $user->getId(), PDO::PARAM_INT) )); $qry->where($where); $qry->execute(); } /** * Get an array of users that are NOT a member of this group * @return array of users */ private function getNonMembers() { $userIds=array(); $memberIds=array(); $users=user::getAll(); $members=$this->getMembers(); foreach ($users as $user) { $userIds[]=$user->getId(); } if ($members) { foreach ($members as $member) { $memberIds[]=$member->getId(); } $nonMemberIds=array_diff($userIds, $memberIds); } else { $nonMemberIds=$userIds; } $nonMembers=array(); foreach ($nonMemberIds as $id) { $nonMembers[]=new user($id); } return $nonMembers; } /** * Create a pulldown to add new members to this group * @param string name for the pulldown field * @return template Pulldown */ public function getNewMemberPulldown($name) { $valueArray=array(); $newMembers=$this->getNonMembers(); $valueArray[0]=null; foreach ($newMembers as $nm) { $nm->lookup(); $valueArray[$nm->getId()]=$nm->getName(); } return template::createPulldown($name, null, $valueArray); } /** * Get links to all members of this group * @return array array of links */ public function getMemberLinks() { $links=array(); $members=$this->getMembers(); if ($members) { foreach ($members as $member) { $member->lookup(); $links[]=$member->getLink(); } } return $links; } } ?> zoph-v0.9.19/php/classes/group/000077500000000000000000000000001415176210700163445ustar00rootroot00000000000000zoph-v0.9.19/php/classes/group/controller.inc.php000066400000000000000000000065271415176210700220220ustar00rootroot00000000000000request["group_id"]); $group->lookup(); $this->setObject($group); $this->doAction(); } protected function actionDisplay() { $this->view = new view\display($this->request, $this->object); } protected function actionDelete() { $this->view = new view\confirm($this->request, $this->object); } protected function actionEdit() { $this->view = new view\update($this->request, $this->object); } protected function actionNew() { $this->view = new view\update($this->request, $this->object); } protected function actionConfirm() { parent::actionConfirm(); $this->view = new view\redirect($this->request, $this->object); $this->view->setRedirect("groups.php"); } /** * Action: update * The update action processes a form as generated after the "edit" action. * The subsequently called view displays the object. * takes care of adding and removing members of the group */ protected function actionUpdate() { $this->object->setFields($this->request->getRequestVars()); if (isset($this->request["_member"]) && ((int) $this->request["_member"] > 0)) { $this->object->addMember(new user((int) $this->request["_member"])); } if (is_array($this->request["_removeMember"])) { foreach ($this->request["_removeMember"] as $user_id) { $this->object->removeMember(new user((int) $user_id)); } } $this->object->update(); $this->view = new view\update($this->request, $this->object); } /** * Action: insert * The insert action processes a form as generated after the "new" action. * The subsequently called view displays a form to make more changes to the group. * this is a change from the generic controller, because group access rights can only * be modified after insertion. */ protected function actionInsert() { parent::actionInsert(); $this->view = new view\update($this->request, $this->object); } } zoph-v0.9.19/php/classes/group/view/000077500000000000000000000000001415176210700173165ustar00rootroot00000000000000zoph-v0.9.19/php/classes/group/view/confirm.inc.php000066400000000000000000000031231415176210700222330ustar00rootroot00000000000000 "group.php?_action=confirm&group_id=" . $this->group->getId(), translate("cancel") => "group.php?group_id=" . $this->group->getId() ); } /** * Output view */ public function view() { return new template("confirm", array( "title" => translate("delete group"), "actionlinks" => $this->getActionlinks(), "mainActionlinks" => null, "obj" => $this->group, )); } } zoph-v0.9.19/php/classes/group/view/display.inc.php000066400000000000000000000036751415176210700222570ustar00rootroot00000000000000 "group.php?_action=edit&group_id=" . $this->group->getId(), translate("delete") => "group.php?_action=delete&group_id=" . $this->group->getId(), translate("new") => "group.php?_action=new", translate("return") => "groups.php" ); } /** * Get view * @return template view */ public function view() { return new template("displayGroup", array( "title" => $this->getTitle(), "actionlinks" => $this->getActionlinks(), "obj" => $this->group, "view" => "album", "fields" => $this->group->getDisplayArray(), "watermark" => conf::get("watermark.enable"), "permissions" => $this->group->getPermissionArray() )); } } zoph-v0.9.19/php/classes/group/view/redirect.inc.php000066400000000000000000000026511415176210700224040ustar00rootroot00000000000000redirect); } /** * Output view */ public function view() { return null; } public function getActionlinks() { return array(); } /** * Set the page to redirect to * @param string redirect target */ public function setRedirect($redirect) { $this->redirect=$redirect; } } zoph-v0.9.19/php/classes/group/view/update.inc.php000066400000000000000000000072001415176210700220600ustar00rootroot00000000000000request["_action"] == "new") { return array( translate("delete") => "group.php?_action=delete&group_id=" . $this->group->getId(), translate("return") => "groups.php" ); } else { return array( translate("delete") => "group.php?_action=delete&group_id=" . $this->group->getId(), translate("new") => "group.php?_action=new", translate("return") => "group.php?group_id=" . $this->group->getId() ); } } /** * Get view * @return template view */ public function view() { $_action = $this->request["_action"]; $tpl=new template("edit", array( "title" => $this->getTitle(), "actionlinks" => $this->getActionlinks(), "mainActionlinks" => null, "obj" => $this->group, )); $form=new form("form", array( "formAction" => "group.php", "onsubmit" => null, "action" => $_action == "new" ? "insert" : "update", "submit" => translate("submit") )); $form->addInputHidden("group_id", $this->group->getId()); $form->addInputText("group_name", $this->group->getName(), translate("group name"), sprintf(translate("%s chars max"), 32), 32); $form->addInputText("description", $this->group->get("description"), translate("description"), sprintf(translate("%s chars max"), 128), 128, 32); if ($_action != "new") { $curMembers=$this->group->getMembers(); $members=new block("members", array( "members" => $curMembers, "group" => $this->group )); $form->addBlock($members); $tpl->addBlock($form); $view=new editPermissions($this->group); $tpl->addBlock($view->view()); } else { $tpl->addBlock(new block("message", array( "class" => "info", "text" => translate("After this group is created it can be given access to albums.") ))); $tpl->addBlock($form); } return $tpl; } public function getTitle() { if ($this->request["_action"] == "new") { return translate("New group"); } else { return parent::getTitle(); } } } zoph-v0.9.19/php/classes/group/view/view.inc.php000066400000000000000000000035301415176210700215520ustar00rootroot00000000000000request = $request; $this->group = $group; } /** * Get headers - null, so default headers will be used * @return null */ public function getHeaders() { return null; } /** * Get title * @return string title */ public function getTitle() { return $this->group->getName(); } /** * Get actionlinks * @return array actionlinks */ abstract protected function getActionlinks(); /** * Output view * @return template */ abstract public function view(); } zoph-v0.9.19/php/classes/import/000077500000000000000000000000001415176210700165225ustar00rootroot00000000000000zoph-v0.9.19/php/classes/import/base.inc.php000066400000000000000000000167331415176210700207270ustar00rootroot00000000000000 0) { $msg=implode($output, "
"); throw new \importAutorotException($msg); } } /** * Import photos * * Takes an array of files and an array of vars and imports them in Zoph * @param Array Files to be imported * @param Array Vars to be applied to the photos. */ public static function photos(Array $files, Array $vars) { $photos=array(); $total=sizeof($files); $cur=0; if (isset($vars["_path"])) { $path=file::cleanupPath("/" . $vars["_path"] . "/"); if (strpos($path, "..") !== false) { log::msg("Illegal characters in path", log::FATAL, log::IMPORT); die(); } } else { $path=""; } foreach ($files as $md5 => $file) { static::progress($cur, $total); $cur++; if ($file instanceof photo) { $photo=$file; $file=$photo->file["orig"]; } else if ($file instanceof file) { $photo=new photo(); } $mime=$file->getMime(); if (conf::get("import.cli.exif")===true && $mime=="image/jpeg") { $exif=process_exif($file); if ($exif) { $photo->setFields($exif); } } if (isset($vars["rating"])) { $rating=$vars["rating"]; if (!(is_numeric($rating) && (1 <= $rating) && ($rating <= 10))) { unset($rating); } unset($vars["rating"]); } if (isset($vars["field"]) && is_array($vars["_field"])) { foreach ($vars["_field"] as $key => $field) { $vars[$field]=$vars["field"][$key]; } unset($vars["_field"]); unset($vars["field"]); } if ($vars) { $photo->setFields($vars); } if (strlen(trim($photo->get("date")))==0) { $date=date("Y-m-d", filemtime($file)); log::msg("Photo has no date set, using filedate (" . $date . ").", log::NOTIFY, log::IMPORT); $photo->set("date", $date); } if (strlen(trim($photo->get("time")))==0) { $time=date("H:i:s", filemtime($file)); log::msg("Photo has no time set, using time from filedate (" . $time . ").", log::NOTIFY, log::IMPORT); $photo->set("time", $time); } if (isset($photo->_path)) { $photo->set("path", $path . "/" . $photo->_path); unset($photo->_path); } else { $photo->set("path", $path); } try { $photo->import($file); } catch (\fileException $e) { log::msg($e->getMessage(), log::FATAL); } if (conf::get("import.cli.thumbs")===true) { try { $photo->thumbnail(false); } catch (\Exception $e) { echo $e->getMessage(); } } if ($photo->insert()) { $rated = false; if (conf::get("import.cli.size")===true) { $photo->updateSize(); } $photo->update(); $photo->updateRelations($vars, "_id"); if (strlen($md5) > 10) { $counter = 0; $categories = $vars["_category_" . $md5 ] ?? false; if (is_array($categories)) { foreach ($categories as $category) { $photo->addTo(new category($category)); } } $phRating = $vars["_rating_" . $md5 ] ?? false; if ($phRating && is_numeric($phRating) && ($phRating >= 1) && ($phRating <= 10)) { $photo->rate($phRating); $rated = true; } } if (isset($rating) && !$rated) { $photo->rate($rating); } if (conf::get("import.cli.hash")===true) { try { $photo->getHash(); } catch (\Exception $e) { echo $e->getMessage(); } } $photos[]=$photo; } else { echo translate("Insert failed.") . "
\n"; } } return $photos; } /** * Import an XML file * * @param string MD5 hash of the filename to import * * This function tries to recognize the XML file by validating them against .xsd files * For now only GPX (1.0 and 1.1) files are recognized. */ public static function XMLimport(file $file) { $xml=new DOMDocument; $xml->Load($file); $schemas = array ( "gpx 1.0" => "xml/gpx10.xsd", "gpx 1.1" => "xml/gpx11.xsd" ); foreach ($schemas as $name => $schema) { if (@$xml->schemaValidate(settings::$php_loc . "/" . $schema)) { log::msg(basename($file) ." is a valid " . $name . " file", log::NOTIFY, log::IMPORT); $xmltype=$name; } } if (!isset($xmltype)) { throw new \importFileNotImportableException(basename($file) . " is not a known XML file."); } else { switch($name) { case "gpx 1.0": case "gpx 1.1": $track=track::getFromGPX($file); $track->insert(); $file->delete(); break; } } } /** * Progress bar * Does not display anything by default, but this function can be redefined * in a child class. * * @param int current * @param int total */ public static function progress($cur, $total) { return 0; } } zoph-v0.9.19/php/classes/import/cli.inc.php000066400000000000000000000050031415176210700205500ustar00rootroot00000000000000=60) { $calccur=$cur/$total*60; $dispcur=floor($calccur); $disptotal=60; } else { $calccur=0; $dispcur=$cur; $disptotal=$total; } $display="["; $display.=str_repeat("|", $dispcur); $rem=round($calccur - $dispcur,2); $num=$total/$disptotal; if ($num > 3) { if ($rem > 0.333 && $rem < 0.666) { $display.="."; } else if ($rem > 0.6666 && $rem < 0.999) { $display.=":"; } else if ($rem > 0.999) { $display.="|"; } } else if ($num == 2) { if ($rem >= 0.5) { $display.="."; } } $display=str_pad($display, $disptotal + 1); $display.="]"; $perc=floor($cur / $total * 100); $display.= " [ $cur / $total (" . $perc . "%) ]"; echo $display; echo str_repeat(chr(8), strlen($display)); } } zoph-v0.9.19/php/classes/import/web.inc.php000066400000000000000000000422131415176210700205620ustar00rootroot00000000000000upload_id=$upload_id; } /** * Import photos * * Takes an array of files and an array of vars and imports them in Zoph * @param Array Files to be imported * @param Array Vars to be applied to the photos. */ public static function photos(Array $files, Array $vars) { // thumbnails have already been created, no need to repeat... conf::set("import.cli.thumbs", false); conf::set("import.cli.exif", true); conf::set("import.cli.size", true); parent::photos($files, $vars); } /** * Return a translated, textual error message from a PHP upload error * * @param int PHP upload error */ public static function handleUploadErrors($error) { $errortext=translate("File upload failed") . "
"; switch ($error) { case UPLOAD_ERR_INI_SIZE: $errortext.=sprintf(translate("The uploaded file exceeds the " . "upload_max_filesize directive (%s) in php.ini."), ini_get("upload_max_filesize")); $errortext.=" " . sprintf(translate("This may also be caused by " . "the post_max_size (%s) in php.ini."), ini_get("post_max_size")); break; case UPLOAD_ERR_FORM_SIZE: $errortext.=sprintf(translate("The uploaded file exceeds the maximum " . "filesize setting in config.inc.php (%s)."), conf::get("import.maxupload")); break; case UPLOAD_ERR_PARTIAL: $errortext.=translate("The uploaded file was only partially uploaded."); break; case UPLOAD_ERR_NO_FILE: $errortext.=translate("No file was uploaded."); break; case UPLOAD_ERR_NO_TMP_DIR: $errortext.=translate("Missing a temporary folder."); break; case UPLOAD_ERR_CANT_WRITE: $errortext.=translate("Failed to write to disk"); break; case UPLOAD_ERR_EXTENSION: $errortext.=translate("A PHP extension stopped the upload. Don't ask me why."); break; default: $errortext.=translate("An unknown file upload error occurred."); } return $errortext; } /** * Process uploaded file * * Catches the uploaded file, runs some checks and moves it into the * upload directory. * @param array PHP _FILE var with data about the uploaded file */ public static function processUpload($file) { $filename=$file["name"]; $tmp_name=$file["tmp_name"]; $error=$file["error"]; if ($error) { // should do some nicer printing to this error some time log::msg(static::handleUploadErrors($error), log::FATAL, log::IMPORT); return false; } $file=new file($tmp_name); $mime=$file->getMime(); if (!$file->type) { log::msg("Illegal filetype: $mime", log::FATAL, log::IMPORT); return false; } $dir=conf::get("path.images") . DIRECTORY_SEPARATOR . conf::get("path.upload"); $realDir=realpath($dir); if ($realDir === false) { log::msg($dir . " does not exist, creating...", log::WARN, log::IMPORT); try { file::createDirRecursive($dir); } catch (\fileDirCreationFailedException $e) { log::msg($dir . " does not exist, and I can not create it. (" . $e->getMessage() . ")", log::FATAL, log::IMPORT); die(); } // doublecheck if path really has been correctly created. $realDir=realpath($dir); if ($realDir === false) { log::msg($dir . " does not exist, and I can not create it.", log::WARN, log::FATAL); } } $dir=$realDir; $dest=$dir . "/" . basename($filename); if (is_writable($dir)) { if (!file_exists($dest)) { move_uploaded_file($tmp_name, $dest); } else { log::msg("A file named " . $filename . " already exists in " . $dir . "", log::FATAL, log::IMPORT); } } else { log::msg("Directory " . $dir . " is not writable", log::FATAL, log::IMPORT); return false; } return true; } /** * Processes a file * * Depending on file type it will either launch a resize or an unpack * function. * This function is called from a javascript call * @param string MD5 hash of the file name. */ public static function processFile($md5) { // continue when hitting fatal error. log::$stopOnFatal=false; $dir=conf::get("path.images") . "/" . conf::get("path.upload") . "/"; $file=file::getFromMD5($dir, $md5); if ($file instanceof file) { $mime=$file->getMime(); $type=$file->type; } else { $type="unknown (file not found)"; } switch($type) { case "image": if ($mime=="image/jpeg" && conf::get("import.rotate")) { static::autorotate($file); } static::resizeImage($file); $return=null; break; case "archive": $return=static::unpackArchive($file); break; case "gpx": static::XMLimport($file); $return=null; break; default: log::msg("Unknown filetype " . $type . " for file" . $file, log::FATAL, log::IMPORT); $return=false; break; } return $return; } /** * Automatically rotate images based on EXIF tag. * @param string filename */ protected static function autorotate($file) { try { parent::autorotate($file); } catch (\importAutorotException $e) { touch($file . ".zophignore"); log::msg($e->getMessage(), log::FATAL, log::IMPORT); die; } } /** * Unpack archive of different types * *WARNING* this function is *not* safe to run on unchecked user-input * use processFile() as a wrapper for this function * @see processFile * @param string full path to file */ private static function unpackArchive(file $file) { $dir = conf::get("path.images") . "/" . conf::get("path.upload"); $mime=$file->getMime(); switch($mime) { case "application/zip": $extr = conf::get("path.unzip"); $msg = "Unzip command"; break; case "application/x-tar": $extr = conf::get("path.untar"); $msg = "Untar command"; break; case "application/gzip": case "application/x-gzip": $extr = conf::get("path.ungz"); $msg = "Ungzip command"; break; case "application/x-bzip2": $extr = conf::get("path.unbz"); $msg = "Unbzip command"; break; } if (empty($extr)) { log::msg("To be able to process an archive of type " . $mime . ", you need to set \"" . $msg . "\" in the configuration screen " . " to a program that can unpack this file.", log::FATAL, log::IMPORT); touch($file . ".zophignore"); return false; } $upload_id=uniqid("zoph_"); $unpack_dir=$dir . "/" . $upload_id; $unpack_file=$unpack_dir . "/" . basename($file); ob_start(); mkdir($unpack_dir); rename($file, $unpack_file); $cmd = "cd " . escapeshellarg($unpack_dir) . " && " . $extr . " " . escapeshellarg($unpack_file) . " 2>&1"; system($cmd); if (file_exists($unpack_file)) { unlink($unpack_file); } $output=ob_end_clean(); log::msg($output, log::NOTIFY, log::IMPORT); $files=file::getFromDir($unpack_dir, true); foreach ($files as $import_file) { $type=$import_file->type; if ($type == "image" || $type == "archive" || $type == "xml") { $import_file->setDestination($dir); try { $import_file->move(); } catch (\fileException $e) { echo $e->getMessage() . "
\n"; } } } static::removeDir($unpack_dir); } /** * Remove dirs * Remove temporary directories that were created when unpacking an archive * Only removes emptry directories, a warning will be displayed when there are still files * left in the directory. This could happen when something went wrong during import or * non-image files were present in the archive. * @param string directory to traverse. */ private static function removeDir(string $dir) { foreach (glob($dir . "/*" , GLOB_ONLYDIR) as $subdir) { static::removeDir($subdir); } rmdir($dir); } /** * Resize an image before import * * @param string filename */ private static function resizeImage($file) { log::msg("resizing" . $file, log::DEBUG, log::IMPORT); $photo = new photo(); $photo->set("path", conf::get("path.upload")); $photo->set("name", basename($file)); ob_start(); $dir=conf::get("path.images") . "/" . conf::get("path.upload"); $thumb_dir=$dir. "/" . THUMB_PREFIX; $mid_dir=$dir . "/" . MID_PREFIX; if (!file_exists($thumb_dir)) { mkdir($thumb_dir); } else if (!is_dir($thumb_dir)) { log::msg("Cannot create " . $thumb_dir . ", file exists.", log::FATAL, log::IMPORT); } if (!file_exists($mid_dir)) { mkdir($mid_dir); } else if (!is_dir($mid_dir)) { log::msg("Cannot create " . $mid_dir . ", file exists.", log::FATAL, log::IMPORT); } try { $photo->thumbnail(); } catch (\Exception $e) { echo "Thumb could not be made: " . $e->getMessage(); touch($file . ".zophignore"); } log::msg("Thumb made succesfully.", log::DEBUG, log::IMPORT); $log=ob_get_contents(); ob_end_clean(); echo $log; } /** * Get XML for Import */ public static function getXML($search) { if ($search=="thumbs") { return static::getThumbsXML(); } } /** * Generate an XML file with thumbs in the import dir */ public static function getThumbsXML() { $xml=new \DOMDocument('1.0','UTF-8'); $root=$xml->createElement("files"); $dir=conf::get("path.images") . DIRECTORY_SEPARATOR . conf::get("path.upload"); $files = file::getFromDir($dir); foreach ($files as $file) { unset($icon); unset($status); unset($rating); $subject = null; $md5=$file->getMD5(); $type=$file->type; switch ($type) { case "image": $thumb=THUMB_PREFIX . DIRECTORY_SEPARATOR . THUMB_PREFIX . "_" . $file->getName(); $mid=MID_PREFIX . DIRECTORY_SEPARATOR . MID_PREFIX . "_" . $file->getName(); if (file_exists($dir . DIRECTORY_SEPARATOR . $thumb) && file_exists($dir . DIRECTORY_SEPARATOR . $mid)) { $status="done"; $xmp = new xmpreader($file); $data=new xmpdecoder($xmp->getXMP()); if (sizeof($data)===0) { $data = new xmpdecoder($xmp->getXMPfromSidecar()); } $subjects = $data->getSubjects(); $rating = $data -> getRating(); } else { $icon=template::getImage("icons/pause.png"); $status="waiting"; } break; case "archive": $icon=template::getImage("icons/archive.png"); $status="waiting"; break; case "gpx": $icon=template::getImage("icons/tracks.png"); $status="done"; break; case "ignore": $icon=template::getImage("icons/error.png"); $status="ignore"; break; case "xmp": // Don't show XMP files // Maybe at some point we can create an icon with the photo showing // an sidecar file was found for this photo continue 2; break; } $xmlfile=$xml->createElement("file"); $xmlfile->setAttribute("name", $file->getName()); $xmlfile->setAttribute("type",$type); $xmlmd5=$xml->createElement("md5", $md5); $xmlfile->appendChild($xmlmd5); if (!empty($icon)) { $xmlicon=$xml->createElement("icon", $icon); $xmlfile->appendChild($xmlicon); } if (!empty($status)) { $xmlstatus=$xml->createElement("status", $status); $xmlfile->appendChild($xmlstatus); } if (isset($subjects) && !empty($subjects)) { $xmlsubjects=$xml->createElement("subjects"); foreach ($subjects as $subject) { $xmlsubjects->appendChild($xml->createElement("subject", $subject)); } $xmlfile->appendChild($xmlsubjects); } if (isset($rating)) { $xmlfile->appendChild($xml->createElement("rating", $rating)); } $root->appendChild($xmlfile); } $xml->appendChild($root); return $xml; } /** * Retry making of thumbnails * * This function reacts to a click on the "retry" link in the thumbnail * list on the import page. It looks up which file is referenced by the * supplied MD5 and deleted thumbnail, mid and 'ignore" files, this will * cause the webinterface to retry making thumbnail and midsize images * * @param string md5 hash of the filename */ public static function retryFile($md5) { $dir=conf::get("path.images") . "/" . conf::get("path.upload"); $file=file::getFromMD5($dir, $md5); // only delete "related files", not the referenced file. $file->delete(true, true); } /** * Delete a file * * Deletes a file referenced by the MD5 hash of the filename and all * related files, such as thumbnail, midsize images and "ignore" files. * @param string md5 hash of the filename */ public static function deleteFile($md5) { $dir=conf::get("path.images") . "/" . conf::get("path.upload"); $file=file::getFromMD5($dir, $md5); $file->delete(true); } /** * Get a file list from a list of MD5 hashes. * * Take a list of MD5 hashes (in $vars["_import_image"]) and return an * array of @see file objects * @param Array $vars */ public static function getFileList(Array $import) { foreach ($import as $md5) { $file=file::getFromMD5(conf::get("path.images") . "/" . conf::get("path.upload"), $md5); if (!empty($file)) { $files[$md5]=$file; } } if (is_array($files)) { return $files; } else { log::msg("No files specified", log::FATAL, log::IMPORT); return false; } } } ?> zoph-v0.9.19/php/classes/language.inc.php000066400000000000000000000244151415176210700202620ustar00rootroot00000000000000 * # Optional comments * # English=Translation * # English=Translation * The file MUST be UTF8 encoded. * @author Jeroen Roos * @package Zoph */ class language { public $iso; public $name; private $filename; private $translations=array(); /** * @var string This defines what the base language is, the language the strings in the * sourcecode are in. */ public static $base="en"; public static $base_name="English"; const LANG_DIR="lang"; /** * @param string iso ISO definition of the language, usually 2 letters or * two letters dash two letters, for example nl en-ca. * This is also the name of the file it will try to read. */ function __construct($iso) { $this->name=$iso; $this->filename=settings::$php_loc . "/" . static::LANG_DIR. "/" . $iso; $this->iso=strtolower($iso); } /** * Open the file * @return int filedescriptor file */ private function openFile() { if (file_exists($this->filename) && is_readable($this->filename)) { try { $file=fopen($this->filename, "r"); } catch (Exception $e) { log::msg("Could not read language file $this->filename: " . "
" . $e->getMessage() . "
", log::ERROR, log::LANG); return false; } return $file; } else { return false; } } /** * Read and parse the header of the file. * Unless DEBUG is on, nothing will be mentionned about files with * a wrong header, they will be silently ignored. * @return bool true|false */ function readHeader() { $file=$this->openFile(); if (!$file) { return false; } $header=fgets($file); $zoph_header="# zoph language file - "; if (strtolower(substr($header,0,23))!=$zoph_header) { log::msg("Incorrect language header in " . $this->filename . "", log::ERROR, log::LANG); log::msg("
" . $header. "
", log::DEBUG, log::LANG); return false; } else { $this->name=substr($header,23); fclose($file); return true; } } /** * Read the strings from the file * @return bool true|false */ function read() { $file=$this->openFile(); if (!$file) { return false; } while ($line=fgets($file)) { if ($line[0] == "#") { log::msg("" . $this->iso . ":" . $line, log::MOREDEBUG, log::LANG); } else { $strings=explode("=",$line); $this->translations[$strings[0]]=trim($strings[1]); } } fclose($file); return true; } /** * Translate the given string * @param string|array The string or array to be translated * @param bool If true add [tr] before any string that cannot be * translated. * @return string The translated string */ function translate($string, $error = true) { $tag=""; if (is_array($string)) { return $this->translateArray($string, $error); } if (array_key_exists($string, $this->translations)) { return trim($this->translations[$string]); } else { if ($error && !($this->iso==static::$base)) { $tag = "[tr] "; } return $tag . $string; } } /** * Translate an array * translates all the values in an array, not the keys. * @param array The array to be translated * @param bool If true add [tr] before any string that cannot be * translated. * @return string The translated array */ private function translateArray($array, $error = true) { $tr=array(); foreach ($array as $key=>$string) { $tr[$key]=translate($string, $error); } return $tr; } /** * Get all languages * @return array array of language objects */ public static function getAll() { $langs=array(); $dir=settings::$php_loc . "/" . static::LANG_DIR; if (is_dir($dir) && is_readable($dir)) { foreach (glob($dir . "/*") as $filename) { if (!is_dir($filename) && is_readable($filename)) { $iso=basename($filename); if ($iso == strtolower($iso)) { # making filename lowercase, so we won't include # any capitalized filenames... Zoph will not able # to find them back later... # is isocode nl file NL Nl or nl? $lang=new language($iso); if ($lang->readHeader()) { $langs[$iso]=$lang; } } else { log::msg("Language files should have lowercase names, cannot open " . $filename . "", log::WARN, log::LANG); } } else { log::msg("Cannot read " . $filename . ", skipping. ", log::ERROR, log::LANG); } } } else { log::msg("Cannot read language dir!", log::WARN, log::LANG); } $base_lang=new language(static::$base); $base_lang->name=static::$base_name; $langs[static::$base]=$base_lang; ksort($langs); return $langs; } /** * Check if file for a certain language exists * @param string ISO code for language * @return string null|iso */ public static function exists($iso) { $dir=settings::$php_loc . "/" . static::LANG_DIR; $file=$dir . '/' . $iso; if (file_exists($file) && is_file($file)) { return $iso; } else { return null; } } /** * Load the first available language, or fall back to a default * @param array Array of languages to try. * @return language language object */ public static function load($langs) { array_push($langs, conf::get("interface.language"), static::$base); foreach ($langs as $l) { log::msg("Trying to load language: " . $l . "", log::DEBUG, log::LANG); if (static::exists($l)) { $lang=new language($l); if ($lang->readHeader() && $lang->read()) { log::msg("Loaded language: " . $l . "
", log::DEBUG, log::LANG); return $lang; } } else if ($l==static::$base) { # If it is the base language, no file needs to exist log::msg("Using base language: " . $l . "", log::NOTIFY, log::LANG); $lang=new language($l); return $lang; } } log::msg("No languages found, falling back to default: " . static::$base . "", log::NOTIFY, log::LANG); return new language(static::$base); } /** * Get HTTP_ACCEPT_LANG and interprete it * @return array array of languages in preference order */ public static function httpAccept() { $langs=array(); $genlangs=array(); $return=array(); if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { $accept_langs=explode(",", $_SERVER["HTTP_ACCEPT_LANGUAGE"]); foreach ($accept_langs as $al) { # Some browers add a 'quality' identifier to indicate # the preference of this language, something like en;q=1.0 $l=explode(";",$al); $langs[]=strtolower($l[0]); # A user could select a "sublanguage" such as en-gb for British # English, or de-ch for Swiss German to make sure that # Zoph offers these users English or German, unless the more # specific one is available (Zoph has a Canadian English # translation for example), we add both en-gb and en to the list if (strpos($l[0], "-")) { $genlang=explode("-", $l[0]); $genlangs[]=strtolower($genlang[0]); } } $return=array_unique(array_merge($langs, $genlangs)); log::msg("Client accepts language(s):: " . $_SERVER["HTTP_ACCEPT_LANGUAGE"], log::DEBUG, log::LANG); log::msg("Zoph's interpretation: " . implode(", ", $return), log::DEBUG, log::LANG); } return $return; } public function getAllTranslations() { return $this->translations; } public static function getCurrentISO() { global $lang; return $lang->iso; } } /** * Translate the given string * @param string The string to be translated * @param bool If true add [tr] before any string that cannot be * translated. * @return string The translated string */ function translate($str, $error=true){ global $lang; if ($lang instanceof language) { return $lang->translate($str, $error); } else { return $str; } } ?> zoph-v0.9.19/php/classes/mailMime.inc.php000066400000000000000000000511531415176210700202300ustar00rootroot00000000000000 and * @author Sascha Schumann * @author Richard Heyes * @author Tomas V.V.Cox (port to PEAR) * * @author Jeroen Roos * * @package Zoph */ /** * Mime mail composer class. Can handle: text and html bodies, embedded html * images and attachments. * @author Tobias Ratschiller and * @author Sascha Schumann * @author Richard Heyes * @author Tomas V.V.Cox (port to PEAR) * * @author Jeroen Roos * * @package Zoph */ class mailMime { /** @var string Contains the plain text part of the email */ private $txtbody; /** @var string Contains the html part of the email */ private $htmlbody; /** @var array list of the attached images */ private $html_images = array(); /** @var array list of the attachments */ private $parts = array(); /** @var array Build parameters */ private $build_params = array(); /** @var array Headers for the mail */ private $headers = array(); /** * Constructor function */ public function __construct() { $this->build_params = array( 'text_encoding' => '7bit', 'html_encoding' => 'quoted-printable', '7bit_wrap' => 998, 'html_charset' => 'utf-8', 'text_charset' => 'utf-8', 'head_charset' => 'utf-8' ); } /** * Accessor function to set the body text. Body text is used if * it's not an html mail being sent or else is used to fill the * text/plain part that emails clients who don't support * html should show. * * @param string Either a string or the file name with the contents * @param bool If true the first param should be treated as a file name, * else as a string (default) * @param bool If true the text or file is appended to the existing body, * else the old body is overwritten * @return bool true on success */ public function setTXTBody($data, $isfile = false, $append = false) { if (!$isfile) { if (!$append) { $this->txtbody = $data; } else { $this->txtbody .= $data; } } else { $cont = $this->file2str($data); if (!$append) { $this->txtbody = $cont; } else { $this->txtbody .= $cont; } } return true; } /** * Adds a html part to the mail * * @param string Either a string or the file name with the contents * @param bool If true the first param should be treated as a file name, * else as a string (default) * @return bool true on succes */ public function setHTMLBody($data, $isfile = false) { if (!$isfile) { $this->htmlbody = $data; } else { $cont = $this->file2str($data); $this->htmlbody = $cont; } return true; } /** * Adds an image to the list of embedded images. The source is a string containing the image. * * @paramstring The image data. * @param string The file name * @param string The content type * @return bool true */ public function addHTMLImageFromString($filedata, $filename, $c_type='application/octet-stream') { $filename = basename($filename); $this->html_images[] = array( 'body' => $filedata, 'name' => $filename, 'c_type' => $c_type, 'cid' => md5(uniqid(time())) ); return true; } /** * Adds an image to the list of embedded images. The source is a file on disk. * * @param string The file to be used as attachment * @param string The content type * @param string encoding. */ public function addHTMLImageFromFile($file, $c_type='application/octet-stream') { $filedata = $this->file2str($file); return $this->addHTMLImageFromString($filedata, $file, $c_type); } /** * Adds a file to the list of attachments. The source is a string containing the * contents of the file. * * @param string The file data to use as attachment * @param string The content type * @param string The filename of the attachment. * @param string encoding. * @throws mailException */ public function addAttachmentFromString($filedata, $filename, $c_type = 'application/octet-stream', $encoding = 'base64') { if (empty($filename)) { throw new mailException("The supplied filename for the attachment can\'t be empty"); } $filename = basename($filename); $this->parts[] = array( 'body' => $filedata, 'name' => $filename, 'c_type' => $c_type, 'encoding' => $encoding ); } /** * Adds a file to the list of attachments. The source is a file on disk. * * @param string The file to be used as attachment * @param string The content type * @param string encoding. */ public function addAttachmentFromFile($file, $c_type = 'application/octet-stream', $encoding = 'base64') { $filedata=$this->file2str($file); $this->addAttachmentFromString($filedata, $file, $c_type, $encoding); } /** * Get the contents of the given file name as string * * @param string path of file to process * @return string contents of $file_name * @throws mailException */ private function file2str($file_name) { if (!is_readable($file_name)) { throw new mailException('File is not readable ' . $file_name); } if (!$fd = fopen($file_name, 'rb')) { throw new mailException('Could not open ' . $file_name); } $filesize = filesize($file_name); if ($filesize == 0){ $cont = ""; }else{ $cont = fread($fd, $filesize); } fclose($fd); return $cont; } /** * Adds a text subpart to the mailMimePart object and * returns it during the build process. * * @param mixed The object to add the part to, or null if a new object is to be created. * @param string The text to add. * @return mailMimePart The text mailMimePart object */ private function addTextPart($obj, $text) { $params['content_type'] = 'text/plain'; $params['encoding'] = $this->build_params['text_encoding']; $params['charset'] = $this->build_params['text_charset']; if (is_object($obj)) { return $obj->addSubpart($text, $params); } else { return new mailMimePart($text, $params); } } /** * Adds a html subpart to the mailMimePart object and * returns it during the build process. * * @param mixed The object to add the part to, or null if a new object is to be created. * @return mailMimePart The html mailMimePart object */ private function addHtmlPart($obj) { $params['content_type'] = 'text/html'; $params['encoding'] = $this->build_params['html_encoding']; $params['charset'] = $this->build_params['html_charset']; if (is_object($obj)) { return $obj->addSubpart($this->htmlbody, $params); } else { return new mailMimePart($this->htmlbody, $params); } } /** * Creates a new mimePart object, using multipart/mixed as * the initial content-type and returns it during the * build process. * * @return mailMimePart The multipart/mixed mailMimePart object */ private function addMixedPart() { $params['content_type'] = 'multipart/mixed'; return new mailMimePart('', $params); } /** * Adds a multipart/alternative part to a mimePart * object (or creates one), and returns it during * the build process. * * @param mixed The object to add the part to, or * null if a new object is to be created. * @return mailMimePart The multipart/mixed mailMimePart object */ private function addAlternativePart($obj) { $params['content_type'] = 'multipart/alternative'; if (is_object($obj)) { return $obj->addSubpart('', $params); } else { return new mailMimePart('', $params); } } /** * Adds a multipart/related part to a mailMimePart * object (or creates one), and returns it during * the build process. * * @param mixed The object to add the part to, or * null if a new object is to be created * @return mailMimePart The multipart/mixed mimePart object */ private function addRelatedPart($obj) { $params['content_type'] = 'multipart/related'; if (is_object($obj)) { return $obj->addSubpart('', $params); } else { return new mailMimePart('', $params); } } /** * Adds an html image subpart to a mailMimePart object * and returns it during the build process. * * @param mailMimePart The mailMimePart to add the image to * @param array The image information * @return mailMimePart The image mailMimePart object */ private function addHtmlImagePart(mailMimePart $obj, $value) { $params['content_type'] = $value['c_type']; $params['encoding'] = 'base64'; $params['disposition'] = 'inline'; $params['dfilename'] = $value['name']; $params['cid'] = $value['cid']; $obj->addSubpart($value['body'], $params); } /** * Adds an attachment subpart to a mailMimePart object * and returns it during the build process. * * @param mailMimePart The mailMimePart to add the image to * @param array The attachment information * @return mailMimePart The image mailMimePart object */ private function addAttachmentPart(mailMimePart $obj, $value) { $params['content_type'] = $value['c_type']; $params['encoding'] = $value['encoding']; $params['disposition'] = 'attachment'; $params['dfilename'] = $value['name']; $obj->addSubpart($value['body'], $params); } /** * Builds the multipart message from the list ($this->parts) and * returns the mime content. * * @param array Build parameters that change the way the email * is built. Should be associative. Can contain: * text_encoding - What encoding to use for plain text * Default is 7bit * html_encoding - What encoding to use for html * Default is quoted-printable * 7bit_wrap - Number of characters before text is * wrapped in 7bit encoding * Default is 998 * html_charset - The character set to use for html. * Default is iso-8859-1 * text_charset - The character set to use for text. * Default is iso-8859-1 * head_charset - The character set to use for headers. * Default is iso-8859-1 * @return string The mime content */ public function get($build_params = null) { if (isset($build_params)) { foreach ($build_params as $key => $value) { $this->build_params[$key] = $value; } } if (!empty($this->html_images) && isset($this->htmlbody)) { foreach ($this->html_images as $value) { $regex = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' . preg_quote($value['name'], '#') . '\3#'; $rep = '\1\2=\3cid:' . $value['cid'] .'\3'; $this->htmlbody = preg_replace($regex, $rep, $this->htmlbody ); } } $null = null; $attachments = !empty($this->parts) ? true : false; $html_images = !empty($this->html_images) ? true : false; $html = !empty($this->htmlbody) ? true : false; $text = (!$html && !empty($this->txtbody)) ? true : false; switch (true) { case $text && !$attachments: $message = $this->addTextPart($null, $this->txtbody); break; case !$text && !$html && $attachments: $message = $this->addMixedPart(); foreach ($this->parts as $part) { $this->addAttachmentPart($message, $part); } break; case $text && $attachments: $message = $this->addMixedPart(); $this->addTextPart($message, $this->txtbody); foreach ($this->parts as $part) { $this->addAttachmentPart($message, $part); } break; case $html && !$attachments && !$html_images: if (isset($this->txtbody)) { $message = $this->addAlternativePart($null); $this->addTextPart($message, $this->txtbody); $this->addHtmlPart($message); } else { $message =$this->addHtmlPart($null); } break; case $html && !$attachments && $html_images: if (isset($this->txtbody)) { $message =$this->addAlternativePart($null); $this->addTextPart($message, $this->txtbody); $related = $this->addRelatedPart($message); } else { $message = $this->addRelatedPart($null); $related = $message; } $this->addHtmlPart($related); foreach ($this->html_images as $img) { $this->addHtmlImagePart($related, $img); } break; case $html && $attachments && !$html_images: $message = $this->addMixedPart(); if (isset($this->txtbody)) { $alt = $this->addAlternativePart($message); $this->addTextPart($alt, $this->txtbody); $this->addHtmlPart($alt); } else { $this->addHtmlPart($message); } foreach ($this->parts as $part) { $this->addAttachmentPart($message, $part); } break; case $html && $attachments && $html_images: $message = $this->addMixedPart(); if (isset($this->txtbody)) { $alt = $this->addAlternativePart($message); $this->addTextPart($alt, $this->txtbody); $rel = $this->addRelatedPart($alt); } else { $rel = $this->addRelatedPart($message); } $this->addHtmlPart($rel); foreach ($this->html_images as $img) { $this->addHtmlImagePart($rel, $img); } foreach ($this->parts as $part) { $this->addAttachmentPart($message, $part); } break; } if (isset($message)) { $output = $message->encode(); $this->headers = array_merge($this->headers, $output['headers']); return $output['body']; } else { return false; } } /** * Returns an array with the headers needed to prepend to the email * (MIME-Version and Content-Type). Format of argument is: * $array['header-name'] = 'header-value'; * * @param array Assoc array with any extra headers. Optional. * @return array Assoc array with the mime headers */ public function headers(array $xtra_headers = null) { // Content-Type header should already be present, // So just add mime version header $headers['MIME-Version'] = '1.0'; if (isset($xtra_headers)) { $headers = array_merge($headers, $xtra_headers); } $this->headers = array_merge($headers, $this->headers); return $this->encodeHeaders($this->headers); } /** * Get the text version of the headers * (useful if you want to use the PHP mail() function) * * @param array headers Assoc array with any extra headers. Optional. * @return string Plain text headers */ public function txtHeaders(array $xtra_headers = null) { $headers = $this->headers($xtra_headers); $ret = ''; foreach ($headers as $key => $val) { $ret .= "$key: $val" . PHP_EOL; } return $ret; } /** * Sets the Subject header * * @param string $subject String to set the subject to */ public function setSubject($subject) { $this->headers['Subject'] = $subject; } /** * Set an email to the From (the sender) header * * @param string $email The email address to add */ public function setFrom($email) { $this->headers['From'] = $email; } /** * Add an email to the Cc (carbon copy) header * (multiple calls to this method are allowed) * * @param string The email address to add */ public function addCc($email) { if (isset($this->headers['Cc'])) { $this->headers['Cc'] .= ", $email"; } else { $this->headers['Cc'] = $email; } } /** * Add an email to the Bcc (blind carbon copy) header * (multiple calls to this method are allowed) * * @param string The email address to add */ public function addBcc($email) { if (isset($this->headers['Bcc'])) { $this->headers['Bcc'] .= ", $email"; } else { $this->headers['Bcc'] = $email; } } /** * Encodes a header as per RFC2047 * * @param string The header data to encode * @return string Encoded data */ private function encodeHeaders($input) { foreach ($input as $hdr_name => $hdr_value) { preg_match_all('/(\w*[\x80-\xFF]+\w*)/', $hdr_value, $matches); foreach ($matches[1] as $value) { $replacement = preg_replace('/([\x80-\xFF])/e', '"=" . strtoupper(dechex(ord("\1")))', $value); $hdr_value = str_replace($value, '=?' . $this->build_params['head_charset'] . '?Q?' . $replacement . '?=', $hdr_value); } $input[$hdr_name] = $hdr_value; } return $input; } } // End of class /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ ?> zoph-v0.9.19/php/classes/mailMimePart.inc.php000066400000000000000000000250471415176210700210620ustar00rootroot00000000000000 * @author Jeroen Roos * * @package Zoph */ /** * Raw mime encoding class * * What is it? * This class enables you to manipulate and build * a mime email from the ground up. * * Why use this instead of mime.php? * mime.php is a userfriendly api to this class for * people who aren't interested in the internals of * mime mail. This class however allows full control * over the email. * * Eg. * * // Since multipart/mixed has no real body, (the body is * // the subpart), we set the body argument to blank. * * $params['content_type'] = 'multipart/mixed'; * $email = new mailMimePart('', $params); * * // Here we add a text part to the multipart we have * // already. Assume $body contains plain text. * * $params['content_type'] = 'text/plain'; * $params['encoding'] = '7bit'; * $text = $email->addSubPart($body, $params); * * // Now add an attachment. Assume $attach is * the contents of the attachment * * $params['content_type'] = 'application/zip'; * $params['encoding'] = 'base64'; * $params['disposition'] = 'attachment'; * $params['dfilename'] = 'example.zip'; * $attach =& $email->addSubPart($body, $params); * * // Now build the email. Note that the encode * // function returns an associative array containing two * // elements, body and headers. You will need to add extra * // headers, (eg. Mime-Version) before sending. * * $email = $message->encode(); * $email['headers'][] = 'Mime-Version: 1.0'; * * * Further examples are available at http://www.phpguru.org * * TODO: * - Set encode() to return the $obj->encoded if encode() * has already been run. Unless a flag is passed to specifically * re-build the message. * * @author Richard Heyes * @author Jeroen Roos * @version 1.13 * * @package Zoph */ class mailMimePart { /** @var string The encoding type of this part */ private $encoding; /** @var array An array of subparts */ private $subparts; /** @var string The output of this part after being built */ private $encoded; /** @var array Headers for this part */ private $headers; /** @var string The body of this part (not encoded) */ private $body; /** * Constructor. * * Sets up the object. * * @param string The body of the mime part if any. * @param array An associative array of parameters: * content_type - The content type for this part eg multipart/mixed * encoding - The encoding to use, 7bit, 8bit, base64, or quoted-printable * cid - Content ID to apply * disposition - Content disposition, inline or attachment * dfilename - Optional filename parameter for content disposition * description - Content description * charset - Character set to use */ public function __construct($body = '', $params = array()) { foreach ($params as $key => $value) { switch ($key) { case 'content_type': $headers['Content-Type'] = $value . (isset($charset) ? '; charset="' . $charset . '"' : ''); break; case 'encoding': $this->encoding = $value; $headers['Content-Transfer-Encoding'] = $value; break; case 'cid': $headers['Content-ID'] = '<' . $value . '>'; break; case 'disposition': $headers['Content-Disposition'] = $value . (isset($dfilename) ? '; filename="' . $dfilename . '"' : ''); break; case 'dfilename': if (isset($headers['Content-Disposition'])) { $headers['Content-Disposition'] .= '; filename="' . $value . '"'; } else { $dfilename = $value; } break; case 'description': $headers['Content-Description'] = $value; break; case 'charset': if (isset($headers['Content-Type'])) { $headers['Content-Type'] .= '; charset="' . $value . '"'; } else { $charset = $value; } break; } } // Default content-type if (!isset($headers['Content-Type'])) { $headers['Content-Type'] = 'text/plain'; } //Default encoding if (!isset($this->encoding)) { $this->encoding = '7bit'; } // Assign stuff to member variables $this->encoded = array(); $this->headers = $headers; $this->body = $body; } /** * Encodes and returns the email. Also stores it in the encoded member variable * * @return array An associative array containing two elements, * body and headers. The headers element is itself * an indexed array. */ public function encode() { $encoded =&$this->encoded; if (!empty($this->subparts)) { srand((double)microtime()*1000000); $boundary = '=_' . md5(rand() . microtime()); $this->headers['Content-Type'] .= ';' . PHP_EOL . "\t" . 'boundary="' . $boundary . '"'; // Add body parts to $subparts $count=count($this->subparts); for ($i = 0; $i < $count; $i++) { $headers = array(); $tmp = $this->subparts[$i]->encode(); foreach ($tmp['headers'] as $key => $value) { $headers[] = $key . ': ' . $value; } $subparts[] = implode(PHP_EOL, $headers) . PHP_EOL . PHP_EOL . $tmp['body']; } $encoded['body'] = '--' . $boundary . PHP_EOL . implode('--' . $boundary . PHP_EOL, $subparts) . '--' . $boundary.'--' . PHP_EOL; } else { $encoded['body'] = $this->getEncodedData($this->body, $this->encoding) . PHP_EOL; } // Add headers to $encoded $encoded['headers'] =$this->headers; return $encoded; } /** * Adds a subpart to current mime part and returns * a reference to it * * @param string The body of the subpart, if any. * @param array The parameters for the subpart, same * as the $params argument for constructor. * @return mailMimePart the part you just added. */ public function addSubPart($body, $params) { $this->subparts[] = new mailMimePart($body, $params); return $this->subparts[count($this->subparts) - 1]; } /** * Returns encoded data based upon encoding passed to it * * @param string The data to encode. * @param string The encoding type to use, 7bit, base64, or quoted-printable. */ private function getEncodedData($data, $encoding) { if ($encoding=="quoted-printable") { return $this->quotedPrintableEncode($data); } else if ($encoding=="base64") { return rtrim(chunk_split(base64_encode($data), 76, PHP_EOL)); } else { return $data; } } /** * Encodes data to quoted-printable standard. * * @param string The data to encode * @param int Optional max line length. Should not be more than 76 chars */ private function quotedPrintableEncode($input , $line_max = 76) { $lines = preg_split("/\r?\n/", $input); $eol = PHP_EOL; $escape = '='; $output = ''; foreach ($lines as $line) { $linlen = strlen($line); $newline = ''; for ($i = 0; $i < $linlen; $i++) { $char = substr($line, $i, 1); $dec = ord($char); if (($dec == 32) && ($i == ($linlen - 1))) { // convert space at eol only $char = '=20'; } else if (($dec == 9) && ($i == ($linlen - 1))) { // convert tab at eol only $char = '=09'; } else if ($dec == 9) { // Do nothing if a tab. } else if (($dec == 61) OR ($dec < 32) OR ($dec > 126)) { $char = $escape . strtoupper(sprintf('%02s', dechex($dec))); } if ((strlen($newline) + strlen($char)) >= $line_max) { // PHP_EOL is not counted $output .= $newline . $escape . $eol; // soft line break; " =\r\n" is okay $newline = ''; } $newline .= $char; } // end of for $output .= $newline . $eol; } $output = substr($output, 0, -1 * strlen($eol)); // Don't want last crlf return $output; } } // End of class /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ ?> zoph-v0.9.19/php/classes/page.inc.php000066400000000000000000000114111415176210700174030ustar00rootroot00000000000000display(); } /** * Insert a new page into the db */ public function insert() { $this->set("date", "now()"); parent::insert(); $this->lookup(); } /** * Update an existing page in the db */ public function update() { $this->set("timestamp", "now()"); parent::update(); $this->lookup(); } /** * Delete a page from the db */ public function delete() { if (!$this->getId()) { return; } parent::delete(array("pages_pageset")); } /** * Return an array of fields to display * @todo Returns HTML * @return array array of fields */ public function getDisplayArray() { $zophcode = new zophCode\parser($this->get("text")); $text="
" . $zophcode . "
"; return array( translate("title") => e($this->get("title")), translate("date") => e($this->get("date")), translate("updated") => e($this->get("timestamp")), translate("text") => $text ); } /** * Parse Zophcode * @return string parsed code */ public function display() { return new zophCode\parser($this->get("text")); } /** * Get the position of a page in a pageset * @param pageset The pageset to look in */ public function getOrder(pageset $pageset) { $qry=new select(array("pgps" => "pages_pageset")); $qry->addFields(array("page_order")); $where=new clause("pageset_id=:psid"); $where->addAnd(new clause("page_id=:pageid")); $qry->addParam(new param(":psid", $pageset->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":pageid", $this->getId(), PDO::PARAM_INT)); $qry->where($where); $qry->addLimit(1); $stmt=$qry->execute(); if ($stmt->rowCount()) { return intval($stmt->fetchColumn()); } else { return false; } } /** * Get the pagesets this page is in */ public function getPagesets() { $qry=new select(array("pgps" => "pages_pageset")); $qry->addFields(array("pageset_id")); $where=new clause("page_id=:pageid"); $qry->addParam(new param(":pageid", $this->getId(), PDO::PARAM_INT)); $qry->where($where); return pageset::getRecordsFromQuery($qry); } /** * Get a table of pages * @param array array of pages to show * @param pageset Pageset to display * @return block template to display */ public static function getTable(array $pages = null, pageset $pageset=null) { if (is_null($pages)) { $pages=page::getAll(); } $lpages=array(); foreach ($pages as $page) { $page->lookup(); $lpages[]=$page; } return new block("pages", array( "pages" => $lpages, "pageset" => $pageset )); } } zoph-v0.9.19/php/classes/pageset.inc.php000066400000000000000000000267311415176210700201320ustar00rootroot00000000000000set("date", "now()"); } /** * Update existing pageset in db */ public function update() { $this->set("timestamp", "now()"); parent::update(); $this->lookup(); } /** * Delete pageset from db * Also delete page-pageset relations */ public function delete() { if (!$this->get("pageset_id")) { return; } parent::delete(array("pages_pageset")); } /** * Get an array of information to be displayed for this pageset */ public function getDisplayArray() { return array( translate("title") => $this->get("title"), translate("date") => $this->get("date"), translate("updated") => $this->get("timestamp"), translate("created by", false) => $this->getUser()->getLink(), translate("show original page") => translate($this->get("show_orig"), 0), translate("position of original") => translate($this->get("orig_pos"), 0) ); } /** * Get a dropdown to select what to do with the original (zoph default) page to * be displayed */ public function getOriginalDropdown() { return template::createPulldown("show_orig", $this->get("show_orig"), array( "never" => translate("Never", 0), "first" => translate("On first page", 0), "last" => translate("On last page", 0), "all" => translate("On all pages", 0) ) ); } /** * Get the pages in this pageset * @param int Specific page to get instead of all */ public function getPages($pagenum=null) { $qry=new select(array("pps" => "pages_pageset")); $qry->addFields(array("page_id")); $qry->where(new clause("pageset_id=:pagesetid")); $qry->addParam(new param(":pagesetid", $this->getId(), PDO::PARAM_INT)); $qry->addOrder("page_order"); if ($pagenum) { $qry->addLimit(1, (int) $pagenum); } return page::getRecordsFromQuery($qry); } /** * Get the number of pages in this pageset */ public function getPageCount() { $qry=new select(array("pps" => "pages_pageset")); $qry->addFunction(array("count" => "COUNT(page_id)")); $qry->where(new clause("pageset_id=:pagesetid")); $qry->addParam(new param(":pagesetid", $this->getId(), PDO::PARAM_INT)); return $qry->getCount(); } /** * Add a page to this set * @param page Page to add * @todo If the page already exists in this pageset, it fails silently * because, at this moment a page cannot be more than once in a pageset * Someday, this should either give a nice error or this limitation * should be removed. */ public function addPage(page $page) { if (!$page->getOrder($this)) { $qry=new insert(array("pages_pageset")); $qry->addParam(new param(":pageset_id", $this->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":page_id", $page->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":page_order", $this->getMaxOrder() + 1, PDO::PARAM_INT)); $qry->execute(); } } /** * Remove a page from this pageset * @param page Page to remove */ public function removePage(page $page) { $qry=new delete(array("pages_pageset")); $where=new clause("pageset_id=:pagesetid"); $where->addAnd(new clause("page_id=:pageid")); $qry->addParam(new param(":pagesetid", $this->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":pageid", $page->getId(), PDO::PARAM_INT)); $qry->where($where); $qry->execute(); } /** * Move a page up in the order list * @param page Page to move up */ public function moveUp(page $page) { $order=$page->getOrder($this); if ($order>=2) { $currentOrder=new param(":curorder", $order, PDO::PARAM_INT); $newOrder=new param(":neworder", $this->getPrevOrder($order), PDO::PARAM_INT); $pageId=new param(":pageid", $page->getId(), PDO::PARAM_INT); $this->move($currentOrder, $newOrder, $pageId); } } /** * Move a page down in the order list * @param page Page to move down */ public function moveDown(page $page) { $order=$page->getOrder($this); $max=$this->getMaxOrder(); if ($order!=0 && $order<$max) { $currentOrder=new param(":curorder", $order, PDO::PARAM_INT); $newOrder=new param(":neworder", $this->getNextOrder($order), PDO::PARAM_INT); $pageId=new param(":pageid", $page->getId(), PDO::PARAM_INT); $this->move($currentOrder, $newOrder, $pageId); } } /** * Move a page up or down in a pageset * First, it changes the page that has the new order for the page we want to move * to the old order for that page. * For example, if we have a pageset with 2 pages, page 1 and 2, in that order: * pageId = 1, order = 1 * pageId = 2, order = 2 * [step 1] * We are going to move page 2 up, then after the first action, it will look like this: * pageId = 1, order = 1 * pageId = 2, order = 1 * [step 2] * Then finally, we update the order for the page we are actually moving: * pageId = 1, order = 2 * pageId = 2, order = 1 * @param param currentOrder: a database parameter for the current order * @param param newOder: a database parameter for the new order * @param param pageId: a database parameter for the pageId. */ private function move(param $currentOrder, param $newOrder, param $pageId) { $pagesetId=new param(":pagesetid", $this->getId(), PDO::PARAM_INT); // [step 1] $qry=new update(array("pages_pageset")); $qry->addSet("page_order", "curorder"); $where=new clause("page_order=:neworder"); $where->addAnd(new clause("pageset_id=:pagesetid")); $qry->where($where); $qry->addParams(array($currentOrder, $newOrder, $pagesetId)); $qry->execute(); // [step 2] $qry=new update(array("pages_pageset")); $qry->addSet("page_order", "neworder"); $where=new clause("page_id=:pageid"); $where->addAnd(new clause("pageset_id=:pagesetid")); $qry->where($where); $qry->addParams(array($newOrder, $pageId, $pagesetId)); $qry->execute(); } /** * Get the highest used page_order value for this pageset * @return int maximum page_order */ private function getMaxOrder() { $qry=new select(array("pps" => "pages_pageset")); $qry->addFunction(array("max_order" => "MAX(page_order)")); $qry->where(new clause("pageset_id=:pagesetid")); $qry->addParam(new param(":pagesetid", $this->getId(), PDO::PARAM_INT)); $stmt=$qry->execute(); return intval($stmt->fetchColumn()); } /** * Get Next order * If pages have been deleted, the page_order field may no longer * be nicely numbered 1, 2, 3, etc. but there may be holes in the list * so this function and getPrevOrder() determine the next or previous * value of page_order. * @param int Get the next order after... */ private function getNextOrder($order) { $qry=new select(array("pps" => "pages_pageset")); $qry->addFunction(array("next_order" => "MIN(page_order)")); $where=new clause("pageset_id=:pagesetid"); $where->addAnd(new clause("page_order>:order")); $qry->where($where); $qry->addParam(new param(":pagesetid", $this->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":order", $order, PDO::PARAM_INT)); $stmt=$qry->execute(); return intval($stmt->fetchColumn()); } /** * Get previous order * If pages have been deleted, the page_order field may no longer * be nicely numbered 1, 2, 3, etc. but there may be holes in the list * so this function and getiNextOrder() determine the next or previous * value of page_order. * @param int Get the previous order before... */ private function getPrevOrder($order) { $qry=new select(array("pps" => "pages_pageset")); $qry->addFunction(array("prev_order" => "MAX(page_order)")); $where=new clause("pageset_id=:pagesetid"); $where->addAnd(new clause("page_order<:order")); $qry->where($where); $qry->addParam(new param(":pagesetid", $this->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":order", $order, PDO::PARAM_INT)); $stmt=$qry->execute(); return intval($stmt->fetchColumn()); } /** * Get the user who created this pageset * @return user the user */ public function getUser() { $user = new user($this->get("user")); $user->lookup(); return $user; } /** * Get table of pagesets * @param array pagesets to put in the table (default: all) * @return block template block with all pagesets */ public static function getTable(array $pagesets=null) { if (!$pagesets) { $pagesets=pageset::getAll(); } $lpagesets=array(); foreach ($pagesets as $pageset) { $pageset->lookup(); $lpagesets[]=$pageset; } return new block("pagesets", array( "pagesets" => $lpagesets )); } } zoph-v0.9.19/php/classes/permissions.inc.php000066400000000000000000000143741415176210700210550ustar00rootroot00000000000000set("group_id", $gid); $this->set("album_id", $aid); } /** * Get the Id of this object * since this object has a composite Id, it will return an array * @return array [ group_id , album_id ] */ public function getId() { return array( "group_id" => (int) $this->get("group_id"), "album_id" => (int) $this->get("album_id") ); } /** * Get name of the group in this permission * @return string group name */ public function getGroupName() { $group=new group($this->get("group_id")); $group->lookup(); return $group->getName(); } /** * Get name of the album in this permission * @return string album name */ public function getAlbumName() { $album=new album($this->get("album_id")); $album->lookup(); return $album->getName(); } /** * Insert a new permissions object into the db * Because of the way permissions work, if the album in question is a child of another * album (which it will be in most cases - except for the root album), this will * work it's way up in the album tree, until there is an album that this user already * has access to. */ public function insert() { // check if this entry already exists if ($this->lookup()) { return; } // insert records for ancestor albums if they don't exist $album = new album($this->get("album_id")); $album->lookup(); if ($album->get("parent_album_id") > 0) { $gp = new self($this->get("group_id"), $album->get("parent_album_id")); $gp->set("access_level", $this->get("access_level")); $gp->set("watermark_level", $this->get("watermark_level")); $gp->set("writable", $this->get("writable")); $gp->insert(); } parent::insert(); $this->permitSubalbums(); } /** * Update an already existing permission in the database * Permissions are propagated to subalbums if the setting is changed. */ public function update() { $current = new self($this->get("group_id"), $this->get("album_id")); $current->lookup(); parent::update(); if ($current->get("subalbums") === "0" && $this->get("subalbums") === "1") { $this->permitSubalbums(); } } /** * Delete a Permissions object from the db * Because of the way permissions work, if the album in question has children, * this will work it's way DOWN in the album tree, to remove access rights to * any descendant albums. */ public function delete() { // delete records for descendant albums if they exist $album = new album($this->get("album_id")); $album->lookup(); $children = $album->getChildren(); foreach ($children as $child) { $gp = new self($this->get("group_id"), $child->get("album_id")); if ($gp->lookup()) { $gp->delete(); } } parent::delete(); } /** * If this permission has "grant to subalbums" set, we will go through the * children of the album in question and add permissions for those albums as * well */ private function permitSubalbums() { if ($this->get("subalbums")) { $this->lookup(); $album = new album($this->get("album_id")); $album->lookup(); $children = $album->getChildren(); foreach ($children as $child) { $gp = new self($this->get("group_id"), $child->get("album_id")); $gp->set("access_level", $this->get("access_level")); $gp->set("watermark_level", $this->get("watermark_level")); $gp->set("writable", $this->get("writable")); $gp->set("subalbums", $this->get("subalbums")); $gp->insert(); } } } } ?> zoph-v0.9.19/php/classes/permissions/000077500000000000000000000000001415176210700175635ustar00rootroot00000000000000zoph-v0.9.19/php/classes/permissions/controller.inc.php000066400000000000000000000117021415176210700232300ustar00rootroot00000000000000request["_action"]=="updatealbums") { $this->setObject(new group($this->request["group_id"])); } else if ($this->request["_action"]=="updategroups") { $this->setObject(new album($this->request["album_id"])); } $this->doAction(); } /** * Process changes to group permissions */ protected function actionUpdategroups() { // Check if the "Grant access to all groups" checkbox is ticked if ($this->request["_access_level_all_checkbox"]) { $groups = group::getAll(); foreach ($groups as $group) { $permissions = new permissions($group->getId(), $this->object->getId()); $permissions->setFields($this->request->getRequestVars(), "", "_all"); if (!conf::get("watermark.enable")) { $permissions->set("watermark_level", 0); } $permissions->insert(); } } $groups = $this->object->getPermissionArray(true); foreach ($groups as $group) { $group->lookup(); $id=$group->getId(); if (isset($this->request["_remove_permission_group__$id"])) { $permissions = new permissions($id, $this->object->getId()); $permissions->delete(); } else { $permissions = new permissions(); $permissions->setFields($this->request->getRequestVars(), "", "__$id"); $permissions->update(); } } // Check if new album should be added if ($this->request["group_id_new"]) { $permissions = new permissions(); $permissions->setFields($this->request->getRequestVars(), "", "_new"); if (!conf::get("watermark.enable")) { $permissions->set("watermark_level", 0); } $permissions->insert(); } $this->view="album"; } /** * Process changes to album permissions */ protected function actionUpdatealbums() { // Check if the "Grant access to all albums" checkbox is ticked if ($this->request["_access_level_all_checkbox"]) { $albums = album::getAll(); foreach ($albums as $alb) { $permissions = new permissions($this->object->getId(), $alb->getId()); $permissions->setFields($this->request->getRequestVars(), "", "_all"); if (!conf::get("watermark.enable")) { $permissions->set("watermark_level", 0); } $permissions->insert(); } } $albums = $this->object->getAlbums(); foreach ($albums as $album) { $album->lookup(); $id=$album->getId(); if (isset($this->request["_remove_permission_album__$id"])) { $permissions = new permissions($this->object->getId(), $id); $permissions->delete(); } else { $permissions = new permissions(); $permissions->setFields($this->request->getRequestVars(), "", "__$id"); $permissions->update(); } } // Check if new album should be added if ($this->request["album_id_new"]) { $permissions = new permissions(); $permissions->setFields($this->request->getRequestVars(), "", "_new"); if (!conf::get("watermark.enable")) { $permissions->set("watermark_level", 0); } $permissions->insert(); } $this->view="group"; } } zoph-v0.9.19/php/classes/permissions/view/000077500000000000000000000000001415176210700205355ustar00rootroot00000000000000zoph-v0.9.19/php/classes/permissions/view/edit.inc.php000066400000000000000000000055551415176210700227550ustar00rootroot00000000000000object=$obj; } /** * Output view */ public function view() { $accessLevelAll=new block("formInputText", array( "label" => null, "name" => "access_level_all", "size" => 4, "maxlength" => 2, "value" => "5" )); $wmLevelAll=new block("formInputText", array( "label" => null, "name" => "watermark_level_all", "size" => 4, "maxlength" => 2, "value" => "5" )); $accessLevelNew=new block("formInputText", array( "label" => null, "name" => "access_level_new", "size" => 4, "maxlength" => 2, "value" => "5" )); $wmLevelNew=new block("formInputText", array( "label" => null, "name" => "watermark_level_new", "size" => 4, "maxlength" => 2, "value" => "5" )); $class = get_class($this->object); $edit = $this->object instanceof album ? "group" : "album"; $gp = new block("editPermissions", array( "watermark" => conf::get("watermark.enable"), "edit" => $edit, "fixed" => get_class($this->object), "id" => $this->object->getId(), "edit_id" => $edit . "_id", "accessLevelAll" => $accessLevelAll, "wmLevelAll" => $wmLevelAll, "accessLevelNew" => $accessLevelNew, "wmLevelNew" => $wmLevelNew, "permissions" => $this->object->getPermissionArray() )); return $gp; } } zoph-v0.9.19/php/classes/person.inc.php000066400000000000000000000651071415176210700200100ustar00rootroot00000000000000set("createdby", (int) user::getCurrent()->getId()); $this->setDatesNull(); return parent::insert(); } /** * Update an existing record in the database */ public function update() { $this->setDatesNull(); return parent::update(); } /** * Set dates to NULL if they're set to an empty string */ private function setDatesNull() { if ($this->get("dob")==="") { $this->set("dob", null); } if ($this->get("dod")==="") { $this->set("dod", null); } } /** * Add this person to a photo. * This records in the database that this person appears on the photo * @param photo Photo to add the person to */ public function addPhoto(photo $photo, $row = 0) { $pos = (new photo\people($photo))->getLastPos($row); $pos++; $qry=new insert(array("photo_people")); $qry->addParams(array( new param(":photo_id", (int) $photo->getId(), PDO::PARAM_INT), new param(":person_id", (int) $this->getId() , PDO::PARAM_INT), new param(":position", (int) $pos, PDO::PARAM_INT), new param(":row", (int) $row, PDO::PARAM_INT) )); $qry->execute(); } /** * Remove person from a photo * @param photo photo to remove the person from */ public function removePhoto(photo $photo) { // First, get the position for the person who is about to be removed $qry=new select(array("photo_people")); $where=new clause("photo_id=:photo_id"); $where->addAnd(new clause("person_id=:person_id")); $params=array( new param(":photo_id", (int) $photo->getId(), PDO::PARAM_INT), new param(":person_id", (int) $this->getId(), PDO::PARAM_INT) ); $qry->where($where); $qry->addParams($params); $result=db::query($qry)->fetch(PDO::FETCH_ASSOC); $row=$result["row"]; $pos=$result["position"]; $qry=new delete("photo_people"); $qry->where($where); $qry->addParams($params); $qry->execute(); $qry=new update(array("photo_people")); $where=new clause("photo_id=:photo_id"); $where->addAnd(new clause("position>:pos")); $where->addAnd(new clause("row=:row")); $qry->addSetFunction("position=position-1"); $params=array( new param(":photo_id", (int) $photo->getId(), PDO::PARAM_INT), new param(":pos", (int) $pos, PDO::PARAM_INT), new param(":row", (int) $row, PDO::PARAM_INT) ); $qry->addParams($params); $qry->where($where); $qry->execute(); } /** * Lookup from database */ public function lookup() { parent::lookup(); $this->lookupPlaces(); } /** * Lookup home and work for this person */ private function lookupPlaces() { if ($this->get("home_id") > 0) { $this->home = new place($this->get("home_id")); $this->home->lookup(); } if ($this->get("work_id") > 0) { $this->work = new place($this->get("work_id")); $this->work->lookup(); } } /** * Returns a photographer object for this person * @return photographer */ public function getPhotographer() { $photographer=new photographer($this->getId()); $photographer->lookup(); return $photographer; } /** * Delete this person * @todo calls 'die' */ public function delete() { $id=(int) $this->getId(); if (!is_numeric($id)) { die("person_id is not numeric"); } $params=array( new param(":id", (int) $id, PDO::PARAM_INT) ); $qry=new update(array("people")); $qry->addSetFunction("father_id=null"); $where=new clause("father_id=:id"); $qry->where($where); $qry->addParams($params); $qry->execute(); $qry=new update(array("people")); $qry->addSetFunction("mother_id=null"); $where=new clause("mother_id=:id"); $qry->where($where); $qry->addParams($params); $qry->execute(); $qry=new update(array("people")); $qry->addSetFunction("spouse_id=null"); $where=new clause("spouse_id=:id"); $qry->where($where); $qry->addParams($params); $qry->execute(); $qry=new update(array("photos")); $qry->addSetFunction("photographer_id=null"); $where=new clause("photographer_id=:id"); $qry->where($where); $qry->addParams($params); $qry->execute(); parent::delete(array("photo_people", "circles_people")); } /** * Get gender * @return string "male|female" */ private function getGender() { if ($this->get("gender") == 1) { return translate("male"); } if ($this->get("gender") == 2) { return translate("female"); } } /** * Get father of this person * @return person father */ private function getFather() { return static::getFromId($this->get("father_id")); } /** * Get mother of this person * @return person mother */ private function getMother() { return static::getFromId($this->get("mother_id")); } /** * Get spouse of this person * @return person spouse */ private function getSpouse() { return static::getFromId($this->get("spouse_id")); } /** * Get children * Since people cannot be nested, always returns null */ public function getChildren() { return null; } /** * Get name for this person * @return string name */ public function getName() { $this->lookup(); $name = $this->getShortName(); if ($this->get("last_name")) { $name .= " " . $this->get("last_name"); } return $name; } /** * Get full name for this person, no 'called' and including middle name * @return string name */ public function getFullName() { $fn = array( $this->get("first_name"), $this->get("middle_name"), $this->get("last_name") ); // array_filter removes empty elements from the array, to eliminate duplicate spaces return implode(" ", array_filter($fn)); } /** * Get short name for this person, if 'called' is set, returns 'called' otherwise the first name * @return string name */ public function getShortName() { return $this->get("called") ? $this->get("called") : $this->get("first_name"); } /** * Get mail address for this person * @return string mailaddress */ public function getEmail() { return $this->get("email"); } /** * HTML display of this person * Returns only name for this person * @return string name */ public function toHTML() { return $this->getName(); } /** * Get a link to this person * @todo Not proper OO, parent function does not have parameter * @todo returns HTML * @param int|bool show last name in link */ public function getLink($show_last_name = 1) { if ($show_last_name) { $name = $this->getName(); } else { $name = $this->get("called") ? $this->get("called") : $this->get("first_name"); } return "getURL() . "\">$name"; } /** * Get URL for this person */ public function getURL() { return "person.php?person_id=" . $this->getId(); } /** * Get an array of the properties of this person object, for display * @return array */ public function getDisplayArray() { $mother=$this->getMother(); $father=$this->getFather(); $spouse=$this->getSpouse(); $dob = new Time($this->get("dob")); $dod = new Time($this->get("dod")); $display=array( translate("called") => e($this->get("called")), translate("full name") => $this->getFullName(), translate("date of birth") => $dob->getLink(), translate("date of death") => $dod->getLink(), translate("gender") => e($this->getGender())); if ($mother instanceof person) { $display[translate("mother")] = $mother->getLink(); } if ($father instanceof person) { $display[translate("father")] = $father->getLink(); } if ($spouse instanceof person) { $display[translate("spouse")] = $spouse->getLink(); } return $display; } /** * Return the number of photos this person appears on * @return int count */ public function getPhotoCount() { return sizeof(collection::createFromVars(array( "person_id" => $this->getId() ))); } /** * Return the number of photos this person appears on. * Wrapper around getPhotoCount() because there is no * concept of sub-persons. * @return int count */ public function getTotalPhotoCount() { return $this->getPhotoCount(); } /** * Get coverphoto for this person. * @param string how to select a coverphoto: oldest, newest, first, last, random, highest * @return photo coverphoto * @todo This function is almost equal to category::getAutoCover(), should be merged */ public function getAutoCover($autocover=null) { $coverphoto=$this->getCoverphoto(); if ($coverphoto instanceof photo) { return $coverphoto; } $qry=new select(array("p" => "photos")); $qry->addFunction(array("photo_id" => "DISTINCT ar.photo_id")); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id") ->join(array("pp" => "photo_people"), "p.photo_id = pp.photo_id"); $where=new clause("pp.person_id=:id"); $qry->addParam(new param(":id", $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry=selectHelper::getAutoCoverOrder($qry, $autocover); $qry->where($where); $coverphotos=photo::getRecordsFromQuery($qry); $coverphoto=array_shift($coverphotos); if ($coverphoto instanceof photo) { $coverphoto->lookup(); return $coverphoto; } } /** * Set first, middle, last and called name from single string * "first", "first last", "first middle last last last" * or "first:middle:last:called" * @param string name */ public function setName($name) { if (strpos($name, ":")!==false) { $name_array=array_pad(explode(":", $name),4,null); $this->set("first_name", $name_array[0]); $this->set("middle_name", $name_array[1]); $this->set("last_name", $name_array[2]); $this->set("called", $name_array[3]); } else { $name_array=explode(" ", $name); switch (sizeof($name_array)) { case 0: // shouldn't happen.. die("something went wrong, report a bug"); break; case 1: // Only one word, assume this is a first name $this->set("first_name", $name_array[0]); break; case 2: // Two words, asume this is first & last $this->set("first_name", $name_array[0]); $this->set("last_name", $name_array[1]); break; default: // 3 or more, assume first two are first, middle, rest is last $this->set("first_name", array_shift($name_array)); $this->set("middle_name", array_shift($name_array)); $this->set("last_name", implode(" ", $name_array)); break; } } } /** * Get details (statistics) about this person from db * @return array Array with statistics * @todo this function is almost equal to category::getDetails() they should be merged */ public function getDetails() { $qry=new select(array("p" => "photos")); $qry->addFunction(array( "count" => "COUNT(DISTINCT p.photo_id)", "oldest" => "MIN(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "newest" => "MAX(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "first" => "MIN(p.timestamp)", "last" => "MAX(p.timestamp)", "lowest" => "ROUND(MIN(ar.rating),1)", "highest" => "ROUND(MAX(ar.rating),1)", "average" => "ROUND(AVG(ar.rating),2)")); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id"); $qry->addGroupBy("p.photographer_id"); $where=new clause("p.photographer_id=:photographerid"); $qry->addParam(new param(":photographerid", $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $result=db::query($qry); if ($result) { return $result->fetch(PDO::FETCH_ASSOC); } else { return null; } } /** * Turn the array from @see getDetails() into XML * @param array Don't fetch details, but use the given array */ public function getDetailsXML(array $details=null) { if (!isset($details)) { $details=$this->getDetails(); } $details["title"]=translate("Photos taken by this person:", false); return parent::getDetailsXML($details); } /** * Get array of circles this person is a member of * @return array of circles */ public function getCircles() { $qry=new select(array("cp" => "circles_people")); $qry->addFields(array("circle_id")); $qry->where(new clause("person_id=:personid")); $qry->addParam(new param(":personid", (int) $this->getId(), PDO::PARAM_INT)); return circle::getRecordsFromQuery($qry); } /** * Return whether the currently logged on user can see this person * @param user Use this user instead of the logged in one * @return bool whether or not this person should be visible */ public function isVisible(user $user=null) { if (!$user) { $user=user::getCurrent(); } $all=static::getAllPeopleAndPhotographers(); $ids=array(); foreach ($all as $person) { $ids[]=$person->getId(); } return (in_array($this->getId(), $ids) || $user->isAdmin()); } /** * Is the user the creator of this person? * @param user check for this user | current logged on user */ public function isCreator(user $user=null) { if (!$user) { $user=user::getCurrent(); } return ($user->getId()===$this->get("createdby")); } /** * Lookup person by name; * @param string name * @param bool use 'like' lookup instead of exact lookup */ public static function getByName($name, $like=false) { if (empty($name)) { return false; } $qry=new select(array("ppl" => "people")); $qry->addFields(array("person_id")); $where=new clause("CONCAT_WS(\" \", lower(first_name), lower(last_name))" . ( $like ? " LIKE :name" : "=lower(:name)") ); $qry->addParam(new param(":name", $like ? "%" . $name . "%" : $name, PDO::PARAM_STR)); $qry->where($where); return static::getRecordsFromQuery($qry); } /** * Get Top N people */ public static function getTopN() { $user=user::getCurrent(); $qry=new select(array("ppl" => "people")); $qry->addFields(array("person_id", "first_name", "last_name")); $qry->addFunction(array("count" => "count(distinct pp.photo_id)")); $qry->join(array("pp" => "photo_people"), "ppl.person_id=pp.person_id"); $qry->addGroupBy("ppl.person_id"); $qry->addOrder("count DESC")->addOrder("ppl.last_name")->addOrder("ppl.first_name"); $qry->addLimit((int) $user->prefs->get("reports_top_n")); $qry = selectHelper::expandQueryForUser($qry); return parent::getTopNfromSQL($qry); } /** * Get all people * @param string part of name to search for * @param bool Search for first name */ public static function getAll($search=null, $search_first = false) { $user=user::getCurrent(); $where=null; $qry=new select(array("ppl" => "people")); $qry->addFields(array("ppl.*"), true); if (!is_null($search)) { $where=static::getWhereForSearch($search, $search_first); $qry->addParam(new param("search", $search, PDO::PARAM_STR)); if ($search_first) { $qry->addParam(new param("searchfirst", $search, PDO::PARAM_STR)); } } $qry->addOrder("last_name")->addOrder("called")->addOrder("first_name"); $qry = selectHelper::expandQueryForUser($qry); if ($where instanceof clause) { $qry->where($where); } if (!$user->canSeeAllPhotos() && $user->canEditOrganizers()) { $subqry=new select(array("ppl" => "people")); $subqry->addFields(array("ppl.*"), true); $subwhere=new clause("ppl.createdby=:ownerid"); $subqry->addParam(new param(":ownerid", (int) $user->getId(), PDO::PARAM_INT)); if (!is_null($search)) { $subwhere->addAnd(static::getWhereForSearch($search, $search_first, "subsearch")); $subqry->addParam(new param("subsearch", $search, PDO::PARAM_STR)); if ($search_first) { $subqry->addParam(new param("subsearchfirst", $search, PDO::PARAM_STR)); } } $subqry->where($subwhere); $qry->union($subqry); } return static::getRecordsFromQuery($qry); } /** * Get XML tree of people * @param string string to search for * @param DOMDocument XML document to add children too * @param DOMElement root node * @return DOMDocument XML Document */ public static function getXMLdata($search, DOMDocument $xml, DOMElement $rootnode) { if ($search=="") { $search=null; } $records=static::getAll($search,true); $idname=static::$primaryKeys[0]; foreach ($records as $record) { $record->lookup(); $newchild=$xml->createElement(static::XMLNODE); $key=$xml->createElement("key"); $title=$xml->createElement("title"); $key->appendChild($xml->createTextNode($record->get($idname))); $title->appendChild($xml->createTextNode($record->getName())); $newchild->appendChild($key); $newchild->appendChild($title); $rootnode->appendChild($newchild); } $xml->appendChild($rootnode); return $xml; } /** * Get autocomplete preference for people for the current user * @return bool whether or not to autocomplete */ public static function getAutocompPref() { $user=user::getCurrent(); return ($user->prefs->get("autocomp_people") && conf::get("interface.autocomplete")); } /** * Get array to build select box * @return array */ public static function getSelectArray() { if (isset(static::$sacache)) { return static::$sacache; } $ppl[""] = ""; $people_array = static::getAll(); foreach ($people_array as $person) { $person->lookup(); $ppl[$person->getId()] = ($person->get("last_name") ? $person->get("last_name") . ", " : "") . ($person->get("called") ? $person->get("called") : $person->get("first_name")); } return $ppl; } /** * Get number of people for a specific user * @return int count */ public static function getCountForUser() { if (user::getCurrent()->canSeeAllPhotos()) { return static::getCount(); } else { $allowed=array(); $people=static::getAll(); $photographers=photographer::getAll(); foreach ($people as $person) { $allowed[]=$person->getId(); } foreach ($photographers as $photographer) { $allowed[]=$photographer->getId(); } $allowed=array_unique($allowed); return count($allowed); } } /** * Get all people and all photographers for the current logged on user * @param string only return people whose name starts with this string * @return int count */ public static function getAllPeopleAndPhotographers($search = null) { $user=user::getCurrent(); $allowed=array(); $qry=new select(array("ppl" => "people")); $qry->addOrder("ppl.last_name")->addOrder("ppl.called")->addOrder("ppl.first_name"); if (!$user->canSeeAllPhotos()) { $people=(array)static::getAll($search); $photographers=(array)photographer::getAll($search); foreach ($people as $person) { $person->lookup(); $allowed[]=$person->getId(); } foreach ($photographers as $photographer) { $photographer->lookup(); $allowed[]=$photographer->getId(); } $allowed=array_unique($allowed); if (count($allowed)==0) { return null; } $param=new param(":person_ids", $allowed, PDO::PARAM_INT); $qry->where(clause::InClause("person_id", $param)); $qry->addParam($param); } else if ($search!==null) { $qry->addParam(new param("search", $search, PDO::PARAM_STR)); $qry->where(static::getWhereForSearch($search)); } $all=static::getRecordsFromQuery($qry); $ids=array(); foreach ($all as $person) { $ids[$person->getId()]=$person; } // Add the person assigned to this user $personId=$user->get("person_id"); if ($personId) { $person=new person($personId); $person->lookup(); $pattern="/^" . $search . "/i"; if (is_null($search) || preg_match($pattern, $person->get("last_name"))) { $ids[$personId]=$person; } } return $ids; } /** * Get all people and all photographers for the currently logged on user * that are NOT a member of a circle */ public static function getAllNoCircle() { $all = static::getAllPeopleAndPhotographers(); $circles = circle::getRecords(); $return=array(); foreach ($all as $person) { $return[$person->getId()] = $person; } foreach ($circles as $circle) { $members=$circle->getMembers(); foreach ($members as $member) { if (isset($return[$member->getId()])){ unset($return[$member->getId()]); } } } return $return; } /** * Get SQL WHERE clause to search for people * @param string search string * @param bool search for first name * @param string use this as parameter name - necessary when multiple people searches are used in one query * for example for (person1 AND person2) searches. */ public static function getWhereForSearch($search, $search_first=false, $paramname="search") { $where=null; if ($search!==null) { if ($search==="") { $where=new clause("ppl.last_name=''"); $where->addOr(new clause("ppl.last_name is null")); } else { $where=new clause("ppl.last_name like lower(concat(:" . $paramname . ",'%'))"); if ($search_first) { $where->addOr( new clause("ppl.first_name like lower(concat(:" . $paramname . "first, '%'))") ); } } } return $where; } } ?> zoph-v0.9.19/php/classes/person/000077500000000000000000000000001415176210700165165ustar00rootroot00000000000000zoph-v0.9.19/php/classes/person/controller.inc.php000066400000000000000000000077201415176210700221700ustar00rootroot00000000000000_action=="new") { $this->setObject(new person()); $this->doAction(); } else { try { $person=$this->getPersonFromRequest(); } catch (personNotAccessibleSecurityException $e) { log::msg($e->getMessage(), log::WARN, log::SECURITY); $person=null; } if ($person instanceof person) { $this->setObject($person); $this->doAction(); } else { $this->view = "notfound"; } } } /** * Get the person based on the query in the request * @throws personNotAccessibleSecurityException */ private function getPersonFromRequest() { $user=user::getCurrent(); if (isset($this->request["name"])) { $people = person::getByName($this->request["name"]); if ($people && count($people) == 1) { $person = array_shift($people); } } else if (isset($this->request["person_id"])) { $person = new person($this->request["person_id"]); $person->lookup(); } if ($user->isAdmin() || $person->isVisible()) { return $person; } throw new photoNotAccessibleSecurityException( "Security Exception: person " . $person->getId() . " is not accessible for user " . $user->getName() . " (" . $user->getId() . ")" ); } /** * Do action 'confirm' */ public function actionConfirm() { $user = user::getCurrent(); if ($user->canEditOrganizers()) { parent::actionConfirm(); } else { $this->view="display"; } } /** * Do action 'delete' */ public function actionDelete() { $user = user::getCurrent(); if ($user->canEditOrganizers()) { parent::actionDelete(); } else { $this->view="display"; } } /** * Do action 'edit' */ public function actionEdit() { $user = user::getCurrent(); if ($user->canEditOrganizers()) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'update' */ public function actionUpdate() { $user=user::getCurrent(); if ($user->canEditOrganizers()) { parent::actionUpdate(); } $this->view="display"; } } zoph-v0.9.19/php/classes/person/view/000077500000000000000000000000001415176210700174705ustar00rootroot00000000000000zoph-v0.9.19/php/classes/person/view/common.inc.php000066400000000000000000000042441415176210700222450ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->person=$person; } /** * Get actionlinks */ abstract protected function getActionlinks(); /** * Output view */ abstract public function view(); /** * Get the title for this view */ public function getTitle() { return translate($this->request["_action"] . " person"); } } zoph-v0.9.19/php/classes/person/view/confirm.inc.php000066400000000000000000000044201415176210700224060ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->person=$person; } /** * Output view */ public function view() { $cover = $this->person->getCoverphoto(); if ($cover instanceof photo) { $cover = $cover->getImageTag(THUMB_PREFIX); } $actionlinks=array( translate("confirm") => "person.php?_action=confirm&person_id=" . $this->person->getId(), translate("cancel") => "person.php?person_id=" . $this->person->getId() ); return new template("confirm", array( "title" => $this->getTitle(), "actionlinks" => $actionlinks, "mainActionlinks" => null, "obj" => $this->person, "image" => $cover )); } public function getTitle() { return sprintf(translate("Confirm deletion of '%s'"), $this->person->getName()); } } zoph-v0.9.19/php/classes/person/view/display.inc.php000066400000000000000000000141711415176210700224220ustar00rootroot00000000000000person->getPhotoCount(); $photosBy = $this->person->getPhotographer()->getPhotoCount(); $selection=null; $actionlinks=null; if ($user->canEditOrganizers()) { $actionlinks=array( translate("edit") => "person.php?_action=edit&person_id=" . $this->person->getId(), translate("delete") => "person.php?_action=delete&person_id=" . $this->person->getId(), translate("new") => "person.php?_action=new" ); if ($this->person->get("coverphoto")) { $actionlinks[translate("unset coverphoto")]="person.php?_action=update&person_id=" . $this->person->getId() . "&coverphoto=NULL"; } try { $selection=new selection($_SESSION, array( "coverphoto" => "person.php?_action=update&person_id=" . $this->person->getId() . "&coverphoto=", "return" => "_return=person.php&_qs=person_id=" . $this->person->getId() )); } catch (photoNoSelectionException $e) { $selection=null; } } try { $pageset=$this->person->getPageset(); $page=$this->person->getPage($this->request, $this->request["pagenum"]); $showOrig=$this->person->showOrig($this->request["pagenum"]); } catch (pageException $e) { $showOrig=true; $page=null; } $mainActionlinks=array(); if ($photosOf > 0) { $mainActionlinks[$photosOf . " " . translate("photos of")] = "photos.php?person_id=" . $this->person->getId(); } if ($photosBy > 0) { $mainActionlinks[$photosBy . " " . translate("photos by")] = "photos.php?photographer_id=" . $this->person->getId(); } $tpl=new template("display", array( "title" => $this->getTitle(), "obj" => $this->person, "actionlinks" => $actionlinks, "mainActionlinks" => $mainActionlinks, "selection" => $selection, "page" => $page, "pageTop" => $this->person->showPageOnTop(), "pageBottom" => $this->person->showPageOnBottom(), "showMain" => $showOrig )); /** * @todo All the link blocks here could be generated by the objects themselves * saving a huge amount of more-or-less duplicate code */ if ($user->canSeePeopleDetails()) { $dl=$this->person->getDisplayArray(); if ($this->person->getEmail()) { $mail=new block("link", array( "href" => "mailto:" . e($this->person->getEmail()), "target" => "", "link" => e($this->person->getEmail()) )); $dl[translate("email")]=$mail; } if ($this->person->home) { $home=new block("link", array( "href" => "place.php?place_id=" . $this->person->get("home_id"), "target" => "", "link" => $this->person->home->get("title") )); $dl[translate("home location")]=$home; } if ($this->person->work) { $home=new block("link", array( "href" => "place.php?place_id=" . $this->person->get("work_id"), "target" => "", "link" => $this->person->work->get("title") )); $dl[translate("work location")]=$home; } } if ($this->person->get("notes")) { $dl[translate("notes")]=$this->person->get("notes"); } $circles=$this->person->getCircles(); if ($circles) { $circleLinks=array(); foreach ($circles as $circle) { $circle->lookup(); $circleLinks[]= new block("link", array( "href" => $circle->getURL(), "target" => "", "link" => $circle->getName() )); } $dl[translate("circles")]=implode(", ", $circleLinks); } $tpl->addBlock(new block("definitionlist", array( "dl" => $dl, "class" => "" ))); return $tpl; } /** * Get the title for this view */ public function getTitle() { return $this->person->getName(); } } zoph-v0.9.19/php/classes/person/view/notfound.inc.php000066400000000000000000000030161415176210700226050ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); } /** * Output view */ public function view() { return new template("notFound", array( "title" => $this->getTitle(), "msg" => translate("Person not found") )); } /** * Get the title for this view */ public function getTitle() { return translate("Person not found"); } } zoph-v0.9.19/php/classes/person/view/redirect.inc.php000066400000000000000000000031231415176210700225510ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->person=$person; } /** * Output view */ public function view() { redirect($this->redirect); } /** * Set the page to redirect to * @param string redirect target */ public function setRedirect($redirect) { $this->redirect=$redirect; } } zoph-v0.9.19/php/classes/person/view/update.inc.php000066400000000000000000000040751415176210700222410ustar00rootroot00000000000000request["_action"] != "new") { $actionlinks[translate("new")] = "person.php?_action=new"; } return $actionlinks; } /** * Output the view */ public function view() { $user = user::getCurrent(); if ($this->request["_action"] == "new") { $action = "insert"; } else if ($this->request["_action"] == "edit") { $action = "update"; } else { // Safety net. This should not happen. $action = $this->request["_action"]; } $tpl = new template("editPerson", array( "actionlinks" => $this->getActionlinks(), "action" => $action, "person" => $this->person, "title" => $this->getTitle() )); return $tpl; } } zoph-v0.9.19/php/classes/photo.inc.php000066400000000000000000001426221415176210700176310ustar00rootroot00000000000000get("name"); $image_path = conf::get("path.images") . "/" . $this->get("path") . "/"; if ($type) { $image_path .= $type . "/" . $type . "_"; } $image_path .= $name; if (!file_exists($image_path)) { throw new photoNotFoundException($name . " could not be found"); } $mtime = filemtime($image_path); $filesize = filesize($image_path); $gmt_mtime = gmdate('D, d M Y H:i:s', $mtime) . ' GMT'; // we assume that the client generates proper RFC 822/1123 dates // (should work for all modern browsers and proxy caches) if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $_SERVER['HTTP_IF_MODIFIED_SINCE'] == $gmt_mtime) { $header["http_status"]="HTTP/1.1 304 Not Modified"; $jpeg=null; } else { $file=new file($image_path); $image_type=$file->getMime(); if ($image_type) { $header["Content-Length"] = $filesize; $header["Content-Disposition"]="inline; filename=" . $name; $header["Last-Modified"]=$gmt_mtime; $header["Content-type"]=$image_type; $jpeg=file_get_contents($image_path); } } return array($header, $jpeg); /** * @todo error handling */ } /** * Lookup a photo, considering access rights */ public function lookup() { if (!$this->getId()) { return; } $qry = new select(array("p" => "photos")); $distinct=true; $qry->addFields(array("*"), $distinct); $where=new clause("p.photo_id=:photoid"); $qry->addParam(new param(":photoid", (int) $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $photo = $this->lookupFromSQL($qry); if ($photo) { $this->lookupPhotographer(); $this->lookupLocation(); } return $photo; } /** * Lookup a photo, ignoring access rights */ public function lookupAll() { $qry = new select(array("p" => "photos")); $qry->where(new clause("p.photo_id=:photoid")); $qry->addParam(new param(":photoid", (int) $this->getId(), PDO::PARAM_INT)); $photo = $this->lookupFromSQL($qry); return $photo; } /** * Lookup photographer of this photo */ private function lookupPhotographer() { if ($this->get("photographer_id") > 0) { $this->photographer = new photographer($this->get("photographer_id")); $this->photographer->lookup(); } else { $this->photographer=null; } } /** * Lookup location of this photo */ private function lookupLocation() { if ($this->get("location_id") > 0) { $this->location = new place($this->get("location_id")); $this->location->lookup(); } else { $this->location=null; } } /** * Delete this photo from database */ public function delete() { if (conf::get("path.trash")) { $this->lookup(); $mid=new file($this->getFilePath(MID_PREFIX)); $mid->delete(); $thumb=new file($this->getFilePath(THUMB_PREFIX)); $thumb->delete(); $photo=new file($this->getFilePath()); $trash=conf::get("path.images") . DIRECTORY_SEPARATOR . conf::get("path.trash"); if (!file_exists($trash)) { file::createDirRecursive($trash); } $photo->setDestination($trash); $photo->backup=true; $photo->move(); } parent::delete(array( "photo_people", "photo_categories", "photo_albums", "photo_ratings", "photo_comments") ); } /** * Update photo object in the db */ public function update() { if (empty($this->get("time_corr"))) { $this->set("time_corr", 0); } return parent::update(); } /** * Insert new photo object in the db */ public function insert() { if (empty($this->get("time_corr"))) { $this->set("time_corr", 0); } return parent::insert(); } /** * Update photo relations, such as albums, categories, etc. * @param array array of variables to update * @param string suffix for varnames */ public function updateRelations(array $vars, $suffix = "") { $albums=album::getFromVars($vars, $suffix); $categories=category::getFromVars($vars, $suffix); $people=person::getFromVars($vars, $suffix); // Albums if (!empty($vars["_remove_album$suffix"])) { foreach ((array) $vars["_remove_album$suffix"] as $alb) { $this->removeFrom(new album($alb)); } } if (isset($this->_album_id)) { $albums=array_merge($albums, $this->_album_id); unset($this->_album_id); } foreach ($albums as $album) { $this->addTo(new album($album)); } // Categories if (!empty($vars["_remove_category$suffix"])) { foreach ((array) $vars["_remove_category$suffix"] as $cat) { $this->removeFrom(new category($cat)); } } if (isset($this->_category_id)) { $categories=array_merge($categories, $this->_category_id); unset($this->_category_id); } foreach ($categories as $cat) { $this->addTo(new category($cat)); } // People if (!empty($vars["_remove_person$suffix"])) { foreach ((array) $vars["_remove_person$suffix"] as $pers) { $this->removeFrom(new person($pers)); } } if (isset($this->_person_id)) { $people=array_merge($people, $this->_person_id); unset($this->_person_id); } foreach ($people as $person) { $this->addTo(new person($person)); } if (isset($this->_rate)) { $this->rate($this->_rate); } } /** * Determine whether a specific user has "write" rights * on this photo * @param user The user to check * @return bool is writable */ public function isWritableBy(user $user) { if ($user->isAdmin()) { return true; } else { $perm = $user->getPhotoPermissions($this); if ($perm instanceof permissions) { return (bool) $perm->get("writable"); } } return false; } /** * Updates the photo's dimensions and filesize */ public function updateSize() { $file=$this->getFilePath(); list($width, $height)=getimagesize($file); $size=filesize($file); $this->set("size", $size); $this->set("width", $width); $this->set("height", $height); $this->update(); } /** * Rereads EXIF information from file and updates */ public function updateEXIF() { $file=$this->getFilePath(); $exif=process_exif($file); if ($exif) { $this->setFields($exif); $this->update(); } } /** * Get the file name of this photo * @return string file name */ public function getName() { return $this->get("name"); } /** * Add this photo to album, category, person or location * @param organizer album, category, person or location */ public function addTo(organizer $org) { $org->addPhoto($this); } /** * Remove this photo from album, category, person or location * @param organizer album, category, person or location */ public function removeFrom(organizer $org) { $org->removePhoto($this); } /** * Get a list of albums for this photo * @return array of albums */ public function getAlbums() { $user=user::getCurrent(); $qry=new select(array("a" => "albums")); $qry->join(array("pa" => "photo_albums"), "pa.album_id = a.album_id"); $qry->addFields(array("album_id", "parent_album_id", "album")); $where=new clause("pa.photo_id=:photoid"); $qry->addParam(new param(":photoid", (int) $this->getId(), PDO::PARAM_INT)); $qry->addOrder("album"); if (!$user->canSeeAllPhotos()) { $qry->join(array("gp" => "group_permissions"), "gp.album_id=a.album_id"); $qry->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $where->addAnd(new clause("gu.user_id=:userid")); $qry->addParam(new param(":userid", (int) $user->getId(), PDO::PARAM_INT)); if ($user->canEditOrganizers()) { $subqry=new select(array("a" => "albums")); $subqry->addFields(array("album_id", "parent_album_id", "album")); $subqry->join(array("pa" => "photo_albums"), "pa.album_id = a.album_id"); $subwhere=new clause("pa.photo_id=:subphotoid"); $subqry->addParam(new param(":subphotoid", (int) $this->getId(), PDO::PARAM_INT)); $subwhere->addAnd(new clause("a.createdby=:ownerid")); $subqry->addParam(new param(":ownerid", (int) $user->getId(), PDO::PARAM_INT)); $subqry->where($subwhere); $qry->union($subqry); } } $qry->where($where); return album::getRecordsFromQuery($qry); } /** * Get a list of categories for this photo * @return array of categories */ public function getCategories() { $qry=new select(array("c" => "categories")); $qry->join(array("pc" => "photo_categories"), "c.category_id = pc.category_id"); $distinct=true; $qry->addFields(array("category_id"), $distinct); $qry->addFields(array("parent_category_id", "category")); $where=new clause("pc.photo_id=:photoid"); $qry->addParam(new param(":photoid", (int) $this->getId(), PDO::PARAM_INT)); $qry->addOrder("c.category"); $qry->where($where); return category::getRecordsFromQuery($qry); } /** * Import a file into the database * * This function takes a file object and imports it inot the database as a new photo * * @param file The file to be imported */ public function import(file $file) { $this->set("name", $file->getName()); $newPath=$this->get("path") . "/"; if (conf::get("import.dated")) { // This is not really validating the date, just making sure // no-one is playing tricks, such as setting the date to /etc/passwd or // something. $date=$this->get("date"); if (!preg_match("/^[0-9]{2,4}-[0-9]{1,2}-[0-9]{1,2}$/", $date)) { log::msg("Illegal date, using today's", log::ERROR, log::IMPORT); $date=date("Y-m-d"); } if (conf::get("import.dated.hier")) { $newPath .= file::cleanupPath(str_replace("-", "/", $date)); } else { $newPath .= file::cleanupPath(str_replace("-", ".", $date)); } } $toPath="/" . file::cleanupPath(conf::get("path.images") . "/" . $newPath) . "/"; $path=$file->getPath(); file::createDirRecursive($toPath . "/" . MID_PREFIX); file::createDirRecursive($toPath . "/" . THUMB_PREFIX); if ($path ."/" != $toPath) { $file->setDestination($toPath); $files[]=$file; $newname=$file->getDestName(); $midname=MID_PREFIX . "/" . MID_PREFIX . "_" . $newname; $thumbname=THUMB_PREFIX . "/" . THUMB_PREFIX . "_" . $newname; if (file_exists($path . "/". $thumbname)) { $thumb=new file($path . "/" . $thumbname); $thumb->setDestination($toPath . "/" . THUMB_PREFIX . "/"); $files[]=$thumb; } if (file_exists($path . "/". $midname)) { $mid=new file($path . "/" . $midname); $mid->setDestination($toPath . "/" . MID_PREFIX . "/"); $files[]=$mid; } try { foreach ($files as $file) { if (conf::get("import.cli.copy")==false) { $file->checkMove(); } else { $file->checkCopy(); } } } catch (fileException $e) { throw $e; } // We run this loop twice, because we only want to move/copy the // file if *all* files can be moved/copied. try { foreach ($files as $file) { if (conf::get("import.cli.copy")==false) { $new=$file->move(); } else { $new=$file->copy(); } $new->chmod(); } } catch (fileException $e) { throw $e; } $this->set("name", $newname); } // Update the db to the new path; $this->set("path", file::cleanupPath($newPath)); } /** * Return the full path to the file on disk * @param string type of image to return (thumb, mid, or empty for full) * @return string full path. */ public function getFilePath($type = null) { $image_path = conf::get("path.images") . DIRECTORY_SEPARATOR . $this->get("path") . DIRECTORY_SEPARATOR; if ($type==THUMB_PREFIX || $type==MID_PREFIX) { $image_path .= $type . DIRECTORY_SEPARATOR . $type . "_"; } return $image_path . $this->get("name"); } /** * Get an thumbnail image that links to this photo * @param string optional link instead of the default link to the photo page * @return block to display link */ public function getThumbnailLink($href = null) { if (!$href) { $href = "photo.php?photo_id=" . (int) $this->getId(); } return new block("link", array( "href" => $href, "link" => $this->getImageTag(THUMB_PREFIX), "target" => "" )); } /** * Get a link to the fullsize version of this image * @param string What (text or image) to display * @return block to display link */ public function getFullsizeLink($title) { $user=user::getCurrent(); return new block("link", array( "href" => $this->getURL(), "link" => $title, "target" => ($user->prefs->get("fullsize_new_win") ? "_blank" : "") )); } /** * Get the URL to an image * @param string|null "mid" or "thumb" * @return string URL */ public function getURL($type = null) { $url = "image.php?photo_id=" . (int) $this->getId(); if ($type) { $url .= "&type=" . $type; } return $url; } /** * Create an img tag for this photo * @param string type type of image (thumb, mid or null for full) * @return block template block for image tag */ public function getImageTag($type = null) { $this->lookup(); $image_href = $this->getURL($type); $file=$this->getFilePath($type); if (!file_exists($file)) { switch ($type) { case MID_PREFIX: $size="width='" . MID_SIZE . "'"; break; case THUMB_PREFIX: $size="width='" . THUMB_SIZE . "'"; break; default: $size=""; } return new block("img", array( "src" => template::getImage("notfound.png"), "class" => $type, "size" => $size, "alt" => "file not found" )); } list($width, $height, $filetype, $size)=getimagesize($file); $alt = e($this->get("title")); return new block("img", array( "src" => $image_href, "class" => $type, "size" => $size, "alt" => $alt )); } /** * Stores the rating of a photo for a user * @param int rating */ public function rate($rating) { rating::setRating((int) $rating, $this); } /** * Get average rating for this photo * @return float rating */ public function getRating() { return rating::getAverage($this); } /** * Get rating for a specific user * @param user user * @return int rating */ public function getRatingForUser(user $user) { $ratings=rating::getRatings($this, $user); $rating=array_pop($ratings); if ($rating instanceof rating) { return $rating->get("rating"); } } /** * Get details about ratings */ public function getRatingDetails() { return rating::getDetails($this); } /** * Get a GD image resource for this image */ private function getImageResource() { $file = $this->getFilePath(); $resource = null; $image_info = getimagesize($file); switch ($image_info[2]) { case IMAGETYPE_GIF: $resource = imagecreatefromgif ($file); break; case IMAGETYPE_JPEG: $resource = imagecreatefromjpeg($file); break; case IMAGETYPE_PNG: $resource = imagecreatefrompng($file); break; default: break; } return $resource; } /** * Get the photographer for this photo */ public function getPhotographer() { $this->lookup(); return $this->photographer; } /** * Set photographer for this photo * @param photographer the photographer to assign to this photo */ public function setPhotographer(photographer $pg) { $this->set("photographer_id", (int) $pg->getId()); $this->lookupPhotographer(); $this->update(); } /** * Remove photographer */ public function unsetPhotographer() { $this->set("photographer_id", 0); $this->update(); $this->lookupPhotographer(); } /** * Get the location for this photo */ public function getLocation() { $this->lookup(); return $this->location; } /** * Set the location for this photoa * @param place location to set */ public function setLocation(place $loc) { $this->set("location_id", (int) $loc->getId()); $this->update(); $this->lookupLocation(); } /** * Unset the location for this photo */ public function unsetLocation() { $this->set("location_id", 0); $this->update(); $this->lookupLocation(); } /** * Create thumbsize and midsize image * @param bool force (re)create resized image even if it already exist */ public function thumbnail($force=true) { $path=conf::get("path.images") . "/" . $this->get("path") . "/"; $name=$this->get("name"); $midname=MID_PREFIX . "/" . MID_PREFIX . "_" . $name; $thumbname=THUMB_PREFIX . "/" . THUMB_PREFIX . "_" . $name; if (!file_exists($path . $midname) || $force===true) { $this->createThumbnail(MID_PREFIX, MID_SIZE); } if (!file_exists($path . $thumbname) || $force===true) { $this->createThumbnail(THUMB_PREFIX, THUMB_SIZE); } return true; } /** * Create resized image * @param string prefix for newly created image * @param int size for largest size of width/height */ private function createThumbnail($prefix, $size) { $img_src = $this->getImageResource(); $image_info = getimagesize($this->getFilePath()); $width = $image_info[0]; $height = $image_info[1]; if ($width >= $height) { $new_width = $size; $new_height = round(($new_width / $width) * $height); } else { $new_height = $size; $new_width = round(($new_height / $height) * $width); } $img_dst = imagecreatetruecolor($new_width, $new_height); flush(); if (conf::get("import.resize")=="resize") { imagecopyresized($img_dst, $img_src, 0, 0, 0, 0, $new_width, $new_height, $width, $height); } else { imagecopyresampled($img_dst, $img_src, 0, 0, 0, 0, $new_width, $new_height, $width, $height); } flush(); $new_image = conf::get("path.images") . '/' . $this->get("path") . '/' . $prefix . '/' . $prefix . '_' . $this->get("name"); $dir=dirname($new_image); if (!is_writable($dir)) { throw new fileDirNotWritableException("Directory not writable: " . $dir); } if (!imagejpeg($img_dst, $new_image)) { throw new photoThumbCreationFailedException("Could not create " . $prefix . " image"); } imagedestroy($img_dst); imagedestroy($img_src); } /** * Rotate image * @param int degrees (90, 180, 270) */ public function rotate($deg) { if (!conf::get("rotate.enable") || !$this->get('name')) { return; } $dir = conf::get("path.images") . "/" . $this->get("path") . "/"; $name = $this->get('name'); $images[$dir . THUMB_PREFIX . '/' . THUMB_PREFIX . '_' . $name] = $dir . THUMB_PREFIX . '/rot_' . THUMB_PREFIX . '_' . $name; $images[$dir . MID_PREFIX . '/' . MID_PREFIX . '_' . $name] = $dir . MID_PREFIX . '/rot_' . MID_PREFIX . '_' . $name; $images[$dir . $name] = $dir . 'rot_' . $name; if (conf::get("rotate.backup")) { $backup_name = conf::get("rotate.backup.prefix") . $name; // file_exists() check from From Michael Hanke: // Once a rotation had occurred, the backup file won't be // overwritten by future rotations and the original file // is always preserved. if (!file_exists($dir . $backup_name)) { if (!@copy($dir . $name, $dir . $backup_name)) { throw new fileCopyFailedException( sprintf(translate("Could not copy %s to %s."), $name, $backup_name)); } } } // make a system call to convert or jpegtran to do the rotation. foreach ($images as $file => $tmp_file) { if (!file_exists($file)) { throw new fileNotFoundException("Could not find " . $file); } switch (conf::get("rotate.command")) { case "jpegtran": $cmd = 'jpegtran -copy all -rotate ' . escapeshellarg($deg) . ' -outfile ' . escapeshellarg($tmp_file) . ' ' . escapeshellarg($file); break; case "convert": default: $cmd = 'convert -rotate ' . escapeshellarg($deg) . ' ' . escapeshellarg($file) . ' ' . escapeshellarg($tmp_file); } $cmd .= ' 2>&1'; $output = system($cmd); if ($output) { // error throw new zophException(translate("An error occurred. ") . $output); } rename($tmp_file, $file); } // update the size and dimensions // (only if original was rotated) $this->update(); $this->updateSize(); } /** * Get an array of properties for this object, to display this info * @return array photo properties */ public function getDisplayArray() { $date=new Time($this->getReverseDate()); $loclink=""; if ($this->location instanceof place) { $loclink =new block("link", array( "href" => $this->location->getURL(), "link" => $this->location->getName(), "target" => "" )); } $pglink=""; if ($this->photographer instanceof photographer) { $pglink =new block("link", array( "href" => $this->photographer->getURL(), "link" => $this->photographer->getName(), "target" => "" )); } return array( translate("title") => $this->get("title"), translate("location") => $loclink, translate("view") => $this->get("view"), translate("date") => $date->getLink(), translate("time") => $this->getTimeDetails(), translate("photographer") => $pglink ); } /** * Get array of properties of this object, used to build mail message * @return array photo properties * @todo should probably be merged with getDisplayArray */ public function getEmailArray() { return array( translate("title") => $this->get("title"), translate("location") => $this->location ? $this->location->get("title") : "", translate("view") => $this->get("view"), translate("date") => $this->get("date"), translate("time") => $this->get("time"), translate("photographer") => $this->photographer ? $this->photographer->getName() : "", translate("description") => $this->get("description") ); } /** * Get array of (EXIF) camera data for this photo * @return array of EXIF data */ public function getCameraDisplayArray() { return array( "camera make" => $this->get("camera_make"), "camera model" => $this->get("camera_model"), "flash used" => $this->get("flash_used"), "focal length" => $this->get("focal_length"), "exposure" => $this->get("exposure"), "aperture" => $this->get("aperture"), "compression" => $this->get("compression"), "iso equiv" => $this->get("iso_equiv"), "metering mode" => $this->get("metering_mode"), "focus distance" => $this->get("focus_dist"), "ccd width" => $this->get("ccd_width"), "comment" => $this->get("comment")); } /** * Get time this photo was taken, corrected with timezone information * @return string time */ public function getTime() { $this->lookup(); $loc=$this->location; if ($loc instanceof place) { $loc->lookup(); } if ($loc && TimeZone::validate($loc->get("timezone"))) { $place_tz=new TimeZone($loc->get("timezone")); } if (TimeZone::validate(conf::get("date.tz"))) { $camera_tz=new TimeZone(conf::get("date.tz")); } if (!isset($place_tz) && isset($camera_tz)) { // Camera timezone is known, place timezone is not. $place_tz=$camera_tz; } else if (isset($place_tz) && !isset($camera_tz)) { // Place timezone is known, camera timezone is not. $camera_tz=$place_tz; } else if (!isset($place_tz) && !isset($camera_tz)) { $default_tz=new TimeZone(date_default_timezone_get()); $place_tz=$default_tz; $camera_tz=$default_tz; } $place_time=$this->getCorrectedTime($camera_tz, $place_tz); return $place_time; } /** * Get date/time formatted as configured * @return array date, time */ public function getFormattedDateTime() { $date_format=conf::get("date.format"); $time_format=conf::get("date.timeformat"); $place_time=$this->getTime(); $date=$place_time->format($date_format); $time=$place_time->format($time_format); return array($date, $time); } /** * get time in UTC timezone * @return array date, time */ public function getUTCtime() { $date_format=conf::get("date.format"); $time_format=conf::get("date.timeformat"); $default_tz=new TimeZone(date_default_timezone_get()); $place_tz=new TimeZone("UTC"); $camera_tz=$default_tz; if (TimeZone::validate(conf::get("date.tz"))) { $camera_tz=new TimeZone(conf::get("date.tz")); } $place_time=$this->getCorrectedTime($camera_tz, $place_tz); $date=$place_time->format($date_format); $time=$place_time->format($time_format); return array($date, $time); } /** * Returns the date in reverse, so it can be used for sorting */ public function getReverseDate() { $date_format=("Y-m-d"); $place_time=$this->getTime(); $date=$place_time->format($date_format); return $date; } /** * Get corrected time, for given timezone. * Converts the time stored in the database from the 'camera timzone' to the place timezone * @param TimeZone camera timezone, the timezone the camera was set to when this photo was taken * @param TimeZone place timezone, the timezone of the location where this photo was taken * @return Time calculated time */ private function getCorrectedTime(TimeZone $camera_tz, TimeZone $place_tz) { $camera_time=new Time( $this->get("date") . " " . $this->get("time"), $camera_tz); $place_time=$camera_time; $place_time->setTimezone($place_tz); $corr=$this->get("time_corr"); if ($corr) { $place_time->modify($corr . " minutes"); } return $place_time; } /** * Get an overview of the time details. * Shows the time of this photo and the timezones it uses * @return block template block. */ private function getTimeDetails() { $tz=null; if (TimeZone::validate(conf::get("date.tz"))) { $tz=conf::get("date.tz"); } $this->lookup(); $place=$this->location; $place_tz=null; $location=null; if (isset($place)) { $place_tz=$place->get("timezone"); $location=$place->get("title"); } $datetime=$this->getFormattedDateTime(); $tpl=new block("time_details", array( "photo_date" => $this->get("date"), "photo_time" => $this->get("time"), "camera_tz" => $tz, "corr" => $this->get("time_corr"), "location" => $location, "loc_tz" => $place_tz, "calc_date" => $datetime[0], "calc_time" => $datetime[1] )); return $tpl; } /** * Get comments for this photo * @return array of comments */ public function getComments() { $qry=new select(array("pcom" => "photo_comments")); $distinct=true; $qry->addFields(array("comment_id"), $distinct); $where=new clause("pcom.photo_id=:photoid"); $qry->addParam(new param(":photoid", (int) $this->getId(), PDO::PARAM_INT)); $qry->where($where); return comment::getRecordsFromQuery($qry); } /** * Get Related photos * @return array related photos */ public function getRelated() { $user=user::getCurrent(); $allrelated=photoRelation::getRelated($this); if ($user->canSeeAllPhotos()) { return $allrelated; } else { $related=array(); foreach ($allrelated as $photo) { if ($user->getPhotoPermissions($photo)) { $related[]=$photo; } } return $related; } } /** * Get description for a specific related photo * @param photo photo to get relation for * @return string description */ public function getRelationDesc(photo $photo) { return photoRelation::getDescForPhotos($this, $photo); } /** * Returns full EXIF information in a definitionlist * @return string HTML * @todo contains lots of HTML * @todo is a mess */ public function exifToHTML() { if (exif_imagetype($this->getFilePath())==IMAGETYPE_JPEG) { $exif=exif_read_data($this->getFilePath()); if ($exif) { $return="
\n"; foreach ($exif as $key => $value) { if (!is_array($value)) { $return .="
$key
\n" . "
" . preg_replace("/[^[:print:]]/", "", $value) . "
\n"; } else { $return .="
$key
\n" . "
\n" . "
\n"; foreach ($value as $subkey => $subval) { $return .= "
$subkey
\n" . "
" . preg_replace("/[^[:print:]]/", "", $subval) . "
\n"; } $return .= "
\n" . "
\n"; } } $return .= "

"; } else { $return=false; } } else { $return=false; } return $return; } /** * Get a short overview of this photo. * Used in popup-boxes on the map * @return string HTML * @todo contains HTML */ public function getQuicklook() { $title=e($this->get("title")); $file=$this->get("name"); if ($title) { $html="

" . e($title) . "<\/h2>

" . e($file) . "<\/p>"; } else { $html="

" . e($file) . "<\/h2>"; } $html.=str_replace("\n", "", $this->getThumbnailLink()); $html.="

" . $this->get("date") . " " . $this->get("time") . "
"; if ($this->photographer) { $html.=translate("by", 0) . " " . $this->photographer->getLink(1) . "
"; } $html.="<\/small><\/p>"; return $html; } /** * Get Marker to be placed on map * @param string icon to be used. * @return marker instance of marker class */ public function getMarker($icon="geo-photo") { $marker=marker::getFromObj($this, $icon); if (!$marker instanceof marker) { $loc=$this->location; if ($loc instanceof place) { return $loc->getMarker(); } } else { return $marker; } } /** * Get photos taken near this photo * @param int distance in km or miles * @param int limit maxiumum number of photos to return * @param string entity (km or miles) */ public function getNear($distance, $limit=100, $entity="km") { $lat=$this->get("lat"); $lon=$this->get("lon"); if ($lat && $lon) { return static::getPhotosNear( (float) $lat, (float) $lon, (float) $distance, (int) $limit, $entity ); } } /** * Get photos taken near a lat/lon location * @param float latitude * @param float longitude * @param int distance * @param int limit maxiumum number of photos to return * @param string entity (km or miles) */ public static function getPhotosNear($lat, $lon, $distance, $limit, $entity="km") { // If lat and lon are not set, don't bother trying to find // near photos if ($lat && $lon) { if ($entity=="miles") { $distance=(float) $distance * 1.609344; } $qry=new select(array("p" => "photos")); $qry->addFields(array("photo_id")); $qry->addFunction(array("distance" => "(6371 * acos(" . "cos(radians(:lat)) * cos(radians(lat)) * cos(radians(lon) - " . "radians(:lon)) + sin(radians(:lat2)) * sin(radians(lat))))")); $qry->having(new clause("distance <= :dist")); $qry->addParam(new param(":lat", (float) $lat, PDO::PARAM_STR)); $qry->addParam(new param(":lat2", (float) $lat, PDO::PARAM_STR)); $qry->addParam(new param(":lon", (float) $lon, PDO::PARAM_STR)); $qry->addParam(new param(":dist", (float) $distance, PDO::PARAM_STR)); if ($limit) { $qry->addLimit((int) $limit); } $qry->addOrder("distance"); return static::getRecordsFromQuery($qry); } else { return null; } } /** * Get photos from filename * @param string filename * @param string path * @return array photo(s) */ public static function getByName($file, $path=null) { $qry=new select(array("p" => "photos")); $qry->addFields(array("photo_id")); $where=new clause("name = :file"); $qry->addParam(new param(":file", $file, PDO::PARAM_STR)); if (!empty($path)) { $where->addAnd(new clause("path = :path")); $qry->addParam(new param(":path", $path, PDO::PARAM_STR)); } $qry->where($where); return static::getRecordsFromQuery($qry); } /** * Calculate SHA1 hash for a file * @return string SHA1 hash */ private function getHashFromFile() { $this->lookupAll(); $file=$this->getFilePath(); if (file_exists($file)) { return sha1_file($file); } else { throw new fileNotFoundException("File not found:" . $file); } } /** * Get hash for photo. * Returns the hash for a photo, either the file hash, or a salted hash that * can be used to share photos * @param string type file, full or mid * @return string hash */ public function getHash($type="file") { $hash=$this->get("hash"); if (empty($hash)) { try { $hash=$this->getHashFromFile(); $this->set("hash", $hash); $this->update(); } catch (Exception $e) { log::msg($e->getMessage(), log::ERROR, log::IMG); } } switch ($type) { case "file": $return=$hash; break; case "full": $return=sha1(conf::get("share.salt.full") . $hash); break; case "mid": $return=sha1(conf::get("share.salt.mid") . $hash); break; default: die("Unsupported hash type"); break; } return $return; } /** * Set photo's lat/lon from a point object * @param point */ public function setLatLon(point $point) { $this->set("lat", $point->get("lat")); $this->set("lon", $point->get("lon")); } /** * Try to determine the lat/lon position this photo was taken from one or all tracks; * @param track track to use or null to use all tracks * @param int maximum time the time can be off * @param bool Whether to interpolate between 2 found times/positions * @param int Interpolation max_distance: what is the maximum distance between two * points to still interpolate * @param string km / miles entity in which max_distance is measured * @param int Interpolation maxtime Maximum time between to point to still interpolate */ public function getLatLon(track $track=null, $maxtime=300, $interpolate=true, $int_maxdist=5, $entity="km", $int_maxtime=600) { date_default_timezone_set("UTC"); $datetime=$this->getUTCtime(); $utc=strtotime($datetime[0] . " " . $datetime[1]); $qry=new select(array("pt" => "point")); $where=new clause("datetime > :mintime"); $where->addAnd(new clause("datetime < :maxtime")); $qry->addParam(new param(":mintime", date("Y-m-d H:i:s", $utc - $maxtime), PDO::PARAM_STR)); $qry->addParam(new param(":maxtime", date("Y-m-d H:i:s", $utc + $maxtime), PDO::PARAM_STR)); $qry->addParam(new param(":utc", date("Y-m-d H:i:s", $utc), PDO::PARAM_STR)); if ($track) { $where->addAnd(new clause("track_id=:trackid")); $qry->addParam(new param(":trackid", (int) $track->getId(), PDO::PARAM_INT)); } $qry->addOrder("abs(timediff(datetime, :utc)) ASC"); $qry->addLimit(1); $qry->where($where); $points=point::getRecordsFromQuery($qry); if (sizeof($points) > 0 && $points[0] instanceof point) { $point=$points[0]; $pointtime=strtotime($point->get("datetime")); } else { // can't get a point, don't bother trying to interpolate. $interpolate=false; $point=null; } if ($interpolate && ($pointtime != $utc)) { if ($utc>$pointtime) { $p1=$point; $p2=$point->getNext(); } else { $p1=$point->getPrev(); $p2=$point; } if ($p1 instanceof point && $p2 instanceof point) { $p3=point::interpolate($p1, $p2, $utc, $int_maxdist, $entity, $int_maxtime); if ($p3 instanceof point) { $point=$p3; } } } return $point; } /** * Takes an array of photos and returns a subset * * @param array photos to return a subset from * @param array Array should contain first and/or last and/or random to determine * which subset(s) * @param int count Number of each to return * @return array subset of photos */ public static function getSubset(array $photos, array $subset, $count) { $first=array(); $last=array(); $random=array(); $begin=0; $end=null; $max=count($photos); if ($count>$max) { $count=$max; } if (in_array("first", $subset)) { $first=array_slice($photos, 0, $count); $max=$max-$count; $begin=$count; } if (in_array("last", $subset)) { $last=array_slice($photos, -$count); $max=$max-$count; $end=-$count; } if (in_array("random", $subset) && ($max > 0)) { $center=array_slice($photos, $begin, $end); $max=count($center); if ($max!=0) { if ($count>$max) { $count=$max; } $random_keys=(array) array_rand($center, $count); foreach ($random_keys as $key) { $random[]=$center[$key]; } } } $subset=array_merge($first, $random, $last); // remove duplicates due to overlap: $clean_subset=array(); foreach ($subset as $photo) { $clean_subset[$photo->get("photo_id")]=$photo; } return $clean_subset; } public static function getFromHash($hash, $type="file") { $qry=new select(array("p" => "photos")); if (!preg_match("/^[A-Za-z0-9]+$/", $hash)) { die("Illegal characters in hash"); } switch ($type) { case "file": $where=new clause("hash=:hash"); break; case "full": $qry->addParam(new param(":salt", conf::get("share.salt.full"), PDO::PARAM_STR)); $where=new clause("sha1(CONCAT(:salt, hash))=:hash"); break; case "mid": $qry->addParam(new param(":salt", conf::get("share.salt.mid"), PDO::PARAM_STR)); $where=new clause("sha1(CONCAT(:salt, hash))=:hash"); break; default: die("Unsupported hash type"); break; } $qry->addParam(new param(":hash", $hash, PDO::PARAM_STR)); $qry->where($where); $photos=static::getRecordsFromQuery($qry); if (is_array($photos) && sizeof($photos) > 0) { return $photos[0]; } else { throw new photoNotFoundException("Could not find photo from hash"); } } /** * Create a list of fields that can be used to sort photos on * @return array list of fields */ public static function getFields() { return array( "" => "", "date" => "date", "time" => "time", "timestamp" => "timestamp", "name" => "file name", "path" => "path", "title" => "title", "view" => "view", "description" => "description", "width" => "width", "height" => "height", "size" => "size", "aperture" => "aperture", "camera_make" => "camera make", "camera_model" => "camera model", "compression" => "compression", "exposure" => "exposure", "flash_used" => "flash used", "focal_length" => "focal length", "iso_equiv" => "iso equiv", "metering_mode" => "metering mode" ); } /** * Create a list of fields that can be specified during import * @return array list of fields */ public static function getImportFields() { return array( "" => "", "time" => "time", "timestamp" => "timestamp", "aperture" => "aperture", "camera_make" => "camera make", "camera_model" => "camera model", "compression" => "compression", "exposure" => "exposure", "flash_used" => "flash used", "focal_length" => "focal length", "iso_equiv" => "iso equiv", "metering_mode" => "metering mode", "mapzoom" => "mapzoom" ); } /** * Get accumulated disk size for all photos, as used on the info page * @return int size in bytes */ public static function getTotalSize() { $qry=new select(array("p" => "photos")); $qry->addFunction(array("total" => "sum(size)")); return $qry->getCount(); } /** * Get filesize for a set of photos * @param collection photos * @return int size in bytes */ public static function getFilesize(collection $photos) { $bytes=0; foreach ($photos as $photo) { $photo->lookup(); $bytes+=$photo->get("size"); } return $bytes; } } ?> zoph-v0.9.19/php/classes/photo/000077500000000000000000000000001415176210700163415ustar00rootroot00000000000000zoph-v0.9.19/php/classes/photo/collection.inc.php000066400000000000000000000116661415176210700217670ustar00rootroot00000000000000items as $photo) { $ids[] = $photo->getId(); } return $ids; } /** * Remove all photos that have no valid timezone * * This function is needed for geotagging: for photos without a valid * timezone it is not possible to determine the UTC time, needed for geotagging. * @return photo\collection photos with valid timezone */ public function removeNoValidTZ() { $return=array(); log::msg("Number of photos before valid timezone check: " . count($this->items), log::DEBUG, log::GEOTAG); foreach ($this->items as $photo) { $photo->lookup(); $loc=$photo->location; if (get_class($loc)=="place") { $tz=$loc->get("timezone"); if (TimeZone::validate($tz)) { $return[]=$photo; } } } log::msg("Number of photos after valid timezone check: " . count($return), log::DEBUG, log::GEOTAG); $this->items=$return; return $this; } /** * Remove all photos that have lat/lon set * Remove photos that already have lat/lon information set from this collection * * This function is needed for geotagging, so photos that have lat/lon * manually set will not be overwritten * @return photo\collection photos with no lat/lon info */ public function removeWithLatLon() { $return=array(); log::msg("Number of photos before overwrite check: " . count($this->items), log::DEBUG, log::GEOTAG); foreach ($this->items as $photo) { $photo->lookup(); if (!($photo->get("lat") || $photo->get("lon"))) { $return[]=$photo; } } log::msg("Number of photos after overwrite check: " . count($return), log::DEBUG, log::GEOTAG); $this->items=$return; return $this; } /** * Get a subset of photos to do geotagging test on * This will select a subset of photos containing of the first x, last x and or random x photos * from the subset. This is used to give the user a preview of what is going to be geotagged. * @param array subset array that can contain "first", "last" and/or "random" * @param int number of each to select */ public function getSubsetForGeotagging(array $subset, $count) { $begin=0; $max=count($this); $count = min($max, $count); $return = new self; if (in_array("first", $subset)) { $first=$this->subset(0, $count); $max=$max-$count; $begin=$count; $return = $first; } if (in_array("last", $subset)) { $last=$this->subset(-$count); $max=$max-$count; $return = $return->merge($last); } if (in_array("random", $subset) && ($max > 0)) { $center=$this->subset($begin, $max); $max=count($center); if ($max!=0) { $random = $center->random($count); $return = $return->merge($random); } } return $return->renumber(); } /** * Create a new photo\collection from request * @param request web request */ public static function createFromRequest(request $request) { return static::createFromVars($request->getRequestVarsClean()); } /** * Create a new photo\collection from request vars * @param array http request vars */ public static function createFromVars(array $vars) { $search=new search($vars); $photos=photo::getRecordsFromQuery($search->getQuery()); return static::createFromArray($photos, true); } } zoph-v0.9.19/php/classes/photo/controller.inc.php000066400000000000000000000213101415176210700220020ustar00rootroot00000000000000getPhotoFromRequest(); } catch (photoNotAccessibleSecurityException $e) { log::msg($e->getMessage(), log::WARN, log::SECURITY); $photo=null; } if ($photo instanceof photo) { $this->setObject($photo); $this->doAction(); } else { $this->view = "notfound"; } } /** * Get the photo based on the query in the request * @throws photoNotAccessibleSecurityException */ private function getPhotoFromRequest() { $user=user::getCurrent(); if (isset($this->request["photo_id"])) { $photo = new photo($this->request["photo_id"]); $photo->lookup(); } else { $offset = isset($this->request["_off"]) ? $this->request["_off"] : 0; $photoCollection = collection::createFromRequest(request::create()); $this->photocount=sizeof($photoCollection); $this->offset=$offset; $photos=$photoCollection->subset($offset, 1); if ($photos) { $photo = $photos->shift(); $photo->lookup(); } else { $photo = new photo(); } } if ($user->isAdmin() || $user->getPhotoPermissions($photo)) { return $photo; } throw new photoNotAccessibleSecurityException( "Security Exception: photo " . $photo->getId() . " is not accessible for user " . $user->getName() . " (" . $user->getId() . ")" ); } /** * Do action 'confirm' */ public function actionConfirm() { if (user::getCurrent()->canDeletePhotos()) { parent::actionConfirm(); } else { $this->view="display"; } } /** * Do action 'delete' */ public function actionDelete() { if (user::getCurrent()->canDeletePhotos()) { parent::actionDelete(); } else { $this->view="display"; } } /** * Do action 'delrate' */ public function actionDelrate() { if (user::getCurrent()->isAdmin()) { $ratingId=$this->request["_rating_id"]; $rating=new rating((int) $ratingId); $rating->delete(); breadcrumb::init(); $this->redirect = html_entity_decode(breadcrumb::getLast()->getURL()); if (!$this->redirect) { $this->redirect = "zoph.php"; } $this->view="redirect"; } } /** * Do action 'deselect' * @todo the $_SESSION access should be refactored into a separate session class */ public function actionDeselect() { $selectKey=array_search($this->request["photo_id"], $_SESSION["selected_photo"]); if ($selectKey !== false) { unset($_SESSION["selected_photo"][$selectKey]); } $this->redirect=$this->request["_return"] . "?" . $this->request->getPassedQueryString(); $this->view="redirect"; } /** * Do action 'display' */ public function actionDisplay() { $user = user::getCurrent(); if ( $user->prefs->get("auto_edit") && (!isset($this->request["_action"]) || $this->request["_action"] == "search") && $this->object->isWritableBy($user)) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'display' */ public function actionEdit() { $user = user::getCurrent(); if ($this->object->isWritableBy($user)) { $this->view="update"; } else { $this->view="display"; } } /** * Do action 'insert' * There is no "insert" for photos, so go straight to display */ public function actionInsert() { $this->view="display"; } /** * Do action 'lightbox' */ public function actionLightbox() { $this->object->addTo(new album(user::getCurrent()->get("lightbox_id"))); $this->view="display"; } /** * Do action 'unlightbox' * Remove from lightbox */ public function actionUnlightbox() { $this->object->removeFrom(new album(user::getCurrent()->get("lightbox_id"))); $this->redirect="photos.php?" . $this->request->getPassedQueryString(); $this->view="redirect"; } /** * Do action 'new' * There is no "new" for photos, so go straight to display */ public function actionNew() { $this->view="display"; } /** * Do action 'rate' */ public function actionRate() { $user=user::getCurrent(); if (conf::get("feature.rating") && ($user->isAdmin() || $user->get("allow_rating"))) { $rating = $this->request->getPostVar("rating"); $this->object->rate($rating); } breadcrumb::init(); $link = html_entity_decode(breadcrumb::getLast()->getURL()); if (!$link) { $link = "zoph.php"; } // change to proper redirect via view redirect($link); $this->view="redirect"; } /** * Do action 'update' */ public function actionUpdate() { $user=user::getCurrent(); $this->view="display"; if ($this->object->isWritableBy($user)) { $_deg = $this->request["_deg"]; if (conf::get("rotate.enable") && ($_deg && $_deg != 0)) { $this->object->lookup(); try { $this->object->rotate($_deg); } catch (\Exception $e) { die($e->getMessage()); } } if ($this->request["_thumbnail"]) { $this->object->thumbnail(); } unset($this->actionlinks["cancel"]); /** @todo Check if this works */ unset($this->actionlinks["edit"]); $this->object->setFields($this->request->getRequestVars()); // pass again for add people, categories, etc $this->object->updateRelations($this->request->getRequestVars(), "_id"); $this->object->update(); if (!empty($this->request->getPassedQueryString())) { $this->redirect="photo.php?" . $this->request->getPassedQueryString(); $this->view="redirect"; } } } /** * Do action 'select' * Add a photo to the selection, first check if it's not already selected. * @todo the $_SESSION access should be refactored into a separate session class */ public function actionSelect() { $selectKey=false; if (isset($_SESSION["selected_photo"]) && is_array($_SESSION["selected_photo"])) { $selectKey=array_search($this->object->getId(), $_SESSION["selected_photo"]); } if ($selectKey === false) { $_SESSION["selected_photo"][]=$this->object->getId(); } $this->view="display"; } } zoph-v0.9.19/php/classes/photo/data.inc.php000066400000000000000000000045331415176210700205400ustar00rootroot00000000000000photo = $photo; } public function getData() { $loc = ""; $photographer = ""; $datetime = $this->photo->getFormattedDateTime(); $albums = array_map(array("self", "getName"), $this->photo->getAlbums()); $categories = array_map(array("self", "getName"), $this->photo->getCategories()); $people = array_map(array("self", "getName"), (new people($this->photo))->getAll()); if ($this->photo->location) { $loc = $this->photo->location->getName(); } if ($this->photo->photographer) { $photographer = $this->photo->photographer->getName(); } return array( "image" => $this->photo->getURL(), "name" => $this->photo->getName(), "location" => $loc, "title" => $this->photo->get("title"), "datetime" => implode(" ", $datetime), "photographer" => $photographer, "albums" => $albums, "categories" => $categories, "people" => $people, "exifdata" => $this->photo->getCameraDisplayArray() ); } private static function getName(organizer $organizer) { return $organizer->getName(); } } ?> zoph-v0.9.19/php/classes/photo/people.inc.php000066400000000000000000000214071415176210700211120ustar00rootroot00000000000000photo = $photo; } /** * Get an array of people on the photo in question * @return array array of people - to be transformed to JSON */ public function getData() { $people = array(); foreach ($this->getInRows() as $row) { $people[] = array_map(array("self", "getPersonData"), $row); } return array( "photoId" => $this->photo->getId(), "people" => $people ); } /** * Get a list of people on this photo * @return array of people */ public function getAll() { $qry=new select(array("p" => "people")); $qry->join(array("pp" => "photo_people"), "pp.person_id = p.person_id"); $distinct=true; $qry->addFields(array("person_id"), $distinct); $qry->addFields(array("last_name", "first_name", "called", "pp.position", "pp.row")); $where=new clause("pp.photo_id=:photoid"); $qry->addParam(new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT)); $qry->addOrder("pp.row"); $qry->addOrder("pp.position"); $qry->where($where); return person::getRecordsFromQuery($qry); } /** * Get a list of people on this photo, arranged in rows * @return array of people */ public function getInRows() { $rows=array(); $people=$this->getAll(); foreach ($people as $person) { $prow = (int) $person->get("row"); if (!isset($rows[$prow])) { $rows[$prow] = array(); } $rows[$prow][] = $person; } return $rows; } /** * Gets last used position for people on a photo * @param int row to find the last position in * @return int position */ public function getLastPos($row = 0) { $qry=new select(array("pp" => "photo_people")); $qry->addFunction(array("pos" => "max(position)")); $where=new clause("photo_id=:photoid"); $where->addAnd(new clause("row=:row")); $qry->where($where); $qry->addParam(new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":row", (int) $row, PDO::PARAM_INT)); $result=db::query($qry)->fetch(PDO::FETCH_ASSOC); return (int) $result["pos"]; } /** * Set position of a person * the person currently on that position will be moved to the original position of $person * if there is noone on the new position, this function will bail out as it will * be an erroneous call. * @param person person * @param int position to move to */ public function setPosition(person $person, $pos) { list($personRow, $personPos) = $this->getPosition($person); $second=$this->getForPosition($personRow, $pos); if (!$second) { // no person on this position, not doing it return; } $qry = new update(array("photo_people")); $where=new clause("photo_id=:photoid"); $where->addAnd(new clause("person_id=:personid")); $qry->where($where); $qry->addSet("row", "row"); $qry->addSet("position", "pos"); $params = array( new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT), new param(":personid", (int) $person->getId(), PDO::PARAM_INT), new param(":row", (int) $personRow, PDO::PARAM_INT), new param(":pos", (int) $pos, PDO::PARAM_INT) ); $qry->addParams($params); $qry->execute(); $params = array( new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT), new param(":personid", (int) $second->getId(), PDO::PARAM_INT), new param(":row", (int) $personRow, PDO::PARAM_INT), new param(":pos", (int) $personPos, PDO::PARAM_INT) ); $qry->addParams($params); $qry->execute(); } /** * Set row for a person * @param person person * @param int row to move to */ public function setRow(person $person, $row) { // The person is already on row 0, we will have to move everyone up if ($row == -1) { $qry = new update(array("photo_people")); $where=new clause("photo_id=:photoid"); $qry->addSetFunction("row=row+1"); $qry->addParam(new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT)); $qry->where($where); $qry->execute(); $row = 0; } $person->removePhoto($this->photo); $person->addPhoto($this->photo, $row); // We could have ended up with empty rows in the process // this will get rid of them: $rows = $this->getInRows(); $newrow = 0; foreach ($rows as $rowid => $row) { $qry = new update(array("photo_people")); $where=new clause("photo_id=:photoid"); $where->addAnd(new clause("row=:row")); $qry->addSet("row", "newrow"); $qry->addParam(new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":newrow", (int) $newrow, PDO::PARAM_INT)); $qry->addParam(new param(":row", (int) $rowid, PDO::PARAM_INT)); $qry->where($where); $qry->execute(); $newrow++; } } /** * Get the position for a person on the photo * @param person person to find on this photo * @return array return array of (row, position) */ public function getPosition(person $person) { $qry=new select(array("pp" => "photo_people")); $qry->addFields(array("row", "position")); $where=new clause("photo_id=:photoid"); $where->addAnd(new clause("person_id=:personid")); $qry->where($where); $qry->addParam(new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":personid", (int) $person->getId(), PDO::PARAM_INT)); $result=db::query($qry)->fetch(PDO::FETCH_ASSOC); return array( (int) $result["row"], (int) $result["position"] ); } /** * Get the person on this photo, given the position * @param int row * @param int position * @return person */ public function getForPosition($row, $pos) { $qry=new select(array("pp" => "photo_people")); $qry->addFields(array("person_id")); $where=new clause("photo_id=:photoid"); $where->addAnd(new clause("row=:row")); $where->addAnd(new clause("position=:pos")); $qry->where($where); $qry->addParam(new param(":photoid", (int) $this->photo->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":row", (int) $row, PDO::PARAM_INT)); $qry->addParam(new param(":pos", (int) $pos, PDO::PARAM_INT)); $result=db::query($qry)->fetch(PDO::FETCH_ASSOC); if ($result) { return new person($result["person_id"]); } } /** * Get array of data about a person * This method is called by getData() * and relies on a person object that is generated by * self::getInRows, which adds row/pos data to the object * @param person person to get data on * @return array data array */ private static function getPersonData(person $person) { $row = (int) $person->get("row"); $pos = (int) $person->get("position"); return array( "id" => $person->getId(), "name" => $person->getName(), "url" => $person->getURL(), "row" => $row, "position" => $pos ); } } ?> zoph-v0.9.19/php/classes/photo/search.inc.php000066400000000000000000000620561415176210700211000ustar00rootroot00000000000000", ">=", "<", "<=", "like", "not like", "is in photo", "is not in photo"); /** @var Valid conjunction operators */ const CONJ = array("and", "or"); /** @var Valid sort directions */ const SORTDIR = array("asc", "desc"); /** @var Valid search fields */ const FIELDS = array("location_id", "rating", "photographer_id", "date", "time", "timestamp", "name", "path", "title", "view", "description", "width", "height", "size", "aperture", "camera_make", "camera_model", "compression", "exposure", "flash_used", "focal_length", "iso_equiv", "metering_mode"); /** @var Valid text search fields */ const TEXT = array("album", "category", "person", "photographer"); /** @var \db\query Holds the query */ private $qry; /** Holds the variables that are used to build the constraint */ /** @var array holds the request vars */ private $vars; /** * Create seach object based on http request vars * @param array vars http request vars */ public function __construct(array $vars) { $this->qry = new select(array("p" => "photos")); $this->vars = $vars; $this->processVars(); $this->setOrder(); } /** * Get the resulting query * @return \db\query SQL query that can be used to get photos from database */ public function getQuery() { return $this->qry; } /** * Process the fields needed to determine the ORDER in the SQL query */ private function setOrder() { if (isset($this->vars["_order"])) { $order = $this->vars["_order"]; } else { $order = conf::get("interface.sort.order"); } if (isset($this->vars["_dir"])) { $dir = $this->vars["_dir"]; } else { $dir = conf::get("interface.sort.dir"); } if (!in_array(strtolower($dir), static::SORTDIR)) { throw new \illegalValueSecurityException("Illegal sort direction: " . e($dir)); } if (isset($this->vars["_random"])) { // get one random result $this->qry->addOrder("rand()"); $this->qry->addLimit(1); } else { $this->qry->addFields(array($order)); $this->qry->addOrder("p." . $order . " " . $dir); if ($order == "date") { $this->qry->addFields(array("p.time")); $this->qry->addOrder("p.time " . $dir); } $this->qry->addOrder("p.photo_id " . $dir); } } /** * Process variables * This function loops over all the variables and adds the various * contstraints (clauses) to the SQL query */ private function processVars() { foreach ($this->vars as $key => $val) { if (empty($key) || empty($val) || $key[0] == "_" || strpos(" $key", "PHP") == 1) { continue; } // handle refinements of searches $suffix = ""; $hashPos = strrpos($key, "#"); if ($hashPos > 0) { $suffix = substr($key, $hashPos); $key = substr($key, 0, $hashPos); } $index = "_" . $key . $suffix; $origSuffix=$suffix; $suffix=str_replace("#", "_", $suffix); if (!empty($this->vars[$index . "-conj"])) { $conj = $this->vars[$index . "-conj"]; } else { $conj = "and"; } if (!in_array($conj, static::CONJ)) { throw new \illegalValueSecurityException("Illegal conjunction: " . e($conj)); } if (!empty($this->vars[$index . "-op"])) { $op = $this->vars[$index . "-op"]; } else { $op = "="; } if (!in_array($op, static::OPS)) { throw new \illegalValueSecurityException("Illegal operator: " . e($op)); } if (!empty($this->vars[$index . "-children"])) { $object=explode("_", $key); if ($object[0]=="location") { $object[0] = "place"; } $obj=new $object[0]($val); $val=$obj->getBranchIdArray(); } if ($key == "text") { $key = $this->vars["_" . $key . $origSuffix]; if (!in_array($key, static::TEXT)) { throw new \illegalValueSecurityException("Illegal text search: " . e($key)); } $val = e($val); $key = e($key); } // the regexp matches a list of numbers, separated by comma's. if (!is_array($val) && preg_match("/^([0-9]+)(,([0-9]+))+$/", $val)) { $val=explode(",", $val); } if ($key == "person" || $key == "photographer") { $this->processPerson($key, $val, $suffix, $conj); // continue, because processPerson already modifies the query continue; } else if ($key == "album") { $key = "album_id"; $val = $this->processAlbum($val); } else if ($key == "category") { $key = "category_id"; $val = $this->processCategory($val); } if (($key == "album_id" || $key == "category_id") && $op == "like") { $op = "="; } else if (($key == "album_id" || $key == "category_id") && $op == "not like") { $op = "!="; } if ($key == "album_id") { $this->processAlbumId($val, $suffix, $op, $conj); } else if ($key == "category_id") { $this->processCategoryId($val, $suffix, $op, $conj); } else if ($key == "location_id") { $this->processLocationId($val, $suffix, $op, $conj); } else if ($key == "person_id") { $this->processPersonId($val, $suffix, $op, $conj); } else if ($key == "userrating") { $this->processUserRating($val, $suffix, $conj); } else if ($key=="rating") { $this->processRating($val, $suffix, $op, $conj); } else if ($key=="lat" || $key=="lon") { $latlon[$key]=$val; if (!empty($latlon["lat"]) && !empty($latlon["lon"])) { $lat=(float) $latlon["lat"]; $lon=(float) $latlon["lon"]; $this->processLatLon($lat, $lon, $suffix, $conj); } } else { $this->processOtherFields($key, $val, $suffix, $origSuffix, $op, $conj); } } $this->qry = selectHelper::expandQueryForUser($this->qry, user::getCurrent()); $distinct=true; $this->qry->addFields(array("p.photo_id"), $distinct); $this->qry->addFields(array("p.name", "p.path", "p.width", "p.height")); } /** * This can be used to reference persons by name directly from the URL * it's not actually used in Zoph and it's not well documented. * But it could be used to create a URL like http://www.zoph.org/search.php?person=Jeroen Roos * With the help of url rewrite, one could even change that into something like * http://www.zoph.org/person/Jeroen Roos * @param string key name of the field (person|photographer) * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string conj, conjugation, whether this is an AND or OR search */ private function processPerson($key, $val, $suffix, $conj) { $people = person::getByName($val, true); $peopleIds=array(); if ($people && count($people) > 0) { foreach ($people as $person) { $peopleIds[]=$person->getId(); } } else { // the person did not exist, no photos should be found // however, we can't just return 0 here, as there may be an OR clause in the query... $peopleIds[]=-1; } $param=new param(":peopleIds" . $suffix, $peopleIds, PDO::PARAM_INT); $this->qry->addParam($param); if ($key=="person") { $alias = "pp" . substr($suffix, 1); $this->qry->addClause(clause::InClause($alias . ".person_id", $param), $conj); $this->qry->join(array($alias => "photo_people"), "p.photo_id=" . $alias . ".photo_id"); } else if ($key=="photographer") { $this->qry->addClause(clause::InClause("photographer_id", $param), $conj); } } /** * Search for album by name * @param string val value of the field * @return int|array album_id or array of album_ids */ private function processAlbum($val) { $album=album::getByNameHierarchical($val); if ($album instanceof album) { $val=$album->getId(); } else if (is_array($album)) { $val=array(); foreach ($album as $alb) { $val[]=$alb->getId(); } } else { // the album did not exist, no photos should be found // however, we can't just return 0 here, as there may be an OR clause in the query... $val=-1; } return $val; } /** * Search for category by name * @param string val value of the field * @return int|array category_id or array of category_ids */ private function processCategory($val) { $category=category::getByNameHierarchical($val); if ($category instanceof category) { $val=$category->getId(); } else if (is_array($category)) { $val=array(); foreach ($category as $cat) { $val[]=$cat->getId(); } } else { // the category did not exist, no photos should be found // however, we can't just return 0 here, as there may be an OR clause in the query... $val=-1; } return $val; } /** * Search for album by id * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string operator, how the values should be compared (=, !=, like, etc.) * @param string conj, conjugation, whether this is an AND or OR search */ private function processAlbumId($val, $suffix, $op, $conj) { if ($op == "=") { $alias = "pa" . substr($suffix, 1); /* * Because the query builder expects the photo_album table to be aliased to "pa", * the first occurence does not have number suffix */ if ($alias=="pa1") { $alias="pa"; } $this->qry->join(array($alias => "photo_albums"), "p.photo_id=" . $alias . ".photo_id"); if (is_numeric($val)) { $this->qry->addClause(new clause($alias . ".album_id=:albumId" . $suffix), $conj); $this->qry->addParam(new param(":albumId" . $suffix, (int) $val, PDO::PARAM_INT)); } else if (is_array($val)) { $param=new param(":albumIds" . $suffix, $val, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::InClause($alias . ".album_id", $param), $conj); } else { throw new \keyMustBeNumericSecurityException("album_id must be numeric"); } } else { // assume "not in" $exclAlbumsQry=new select(array("p" => "photos")); $exclAlbumsQry->addFields(array("photo_id"), true); $exclAlbumsQry->join(array("pa" => "photo_albums"), "p.photo_id=pa.photo_id"); $param=new param(":albumIds" . $suffix, (array) $val, PDO::PARAM_INT); $exclAlbumsQry->addParam($param); $exclAlbumsQry->where(clause::InClause("pa.album_id", $param)); $exclPhotoIds=$exclAlbumsQry->toArray(); $param=new param(":photoIds" . $suffix, (array) $exclPhotoIds, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::NotInClause("p.photo_id", $param), $conj); } } /** * Search for category by id * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string operator, how the values should be compared (=, !=, like, etc.) * @param string conj, conjugation, whether this is an AND or OR search */ private function processCategoryId($val, $suffix, $op, $conj) { if ($op == "=") { $alias = "pc" . substr($suffix, 1); $this->qry->join(array($alias => "photo_categories"), "p.photo_id=" . $alias . ".photo_id"); if (is_numeric($val)) { $this->qry->addClause(new clause($alias . ".category_id=:categoryId" . $suffix), $conj); $this->qry->addParam(new param(":categoryId" . $suffix, (int) $val, PDO::PARAM_INT)); } else if (is_array($val)) { $param=new param(":categoryIds" . $suffix, $val, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::InClause($alias . ".category_id", $param), $conj); } else { throw new \keyMustBeNumericSecurityException("category_id must be numeric"); } } else { /* assume "not in" */ $exclCategoryQry=new select(array("p" => "photos")); $exclCategoryQry->addFields(array("photo_id"), true); $exclCategoryQry->join(array("pc" => "photo_categories"), "p.photo_id=pc.photo_id"); $param=new param(":categoryIds" . $suffix, (array) $val, PDO::PARAM_INT); $exclCategoryQry->addParam($param); $exclCategoryQry->where(clause::InClause("pc.category_id", $param)); $exclPhotoIds=$exclCategoryQry->toArray(); $param=new param(":photoIds" . $suffix, (array) $exclPhotoIds, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::NotInClause("p.photo_id", $param), $conj); } } /** * Search for location by id * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string operator, how the values should be compared (=, !=, like, etc.) * @param string conj, conjugation, whether this is an AND or OR search */ private function processLocationId($val, $suffix, $op, $conj) { if (is_numeric($val)) { $this->qry->addParam(new param(":locationId" . $suffix, (int) $val, PDO::PARAM_INT)); if ($op == "=") { $this->qry->addClause(new clause("p.location_id=:locationId" . $suffix), $conj); } else { $clause=new clause("p.location_id != :locationId" . $suffix); $clause->addOr(new clause("p.location_id is null")); $this->qry->addClause($clause, $conj); } } else if (is_array($val)) { $param=new param(":locationIds" . $suffix, $val, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::InClause("p.location_id", $param), $conj); } else { throw new \keyMustBeNumericSecurityException("location_id must be numeric"); } } /** * Search for person by id * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string operator, how the values should be compared (=, !=, like, etc.) * @param string conj, conjugation, whether this is an AND or OR search */ private function processPersonId($val, $suffix, $op, $conj) { if ($op == "=") { $alias = "ppl" . substr($suffix, 1); $this->qry->join(array($alias => "photo_people"), "p.photo_id=" . $alias . ".photo_id"); if (is_numeric($val)) { $this->qry->addClause(new clause($alias . ".person_id=:personId" . $suffix), $conj); $this->qry->addParam(new param(":personId" . $suffix, (int) $val, PDO::PARAM_INT)); } else if (is_array($val)) { $param=new param(":personIds" . $suffix, $val, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::InClause($alias . ".person_id", $param), $conj); } else { throw new \keyMustBeNumericSecurityException("person_id must be numeric"); } } else { // assume "not in" $exclPeopleQry=new select(array("p" => "photos")); $exclPeopleQry->addFields(array("photo_id"), true); $exclPeopleQry->join(array("ppl" => "photo_people"), "p.photo_id=ppl.photo_id"); $param=new param(":personIds" . $suffix, (array) $val, PDO::PARAM_INT); $exclPeopleQry->addParam($param); $exclPeopleQry->where(clause::InClause("ppl.person_id", $param)); $exclPhotoIds=$exclPeopleQry->toArray(); $param=new param(":photoIds" . $suffix, (array) $exclPhotoIds, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::NotInClause("p.photo_id", $param), $conj); } } /** * Search for user rating * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string conj, conjugation, whether this is an AND or OR search */ private function processUserRating($val, $suffix, $conj) { $user=user::getCurrent(); if ($user->isAdmin() && isset($this->vars["_userrating_user"])) { $ratingUserId=$this->vars["_userrating_user"]; } else { $ratingUserId=$user->getId(); } if ($val != "null") { $alias = "pr" . substr($suffix, 1); $this->qry->join(array($alias => "photo_ratings"), "p.photo_id=" . $alias . ".photo_id"); $clause=new clause($alias . ".user_id=:ratingUserId" . $suffix); $clause->addAnd(new clause($alias . ".rating=:rating" . $suffix)); $this->qry->addParam(new param(":ratingUserId", $ratingUserId, PDO::PARAM_INT)); $this->qry->addParam(new param(":rating", $val, PDO::PARAM_INT)); $this->qry->addClause($clause, $conj); } else { $noRateQry=new select(array("pr" => "photo_ratings")); $noRateQry->addFields(array("photo_id"), true); $noRateQry->where(new clause("pr.user_id=:ratingUserId")); $noRateQry->addParam(new param(":ratingUserId", $ratingUserId, PDO::PARAM_INT)); $photoIds=$noRateQry->toArray(); if (sizeof($photoIds) > 0) { $param=new param(":photoIds" . $suffix, (array) $photoIds, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::NotInClause("p.photo_id", $param), $conj); } } } /** * Search for rating * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string operator, how the values should be compared (=, !=, like, etc.) * @param string conj, conjugation, whether this is an AND or OR search */ private function processRating($val, $suffix, $op, $conj) { $alias = "vpr" . substr($suffix, 1); $this->qry->join(array($alias => "view_photo_avg_rating"), "p.photo_id=" . $alias . ".photo_id"); if ($val=="null") { if ($op == "!=") { $clause=new clause($alias . ".rating is not null"); } else if ($op == "=") { $clause=new clause($alias . ".rating is null"); } } else { $this->qry->addParam(new param(":rating" . $suffix, $val, PDO::PARAM_INT)); $clause=new clause($alias . ".rating " . $op . " :rating" . $suffix); } $this->qry->addClause($clause, $conj); } /** * Search for Latitude / longitude * @param float latitude value * @param float longitude value * @param string suffix, the suffix can be used to search for the same field multiple times * @param string conj, conjugation, whether this is an AND or OR search */ private function processLatLon($lat, $lon, $suffix, $conj) { $ids=array(); $distance=(float) $this->vars["_latlon_distance"]; if (isset($this->vars["_latlon_entity"]) && $this->vars["_latlon_entity"]=="miles") { $distance=$distance * 1.609344; } if (isset($this->vars["_latlon_photos"])) { $photos=photo::getPhotosNear($lat, $lon, $distance, null); if ($photos) { foreach ($photos as $photo) { $ids[]=$photo->getId(); } } } if (isset($this->vars["_latlon_places"])) { $places=place::getPlacesNear($lat, $lon, $distance, null); foreach ($places as $place) { $photos=$place->getPhotos(user::getCurrent()); foreach ($photos as $photo) { $ids[]=$photo->getId(); } } } if ($ids) { $param=new param(":photoIds" . $suffix, $ids, PDO::PARAM_INT); $this->qry->addParam($param); $this->qry->addClause(clause::InClause("p.photo_id", $param), $conj); } else { // No photos were found $this->qry->addClause(new clause("p.photo_id=-1"), $conj); } } /** * Search for other fields * @param string key name of the field * @param string val value of the field * @param string suffix, the suffix can be used to search for the same field multiple times * @param string original suffix, the unprocessed suffix * @param string operator, how the values should be compared (=, !=, like, etc.) * @param string conj, conjugation, whether this is an AND or OR search */ private function processOtherFields($key, $val, $suffix, $origSuffix, $op, $conj) { // any other field $clause=null; /* if the key name starts with is "field", we replace te keyname with the contents of _field#0, which holds the real field name */ if (strncasecmp($key, "field", 5) == 0) { $key = $this->vars["_" . $key . $origSuffix]; } if (!in_array($key, static::FIELDS)) { throw new \illegalValueSecurityException("Illegal field: " . e($key)); } $val = e($val); $key = e($key); if ($val=="null") { if ($op == "!=") { $clause=new clause("p." . $key . " is not null"); } else if ($op == "=") { $clause=new clause("p." . $key . " is null"); } } else { $clause=new clause("p." . $key . " " . $op . " :" . $key . $suffix); if ($op == "like" || $op == "not like") { $val="%" . $val . "%"; } else if ($op == "!=") { $clause->addOr(new clause("p." . $key . " is null")); } $this->qry->addParam(new param(":" . $key . $suffix, $val, PDO::PARAM_STR)); } if ($clause instanceof clause) { $this->qry->addClause($clause, $conj); } } } ?> zoph-v0.9.19/php/classes/photo/view/000077500000000000000000000000001415176210700173135ustar00rootroot00000000000000zoph-v0.9.19/php/classes/photo/view/common.inc.php000066400000000000000000000172521415176210700220730ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->photo=$photo; } /** * Get permissions for the current photo * @todo refactor out */ protected function getPermissions() { $user=user::getCurrent(); if (!$user->isAdmin()) { return $user->getPhotoPermissions($this->photo); } } /** * Create "next" "previous" and "up" links for the photo page */ public function setLinks() { $act = ""; if (isset($this->request["_off"])) { $user=user::getCurrent(); $ignore=array("_off", "_action"); $cols = (int) $this->request["_cols"]; $rows = (int) $this->request["_rows"]; $offset = (int) $this->request["_off"]; $_action = $this->request["_action"]; $cols = $cols ? $cols : $user->prefs->get("num_cols"); $rows = $rows ? $rows : $user->prefs->get("num_rows"); $cells = $cols * $rows; $upQs = http_build_query($this->request->getUpdatedVars(null, null, $ignore)); if ($cells) { $off = $cells * floor($offset / ($cells)); $upQs .= "&_off=" . $off; } $this->upURL="photos.php?" . $upQs; if (isset($_action) && !empty($_action="")) { $act="_action=" . $_action . "&"; } if ($offset > 0) { $newoffset = $offset - 1; $this->prevURL=$this->request->getServerVar("PHP_SELF") . "?" . $act . htmlentities( str_replace("_off=$offset", "_off=$newoffset", $this->request->getReturnQueryString()) ); } if ($offset + 1 < $this->photocount) { $newoffset = $offset + 1; $this->nextURL = $this->request->getServerVar("PHP_SELF") . "?" . $act . htmlentities( str_replace("_off=$offset", "_off=$newoffset", $this->request->getReturnQueryString()) ); } } } /** * Get selection block for current photo * @return block template block */ protected function getSelection() { try { $selection=new selection($_SESSION, array( "relate" => "relation.php?_action=new&photo_id_1=" . $this->photo->getId() . "&photo_id_2=", "return" => "_return=photo.php&_qs=" . $this->request->getEncodedQueryString() ), $this->photo); } catch (\photoNoSelectionException $e) { $selection=null; } return $selection; } /** * Get 'share' block for current photo * @return block template block */ protected function getShare() { if (conf::get("share.enable") && (user::getCurrent()->isAdmin() || user::getCurrent()->get("allow_share"))) { $hash=$this->photo->getHash(); $fullHash=sha1(conf::get("share.salt.full") . $hash); $midHash=sha1(conf::get("share.salt.mid") . $hash); $fullLink=url::get() . "image.php?hash=" . $fullHash; $midLink=url::get() . "image.php?hash=" . $midHash; $share=new template("photo_share", array( "hash" => $hash, "full_link" => $fullLink, "mid_link" => $midLink )); } return isset($share) ? $share : null; } /** * Get the title for the current photo * @param int number of photos * @param int offset from first photo * @return block template block */ protected function getTitle($photoCount, $offset) { if ($photoCount) { return sprintf(translate("photo %s of %s"), ($offset + 1), $photoCount); } else if ($this->photo instanceof photo && !empty($this->photo->get("name"))) { return $this->photo->get("name"); } else { return translate("photo"); } } /** * Create the actionlinks for this page */ protected function getActionlinks() { $user=user::getCurrent(); $regexAction=array("/_action=\w+&?/"); $actionlinks=array(); if (conf::get("feature.mail")) { $actionlinks[translate("email")]="mail.php?_action=compose&photo_id=" . $this->photo->getId(); } if ($user->isAdmin() || $this->getPermissions()->get("writable")) { $actionlinks[translate("edit")]="photo.php?_action=edit&" . $this->request->getReturnQueryString(); } if ($user->isAdmin() || ($user->canDeletePhotos() && $this->getPermissions()->get("writable"))) { $actionlinks[translate("delete")]="photo.php?_action=delete&photo_id=" . $this->photo->getId() . "&_qs=" . $this->request->getEncodedQueryString(); } if ($user->get("lightbox_id")) { $actionlinks[translate("lightbox")]="photo.php?_action=lightbox&" . $this->request->getQueryString(); } if (conf::get("feature.comments") && (user::getCurrent()->canLeaveComments())) { $actionlinks[translate("add comment")]="comment.php?_action=new&photo_id=" . $this->photo->getId(); } if ($user->isAdmin()) { $actionlinks[translate("select")]="photo.php?_action=select&" . $this->request->getReturnQueryString(); } return $actionlinks; } /** * Output view */ abstract public function view(); } zoph-v0.9.19/php/classes/photo/view/confirm.inc.php000066400000000000000000000041471415176210700222370ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->photo=$photo; } /** * Output view */ public function view() { $actionlinks=array( translate("confirm") => "photo.php?_action=confirm&photo_id=" . $this->photo->getId() . "&_qs=" . $this->request->getPassedQueryString()->encode(), translate("cancel") => "photo.php?" . $this->request->getPassedQueryString() ); return new template("confirm", array( "title" => translate("delete photo"), "actionlinks" => $actionlinks, "mainActionlinks" => null, "obj" => $this->photo, "image" => $this->photo->getImageTag(MID_PREFIX) )); } } zoph-v0.9.19/php/classes/photo/view/display.inc.php000066400000000000000000000136211415176210700222440ustar00rootroot00000000000000photo->getComments(); if ($comments) { $commentTpl=new block("comments", array( "comments" => $comments )); } return isset($commentTpl) ? $commentTpl : null; } } /** * Get related photos for the current photo * @return block template block */ private function getRelated() { $related=$this->photo->getRelated(); if ($related) { $tplRelated=new block("related_photos", array( "photo" => $this->photo, "related" => $related, "admin" => (bool) user::getCurrent()->isAdmin() )); } return isset($tplRelated) ? $tplRelated : null; } /** * Get EXIF information for current photo * @return block template block */ private function getExif() { if (user::getCurrent()->prefs->get("allexif")) { $exif=new block("exif", array( "allexif" => $this->photo->exifToHTML() )); } return isset($exif) ? $exif : null; } /** * Get rating block (display) for current photo * @return block template block */ private function getRating() { $rating = $this->photo->getRating(); if ($rating && user::getCurrent()->isAdmin()) { $rating=$this->photo->getRatingDetails(); } return $rating; } /** * Get rating block (to rate) for current photo * @return block template block */ private function getRatingForm() { $user = user::getCurrent(); if (conf::get("feature.rating") && $user->canRatePhotos()) { $ratingForm=new block("formRating", array( "ratingPulldown"=> rating::createPulldown("rating", $this->photo->getRatingForUser($user)), "photoId" => $this->photo->getId() )); } return isset($ratingForm) ? $ratingForm : null; } /** * Get action links * @return array action links */ protected function getActionlinks() { $actionlinks=parent::getActionlinks(); unset($actionlinks[translate("cancel")]); unset($actionlinks[translate("return")]); return $actionlinks; } /** * Output view */ public function view() { $user = user::getCurrent(); $photo = $this->photo; $photo->lookup(); if (!$user->isAdmin()) { $permissions = $user->getPhotoPermissions($photo); } $camInfo = $user->prefs->get("camera_info") ? $photo->getCameraDisplayArray() : null; $calendar = new calendar(); $calendar->setSearchField("timestamp"); if ($user->canBrowsePeople()) { $people=(new photoPeople($photo))->getInRows(); } $timestamp = new Time($photo->get("timestamp")); $tpl = new template("displayPhoto", array( "photo" => $photo, "actionlinks" => $this->getActionlinks(), "title" => $this->getTitle($this->photocount, $this->offset), "selection" => $this->getSelection(), "prev" => $this->prevURL, "up" => $this->upURL, "next" => $this->nextURL, "full" => $full=$photo->getFullsizeLink($photo->get("name")), "size" => template::getHumanReadableBytes((int)$photo->get("size")), "share" => $this->getShare(), "image" => $photo->getFullsizeLink($photo->getImageTag(MID_PREFIX)), "people" => isset($people) ? $people : null, "fields" => $photo->getDisplayArray(), "rating" => $this->getRating(), "ratingForm" => $this->getRatingForm(), "albums" => template::createLinkList($photo->getAlbums()), "categories" => template::createLinkList($photo->getCategories()), "timestampURL" => $calendar->getDateLink($timestamp), "timestamp" => $timestamp->getFormatted(), "description" => trim($photo->get("description")), "camInfo" => $camInfo, "related" => $this->getRelated(), "exifdetails" => $this->getExif(), "comments" => $this->getComments() )); if (conf::get("maps.provider")) { $map = new map(); $photos=$photo->getNear(100); $photos[]=$photo; $map->addMarkers($photos, $user); $tpl .= $map; } return $tpl; } } zoph-v0.9.19/php/classes/photo/view/notfound.inc.php000066400000000000000000000026051415176210700224330ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); } /** * Output view */ public function view() { return new template("notFound", array( "title" => translate("Photo"), "msg" => translate("No photo was found") )); } } zoph-v0.9.19/php/classes/photo/view/redirect.inc.php000066400000000000000000000031131415176210700223730ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->photo=$photo; } /** * Output view */ public function view() { redirect($this->redirect); } /** * Set the page to redirect to * @param string redirect target */ public function setRedirect($redirect) { $this->redirect=$redirect; } } zoph-v0.9.19/php/classes/photo/view/update.inc.php000066400000000000000000000100611415176210700220540ustar00rootroot00000000000000prefs->get("auto_edit")) { $actionlinks[translate("return")]="photo.php?" . $this->request->getReturnQueryString(); } else { $actionlinks[translate("display")]="photo.php?_action=display&" . $this->request->getReturnQueryString(); } return $actionlinks; } /** * Output the view */ public function view() { $user = user::getCurrent(); $photo = $this->photo; $warning = ""; $autocomppeople = "true"; if (!person::getAutocompPref()) { $warning = new block("message", array( "class" => "warning", "text" => translate("Autocomplete for people is needed to add people to a photo") )); $autocomppeople = "false"; } $tpl = new template("editPhoto", array( "warning" => $warning, "autocomppeople" => $autocomppeople, "photo" => $photo, "title" => $this->getTitle($this->photocount, $this->offset), "selection" => $this->getSelection(), "admin" => (bool) $user->isAdmin(), "actionlinks" => $this->getActionlinks(), "return_qs" => $this->request->getReturnQueryString(), "rotate" => conf::get("rotate.enable"), "prev" => $this->prevURL, "up" => $this->upURL, "next" => $this->nextURL, "full" => $photo->getFullsizeLink($photo->get("name")), "size" => template::getHumanReadableBytes((int) $photo->get("size")), "share" => $this->getShare(), "image" => $photo->getFullsizeLink($photo->getImageTag(MID_PREFIX)), "albums" => $photo->getAlbums($user), "categories" => $photo->getCategories($user), "locPulldown" => place::createPulldown("location_id", $photo->get("location_id")), "pgPulldown" => photographer::createPulldown("photographer_id", $photo->get("photographer_id")), "albumPulldown" => album::createPulldown("_album_id[0]"), "catPulldown" => category::createPulldown("_category_id[0]", ""), "zoomPulldown" => place::createZoomPulldown($photo->get("mapzoom")), "show" => getvar("_show") )); if (conf::get("maps.provider")) { $map=new map(); $map->setEditable(); $map->setCenterAndZoomFromObj($photo); $map->addMarkers(array($photo), $user); $tpl .= $map; } return $tpl; } } zoph-v0.9.19/php/classes/photoRelation.inc.php000066400000000000000000000147261415176210700213320ustar00rootroot00000000000000set("photo_id_1", $photo1->getId()); $this->set("photo_id_2", $photo2->getId()); } /** * Get id * @return array ids */ public function getId() { return array( "photo_id_1" => (int) $this->get("photo_id_1"), "photo_id_2" => (int) $this->get("photo_id_2") ); } /** * Lookup in database. * Tries to look up in the database, first (photo_1, photo_2), then (photo_2, photo_1) * @return bool success or not */ public function lookup() { if (!parent::lookup()) { $photoId1=$this->get("photo_id_1"); $photoId2=$this->get("photo_id_2"); $this->set("photo_id_1", $photoId2); $this->set("photo_id_2", $photoId1); return parent::lookup(); } else { return true; } } /** * Get description. * Get description of the photo in the first param for the current relation * @param photo Photo to get description for * @throws relationException if you try to lookup a photo that is not part of this relation * @return string description */ public function getDesc(photo $photo) { if ($photo->getId() == $this->get("photo_id_1")) { return $this->get("desc_1"); } else if ($photo->getId() == $this->get("photo_id_2")) { return $this->get("desc_2"); } else { throw new relationException("photo not in relation"); } } /** * Set description. * Set description of the photo in the first param for the current relation * @param photo Photo to set description for * @param string description * @throws relationException if you try to lookup a photo that is not part of this relation */ public function setDesc(photo $photo, $desc) { if ($photo->getId() == $this->get("photo_id_1")) { $this->set("desc_1", $desc); } else if ($photo->getId() == $this->get("photo_id_2")) { $this->set("desc_2", $desc); } else { throw new relationException("photo not in relation"); } } /** * Define a relation between two photos, with descriptions. * Automatically creates new or updates existing relation * @param photo first photo * @param photo second photo * @param string description for first photo * @param string description for second photo */ public static function defineRelation(photo $photo1, photo $photo2, $desc1, $desc2) { $rel=new photoRelation($photo1, $photo2); $exists=$rel->lookup(); $rel->setDesc($photo1, $desc1); $rel->setDesc($photo2, $desc2); if ($exists===true) { $rel->update(); } else { $rel->insert(); } } /** * Get related photos * @param photo photo to get relations for * @return array of photos */ public static function getRelated(photo $photo) { $qry=new select(array("pr" => "photo_relations")); $qry->addFunction(array("photo_id" => "photo_id_1")); $where=new clause("photo_id_2=:photoid2"); $qry->addParam(new param(":photoid2", (int) $photo->getId(), PDO::PARAM_INT)); $qry->where($where); $qry2=new select(array("pr" => "photo_relations")); $qry2->addFunction(array("photo_id" => "photo_id_2")); $where2=new clause("photo_id_1=:photoid1"); $qry2->addParam(new param(":photoid1", (int) $photo->getId(), PDO::PARAM_INT)); $qry2->where($where2); $qry->union($qry2); $related=photo::getRecordsFromQuery($qry); return $related; } /** * Get relation for 2 specific photos. * Order of photos is not important * @param photo first photo * @param photo second photo * @returns photoRelation|bool relation, if found or false */ public static function getRelationForPhotos(photo $photo1, photo $photo2) { $rel=new photoRelation($photo1, $photo2); if (!$rel->lookup()) { return false; } return $rel; } /** * Get relation for 2 specific photos. * Returns description for SECOND photo. * @param photo first photo * @param photo second photo * @returns string description */ public static function getDescForPhotos(photo $photo1, photo $photo2) { $rel=static::getRelationForPhotos($photo1, $photo2); if ($rel instanceof photoRelation) { return $rel->getDesc($photo2); } } } ?> zoph-v0.9.19/php/classes/photographer.inc.php000066400000000000000000000100621415176210700211720ustar00rootroot00000000000000setPhotographer($this); } /** * Remove person from a photo * @param photo photo to remove the person from */ public function removePhoto(photo $photo) { $current=$photo->getPhotographer(); if ($current instanceof photographer && $current->getId() == $this->getId()) { $photo->unsetPhotographer(); } } /** * Return the number of photos this person has taken * @return int count */ public function getPhotoCount() { return sizeof(collection::createFromVars(array( "photographer_id" => $this->getId() ))); } /** * Get all photographers * @param string search for names that begin with this string * @param bool also search first name * @return array list of photographer objects */ public static function getAll($search = null, $search_first = false) { $where=null; $qry=new select(array("ppl" => "people")); if (!user::getCurrent()->canSeeAllPhotos()) { $ids=array(); $subqry = new select(array("p" => "photos")); $subqry->addFunction(array("person_id" => "DISTINCT p.photographer_id")); $subqry->join(array("ppl" => "people"), "p.photographer_id=ppl.person_id"); if ($search != null) { $where=static::getWhereForSearch($search, $search_first); $subqry->where($where); $subqry->addParam(new param(":search", $search, PDO::PARAM_STR)); if ($search_first) { $subqry->addParam(new param(":searchfirst", $search, PDO::PARAM_STR)); } } $subqry = selectHelper::expandQueryForUser($subqry); $photographers=static::getRecordsFromQuery($subqry); if (sizeof($photographers) == 0) { return null; } foreach ($photographers as $photographer) { $ids[]=$photographer->getId(); } $param=new param(":person_ids", $ids, PDO::PARAM_INT); $where=clause::InClause("person_id", $param); $qry->addParam($param); } else if ($search != null) { $qry->where(static::getWhereForSearch($search, $search_first)); $qry->addParam(new param("search", $search, PDO::PARAM_STR)); if ($search_first) { $qry->addParam(new param("searchfirst", $search, PDO::PARAM_STR)); } } if ($where instanceof clause) { $qry->where($where); } $qry->addOrder("ppl.last_name")->addOrder("ppl.called")->addOrder("ppl.first_name"); return static::getRecordsFromQuery($qry); } } ?> zoph-v0.9.19/php/classes/photos/000077500000000000000000000000001415176210700165245ustar00rootroot00000000000000zoph-v0.9.19/php/classes/photos/controller.inc.php000066400000000000000000000071711415176210700221760ustar00rootroot00000000000000params = new params($request); $this->lbPhotos = $this->params->getLightBox(); $this->collection = collection::createFromRequest($request); $this->toDisplay = $this->collection->subset( $this->params->offset, $this->params->cells); } /** * Get the photo collection for the current request * @return photo\collection */ public function getPhotos() { return $this->collection; } /** * Get the photo collection that should be displayed * this is different from @see getPhotos() as this takes paging * into account, so it will usually only return a subset of photos * @return photo\collection */ public function getDisplay() { return $this->toDisplay; } /** * Get the photo_ids that are in the user's lightbox * @return array */ public function getLightbox() { return $this->lbPhotos; } /** * Get a params object, containing various variables to do with the interface * such as sort order, number of columns, rows, etc. * @return photos\params Params object */ public function getParams() { return $this->params; } /** * Do action 'display' */ public function actionDisplay() { $this->view="display"; } /** * Do action 'edit' * @todo not implemented yet * @codeCoverageIgnore */ public function actionEdit() { $this->view="edit"; } /** * Do action 'search' * @todo not implemented yet * @codeCoverageIgnore */ public function actionSearch() { $this->view="search"; } } zoph-v0.9.19/php/classes/photos/params.inc.php000066400000000000000000000143551415176210700213000ustar00rootroot00000000000000request = $request; $user=user::getCurrent(); $this->cols = (int) ($request["_cols"] ?? $user->prefs->get("num_cols")); $this->rows = (int) ($request["_rows"] ?? $user->prefs->get("num_rows")); $this->offset= (int) ($request["_off"] ?? $_off = 0); $this->order = $request["_order"] ?? conf::get("interface.sort.order"); $this->dir = $request["_dir"] ?? conf::get("interface.sort.dir"); $this->show = $request["_show"]; $this->cells = $this->rows * $this->cols; if (!preg_match("/^[a-zA-Z_]*$/", $this->order)) { throw new illegalValueSecurityException("Illegal characters in _order"); } $this->collection = collection::createFromRequest($request); $this->toDisplay = $this->collection->subset($this->offset, $this->cells); $this->setLightbox(); $this->setCounts(); $this->setTitle(); $this->setPager(); } /** * Set various calculated values */ private function setCounts() { $this->displayCount = sizeof($this->toDisplay); $this->photoCount=(sizeof($this->collection)); $this->pageCount = ceil($this->photoCount / $this->cells); } /** * Set pager * Create a pager and store it so the view can later retrieve it */ private function setPager() { $user=user::getCurrent(); $this->pager = new pager( $this->offset, $this->photoCount, $this->pageCount, $this->cells, $user->prefs->get("max_pager_size"), $this->request->getRequestVarsClean(), "_off" ); } /** * Set page and titlebar titles * Set the titles for this page, based on where in the photo collection we are */ private function setTitle() { if ($this->photoCount) { $currentPage = floor($this->offset / $this->cells) + 1; $num = min($this->cells, $this->displayCount); $this->title = sprintf(translate($this->name . " (Page %s/%s)", 0), $currentPage, $this->pageCount); $this->titleBar = sprintf(translate("photos %s to %s of %s"), ($this->offset + 1), ($this->offset + $num), $this->photoCount); } else { $this->title = translate("No Photos Found"); $this->titleBar = translate("photos"); } } /** * Get additional CSS * increases the width of the main window in case that is needed to * display the configured number of columns * @return string CSS code */ public function getStyle() { if (!($this->displayCount == 0 || $this->cols <= 4)) { $width = ((THUMB_SIZE + 14) * $this->cols) + 25; $defaultWidth= conf::get("interface.width"); if ($width > $defaultWidth || strpos($defaultWidth, "%")) { return "body { width: " . $width . "px; }\n"; } } } /** * Get an array of photo_ids in this user's lightbox * @return array */ public function getLightbox() { return $this->lightbox; } /** * Get pager * retrieve a stored pager * @return template\block pager template */ public function getPager() { return $this->pager; } /** * Create an array of photo_ids in this user's lightbox */ private function setLightbox() { $lightbox = user::getCurrent()->getLightbox(); if ($lightbox) { if ($this->request["album_id"] == $lightbox->getId()) { $this->name = "Lightbox"; } $lbPhotos = array_map( function($photo) { return $photo->getId(); }, $lightbox->getPhotos()); $this->lightbox=$lbPhotos; } } } zoph-v0.9.19/php/classes/photos/view/000077500000000000000000000000001415176210700174765ustar00rootroot00000000000000zoph-v0.9.19/php/classes/photos/view/display.inc.php000066400000000000000000000137751415176210700224410ustar00rootroot00000000000000params->dir) { case "asc": $up = template::getImage("up1.gif"); $down = template::getImage("down2.gif"); break; case "desc": $up = template::getImage("up2.gif"); $down = template::getImage("down1.gif"); break; } $sortupurl = "photos.php?" . http_build_query( $this->request->getUpdatedVars("_dir", "asc")); $sortdownurl = "photos.php?" . http_build_query( $this->request->getUpdatedVars("_dir", "desc")); $viewformvars = $this->request->getUpdatedVars(null, null, array ("_rows", "_cols", "_order", "_button")); $order=template::createPhotoFieldPulldown("_order", $this->params->order); $rowsDropdown = template::createIntegerPulldown("_rows", $this->params->rows, 1, 20); $colsDropdown = template::createIntegerPulldown("_cols", $this->params->cols, 1, 20); $viewform=new form("viewSettings", array( "sortup" => $up, "sortdown" => $down, "order" => $order, "rowsDropdown" => $rowsDropdown, "colsDropdown" => $colsDropdown, "sortupurl" => $sortupurl, "sortdownurl" => $sortdownurl )); $viewform->addHiddenFields($viewformvars); $tpl=new template("displayPhotos", array( "title" => $this->params->titleBar, "actionlinks" => $this->getActionlinks(), "viewsettings" => $viewform, "displaycount" => $this->params->displayCount, "cols" => $this->params->cols, "thumbnails" => $this->getThumbs(), "thActionlinks" => $this->getThumbActionlinks(), "pager" => $this->params->getPager(), "map" => $this->getMap() )); return $tpl; } /** * Set photo collection * @param photo\collection photos for this view */ public function setPhotos(photoCollection $photos) { $this->photos = $photos; } /** * Set photo collection to be displayed * @param photo\collection photos to display */ public function setDisplay(photoCollection $display) { $this->display = $display; } /** * Set photoids for the lightbox of the current user * @param array|null of photoids */ public function setLightbox(?array $lightbox) { $this->lightbox = (array) $lightbox; } /** * Get an array of thumbnails to display * @param array of @see template\block */ private function getThumbs() { $offset = $this->params->offset; $ignore = array("_action", "_photo_id"); $thumbs = array(); foreach ($this->display as $photo) { if (isset($this->request["_random"])) { $thumbs[$offset] = $photo->getThumbnailLink(); } else { $vars = $this->request->getUpdatedVars("_off", $offset, $ignore); $qs = http_build_query($vars); $thumbs[$offset] = $photo->getThumbnailLink("photo.php?" . $qs); } $offset++; } return $thumbs; } /** * Get actionlinks for thumbnails * at this moment only to remove from lightbox * @return array links */ private function getThumbActionlinks() { $offset = $this->params->offset; $actionlinks = array(); foreach ($this->display as $photo) { if (array_search($photo->getId(), $this->lightbox) !== false) { $photoId = (int) $photo->getId(); $qs = $this->request->getEncodedQueryString(); $actionlinks[$offset] = new block("actionlinks", array( "actionlinks" => array( "x" => "photo.php?_action=unlightbox&photo_id=" . $photoId . "&_qs=" . $qs ) )); } else { $actionlinks[$offset] = ""; } $offset++; } return $actionlinks; } /** * Get map to display with this page * @return geo\map map template */ private function getMap() { if (conf::get("maps.provider")) { $map=new map(); foreach ($this->display as $photo) { $photo->lookup(); $marker=$photo->getMarker(); if ($marker instanceof marker) { $map->addMarker($marker); } } return $map; } } } zoph-v0.9.19/php/classes/photos/view/view.inc.php000066400000000000000000000051121415176210700217300ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->params=$params; } /** * Get actionlinks */ protected function getActionlinks() { $user=user::getCurrent(); $qs=$this->request->getQueryString(); $actionlinks=array(); if ($this->vars["_action"] ?? "display" == "search") { $search = new search(); $search->setSearchURL($this->request); $actionlinks[translate("save search")] = "search.php?_action=new&_qs=" . $search->getSearchQS(); } if ($user->isAdmin()) { $actionlinks[translate("edit")] = "edit_photos.php?" . $qs; $actionlinks[translate("geotag")] = "tracks.php?_action=geotag&" . $qs; } $actionlinks[translate("slideshow")] = "slideshow.php?" . $qs; if (conf::get("feature.download") && ($user->get("download") || $user->isAdmin())) { $actionlinks[translate("download")] = "download.php?" . $qs; } return $actionlinks; } /** * Output view */ abstract public function view(); /** * Get the title for this view */ public function getTitle() { return $this->params->title; } } zoph-v0.9.19/php/classes/place.inc.php000066400000000000000000000532341415176210700175640ustar00rootroot00000000000000setLocation($this); } /** * Remove a photo from this place * @param photo photo to remove */ public function removePhoto(photo $photo) { if ($photo->getLocation() == $this) { $photo->unsetLocation(); } } /** * Insert place into database */ public function insert() { if ($this->get("timezone_id")) { $this->TZidToTimezone(); } unset($this->fields["timezone_id"]); parent::insert(); } /** * Update existing place with new data */ public function update() { if ($this->get("timezone_id")) { $this->TZidToTimezone(); } unset($this->fields["timezone_id"]); parent::update(); } /** * Delete this place from database */ public function delete() { $locid=new param(":locid", (int) $this->getId(), PDO::PARAM_INT); $locidNull=new param(":locidnull", null, PDO::PARAM_INT); $qry=new update(array("p" => "photos")); $qry->where(new clause("location_id=:locid")); $qry->addSet("location_id", "locidnull"); $qry->addParam($locid); $qry->addParam($locidNull); try { db::query($qry); } catch (PDOException $e) { log::msg("Could not remove references", log::FATAL, log::DB); } $qry=new update(array("ppl" => "people")); $qry->where(new clause("home_id=:locid")); $qry->addSet("home_id", "locidnull"); $qry->addParam($locid); $qry->addParam($locidNull); try { db::query($qry); } catch (PDOException $e) { log::msg("Could not remove references", log::FATAL, log::DB); } $qry=new update(array("ppl" => "people")); $qry->where(new clause("work_id=:locid")); $qry->addSet("work_id", "locidnull"); $qry->addParam($locid); $qry->addParam($locidNull); try { db::query($qry); } catch (PDOException $e) { log::msg("Could not remove references", log::FATAL, log::DB); } parent::delete(); } /** * Return whether the currently logged on user can see this place * @param user Use this user instead of the logged in one * @return bool whether or not this place should be visible */ public function isVisible(user $user=null) { if (!$user) { $user=user::getCurrent(); } $count=$this->getTotalPhotoCount(); return ($count > 0 || $user->isCreator($this) || $user->isAdmin()); } /** * Get children of this place * @param string optional order * @return array of places. */ public function getChildren($order=null) { $qry=new select(array("pl" => "places")); $qry->addFields(array("*", "name"=>"title")); $qry->join(array("p" => "photos"), "pl.place_id=p.location_id", "LEFT"); $where=new clause("parent_place_id=:placeid"); $qry->addParam(new param(":placeid", (int) $this->getId(), PDO::PARAM_INT)); $qry->addGroupBy("pl.place_id"); if ($order=="sortname") { # places do not have a sortname $order=null; } $qry=selectHelper::addOrderToQuery($qry, $order); if ($order!="name") { $qry->addOrder("name"); } if (!user::getCurrent()->canSeeAllPhotos()) { $places=static::getAll(); $placeIds=array(); foreach ($places as $place) { $placeIds[]=$place->getId(); } if (sizeof($placeIds)==0) { return array(); } $ids=new param(":placeid", $placeIds, PDO::PARAM_INT); $qry->addParam($ids); $where->addAnd(clause::InClause("pl.place_id", $ids)); } $qry->where($where); $this->children=static::getRecordsFromQuery($qry); return $this->children; } /** * Converts timezone id for this place into a named timezone */ private function TZidToTimezone() { $tzkey=$this->get("timezone_id"); if ($tzkey>0) { $tzarray=TimeZone::getSelectArray(); $tz=$tzarray[$tzkey]; $this->set("timezone", $tz); } else { $this->set("timezone", null); } unset($this->fields["timezone_id"]); } /** * Get the name of this place * @return string name of this place */ public function getName() { return $this->get("title"); } /** * Get address as template block */ public function getAddress() { $address = array(); if ($this->get("address")) { $address[]= e($this->get("address")); } if ($this->get("address2")) { $address[]= e($this->get("address2")); } $city=""; if ($this->get("city")) { $city=e($this->get("city")); if ($this->get("state")) { $city .= ", " . e($this->get("state")); } } else if ($this->get("state")) { $city .= e($this->get("state")); } if ($this->get("zip")) { $city.=" " . e($this->get("zip")); } $address[]=$city; if ($this->get("country")) { $address[]=e($this->get("country")); } $tpl=new block("multiline", array( "class" => "address", "lines" => $address )); return $tpl; } /** * Display this place's data * @todo returns HTML */ public function toHTML() { $html = $this->getAddress(); if ($this->get("url")) { $html .= "

\n"; $html .= "get("url")) . "\">"; $html .= e($this->get("urldesc")) . ""; } return $html; } /** * Return an array with this place's data */ public function getDisplayArray() { return array( translate("address") => $this->get("address"), translate("address") . "2" => $this->get("address2"), translate("city") => $this->get("city"), translate("state") => $this->get("state"), translate("zip") => $this->get("zip"), translate("country") => $this->get("country"), translate("notes") => $this->get("notes"), translate("timezone") => $this->get("timezone")); } /** * Get photos in this place */ public function getPhotos() { $qry=new select(array("p" => "photos")); $qry->addFields(array("photo_id")); $where=new clause("location_id=:locid"); $qry->addParam(new param("locid", (int) $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); return photo::getRecordsFromQuery($qry); } /** * Get count of photos in this place * @return int count */ public function getPhotoCount() { $qry=new select(array("p" => "photos")); $qry->addFunction(array("count" => "COUNT(DISTINCT(p.photo_id))")); $where=new clause("location_id=:locid"); $qry->addParam(new param("locid", (int) $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); return $qry->getCount(); } /** * Get count of photos in this place and it's children * @return int count */ public function getTotalPhotoCount() { $this->lookup(); $qry=new select(array("p" => "photos")); $qry->addFunction(array("count" => "COUNT(DISTINCT(p.photo_id))")); $idList=null; $this->getBranchIdArray($idList); $ids=new param(":locid", $idList, PDO::PARAM_INT); $qry->addParam($ids); $where=clause::InClause("p.location_id", $ids); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); return $qry->getCount(); } /** * Get coverphoto for this place. * @param string how to select a coverphoto: oldest, newest, first, last, random, highest * @param bool choose autocover from this place AND children * @return photo coverphoto */ public function getAutoCover($autocover=null,$children=false) { $coverphoto=$this->getCoverphoto(); if ($coverphoto instanceof photo) { return $coverphoto; } $qry=new select(array("p" => "photos")); $qry->addFunction(array("photo_id" => "DISTINCT ar.photo_id")); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id"); if ($children) { $ids=new param(":ids",$this->getBranchIdArray(), PDO::PARAM_INT); $qry->addParam($ids); $where=clause::InClause("p.location_id", $ids); } else { $where=new clause("p.location_id=:id"); $qry->addParam(new param(":id", $this->getId(), PDO::PARAM_INT)); } $qry = selectHelper::expandQueryForUser($qry); $qry=selectHelper::getAutoCoverOrder($qry, $autocover); $qry->where($where); $coverphotos=photo::getRecordsFromQuery($qry); $coverphoto=array_shift($coverphotos); if ($coverphoto instanceof photo) { $coverphoto->lookup(); return $coverphoto; } else if (!$children) { // No photos found in this place... let's look again, but now // also in subplaces... return $this->getAutoCover($autocover, true); } } /** * Get Marker to be placed on map * @param string icon to be used. * @return marker instance of marker class */ public function getMarker($icon="geo-place") { return marker::getFromObj($this, $icon); } /** * Get details (statistics) about this place from db * @return array Array with statistics */ public function getDetails() { $qry=new select(array("p" => "photos")); $qry->addFunction(array( "count" => "COUNT(DISTINCT p.photo_id)", "oldest" => "MIN(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "newest" => "MAX(DATE_FORMAT(CONCAT_WS(' ',p.date,p.time), GET_FORMAT(DATETIME, 'ISO')))", "first" => "MIN(p.timestamp)", "last" => "MAX(p.timestamp)", "lowest" => "ROUND(MIN(ar.rating),1)", "highest" => "ROUND(MAX(ar.rating),1)", "average" => "ROUND(AVG(ar.rating),2)")); $qry->join(array("ar" => "view_photo_avg_rating"), "p.photo_id = ar.photo_id"); $qry->addGroupBy("p.location_id"); $where=new clause("p.location_id=:locid"); $qry->addParam(new param(":locid", $this->getId(), PDO::PARAM_INT)); $qry = selectHelper::expandQueryForUser($qry); $qry->where($where); $result=db::query($qry); if ($result) { return $result->fetch(PDO::FETCH_ASSOC); } else { return null; } } /** * Turn the array from @see getDetails() into XML * @param array Don't fetch details, but use the given array */ public function getDetailsXML(array $details=null) { if (!isset($details)) { $details=$this->getDetails(); } $details["title"]=translate("In this place:", false); return parent::getDetailsXML($details); } /** * Get places near this place * @param int distance in km or miles * @param int limit maxiumum number of photos to return * @param string entity (km or miles) */ public function getNear($distance, $limit=100, $entity="km") { $lat=$this->get("lat"); $lon=$this->get("lon"); if ($lat && $lon) { return static::getPlacesNear((float) $lat, (float) $lon, (float) $distance, (int) $limit, $entity); } } /** * Get places near certain lat/lon * @param float latitude * @param float longitude * @param int distance * @param int limit number of returned places * @param string entity: km|miles * @return array places */ public static function getPlacesNear($lat, $lon, $distance, $limit, $entity="km") { // If lat and lon are not set, don't bother trying to find // near locations if ($lat && $lon) { if ($entity=="miles") { $distance=(float) $distance * 1.609344; } $qry=new select(array("pl" => "places")); $qry->addFields(array("place_id")); $qry->addFunction(array("distance" => "(6371 * acos(" . "cos(radians(:lat)) * cos(radians(lat)) * cos(radians(lon) - " . "radians(:lon)) + sin(radians(:lat2)) * sin(radians(lat))))")); $qry->having(new clause("distance <= :dist")); $qry->addParam(new param(":lat", (float) $lat, PDO::PARAM_STR)); $qry->addParam(new param(":lat2", (float) $lat, PDO::PARAM_STR)); $qry->addParam(new param(":lon", (float) $lon, PDO::PARAM_STR)); $qry->addParam(new param(":dist", (float) $distance, PDO::PARAM_STR)); if ($limit) { $qry->addLimit((int) $limit); } $qry->addOrder("distance"); return static::getRecordsFromQuery($qry); } else { return null; } } /** * Get Quick preview as used on the map display * @todo Outputs HTML */ public function getQuicklook() { $cover=""; $autocover=$this->getAutoCover(user::getCurrent()->prefs->get("autothumb")); if ($autocover instanceof photo) { $cover=$autocover->getImageTag(THUMB_PREFIX); } $html="

getURL() . "\">" . $this->getName() . "<\/h2>"; $html.="" . $this->getAddress() . "<\/small>
"; $html.=$cover; $count=$this->getPhotoCount(); $totalcount=$this->getTotalPhotoCount(); $html.="
" . e(sprintf(translate("There are %s photos"), $count) . " " . translate("in this place")) . "
"; if ($count!=$totalcount) { $html.=e(sprintf(translate("There are %s photos"),$totalcount) . " " . translate("in this place") . " " . translate("or its children")) . "
"; } $html.="<\/small>"; return str_replace("\n", "", $html); } /** * Guess the timezone based on lat/lon information */ public function guessTZ() { $lat=$this->get("lat"); $lon=$this->get("lon"); $timezone=$this->get("timezone"); if ((!$timezone && $lat && $lon)) { $tz=TimeZone::guess($lat, $lon); return $tz; } return null; } /** * Set the timezone for all places under this place to the same timezone */ public function setTzForChildren() { $tz=$this->get("timezone"); $places=$this->getBranchIdArray($places); if ($places) { foreach ($places as $place_id) { $place=new place($place_id); $place->set("timezone", $tz); $place->update(); } } } /** * Lookup place by name; * @param string name */ public static function getByName($name) { if (empty($name)) { return false; } $qry=new select(array("pl" => "places")); $qry->addFields(array("place_id")); $qry->where(new clause("lower(title)=:name")); $qry->addParam(new param(":name", strtolower($name), PDO::PARAM_STR)); return static::getRecordsFromQuery($qry); } /** * Get Top N people */ public static function getTopN() { $user=user::getCurrent(); $qry=new select(array("pl" => "places")); $qry->addFields(array("place_id", "title")); $qry->addFunction(array("count" => "count(distinct p.photo_id)")); $qry->join(array("p" => "photos"), "pl.place_id=p.location_id"); $qry->addGroupBy("p.location_id"); $qry->addOrder("count DESC")->addOrder("pl.title"); $qry->addLimit((int) $user->prefs->get("reports_top_n")); $qry = selectHelper::expandQueryForUser($qry); return parent::getTopNfromSQL($qry); } /** * Get count of places */ public static function getCount() { if (user::getCurrent()->canSeeAllPhotos()) { return parent::getCount(); } else { $qry=new select(array("p"=>"photos")); $qry->addFunction(array("count" => "COUNT(DISTINCT location_id)")); $qry = selectHelper::expandQueryForUser($qry); return $qry->getCount(); } } /** * Get all places */ public static function getAll() { $user=user::getCurrent(); if ($user->canSeeAllPhotos()) { return static::getRecords(); } else { $qry=new select(array("pl" => "places")); $qry->addFields(array("place_id")); $qry->join(array("p" => "photos"), "p.location_id=pl.place_id"); $qry = selectHelper::expandQueryForUser($qry); if ($user->canEditOrganizers()) { $subqry=new select(array("pl" => "places")); $subqry->addFields(array("place_id")); $subqry->where(new clause("pl.createdby=:ownerid")); $subqry->addParam(new param(":ownerid", (int) $user->getId(), PDO::PARAM_INT)); $qry->union($subqry); } $places=static::getRecordsFromQuery($qry); $qry=new select(array("pl" => "places")); $ids=static::getAllAncestors($places); if (sizeof($ids)==0) { return array(); } $ids=new param(":placeid", array_values($ids), PDO::PARAM_INT); $qry->addParam($ids); $qry->where(clause::InClause("pl.place_id", $ids)); return static::getRecordsFromQuery($qry); } } /** * Get autocomplete preferences for people for this user */ public static function getAutocompPref() { $user=user::getCurrent(); return ($user->prefs->get("autocomp_people") && conf::get("interface.autocomplete")); } /** * Create pulldown for zoom * @param int current value * @param name name for select box */ public static function createZoomPulldown($val = "", $name = "mapzoom") { $zoom_array = array( "0" => translate("0 - world", 0), "1" => translate("1",0), "2" => translate("2 - continent",0), "3" => translate("3",0), "4" => translate("4",0), "5" => translate("5",0), "6" => translate("6 - country",0), "7" => translate("7",0), "8" => translate("8",0), "9" => translate("9 - city",0), "10" => translate("10",0), "11" => translate("11",0), "12" => translate("12 - neighborhood",0), "13" => translate("13",0), "14" => translate("14",0), "15" => translate("15",0), "16" => translate("16 - street",0), "17" => translate("17",0), "18" => translate("18 - house",0)); return template::createPulldown($name, $val, $zoom_array); } } ?> zoph-v0.9.19/php/classes/prefs.inc.php000066400000000000000000000045311415176210700176130ustar00rootroot00000000000000colorScheme && $this->colorScheme->get("name") != null && !$force) { return $this->colorScheme; } if ($this->get("color_scheme_id")) { $this->colorScheme = new colorScheme($this->get("color_scheme_id")); $this->colorScheme->lookup(); // make sure it was actually found if ($this->colorScheme->get("name") != null) { return $this->colorScheme; } } return 0; } public function load($force = 0) { if ($this->lookupColorScheme($force)) { colorScheme::setCurrent($this->colorScheme); } } } ?> zoph-v0.9.19/php/classes/rating.inc.php000066400000000000000000000251111415176210700177550ustar00rootroot00000000000000getId(); } if ($user instanceof user) { $constraints["user_id"] = (int) $user->getId(); if ($user->get("allow_multirating")) { // This user is allowed to rate the same photoe multiple // times, however we will allow only one from the same IP $constraints["ipaddress"] = e($_SERVER["REMOTE_ADDR"]); } } return static::getRecords(null, $constraints); } /** * Get average rating for a photo * @param photo photo to get rating for * @return float average rating */ public static function getAverage(photo $photo) { $qry=new select(array("pr" => "photo_ratings")); $qry->addFunction(array("average"=>"AVG(rating)")); $qry->where(new clause("photo_id=:photoid")); $qry->addParam(new param(":photoid", (int) $photo->getId(), PDO::PARAM_INT)); $qry->addGroupBy("photo_id"); try { $result = db::query($qry); $row = $result->fetch(PDO::FETCH_ASSOC); } catch (PDOException $e) { log::msg("Rating recalculation failed", log::FATAL, log::DB); } if ($row === false) { $avg = null; } else { $avg = (round(100 * $row["average"])) / 100.0; } // Not sure if this still needed if ($avg == 0) { $avg = null; } return $avg; } /** * Get the user who made this rating * @return user user */ public function getUser() { $user=new user($this->get("user_id")); $user->lookup(); return $user; } /** * Add a new rating to the database * @param int rating * @param photo Photo to rate */ public static function setRating($rating, photo $photo) { if ((int) $rating === 0) { return; } $user=user::getCurrent(); $user->lookup(); if (!($user->isAdmin() || $user->get("allow_rating"))) { return; } if (isset($_SERVER["REMOTE_ADDR"])) { $ip = e($_SERVER["REMOTE_ADDR"]); } else { // For CLI access $host = gethostname(); $ip = e(gethostbyname($host)); } $current_ratings=static::getRatings($photo, $user); if (sizeof($current_ratings) > 0) { $cur_rating=array_pop($current_ratings); $cur_rating->set("rating", (int) $rating); $cur_rating->set("ipaddress", $ip); $cur_rating->update(); } else { $new_rating=new rating(); $new_rating->set("photo_id", (int) $photo->getId()); $new_rating->set("user_id", (int) $user->getId()); $new_rating->set("rating", (int) $rating); $new_rating->set("ipaddress", $ip); $new_rating->insert(); } } /** * Get details about rating for a specific photo * @param photo photo to get details for * @return block template block to display details */ public static function getDetails(photo $photo) { $rating=static::getAverage($photo); $ratings=static::getRatings($photo); $tpl=new block("rating_details",array( "rating" => $rating, "ratings" => $ratings, "photo_id" => $photo->getId() )); return $tpl; } /** * Get array that shows the distribution of ratings * @return array array of rating => count pairs; */ public static function getPhotoCount() { $subqry=new select(array("p" => "photos")); $subqry->addFields(array("photo_id")); $subqry->addFunction(array("rating" => "FLOOR(AVG(pr.rating)+0.5)")); $subqry->join(array("pr" => "photo_ratings"), "p.photo_id = pr.photo_id", "LEFT"); $subqry->addGroupBy("p.photo_id"); $subqry=selectHelper::expandQueryForUser($subqry); $qry=new select(array("avg_rating" => $subqry)); $qry->addFields(array("rating")); $qry->addFunction(array("count"=>"COUNT(*)")); $qry->addGroupBy("rating"); $qry->addOrder("rating"); try { $result = db::query($qry); } catch (PDOException $e) { log::msg("Rating grouping failed", log::FATAL, log::DB); } $ratings=array_fill(0, 11, 0); while ($row = $result->fetch(PDO::FETCH_ASSOC)) { $rating=(int) $row["rating"]; $ratings[$rating]=(int) $row["count"]; } return $ratings; } /** * Get array that shows the distribution of ratings * as given by a specific user * @param user the user to get count for * @return array array of rating => count pairs; */ public static function getPhotoCountForUser(user $user) { $qry = new select(array("pr" => "photo_ratings")); $qry->addFunction(array( "rating" => "ROUND(rating)", "count" => "COUNT(*)" )); $qry->where(new clause("user_id=:userid")); $qry->addParam(new param(":userid", (int) $user->getId(), PDO::PARAM_INT)); $qry->addGroupBy("ROUND(rating)"); $qry->addOrder("ROUND(rating)"); try { $result = db::query($qry); } catch (PDOException $e) { log::msg("Rating grouping failed", log::FATAL, log::DB); } $ratings=array_fill(1, 10, 0); while ($row = $result->fetch(PDO::FETCH_ASSOC)) { $rating=(int) $row["rating"]; $ratings[$rating]=(int) $row["count"]; } return $ratings; } /** * Turn the array from `getPhotoCountForUser()` into * an array that can be fed to the bar_graph template * @param user user to create graph for * @return array graph array */ public static function getGraphArrayForUser(user $user) { $ratings=static::getPhotoCountForUser($user); $max = max($ratings); if ($max == 0) { // no ratings $max=100; } $link=array( "_action" => translate("search"), "_userrating_user" => (int) $user->getId() ); foreach ($ratings as $rating=>$count) { $graph[$rating]=array( "count" => (int) $count, "width" => round($count / $max * 100, 2), "value" => (int) $rating ); if ($count > 0) { $link["userrating"]=$rating; $graph[$rating]["link"]="photos.php?" . http_build_query($link); } } return $graph; } /** * Turn array from `getPhotoCount()` into an array that * can be fed to the template * @return array graph array */ public static function getGraphArray() { $ratings=static::getPhotoCount(); $max = max($ratings); if ($max == 0) { // no ratings $max=100; } $link=array( "_rating_op" => array(">=","<"), "_action" => translate("search") ); foreach ($ratings as $rating=>$count) { $graph[$rating]=array( "count" => (int) $count, "width" => round($count / $max * 100, 2), "value" => (int) $rating ); if ($count > 0) { if ($rating == 0) { $graph[0]["link"] = "photos.php?rating=null"; $graph[0]["value"] = translate("not rated"); } else { $link["rating"]=array($rating - 0.5, $rating + 0.5); $graph[$rating]["link"]="photos.php?" . http_build_query($link); } } } return $graph; } /** * Create a pulldown to select ratings * @param string name * @param int value to make 'selected' * @return block pulldown template block */ public static function createPulldown($name = "rating", $val = null) { $ratingArray = array( "1" => translate("1 - close your eyes", 0), "2" => translate("2", 0), "3" => translate("3", 0), "4" => translate("4", 0), "5" => translate("5 - so so", 0), "6" => translate("6", 0), "7" => translate("7", 0), "8" => translate("8", 0), "9" => translate("9", 0), "10" => translate("10 - museum", 0) ); if (empty($val)) { $ratingArray = array("0" => translate("not rated", 0)) + $ratingArray; } return template::createPulldown($name, $val, $ratingArray, false, "zRating.ratingButton(\"ratingsubmit\", \"rating\")"); } } zoph-v0.9.19/php/classes/report.inc.php000066400000000000000000000034751415176210700200150ustar00rootroot00000000000000 photo::getCount(), translate("size of photos") => "$size", translate("number of photos in an album") => $album->getTotalPhotoCount(), translate("number of categorized photos") => $category->getTotalPhotoCount(), translate("number of people") => person::getCount(), translate("number of places") => place::getCount() ); } } zoph-v0.9.19/php/classes/search.inc.php000066400000000000000000000121571415176210700177440ustar00rootroot00000000000000set("timestamp", "now()"); parent::update(); } /** * Lookup an existing search in the db */ public function lookup() { $user = user::getCurrent(); $qry = new select(array("ss" => "saved_search")); $where = new clause("search_id=:searchid"); $qry->addParam(new param(":searchid", $this->getId(), PDO::PARAM_INT)); if (!$user->isAdmin()) { $clause = new clause("owner=:owner"); $qry->addParam(new param(":owner", $user->getId(), PDO::PARAM_INT)); $clause->addOr(new clause("public=TRUE")); $where->addAnd($clause); } $qry->where($where); return $this->lookupFromSQL($qry); } /** * Get the name of this search */ public function getName() { return $this->get("name"); } /** * Dummy function that acts as a placeholder for functionality that should be created * someday * @todo This should be created some time, but might slow down too much */ public function getPhotoCount() { } /** * Display the search */ public function getLink() { $user=user::getCurrent(); $tplData=array( "href" => $this->getSearchURL() . "&_action=search", "link" => $this->getName(), "target" => "" ); if ($this->get("owner") != $user->get("user_id")) { $owner=new user($this->get("owner")); $owner->lookup(); $tplData["owner"]=$owner; } return new block("savedSearch", $tplData); } /** * Set search url from request * @param request web request */ public function setSearchURL(request $request) { $vars=$request->getRequestVarsClean(); unset($vars["_action"]); unset($vars["_crumb"]); $urlVars=array(); foreach ($vars as $key => $val) { // Change key#0 into key[0]: $key=preg_replace("/\#([0-9]+)/", "[$1]", $key); // Change key[0]-children into key_children[0] because everything // after ] in a variable name is lost fix for bug#2890387 $key=preg_replace("/\[(.+)\]-([a-z]+)/", "_$2[$1]", $key); $urlVars[]=e($key) . "=" . e($val); } $this->set("search", implode("&", $urlVars)); } /** * Get search query string * @return string urlencoded query string */ public function getSearchQS() { return urlencode($this->get("search")); } /** * Get a link to use this search * This is different from getURL(), the URL returned by this function will take you to the * photo page, with the saved search applied. */ public function getSearchURL() { return "search.php?" . $this->get("search"); } /** * Get a link to this search * @return string url */ public function getURL() { return "search.php?search_id=" . $this->getId(); } /** * Get a list of saved searches * @return block template saved searches */ public static function getList() { $user=user::getCurrent(); $searches=static::getRecords("name", array( "owner" => $user->getId(), "public" => "true" ), "OR"); if ($searches) { return new block("savedSearches", array( "searches" => $searches, "user" => $user )); } } } ?> zoph-v0.9.19/php/classes/search/000077500000000000000000000000001415176210700164555ustar00rootroot00000000000000zoph-v0.9.19/php/classes/search/controller.inc.php000066400000000000000000000040231415176210700221200ustar00rootroot00000000000000request["search_id"])) { $search = new search($this->request["search_id"]); $search->lookup(); } else if ($_action=="new") { $search=new search(); $search->setSearchURL($this->request); $search->set("owner", user::getCurrent()->getId()); } else { $search=new search(); } $this->setObject($search); $this->doAction(); } /** * Do action 'search' */ public function actionSearch() { $this->view="photos"; } } zoph-v0.9.19/php/classes/search/view/000077500000000000000000000000001415176210700174275ustar00rootroot00000000000000zoph-v0.9.19/php/classes/search/view/display.inc.php000066400000000000000000000227321415176210700223630ustar00rootroot00000000000000vars=$request->getRequestVars(); } /** * Output view */ public function view() { if (conf::get("maps.provider")) { $map = new map(); $map->setEditable(); if (isset($this->vars["lat"]) && isset($this->vars["lon"])) { $map->addMarker(new marker($this->vars["lat"], $this->vars["lon"], null, null, null)); } } $search = new template("main", array( "title" => translate("Search"), "map" => isset($map) ? $map : null )); $form = new block("searchForm", array()); foreach ($this->getSearchTerms() as $param => $term) { $form->addBlocks($this->buildTerm($param, $term)); } if (conf::get("maps.provider")) { $form->addBlock($this->buildMapTerm()); } $search->addBlocks(array($form)); $search->addBlocks(array(search::getList())); return $search; } /** * Get an array of search terms, this array is used to build the search page * @return array elements ('search terms') to build search page with */ private function getSearchTerms() { return array( "date" => array( "label" => translate("photos taken"), "op" => array("template\\template", "createInequalityOperatorPulldown"), "value" => array("template\\template", "createDaysAgoPulldown"), "value_text" => translate("days ago") ), "timestamp" => array( "label" => translate("photos modified"), "op" => array("template\\template", "createInequalityOperatorPulldown"), "value" => array("template\\template", "createDaysAgoPulldown"), "value_text" => translate("days ago") ), "album_id" => array( "label" => translate("album"), "op" => array("template\\template", "createBinaryOperatorPulldown"), "value" => array("album", "createPullDown"), "child" => "album_id_children", "child_label" => translate("include sub-albums") ), "category_id" => array( "label" => translate("category"), "op" => array("template\\template", "createBinaryOperatorPulldown"), "value" => array("category", "createPullDown"), "child" => "category_id_children", "child_label" => translate("include sub-categories") ), "location_id" => array( "label" => translate("location"), "op" => array("template\\template", "createBinaryOperatorPulldown"), "value" => array("place", "createPullDown"), "child" => "location_id_children", "child_label" => translate("include sub-places") ), "rating" => array( "label" => translate("rating"), "op" => array("template\\template", "createOperatorPulldown"), "value" => array("rating", "createPullDown"), ), "person_id" => array( "label" => translate("person"), "op" => array("template\\template", "createPresentOperatorPulldown"), "value" => array("person", "createPullDown"), ), "photographer_id" => array( "label" => translate("photographer"), "op" => array("template\\template", "createBinaryOperatorPulldown"), "value" => array("photographer", "createPullDown"), ), "field" => array( "label" => array("template\\template", "createPhotoFieldPulldown"), "op" => array("template\\template", "createOperatorPulldown"), ), "text" => array( "label" => array("template\\template", "createPhotoTextPulldown"), "op" => array("template\\template", "createTextOperatorPulldown"), )); } /** * construct template blocks from searchTerms and the GET / POST parameters given to the page * @param string parameter to build searchTerm for * @param array searchTerm array with fields * label : label for the searchterm * op : template containing the operator (=, >, <, etc) * value : template for value of the field (usually a dropdown) * optional * value_text : text to add after the value * optional * child : tickbox for 'include children' * optional * child_text : text for the tickbox * optional */ private function buildTerm($param, array $term) { $blocks=array(); $count = isset($this->vars[$param]) ? sizeof($this->vars[$param]) - 1: 0; for ($i = 0; $i <= $count; $i++) { $conj = isset($this->vars["_${param}_conj"][$i]) ? $this->vars["_${param}_conj"][$i] : null; $op = isset($this->vars["_${param}_op"][$i]) ? $this->vars["_${param}_op"][$i] : null; $value = isset($this->vars[$param][$i]) ? $this->vars[$param][$i] : null; $value = $value == "+" ? "" : $value; if (is_array($term["label"])) { $labelVal = isset($this->vars["_${param}"][$i]) ? $this->vars["_${param}"][$i] : null; $label = call_user_func($term["label"], "_${param}[$i]", $labelVal); $value = template::createInput("${param}[$i]", $value, 20); } else { $label = $term["label"]; $value = call_user_func($term["value"], "${param}[$i]", $value); } $templateParams=array( "inc" => ($i == $count) ? $param . "[" . ($i + 1) ."]": false, "label" => $label, "conj" => template::createConjunctionPulldown("_${param}_conj[$i]", $conj), "op" => call_user_func($term["op"], "_${param}_op[$i]", $op), "value" => $value, "value_text" => isset($term["value_text"]) ? $term["value_text"] : null, ); if (isset($term["child"])) { $children = isset($this->vars["_${term["child"]}"][$i]); $templateParams += array( "child" => "_${term["child"]}[$i]", "child_checked" => $children ? "checked" : "", "child_label" => $term["child_label"] ); } $blocks[]=new block("searchTerm", $templateParams); } return $blocks; } /** * Build search term to search using the map */ private function buildMapTerm() { $conj = isset($this->vars["_latlon_conj"]) ? $this->vars["_latlon_conj"] : null; $value = isset($this->vars["_latlon_distance"]) ? $this->vars["_latlon_distance"] : null; $entity = isset($this->vars["_latlon_entity"]) ? $this->vars["_latlon_entity"] : "km"; $lat = isset($this->vars["lat"]) ? $this->vars["lat"] : null; $lon = isset($this->vars["lon"]) ? $this->vars["lon"] : null; $places = isset($this->vars["_latlon_places"]); $photos = isset($this->vars["_latlon_photos"]); $entityDropdown = template::createPulldown("_latlon_entity", $entity, array("km" => "km", "miles" => "miles")); $valueInput = template::createInput("_latlon_distance", $value, 10); $templateParams=array( "conj" => template::createConjunctionPulldown("_latlon_conj", $conj), "value" => $valueInput, "entity" => $entityDropdown, "places_checked" => $places ? "checked" : "", "photos_checked" => $photos ? "checked" : "", "lat" => template::createInput("lat", $lat, 10), "lon" => template::createInput("lon", $lon, 10) ); return new block("searchTermMap", $templateParams); } } zoph-v0.9.19/php/classes/search/view/photos.inc.php000066400000000000000000000030421415176210700222230ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); } /** * Output view */ public function view() { $request_vars=$this->vars; $request=$this->request; $user=user::getCurrent(); ob_start(); require("photos.php"); return ob_get_clean(); } } zoph-v0.9.19/php/classes/selection.inc.php000066400000000000000000000067101415176210700204620ustar00rootroot00000000000000links=$links; $this->obj=$current; foreach ($session["selected_photo"] as $photo_id) { $photo=new photo($photo_id); $photo->lookup(); $this->photos[]=$photo; } if (sizeof($this->photos) === 0) { throw new photoNoSelectionException("No photos selected"); } } /** * Display the selection div */ public function __toString() { $links=$this->links; $return=$links["return"]; unset($links["return"]); $photos=array(); foreach ($this->photos as $photo) { $actionlinks=array(); if (!($this->obj instanceof photo && ($this->obj->getId() == $photo->getId()))) { foreach ($links as $title => $link) { $actionlinks[$title] = $link . $photo->getId() . "&" . $return; } } $actionlinks["x"] = "photo.php?_action=deselect&photo_id=" . $photo->getId() . "&" . $return; $tplActionlinks=new block("actionlinks",array( "actionlinks" => $actionlinks )); $photos[]=array( "actionlinks" => $tplActionlinks, "photo" => $photo ); } $tpl=new block("selection", array( "count" => count($this->photos), "photos" => $photos )); return (string) $tpl; } } zoph-v0.9.19/php/classes/slideshow/000077500000000000000000000000001415176210700172115ustar00rootroot00000000000000zoph-v0.9.19/php/classes/slideshow/controller.inc.php000066400000000000000000000031411415176210700226540ustar00rootroot00000000000000request = $request; $offset = $this->request["_off"] ?: 0; $_pause = $this->request["_pause"]; $_random = $this->request["_random"]; $clean_vars=$this->request->getRequestVarsClean(); $photoCollection = collection::createFromRequest($this->request); $toDisplay = $photoCollection->subset($offset, 1); $photoCount=sizeof($photoCollection); } /** * Do action 'display' */ public function actionDisplay() { $this->view="display"; } } zoph-v0.9.19/php/classes/slideshow/view/000077500000000000000000000000001415176210700201635ustar00rootroot00000000000000zoph-v0.9.19/php/classes/slideshow/view/display.inc.php000066400000000000000000000032711415176210700231140ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); } /** * Output view */ public function view() { $user = user::getCurrent(); $tpl = new template("slideshow", array( "javascript" => "var duration=" . (int) $user->prefs->get("slideshow_time") . ";" )); $tpl->js = array("js/json.js", "js/slideshow.js", "js/fullscreen.js"); return $tpl; } public function getTitle() { return "Slideshow"; } } zoph-v0.9.19/php/classes/template/000077500000000000000000000000001415176210700170235ustar00rootroot00000000000000zoph-v0.9.19/php/classes/template/block.inc.php000066400000000000000000000033341415176210700214010ustar00rootroot00000000000000vars=$vars; if (!preg_match("/^[A-Za-z0-9_]+$/", $template)) { log::msg("Illegal characters in template", log::FATAL, log::GENERAL); } else { $this->template="templates/default/blocks/" . $template . ".tpl.php"; } } } zoph-v0.9.19/php/classes/template/colorScheme.inc.php000066400000000000000000000123301415176210700225460ustar00rootroot00000000000000 $value) { $this->set($name, $value); } $this->removeOctothorpe(); } } /** * Insert the color scheme in the db */ public function insert() { $this->removeOctothorpe(); parent::insert(); } /** * Update the color scheme in the db */ public function update() { $this->removeOctothorpe(); parent::update(); } public function getName() { return $this->get("name"); } public function getDisplayArray() { $da = parent::getDisplayArray(); unset($da["Name"]); return $da; } /** * Get color from current color scheme * or fall back to default * @param string Name of color to retrieve * @return string #xxxxxx HTML color code */ public static function getColor($color) { if (!is_null(static::$current)) { return "#" . static::$current->get($color); } else { return static::getDefault($color); } } /** * Get all colours from the current scheme * @return array of name => value pairs */ public function getColors() { $this->lookup(); $colors=array(); foreach ($this->fields as $field => $value) { if ($this->isKey($field) || $field=="name") { continue; } $colors[$field]=$this->fields[$field]; } return $colors; } private function removeOctothorpe() { foreach ($this->fields as $field => $value) { $this->set($field, str_replace("#", "", $value)); } } /** * Define a default for each color * for now, this is a fallback for whenever no color scheme has been loaded, * e.g. when the user is not logged in yet. Eventually, it will be possible * to define a "default" color scheme, and then this will only be used in * a worst case fall back (for example when an admin deletes *all* color * schemes. * @param string Name of color to retrieve * @return array of name => #xxxxxx HTML color code pairs * @throws Exception * @todo Maybe a custom Exception should be created. */ private static function getDefault($color) { $cs = self::getDefaults(); if (array_key_exists($color, $cs)) { return $cs[$color]; } else { throw new \Exception("Undefined Color: " . e($color)); } } private static function getDefaults() { return array( "page_bg_color" => "#ffffff", "text_color" => "#000000", "link_color" => "#111111", "vlink_color" => "#444444", "table_bg_color" => "#ffffff", "table_border_color" => "#000000", "breadcrumb_bg_color" => "#ffffff", "title_bg_color" => "#f0f0f0", "title_font_color" => "#000000", "tab_bg_color" => "#000000", "tab_font_color" => "#ffffff", "selected_tab_bg_color" => "#c0c0c0", "selected_tab_font_color" => "#000000" ); } /** * Set current color scheme * @param colorScheme the color scheme to use */ public static function setCurrent(colorScheme $cs) { static::$current=$cs; } } ?> zoph-v0.9.19/php/classes/template/colorScheme/000077500000000000000000000000001415176210700212665ustar00rootroot00000000000000zoph-v0.9.19/php/classes/template/colorScheme/controller.inc.php000066400000000000000000000050041415176210700247310ustar00rootroot00000000000000isAdmin()) { $this->actionDisplay(); } else { parent::__construct($request); if ($request->_action=="new") { $this->setObject(new colorScheme()); $this->doAction(); } else { $colorScheme = new colorScheme($request["color_scheme_id"]); $colorScheme->lookup(); $this->title=$colorScheme->get("name"); $this->setObject($colorScheme); $this->doAction(); } } } /** * Do action 'copy' */ public function actionCopy() { $this->title = translate("Copy Color Scheme"); $this->object->set("name", "copy of " . $this->object->get("name")); $this->object->set("color_scheme_id", 0); $this->view = "update"; } /** * Do action 'update' */ public function actionUpdate() { parent::actionUpdate(); user::getCurrent()->prefs->load(); } } zoph-v0.9.19/php/classes/template/colorScheme/view/000077500000000000000000000000001415176210700222405ustar00rootroot00000000000000zoph-v0.9.19/php/classes/template/colorScheme/view/confirm.inc.php000066400000000000000000000041501415176210700251560ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->colorScheme=$colorScheme; } /** * Output view */ public function view() { $actionlinks=array( "confirm" => "color_scheme.php?_action=confirm&color_scheme_id=" . $this->colorScheme->getId(), "cancel" => "color_schemes.php" ); return new template("confirm", array( "title" => $this->getTitle(), "actionlinks" => $actionlinks, "mainActionlinks" => null, "obj" => $this->colorScheme, )); } public function getTitle() { return translate("Confirm delete ") . $this->colorScheme->getName(); } } zoph-v0.9.19/php/classes/template/colorScheme/view/display.inc.php000066400000000000000000000054101415176210700251660ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->colorScheme=$colorScheme; } /** * Get action links * @return array action links */ protected function getActionlinks() { if (user::getCurrent()->isAdmin()) { return array( translate("edit") => "color_scheme.php?_action=edit&color_scheme_id=" . $this->colorScheme->getId(), translate("copy") => "color_scheme.php?_action=copy&color_scheme_id=" . $this->colorScheme->getId(), translate("delete") => "color_scheme.php?_action=delete&color_scheme_id=" . $this->colorScheme->getId(), translate("new") => "color_scheme.php?_action=new" ); } else { return array(); } } /** * Output view */ public function view() { $tpl = new template("main", array( "title" => $this->getTitle() )); $tpl->addBlock(new block("displayColorScheme", array( "colors" => $this->colorScheme->getDisplayArray() ))); $tpl->addActionlinks($this->getActionlinks()); return $tpl; } public function getTitle() { return $this->colorScheme->getName(); } } zoph-v0.9.19/php/classes/template/colorScheme/view/redirect.inc.php000066400000000000000000000033061415176210700253240ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->colorScheme=$colorScheme; } /** * Output view */ public function view() { redirect($this->redirect); } /** * Set the page to redirect to * @param string redirect target */ public function setRedirect($redirect) { $this->redirect=$redirect; } public function getTitle() { return "Redirect"; } } zoph-v0.9.19/php/classes/template/colorScheme/view/update.inc.php000066400000000000000000000055311415176210700250070ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); $this->colorScheme=$colorScheme; } /** * Create the actionlinks for this page * @return array action links */ protected function getActionlinks() { return array( "return" => "color_schemes.php", ); } /** * Output the view */ public function view() { $user = user::getCurrent(); $action = $this->vars["_action"]; if ($action == "copy" || $action == "new") { $action = "insert"; } else if ($action == "edit") { $action = "update"; } $form = new form("form", array( "formAction" => "color_scheme.php", "onsubmit" => null, "action" => $action, "submit" => translate("save") )); $form->addInputHidden("color_scheme_id", $this->colorScheme->getId()); $name = $this->colorScheme->getName(); $form->addInputText("name", $name, "name", "", 64, 16); $form->addBlock(new block("editColorScheme", array( "colors" => $this->colorScheme->getColors() ))); $tpl = new template("main", array( "actionlinks" => $this->getActionlinks(), "title" => $this->getTitle() )); $tpl->addBlock($form); return $tpl; } public function getTitle() { if ($this->vars["_action"] == "new") { return translate("New Color Scheme"); } return $this->colorScheme->getName(); } } zoph-v0.9.19/php/classes/template/fieldset.inc.php000066400000000000000000000021071415176210700221030ustar00rootroot00000000000000addBlock(template::createInput($name, $value, $maxlength, $label, $size, $hint)); } /** * Add a form field INPUT type password * @param string name * @param string label text for label * @param string input hint * @param int size of the field */ public function addInputPassword($name, $label=null, $hint=null, $size=32) { $this->addBlock(new block("formInputPassword", array( "name" => $name, "label" => e($label), "hint" => e($hint), "size" => (int) $size ))); } /** * Add a form field INPUT type hidden * @param string name * @param string value */ public function addInputHidden($name, $value) { $this->addBlock(new block("formInputHidden", array( "name" => $name, "value" => e($value), ))); } /** * Add hidden fields to this form * @param array array of key -> value pairs to make into hidden fields */ public function addHiddenFields(array $vars) { foreach ($vars as $key => $val) { $this->addInputHidden($key, $val); } } /** * Add a form field INPUT type checkbox * @param string name * @param bool checked * @param string label text for label * @param string input hint */ public function addInputCheckbox($name, $checked, $label, $hint=null) { $this->addBlock(new block("formInputCheckbox", array( "name" => $name, "checked" => $checked, "label" => e($label), "hint" => e($hint), ))); } /** * Add a form field TEXTAREA * @param string name * @param string current / initial value * @param string label text for label * @param int columns * @param int rows */ public function addTextarea($name, $value, $label=null, $cols=40, $rows=4) { $this->addBlock(new block("formTextarea", array( "name" => $name, "value" => e($value), "label" => e($label), "cols" => (int) $cols, "rows" => (int) $rows ))); } /** * Add a form field dropdown * this function is not actually creating the dropdown, but * acts as a wrapper around the dropdown, to add a label * @param string name * @param block dropdown * @param string label text for label */ public function addPulldown($name, block $dropdown, $label) { $this->addBlock(new block("formPulldown", array( "name" => $name, "dropdown" => $dropdown, "label" => $label ))); } /** * add fieldset * @param fieldset fieldset */ public function addFieldset(fieldset $fieldset) { $this->addBlock($fieldset); } } zoph-v0.9.19/php/classes/template/pager.inc.php000066400000000000000000000066201415176210700214060ustar00rootroot00000000000000current=(string) $pageNum; $pageGroup=0; $pages[$pageGroup]=array(); /** @todo pass as variable instead of creating here */ $request = request::create(); if ($current > 0) { $newOffset = max(0, $current - $pageSize); $this->pages[$pageGroup][translate("Prev")]= $url . "?" . http_build_query($request->getUpdatedVars($var, $newOffset)); } if ($numPages > 1) { $midPage = floor($maxSize / 2); $page = $pageNum - $midPage; if ($page <= 0) { $page = 1; } $lastPage = $page + $maxSize - 1; if ($lastPage > $numPages) { $page = $page - $lastPage + $numPages; if ($page <= 0) { $page = 1; } $lastPage = $numPages; } if ($page > 1) { $this->pages[$pageGroup]["1"] = $url . "?" . http_build_query($request->getUpdatedVars($var, 0)); } $pages[++$pageGroup]=array(); while ($page <= $lastPage) { $newOffset = ($page - 1) * $pageSize; $this->pages[$pageGroup][(string) $page] = $url . "?" . http_build_query($request->getUpdatedVars($var, $newOffset)); $page++; } $pages[++$pageGroup]=array(); if ($page <= $numPages) { $this->pages[$pageGroup][(string) $numPages] = $url . "?" . http_build_query($request->getUpdatedVars($var, ($numPages-1) * $pageSize)); } } if ($total > $current + $pageSize) { $newOffset = $current + $pageSize; $this->pages[$pageGroup][translate("Next")]= $url . "?" . http_build_query($request->getUpdatedVars($var, $newOffset)); } } public function __toString() { return (string) $this->getBlock(); } public function getBlock() { return new block("pager", array( "pages" => $this->pages, "current" => $this->current )); } } zoph-v0.9.19/php/classes/template/template.inc.php000066400000000000000000000424371415176210700221310ustar00rootroot00000000000000vars=$vars; if (preg_match("/^[A-Za-z0-9_\-]+$/", $tpl) && preg_match("/^[A-Za-z0-9_\-]+$/", $template)) { $file="templates/" . $tpl . "/" . $template . ".tpl.php"; if (!file_exists($file)) { $file="templates/default/" . $template . ".tpl.php"; } $this->template=$file; $this->vars["css"][]="css.php"; } else { log::msg("Illegal characters in template", log::FATAL, log::GENERAL); } } /** * Get image URL for specific template. * if the image does not exist in the current template, the default will be be returned * This enables template builders to only include the parts of the template that * have been changed * @param string image name * @return string relative image url */ public static function getImage($image) { $tpl=conf::get("interface.template"); if (preg_match("/^[A-Za-z0-9_\-\/\.]+$/", $image) && !preg_match("/\.\./", $image)) { $file="templates/" . $tpl . "/images/" . $image; if (!file_exists($file)) { $file="templates/default/images/" . $image; } return $file; } else { log::msg("Illegal characters in icon name", log::FATAL, log::GENERAL); } } /** * Print the template * * @return string */ public function __toString() { if ($this->vars) { extract($this->vars, EXTR_PREFIX_ALL, "tpl"); } if (!defined("ZOPH")) { define('ZOPH', true); } try { ob_start(); include $this->template; return trim(ob_get_clean()); } catch (\Exception $e) { echo $e->getMessage(); die(); } } /** * Add a block * @param block Block to be added */ public function addBlock(block $block=null) { $this->blocks[]=$block; } /** * Add a page * A page can simply be added to the list of blocks as it can be displayed * with the __toString() function * @param page Page to be added */ public function addPage(page $page) { $this->blocks[]=$page; } /** * Add multiple blocks * @param array Blocks to be added */ public function addBlocks(array $blocks) { foreach ($blocks as $block) { $this->addBlock($block); } } /** * Get the blocks inside this template * @return array blocks */ protected function getBlocks() { return $this->blocks; } /** * Display the blocks inside this template * @return string HTML code for the blocks */ protected function displayBlocks() { $html=""; foreach ($this->getBlocks() as $block) { $html.=$block; } return $html; } /** * Add an actionlink * @param string Title to be displayed * @param string URL */ public function addActionlink($title, $link) { $this->actionlinks[$title]=$link; } /** * Add multiple actionlinks * @param array of actionlinks */ public function addActionlinks(array $al) { foreach ($al as $title => $link) { $this->addActionlink($title, $link); } } /** * Markup an array of actionlinks using the actionlinks template * @param array Optional array of actionlinks, otherwise use the ones in the class */ private function getActionlinks(array $actionlinks=null) { if ($actionlinks==null) { $actionlinks=$this->actionlinks; } if (is_array($actionlinks)) { return new block("actionlinks", array( "actionlinks" => $actionlinks) ); } } /** * Create a link list * Creates a comma separated list of links from the given records. * The class of the records must implement the getLink function. * @param array Array of records to be displayed * @return string Comma separated links to records * @todo Could maybe better move into zophTable? * @todo Should check whether the object is of a supported class */ public static function createLinkList(array $records) { $links = ""; if ($records) { foreach ($records as $rec) { if ($links) { $links .= ", "; } $links .= $rec->getLink(); } } return $links; } /** * Creates an array to be used in the createPulldown methods. The * values of the fields in the name_fields parameter are concatentated * together to construnct the titles of the selections. * @param array Records to be processed * @param array fields to use to contruct title * @return array Array that can be fed to the createPulldown methods. */ public static function createSelectArray(array $records, array $name_fields, $addEmpty=false) { if (empty($records) || !$name_fields) { return array(); } $sa=array(); if ($addEmpty) { $sa[]=" "; } foreach ($records as $rec) { // this only makes sense when there is one key $id = $rec->getId(); $name = ""; foreach ($name_fields as $n) { if ($name) { $name .= " "; } $name .= $rec->get($n); } $sa[$id] = $name; } return $sa; } /** * Create form input field * @param string name of the input * @param string initial value * @param int maximum length * @param string label to be added * @param int|null display size, will be set from maxlength if null */ public static function createInput($name, $value, $maxlength, $label=null, $size=null, $hint=null) { if (!$size) { $size=$maxlength; } return new block("formInputText", array( "label" => e($label), "name" => e($name), "value" => e($value), "size" => (int) $size, "maxlength" => (int) $maxlength, "hint" => e($hint), )); } /** * Create pulldown (select) * @param string name for select box * @param string current value * @param array array of options * @param bool autosubmit form after making a change */ public static function createPulldown($name, $value, $selectArray, $autosubmit=false, $onchange=null) { return new block("select", array( "name" => $name, "id" => preg_replace("/^_+/", "", $name), "options" => $selectArray, "value" => $value, "autosubmit" => (bool) $autosubmit, "onchange" => $onchange )); } /** * Create pulldown (select) to change the view * @param string name for select box * @param string current value * @param bool autosubmit form after making a change */ public static function createViewPulldown($name, $value, $autosubmit=false) { return static::createPulldown($name, $value, array( "list" => translate("List", 0), "tree" => translate("Tree", 0), "thumbs" => translate("Thumbnails", 0)), (bool) $autosubmit); } /** * Create pulldown (select) to determine how the automatic thumbnail is selected * @param string name for select box * @param string current value * @param bool autosubmit form after making a change */ public static function createAutothumbPulldown($name, $value, $autosubmit=false) { return static::createPulldown($name, $value, array( "oldest" => translate("Oldest photo", 0), "newest" => translate("Newest photo", 0), "first" => translate("Changed least recently", 0), "last" => translate("Changed most recently", 0), "highest" => translate("Highest ranked", 0), "random" => translate("Random", 0)), (bool)$autosubmit); } /** * Create pulldown (select) that lists all photo fields * @param string name for select box * @param string current value */ public static function createPhotoFieldPulldown($name, $value) { return static::createPulldown($name, $value, translate(photo::getFields(), 0)); } public static function createPhotoTextPulldown($name, $value) { return template::createPulldown($name, $value, array( "" => "", "album" => translate("album", 0), "category" => translate("category", 0), "person" => translate("person", 0), "photographer" => translate("photographer", 0))); } /** * Create pulldown (select) that lists photo fields for the import page * @param string name for select box * @param string current value */ public static function createImportFieldPulldown($name, $value) { return static::createPulldown($name, $value, translate(photo::getImportFields(), 0)); } /** * Create comparison operator pulldown, tailored for text comparison * @param string name for select box * @param string current value */ public static function createTextOperatorPulldown($name, $value = "=") { return static::createPulldown($name, $value, array( "=" => "=", "!=" => "!=", "like" => translate("like", 0), "not like" => translate("not like", 0) )); } /** * Create comparison operator pulldown * @param string name for select box * @param string current value */ public static function createOperatorPulldown($name, $value = "=") { return static::createPulldown($name, $value, array( "=" => "=", "!=" => "!=", ">" => ">", ">=" => ">=", "<" => "<", "<=" => "<=", "like" => translate("like", 0), "not like" => translate("not like", 0) )); } /** * Create inequality operator [less than/more than] pulldown * @param string name for select box * @param string current value */ public static function createInequalityOperatorPulldown($name, $value = "") { return template::createPulldown($name, $value, array(">" => translate("less than"), "<" => translate("more than"))); } /** * Create pulldown (select) with options "yes" and "no" (translated) * @param string name for select box * @param string current value */ public static function createBinaryOperatorPulldown($name, $value = "=") { return static::createPulldown($name, $value, array( "=" => "=", "!=" => "!=" )); } public static function createPresentOperatorPulldown($name, $value = "=") { return template::createPulldown($name, $value, array( "=" => translate("is in photo", 0), "!=" => translate("is not in photo", 0) )); } /** * Create pulldown (select) with options "yes" and "no" (translated) * @param string name for select box * @param string current value */ public static function createYesNoPulldown($name, $value) { return static::createPulldown($name, $value, array( "0" => translate("No", 0), "1" => translate("Yes", 0) )); } /** * Create conjunction [and/or] pulldown * @param string name for select box * @param string current value */ public static function createConjunctionPulldown($name, $value = "") { return template::createPulldown($name, $value, array("" => "", "and" => translate("and", 0), "or" => translate("or", 0))); } /** * Create pulldown with variable number of days * @param string name for select box * @param string current value */ public static function createDaysAgoPulldown($name, $value) { $dt=new Time(date("Y-m-d")); $dateArray=array("" => ""); $day=new DateInterval("P1D"); for ($i = 1; $i <= conf::get("interface.max.days"); $i++) { $dt->sub($day); $dateArray[$dt->format("Y-m-d")] = $i; } return template::createPulldown($name, $value, $dateArray); } /** * Create pulldown with integers * @param string name for select box * @param string current value */ public static function createIntegerPulldown($name, $value, $min, $max) { $intArray=array(); for ($i = $min; $i <= $max; $i++) { $intArray["$i"] = $i; } return template::createPulldown($name, $value, $intArray); } /** * transforms a size in bytes into a human readable format using * Ki Mi Gi, etc. prefixes * Give me a call if your database grows bigger than 1024 Yobbibytes. :-) * @param int bytes number of bytes * @return string human readable filesize */ public static function getHumanReadableBytes(int $bytes) { if ($bytes==0) { // prevents div by 0 return "0B"; } else { $prefixes=array("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"); $length=floor(log($bytes, 2)/10); return round($bytes/pow(2, 10*($length)), 1) . $prefixes[floor($length)] . "B"; } } /** * Display warning about disabled Javascript */ public static function showJSwarning() { $user=user::getCurrent(); if ((($user->prefs->get("autocomp_albums")) || ($user->prefs->get("autocomp_categories")) || ($user->prefs->get("autocomp_places")) || ($user->prefs->get("autocomp_people")) || ($user->prefs->get("autocomp_photographer"))) && conf::get("interface.autocomplete")) { $warning=new block("message", array( "class" => "warning", "text" => translate("You have enabled autocompletion for one or more dropdown " . "boxes on this page, however, you do not seem to have Javascript " . "support. You should either enable javascript or turn autocompletion " . "off, or this page will not work as expected!") )); $noscript=new block("noscript"); $noscript->addBlocks(array($warning)); return $noscript; } } /** * Get all templates * Search the template directory for directory entries */ public static function getAll() { $templates=array(); foreach (glob(settings::$php_loc . "/templates/*", GLOB_ONLYDIR) as $tpl) { $tpl=basename($tpl); $templates[$tpl]=$tpl; } return $templates; } } zoph-v0.9.19/php/classes/upgrade/000077500000000000000000000000001415176210700166375ustar00rootroot00000000000000zoph-v0.9.19/php/classes/upgrade/controller.inc.php000066400000000000000000000053501415176210700223060ustar00rootroot00000000000000migrations = $migrations ?: new migrations(); $this->doAction(); } /** * Do action 'display' */ public function actionDisplay() { if ($this->migrations->check(conf::get("zoph.version"))) { $this->view=new view\display($this->request, $this->migrations); } else { $this->view=new view\notfound($this->request); } } /** * Do action 'upgrade' */ public function actionUpgrade() { $doneMigrations=array(); if ($this->migrations->check(conf::get("zoph.version"))) { foreach ($this->migrations->get() as $migration) { $results = $migration->up(); $doneMigrations[] = new block("migration", array( "desc" => $migration->getDesc(), "results" => $results )); conf::set("zoph.version", $migration->getTo())->update(); } $this->view=new view\upgrade($this->request, $doneMigrations); } else { $this->view=new view\notfound($this->request); } } /** * Do action 'rollback' */ public function actionRollback() { // Not implemented } } zoph-v0.9.19/php/classes/upgrade/exception.inc.php000066400000000000000000000016751415176210700221270ustar00rootroot00000000000000descUp; } /** * Execute upgrade * @return return array upgrade\result */ final public function up() { $results = array(); foreach ($this->stepsUp as $step) { $desc=array_shift($this->descUp); $results[] =$this->doStep($step, $desc); } return $results; } /** * Execute downgrade * @return return array upgrade\result */ final public function down() { $results = array(); foreach ($this->stepsDown as $step) { $desc=array_shift($this->descDown); $results[] =$this->doStep($step, $desc); } return $results; } /** * Add a step * @param int migration::UP|migration::DOWN - indicate whether this is an upgrade or downgrade step * @param db\query database query to execute for this step * @param string description of the step */ final protected function addStep(int $dir, query $qry, string $desc) { switch ($dir) { case static::UP: $this->stepsUp[] = $qry; $this->descUp[] = $desc; break; case static::DOWN: $this->stepsDown[] = $qry; $this->descDown[] = $desc; break; default: throw new exception("Error"); break; } } /** * Execute a step * @return return upgrade\result */ private function doStep(query $step, string $desc) { try { $step->execute(); $result=result::SUCCESS; $error=null; } catch (\Exception $e) { $result=result::ERROR; $error=$e->getMessage(); } return new result($result, $desc, $error); } /** * Required migration * Are we at an older version than the one this migration brings us to? * if so, it is required to run. * @param string current version * @return bool migration required */ final public function isRequired(string $current) { return (static::TO > $current); } /** * Is this migration the one that brought us to the current state? * if so, it is eligable to run as rollback * @param string current version * @return bool migration is current */ final public function isCurrent(string $current) { return (static::TO == $current); } } zoph-v0.9.19/php/classes/upgrade/migrations.inc.php000066400000000000000000000053521415176210700223010ustar00rootroot00000000000000dir = settings::$php_loc . "/" . $dir; if (!defined("MIGRATIONS")) { define("MIGRATIONS", 1); $migrations = glob($this->dir . "/*.migration.php"); foreach ($migrations as $migration) { $this->register(require_once($migration)); } } } /** * Register a migration * @param migration migration to register */ public function register(migration $migration) { $this->migrations[]=$migration; } /** * Check whether there are any migrations pending * @param string current version * @return bool whether there are any pending migrations */ public function check(string $version) { $this->todo=array(); $this->down=array(); foreach ($this->migrations as $migration) { if ($migration->isRequired($version)) { $this->todo[] = $migration; } if ($migration->isCurrent($version)) { $this->down[] = $migration; } } return !empty($this->todo); } /** * Get pending migrations * @return array pending migrations */ public function get() { return $this->todo; } /** * Get 'down' migration * Get migrations available to migrate down - at this moment this is always one, the last, migration * @return array down migrations */ public function getDown() { return $this->down; } } zoph-v0.9.19/php/classes/upgrade/result.inc.php000066400000000000000000000066521415176210700214470ustar00rootroot00000000000000result = $result; $this->msg = $msg; $this->error = $error; } /** * Display result object * @return string template output */ public function __toString() { switch ($this->result) { case self::TODO: $status = "todo"; $icon = template::getImage("icons/migration.png"); break; case self::SUCCESS: $status = "ok"; $icon = template::getImage("icons/ok.png"); break; case self::INFO: $status = "info"; $icon = template::getImage("icons/info.png"); break; case self::WARNING: $status = "warning"; $icon = template::getImage("icons/warning.png"); break; case self::ERROR: $status = "error"; $icon = template::getImage("icons/error.png"); break; default: $status = "unknown"; $icon = template::getImage("icons/unknown.png"); break; } return (string) new block("result", array( "status"=> $status, "icon" => $icon, "msg" => $this->msg, "error" => $this->error )); } } zoph-v0.9.19/php/classes/upgrade/view/000077500000000000000000000000001415176210700176115ustar00rootroot00000000000000zoph-v0.9.19/php/classes/upgrade/view/display.inc.php000066400000000000000000000063711415176210700225460ustar00rootroot00000000000000migrations = $migrations; if (sizeof($migrations->get()) == 1) { $this->title=$migrations->get()[0]->getDesc(); } } /** * Get http headers * Always returns null as the default headers will be used for this view * @return null */ public function getHeaders() { return null; } /** * View for displaying pending migrations * @return template\template */ public function view() { $showMigrations = array(); foreach ($this->migrations->get() as $migration) { $steps = array(); foreach ($migration->show() as $step) { $steps[] = new result(result::TODO, $step); } $showMigrations[] = new block("migration", array( "desc" => $migration->getDesc(), "results" => $steps )); } $message=new block("message", array( "title" => translate("backup"), "class" => "warning", "text" => translate("it is highly recommended that you make a backup of your database prior to performing an upgrade") )); $message->addActionlinks(array( translate("make backup") => "backup.php?_return=upgrade.php" )); $button = new block("button", array( "button" => translate("start upgrade"), "button_url" => "upgrade.php?_action=upgrade", "button_class" => "big green" )); $tpl=new template("upgrade", array( "title" => conf::get("interface.title"), "version" => VERSION, "migrations" => $showMigrations, )); $tpl->addBlocks(array($message, $button)); return $tpl; } /** * Get title * @return string title */ public function getTitle() { return $this->title ?: translate("Zoph Upgrade"); } } zoph-v0.9.19/php/classes/upgrade/view/notfound.inc.php000066400000000000000000000036351415176210700227350ustar00rootroot00000000000000request=$request; $this->vars=$request->getRequestVars(); } /** * Return headers - always null because the default headers are used * @return null */ public function getHeaders() { return null; } /** * Output view */ public function view() { return new template("upgrade", array( "title" => $this->getTitle(), "version" => VERSION, "migrations" => array(translate("Nothing to do, all upgrades have been done")), "button" => translate("return"), "button_url" => "zoph.php" )); } /** * Get the title for this view */ public function getTitle() { return translate("No upgrade necessary"); } } zoph-v0.9.19/php/classes/upgrade/view/upgrade.inc.php000066400000000000000000000040501415176210700225200ustar00rootroot00000000000000migrations = $migrations; } /** * Get headers, always null, as default headers are used * @return null */ public function getHeaders() { return null; } /** * Retrun the view * @return template\template upgrade view */ public function view() { return new template("upgrade", array( "title" => conf::get("interface.title"), "version" => VERSION, "migrations" => $this->migrations, "button" => translate("finish"), "button_url" => "zoph.php" )); } /** * Get title * @return string title */ public function getTitle() { return $this->title ?: translate("Zoph Upgrade"); } } zoph-v0.9.19/php/classes/user.inc.php000066400000000000000000000424041415176210700174530ustar00rootroot00000000000000set("lastnotify", "now()"); parent::insert(); $default=new prefs(-1); if ($default->lookup()) { $default->set("user_id", $this->getId()); $this->prefs=$default; } else { $this->prefs = new prefs($this->getId()); } $this->prefs->insert(); } /** * Delete a user from the db * also delete the preferences for this user */ public function delete() { parent::delete(array("prefs", "groups_users")); } public function lookup() { parent::lookup(); if ($this->get("lastlogin") == "") { $this->set("lastlogin", null); } } /** * Lookup the person linked to this user */ public function lookupPerson() { $this->person = new person($this->get("person_id")); $this->person->lookup(); } /** * Lookup the preferences of this user */ public function lookupPrefs() { $this->prefs = new prefs($this->getId()); $this->prefs->lookup(); } /** * Is this user an admin? * @return bool */ public function isAdmin() { $this->lookup(); return $this->get("user_class") == 0; } /** * Is this user the default user? * @return bool */ public function isDefault() { $this->lookup(); return (conf::get("interface.user.default") == $this->getId()); } /** * Is this user the creator of this organizer? * @return bool */ public function isCreator(organizer $obj) { $obj->lookup(); return ((int) $obj->get("createdby") === $this->getId()); } /** * When was this user last notified of new albums? * @return string timestamp */ public function getLastNotify() { return $this->get("lastnotify"); } /** * Get a link to this object * @todo should be phased out in favour of @see getURL, since this contains HTML * @return string link */ public function getLink() { return "" . $this->getName() . ""; } /** * Get the username * @return string name */ public function getName() { return $this->get("user_name"); } /** * Get groups for this user * @return array Groups */ public function getGroups() { $qry = new select(array("gu" => "groups_users")); $qry->addFields(array("group_id")); $qry->where(new clause("user_id=:userid")); $qry->addParam(new param(":userid", (int) $this->getId(), PDO::PARAM_INT)); return group::getRecordsFromQuery($qry); } /** * Get album permissions for this user * @param album album to get permissions for * @return permissions permissions object */ public function getAlbumPermissions(album $album) { // An admin or creator of an album always has full permissions on that album if ($this->isAdmin() || $this->isCreator($album)) { $gp=new permissions(0, $album->getId()); $gp->set("access_level", 9); $gp->set("watermark_level", 0); $gp->set("writable", true); return $gp; } $groups=$this->getGroups(); $groupIds=array(); foreach ($groups as $group) { $groupIds[]=(int) $group->getId(); } if (is_array($groupIds) && sizeof($groupIds) > 0) { $qry=new select(array("gp" => "group_permissions")); $where = new clause("album_id=:albumid"); $groups=new param(":groupid", $groupIds, PDO::PARAM_INT); $qry->addParams(array( new param(":albumid", (int) $album->getId(), PDO::PARAM_INT), $groups )); $where->addAnd(clause::InClause("gp.group_id", $groups)); $qry->where($where); $qry->addOrder("access_level DESC") ->addOrder("writable DESC") ->addOrder("watermark_level DESC"); $qry->addLimit(1); $aps=permissions::getRecordsFromQuery($qry); if (is_array($aps) && sizeof($aps) >= 1) { return $aps[0]; } } return null; } /** * Get permissions for a specific photo, for this user * @param photo Photo to get permissions for * @return permissions permissions object */ public function getPhotoPermissions(photo $photo) { $permissions=null; foreach ($photo->getAlbums() as $album) { if ($this->isCreator($album)) { $permissions=new permissions(0,$album->getId()); $permissions->set("access_level", 9); $permissions->set("watermark_level", 0); $permissions->set("writable", true); return $permissions; } } $qry=new select(array("p" => "photos")); $qry->addFields(array("photo_id")); $where=new clause("p.photo_id = :photoid"); $qry->addParam(new param(":photoid", (int) $photo->getId(), PDO::PARAM_INT)); $qry->addParam(new param(":userid", (int) $this->getId(), PDO::PARAM_INT)); $qry->join(array("pa" => "photo_albums"), "pa.photo_id=p.photo_id"); $qry->join(array("gp" => "group_permissions"), "gp.album_id=pa.album_id"); $qry->join(array("gu" => "groups_users"), "gp.group_id=gu.group_id"); $where->addAnd(new clause("gp.access_level>=p.level")); $where->addAnd(new clause("gu.user_id=:userid")); $qry->addFields(array("gp.*")); $qry->addLimit(1); // do ordering to grab entry with most permissions $qry->addOrder("gp.access_level DESC")->addOrder("writable DESC")->addOrder("watermark_level DESC"); $qry->where($where); $gps = permissions::getRecordsFromQuery($qry); if ($gps && sizeof($gps) >= 1) { $permissions=$gps[0]; } if ($this->canSeeAllPhotos()) { if ($permissions instanceof permissions) { $permissions->set("access_level", 9); } else { $permissions=new permissions(); $permissions->set("access_level", 9); $permissions->set("watermark_level", 0); $permissions->set("writable", 0); } if ($this->isAdmin()) { $permissions->set("writable", 1); } } return $permissions; } /** * Check whether this user can see hidden circles * @return bool user can see hidden circles */ public function canSeeHiddenCircles() { return ($this->isAdmin() || $this->get("see_hidden_circles")); } /** * Check whether this user can see all photos. * This means that the permissions checking is bypassed for this user, * as if it is an admin user, but without giving full admin rights * @return bool user can see all photos */ public function canSeeAllPhotos() { return ($this->isAdmin() || $this->get("view_all_photos")); } /** * Check whether this user is allowed to leave comments * @return bool user can leave comments */ public function canLeaveComments() { return ($this->isAdmin() || $this->get("leave_comments")); } /** * Check whether this user is allowed to delete photos * @return bool user can delete photos */ public function canDeletePhotos() { return ($this->isAdmin() || $this->get("delete_photos")); } /** * Check whether this user can edit, add and delete albums, categories, places and people * @return bool user can add, edit and delete albums, categories, places and people */ public function canEditOrganizers() { return ($this->isAdmin() || $this->get("edit_organizers")); } /** * Check whether this user can browse people * @return bool user can see the list of people that are in photos this user can see */ public function canBrowsePeople() { return ($this->canEditOrganizers() || $this->get("browse_people")); } /** * Check whether this user can see details of people (such as address, birthdate, etc.) * @return bool user can see details of people */ public function canSeePeopleDetails() { return ($this->canEditOrganizers() || $this->get("detailed_people")); } /** * Check whether this user can browse places * @return bool user can see the list of places where photos this user can see were taken */ public function canBrowsePlaces() { return ($this->canEditOrganizers() || $this->get("browse_places")); } /** * Check whether this user can browse tracks * @return bool user can see tracks */ public function canBrowseTracks() { return ($this->isAdmin() || $this->get("browse_tracks")); } /** * Check whether this user can see details of places (such as address) * @return bool user can see details of places */ public function canSeePlaceDetails() { return ($this->canEditOrganizers() || $this->get("detailed_places")); } /** * Check wheter this user can rate photos * @return bool user can rate photos */ public function canRatePhotos() { return ($this->isAdmin() || $this->get("allow_rating")); } /** * Get array to display information about this user * @return array of properties to display */ public function getDisplayArray() { $this->lookupPerson(); $da = array( translate("username") => $this->get("user_name"), translate("person") => $this->person->getLink(), translate("class") => $this->get("user_class") == 0 ? "Admin" : "User" ); $desc=$this->getAccessRightsDescription(); foreach ($this->getAccessRightsArray() as $field => $value) { $da[$desc[$field]] = $value == 1 ? translate("Yes") : translate("No"); } $da = array_merge($da, array( translate("last login") => $this->get("lastlogin"), translate("last ip address") => $this->get("lastip") )); if ($this->getLightbox()) { if ($this->getLightbox()->get("album")) { $da[translate("lightbox album")] = $this->getLightbox()->get("album"); } } return $da; } public function getLightbox() { if ($this->get("lightbox_id")) { $lightbox = new album($this->get("lightbox_id")); $lightbox->lookup(); return $lightbox; } } public function getAccessRightsArray() { return array( "view_all_photos" => $this->get("view_all_photos"), "delete_photos" => $this->get("delete_photos"), "browse_people" => $this->get("browse_people"), "browse_places" => $this->get("browse_places"), "browse_tracks" => $this->get("browse_tracks"), "edit_organizers" => $this->get("edit_organizers"), "detailed_people" => $this->get("detailed_people"), "see_hidden_circles" => $this->get("see_hidden_circles"), "detailed_places" => $this->get("detailed_places"), "import" => $this->get("import"), "download" => $this->get("download"), "leave_comments" => $this->get("leave_comments"), "allow_rating" => $this->get("allow_rating"), "allow_multirating" => $this->get("allow_multirating"), "allow_share" => $this->get("allow_share") ); } public function getAccessRightsDescription() { return array( "view_all_photos" => translate("can view all photos"), "delete_photos" => translate("can delete photos"), "browse_people" => translate("can browse people"), "browse_places" => translate("can browse places"), "browse_tracks" => translate("can browse tracks"), "edit_organizers" => translate("can edit albums, categories, places and people"), "detailed_people" => translate("can view details of people"), "see_hidden_circles" => translate("can view hidden circles"), "detailed_places" => translate("can view details of places"), "import" => translate("can import"), "download" => translate("can download zipfiles"), "leave_comments" => translate("can leave comments"), "allow_rating" => translate("can rate photos"), "allow_multirating" => translate("can rate the same photo multiple times"), "allow_share" => translate("can share photos") ); } /** * Load language * This loads the translations of Zoph's web gui * @param bool Even load when already loaded */ public function loadLanguage($force = 0) { $langs=array(); if (!$force && $this->lang != null) { return $this->lang; } if ($this->prefs != null && $this->prefs->get("language") != null) { $langs[] = $this->prefs->get("language"); } $langs=array_merge($langs, language::httpAccept()); $this->lang=language::load($langs); return $this->lang; } /** * Create a graph of the ratings this user has made */ public function getRatingGraph() { return rating::getGraphArrayForUser($this); } /** * Get the comments this user has placed * @return array comments */ public function getComments() { return comment::getRecords("comment_date", array("user_id" => (int) $this->getId())); } /** * Get user object by searching for username * @param string name * @return user user object */ public static function getByName($name) { $users=static::getRecords(null, array("user_name" => $name)); if (sizeof($users)==1) { return $users[0]; } else if (sizeof($users)==0) { throw new userNotFoundException("User not found"); } else { throw new userMultipleFoundException("Multiple users with the same name found"); } } /** * Get all users * @param string sort order * @return array Array of all users */ public static function getAll($order = "user_name") { return static::getRecords($order); } /** * Set currently logged in user * (log in) * @param user user object * @todo a proper framework needs to be made to invalidate caches */ public static function setCurrent(user $user) { category::$categoryCache=null; $user->lookup(); $user->lookupPrefs(); $user->lookupPerson(); static::$current=$user; } /** * Delete currently logged in user * (Log out) */ public static function unsetCurrent() { static::$current=null; } /** * Get currently logged in user */ public static function getCurrent() { return static::$current; } } ?> zoph-v0.9.19/php/classes/watermarkedPhoto.inc.php000066400000000000000000000102041415176210700220060ustar00rootroot00000000000000get("name"); $image_path = conf::get("path.images") . "/" . $this->get("path") . "/" . $name; $image=imagecreatefromjpeg($image_path); $image=$this->watermark($image, $watermark_file, conf::get("watermark.pos.x"), conf::get("watermark.pos.y"), conf::get("watermark.transparency")); ob_start(); imagejpeg($image); imagedestroy($image); $jpeg=ob_get_clean(); $headers["Content-Length"]=strlen($jpeg); $headers["Content-Disposition"]="inline; filename=" . $name; // Return current time as last modified time // this is debatable, we could also send the file time as last modified $headers["Last-Modified"]=gmdate("D, d M Y H:i:s") . ' GMT'; $headers["Content-type"]="image/jpeg"; return array($headers, $jpeg); } } return parent::display($type); } /** * Watermark the photo * @param imageresource photo * @param string GIF image to be used as watermark * @param string position horizontally (center, left or right) * @param string position vertically (center, top or bottom) * @param int transparency (0 = invisible, 100 = no transparency) * @return imageresource watermarked photo */ private function watermark($orig, $watermark, $positionX = "center", $positionY = "center", $transparency = 50) { $wm=imagecreatefromgif($watermark); $width_orig=ImageSX($orig); $height_orig=ImageSY($orig); $width_wm=ImageSX($wm); $height_wm=ImageSY($wm); switch ($positionX) { case "left": $destX = 5; break; case "right": $destX = $width_orig - $width_wm - 5; break; default: $destX = ($width_orig / 2) - ($width_wm / 2); break; } switch ($positionY) { case "top": $destY = 5; break; case "bottom": $destY = $height_orig - $height_wm - 5; break; default: $destY = ($height_orig / 2) - ($height_wm / 2); break; } ImageCopyMerge($orig, $wm, $destX, $destY, 0, 0, $width_wm, $height_wm, $transparency); imagedestroy($wm); return $orig; } } ?> zoph-v0.9.19/php/classes/web/000077500000000000000000000000001415176210700157655ustar00rootroot00000000000000zoph-v0.9.19/php/classes/web/queryString.inc.php000066400000000000000000000057051415176210700216110ustar00rootroot00000000000000qs = $qs; $this->passedQs = $passedQs; } /** * Get Query String * @return string Query String */ public function __toString() { return (string) $this->qs; } /** * Return the query string, urlencoded, it can be passed via an URL */ public function encode() { return urlencode(htmlentities($this)); } /** * Sometimes a form passes a previous query string as part of the data * this is needed to return to the original page. For example, if you have performed * a search and click on a photo, you're not simply sent to that photo, * but the query string for that photo contains the original search * to return to the search after the photo was updated, you need to retrieve that * query string through this function. */ public function getPassed() { return new self($this->passedQs); } /** * Clean the query string by passing regexes * For example removing "_crumb" and "_action": * this->cleanQueryString(array("/_crumb=\d+&?/","/_action=\w+&?/")) * @param array regex to use for cleaning * @return string cleaned query string */ public function regexClean(array $regexes) { $qs = (string) $this; foreach ($regexes as $regex) { $qs = preg_replace($regex, "", $qs); } return new self($qs); } /** * Get the return query string * This could be the passed query string ("_qs") or this function could * clean the current query string, removing "_crumb" and "_action" */ public function getReturn() { $return=$this->getPassed(); if (empty((string)$return)) { $return = $this->regexClean(array( "/_crumb=\d+&?/", "/_action=\w+&?/" )); } return new self($return); } } zoph-v0.9.19/php/classes/web/request.inc.php000066400000000000000000000306551415176210700207470ustar00rootroot00000000000000variable); * * @package Zoph * @author Jeroen Roos */ class request implements ArrayAccess { /** @var holds $_GET variables */ private $get; /** @var holds $_POST variables */ private $post; /** @var holds $_SERVER variables */ private $server; /** @var request vars, holds $_GET for GET requests and $_POST for POST requests actually, a POST request can have GET variables as well, but this has always been how Zoph works, so for now I am not changing this, note that this is *different* from the $_REQUEST superglobals - hence it's not called $request */ private $requestVars; /** @var queryString holds query string */ private $queryString; /** * Create object * @param array array of variables, can contain GET, POST and SERVER */ public function __construct(array $vars) { foreach ([ "GET", "POST", "SERVER" ] as $var) { if (isset($vars[$var])) { $value=new variable($vars[$var]); $prop=strtolower($var); $this->$prop=$value->input(); } } $this->buildRequest(); $this->queryString = new queryString($this->getServerVar("QUERY_STRING"), $this["_qs"]); } /** * Create object and fill with superglobals * @return request new request */ public static function create() { return new self(array( "GET" => $_GET, "POST" => $_POST, "SERVER" => $_SERVER )); } /** * Fill the REQUESTVARS property with either the GET variables * OR the POST variables. * Note that this behaviour is different from PHP's $_REQUEST superglobal */ private function buildRequest() { if (!empty($this->get)) { $this->requestVars=&$this->get; } else { $this->requestVars=&$this->post; } } /** * For ArrayAccess: does the offset exist * @param int|string offset * @return bool offset exists */ public function offsetExists($off) { return (isset($this->get[$off]) || isset($this->post[$off])); } /** * For ArrayAccess: Get value of parameter * if $_GET parameter is available, return it, if it is not but $_POST is available * return that, otherwise null * @param int|string offset * @return mixed value */ public function offsetGet($off) { if (isset($this->get[$off])) { return $this->get[$off]; } else if (isset($this->post[$off])) { return $this->post[$off]; } else { return null; } } /** * For ArrayAccess: Set value of parameter * @param int|string offset * @param mixed value */ public function offsetSet($off, $val) { if (!isset($this->post[$off])) { $this->post[$off]=$val; } else { $this->get[$off]=$val; } } /** * For ArrayAccess: Unset value of parameter * @param int|string offset */ public function offsetUnset($off) { unset($this->get[$off]); unset($this->post[$off]); } /** * For ObjectAccess: Get value of parameter * if $_GET parameter is available, return it, if it is not but $_POST is available * return that, otherwise null * @param int|string offset * @return mixed value */ public function __get($off) { return $this->offsetGet($off); } /** * Get RequestVars * @return array requestvars */ public function getRequestVars() { return (array) $this->requestVars; } /** * Return the query string, urlencoded, it can be passed via an URL */ public function getEncodedQueryString() { return $this->queryString->encode(); } /** * Get the query string that was used to get to this page */ public function getQueryString() { return $this->queryString; } /** * Sometimes a form passes a previous query string as part of the data * this is needed to return to the original page. For example, if you have performed * a search and click on a photo, you're not simply sent to that photo, * but the query string for that photo contains the original search * to return to the search after the photo was updated, you need to retrieve that * query string through this function. */ public function getPassedQueryString() { return $this->queryString->getPassed(); } /** * Clean the query string by passing regexes * For example removing "_crumb" and "_action": * this->cleanQueryString(array("/_crumb=\d+&?/","/_action=\w+&?/")) * @param array regex to use for cleaning * @return string cleaned query string */ public function cleanQueryString(array $regexes) { $qs = $this->getQueryString(); foreach ($regexes as $regex) { $qs = preg_replace($regex, "", $qs); } return $qs; } /** * Get the return query string * This could be the passed query string ("_qs") or this function could * clean the current query string, removing "_crumb" and "_action" */ public function getReturnQueryString() { return $this->queryString->getReturn(); } /** * Get $_SERVER variables * @param Variable to return * @return mixed value */ public function getServerVar($var) { if (isset($this->server[$var])) { return $this->server[$var]; } else { return null; } } /** * Get $_POST variables * @param Variable to return * @return mixed value */ public function getPostVar($var) { if (isset($this->post[$var])) { return $this->post[$var]; } else { return null; } } /** * Update requestvars * Update a variable to a new value, remove ignored keys * always removes PHPSESSID and _crumb variables * @param string variable (key) to add / update * @param mixed value of the new /updated variable * @param array list of key names to remove * @return array updated variables */ public function getUpdatedVars($new, $val, $ignore = array()) { $ignore[] = "PHPSESSID"; $ignore[] = "_crumb"; $vars = $this->getRequestVars(); foreach ($ignore as $key) { unset($vars[$key]); } $vars[$new] = $val; return $vars; } /** * Remove any params without values and operator params without corresponding * fields (e.g. _album_id-op when there is no _album_id). This can be called * once after a search is performed. It allows for shorter urls that are * more readable and easier to debug. * @todo This code is pretty horrible and I wonder if we could do without... */ public function getRequestVarsClean() { $cleanVars = array(); $interimVars = array(); /* First pass through vars will flatten out any arrays in the list. arrays were used in search.php to make the form extensible. -RB */ foreach ((array)$this->requestVars as $key => $val) { // trim empty values if (($key == "_button") || empty($val)) { continue; } if (is_array($val)) { foreach ($val as $subkey => $subval) { if (empty($subval)) { continue; } if (substr($key, -3) == "_op") { // change var_op[key] to var#key_op $newkey = substr($key, 0, -3) . '#' . $subkey . '_op'; } else if (substr($key, -5) == "_conj") { // change var_conj[key] to var#key_conj $newkey = substr($key, 0, -5) . '#' . $subkey . '_conj'; } else if (substr($key, -9) == "_children") { // change var_children[key] to var#key_children $newkey = substr($key, 0, -9) . '#' . $subkey . '_children'; } else { // change var[key] to var#key $newkey = $key . '#' . $subkey; } $interimVars[$newkey] = $subval; } } else { $interimVars[$key] = $val; } } /* Second pass through will get rid of ops and conjs without fields and fix the keys for compatability with the rest of zoph. It will also remove "field" entries without a corresponding "_field" type and vice versa. A hyphen is not valid as part of a variable name in php so underscore was used while processing the form in search.php */ foreach ($interimVars as $key => $val) { // process _var variables if (substr($key, 0, 1) == "_") { //process _op variables if (substr($key, -3) == "_op") { // replace _op with -op to be compatible with the rest of application $key = substr_replace($key, '-', -3, -2); // get rid of ops without fields $field = substr($key, 1, -3); if (empty($interimVars[$field]) && empty($interimVars["_$field"])) { continue; } //process _conj variables } else if (substr($key, -5) == "_conj") { // replace _conj with -conj to be compatible // with the rest of application $key = substr_replace($key, '-', -5, -4); // get rid of ops without fields $field = substr($key, 1, -5); if (empty($interimVars[$field]) && empty($interimVars["_$field"])) { continue; } } else if (substr($key, -9) == "_children") { // process _children variables // replace _children with -children to be compatable // with the rest of application $key = substr_replace($key, '-', -9, -8); // get rid of ops without fields $field = substr($key, 1, -9); if (empty($interimVars[$field]) && empty($interimVars["_$field"])) { continue; } } else { $field = substr($key, 1); } //process "_field" type variables if (substr($field, 0, 5) == "field" && empty($interimVars[$field]) && empty($interimVars["_$field"])) { continue; } } else { //process "field" type variables if (substr($key, 0, 5) == "field" && empty($interimVars["_$key"])) { continue; } } $cleanVars[$key] = $val; } return $cleanVars; } } zoph-v0.9.19/php/classes/web/service/000077500000000000000000000000001415176210700174255ustar00rootroot00000000000000zoph-v0.9.19/php/classes/web/service/controller.inc.php000066400000000000000000000117731415176210700231020ustar00rootroot00000000000000request=$request; $this->doAction($action); } /** * Do the action as requested */ public function doAction($action) { if (in_array($action, $this->actions)) { $function = "action" . ucwords($action); $this->$function(); } } /** * get View * each of the actions dictate a subsequent view in the workflow, * the view can be called by this function * currently, it simply returns a name, in the future an action View object * may be returned. */ public function getView() { return $this->view; } public function getData() { return $this->data; } /** * Do action 'locationLookup' */ public function actionLocationlookup() { $this->view = "json"; $search = $this->request["search"]; $server = $this->request->getServerVar("SERVER_NAME"); $location = new locationLookup($search, $server); $this->data = [ "search" => $search, "lat" => $location->getLat(), "lon" => $location->getLong(), "zoom" => $location->getZoom() ]; } /** * Do action 'photoData' */ public function actionPhotoData() { $this->view = "json"; $photoId = $this->request["photoId"]; $photo = new photo($photoId); if ($photo->lookup()) { $data = new photoData($photo); $this->data = $data->getData(); } } /** * Do action 'photoPeople' */ public function actionPhotoPeople() { $this->view = "json"; $photoId = (int) $this->request["photoId"]; $personId = (int) $this->request["personId"]; $photo = new photo($photoId); $person = new person($personId); $photoPeople = new photoPeople($photo); $user=user::getCurrent(); if (!$photo->isWritableBy($user)) { return; } $action = $this->request["action"]; switch($action) { case "left": list($row, $pos) = $photoPeople->getPosition($person); $photoPeople->setPosition($person, $pos - 1); break; case "right": list($row, $pos) = $photoPeople->getPosition($person); $photoPeople->setPosition($person, $pos + 1); break; case "up": list($row, $pos) = $photoPeople->getPosition($person); $photoPeople->setRow($person, $row - 1); break; case "down": list($row, $pos) = $photoPeople->getPosition($person); $photoPeople->setRow($person, $row + 1); break; case "add": $row = (int) $this->request["row"]; $person->addPhoto($photo); $photoPeople->setRow($person, $row); break; case "remove": $person->removePhoto($photo); break; default: // Not making any changes, just outputting status quo break; } if ($photo->lookup()) { $data = new photoPeople($photo); $this->data = $data->getData(); } } public function actionSearch() { $this->view = "json"; $photoCollection = collection::createFromRequest($this->request); $this->data=$photoCollection->getIds(); return $this->data; } private static function getId(photo $photo) { return $photo->getId(); } } zoph-v0.9.19/php/classes/web/service/view/000077500000000000000000000000001415176210700203775ustar00rootroot00000000000000zoph-v0.9.19/php/classes/web/service/view/json.inc.php000066400000000000000000000021061415176210700226300ustar00rootroot00000000000000data = $data; } public function view() { return json_encode($this->data); } } zoph-v0.9.19/php/classes/web/session.inc.php000066400000000000000000000056531415176210700207420ustar00rootroot00000000000000vars=(array) $vars; } /** * Start the session * @codeCoverageIgnore */ public function start() { ini_set("session.name", settings::$instance); session_set_cookie_params(conf::get("interface.cookie.expire")); session_start(); static::$session=$this; $this->vars=&$_SESSION; } public function logout() { session_destroy(); user::unsetCurrent(); } /** * For ArrayAccess: does the offset exist * @param int|string offset * @return bool offset exists */ public function offsetExists($off) { return (isset($this->vars[$off])); } /** * For ArrayAccess: Get value of parameter * if $_SESSION parameter is available, return it, otherwise null * @param int|string offset * @return mixed value */ public function offsetGet($off) { if (isset($this->vars[$off])) { return $this->vars[$off]; } } /** * For ArrayAccess: Set value of parameter * @param int|string offset * @param mixed value */ public function offsetSet($off, $val) { $this->vars[$off]=$val; } /** * For ArrayAccess: Unset value of parameter * @param int|string offset */ public function offsetUnset($off) { unset($this->vars[$off]); } /** * For ObjectAccess: Get value of parameter * if $_SESSION parameter is available, return it, otherwise null * @param int|string offset * @return mixed value */ public function __get($off) { return $this->offsetGet($off); } public static function getCurrent() { return static::session; } } zoph-v0.9.19/php/classes/web/url.inc.php000066400000000000000000000033111415176210700200460ustar00rootroot00000000000000getServerVar("HTTPS"); if ($https && !empty($https) && $https != "off") { $proto = "https"; } else { $proto = "http"; } $current = $request->getServerVar("SERVER_NAME") . "/" . $request->getServerVar("PHP_SELF"); $dirname = substr($current, 0, strrpos($current, "/") + 1); return $proto . "://" . preg_replace("/\/\//", "/", $dirname); } } zoph-v0.9.19/php/classes/xmp/000077500000000000000000000000001415176210700160145ustar00rootroot00000000000000zoph-v0.9.19/php/classes/xmp/data.inc.php000066400000000000000000000070171415176210700202130ustar00rootroot00000000000000xmpdata = $data; } public function getRDFDescription($off) { return $this["x:xmpmeta"]["rdf:RDF"]["rdf:Description"][$off]; } /** * For ArrayAccess: does the offset exist * @param int|string offset * @return bool offset exists */ public function offsetExists($off) { return (isset($this->xmpdata[$off])); } /** * For ArrayAccess: Get value of parameter * @param int|string offset * @return mixed value */ public function offsetGet($off) { $data = $this->xmpdata[$off] ?? null; if (is_array($data)) { if (sizeof($data) === 0) { return null; } else { return new self($data); } } else { return $data; } } /** * For ArrayAccess: Set value of parameter * @param int|string offset * @param mixed value */ public function offsetSet($off, $val) { $this->xmpdata[$off] = $val; } /** * For ArrayAccess: Unset value of parameter * @param int|string offset */ public function offsetUnset($off) { if (isset($this->xmpdata[$off])) { unset($this->xmpdata[$off]); } } /** * For ObjectAccess: Get value of parameter * @param int|string offset * @return mixed value */ public function __get($off) { return $this->offsetGet($off); } /** * For Iterator access - rewind to begining */ public function rewind() { reset($this->xmpdata); } /** * For Iterator access - get current * @return mix current entry */ public function current() { return current($this->xmpdata); } /** * For Iterator access - get key of current entry * @return int|string current key */ public function key() { return key($this->xmpdata); } /** * For Iterator access - move to next entry and return contents * @return mixed next entry */ public function next() { return next($this->xmpdata); } /** * For Iterator access - is the current entry valid * @return bool valid */ public function valid() { return key($this->xmpdata) !== null; } /** * For Countable access - get size of data */ public function count() { return count($this->xmpdata); } } ?> zoph-v0.9.19/php/classes/xmp/decoder.inc.php000066400000000000000000000032241415176210700207030ustar00rootroot00000000000000xmpdata = $data; } public function getSubjects() { if ($this->xmpdata and !empty($this->xmpdata)) { return $this->xmpdata[0]->getRDFDescription("dc:subject"); } } public function getRating() { if ($this->xmpdata && !empty($this->xmpdata)) { $rating = (int) $this->xmpdata[0]->getRDFDescription("xmp:Rating"); if ($rating != 0) { return $rating; } } } /** * Count number of entries in the data * for Countable access */ public function count() { return sizeof($this->xmpdata); } } zoph-v0.9.19/php/classes/xmp/reader.inc.php000066400000000000000000000153321415176210700205430ustar00rootroot00000000000000file = $file; } /** * Read XMP from sidecar files */ public function getXMPfromSidecar() { log::msg("Loading XMP from sidecar files", log::NOTIFY, log::IMPORT); foreach ($this->XMPext as $ext) { foreach (array($this->file->getName(), $this->file->getNameNoExt()) as $name) { $filename = $this->file->getPath() . "/" . $name . "." . $ext; log::msg("Trying " . $filename, log::DEBUG, log::IMPORT); if (file_exists($filename)) { log::msg("Found " . $filename, log::DEBUG, log::IMPORT); $sidecar = new file($filename); $xmp = new self($sidecar); return $xmp->getXMP(); } } } log::msg("No sidecar file found", log::NOTIFY, log::IMPORT); return array(); } /** * Read XMP from file */ public function getXMP() { $this->file->getMime(); switch ($this->file->type) { case "image": $this->getXMPfromJPEG(); break; case "xmp": $this->getXMPfromXML(); break; default: throw new \Exception("Unknown filetype " . $this->file->type); } $this->readXMPs(); $return = array(); foreach ($this->xmpdata as $xmp) { $return[] = new data($xmp); } return $return; } /** * Read XMP-xml from an XML-file */ private function getXMPfromXML() { $this->XMPs=array(file_get_contents($this->file)); } private function readXMPs() { foreach ($this->XMPs as $xmp) { $this->xmpdata[]=$this->readXMP($xmp); } } /** * Decode XMP data from XML * @param string XML * @return array XMPdata */ public function readXMP($xmp) { $xml = new XMLReader(); $xml->XML($xmp); return $this->decodeXML($xml); } /** * Read XMP-xml from a JPEG * Read JPG in chunks of 1024 bytes to keep memory usage low. */ private function getXMPfromJPEG() { $fp = fopen($this->file, "rb"); $buffer = ""; $open = false; while (($data = fread($fp, 1024)) !== false) { if ($data === "") { break; } $buffer .= $data; if (!$open) { $openpos =strpos($buffer, ""); if ($closepos !== false) { $this->XMPs[] = substr($buffer, $openpos, ($closepos - $openpos) + 12); $open = false; $buffer = substr($buffer, $closepos + 11); } } } } private function decodeXML(XMLReader $xml) { $node=array(); while ($xml->read()) { if ($xml->nodeType==XMLReader::ELEMENT) { $nodename = $xml->name; if (in_array($nodename, self::$containers)) { $container = $nodename; $containerElement = false; } else if (in_array($nodename, self::$containerElements)) { $container = false; $containerElement = true; } else { $container = false; $containerElement = false; } $empty=$xml->isEmptyElement; if ($xml->hasAttributes) { $attr=array(); $xml->moveToFirstAttribute(); do { $attr[$xml->name] = $xml->value; } while ($xml->moveToNextAttribute()); if (!$empty) { $data=array_merge($attr, (array) $this->decodeXML($xml)); } else { $data=$attr; } } else if ($xml->hasValue) { $data = $xml->value; } else { $data=$this->decodeXML($xml); } if ($container) { $node = $data; } else if ($containerElement) { $node[] =$data; } else { if (!is_array($node)) { $node = array($node); } $node[$nodename] = $data; } } else if ($xml->nodeType==XMLReader::TEXT) { $node = $xml->value; } else if ($xml->nodeType==XMLReader::END_ELEMENT) { break; } else if (($xml->nodeType != XMLReader::WHITESPACE) && ($xml->nodeType != XMLReader::SIGNIFICANT_WHITESPACE) && ($xml->nodeType != XMLReader::PI)) { $node[] = $xml->value; } } return $node; } } ?> zoph-v0.9.19/php/classes/zoph.inc.php000066400000000000000000000125641415176210700174610ustar00rootroot00000000000000 "Portuguese translation", "David Baldwin" => "PHP 4.2 compatibility and fixes", "Chris Beauchamp" => "PostgresSQL diff", "Edelhard Becker" => "Debian packages", "Alexandr Bondarev" => "Russian translation", "Roy Bonser" => "Patches for web import function and major improvements of seach page.", "Charles Brunet" => "Various bugfixes", "Nixon Childs" => "Annotated photos", "Francesco Ciattaglia" => "Italian translation", "Mark Cooper" => "rpm packages, makefile, man page and fixes", "Alvaro González Crespo" => "Spanish translation", "Nils Decker" => "zophEdit script", "Antoine Delvaux" => "Updated French translation, EXIF patch and zoph.org domain", "Dominique Dumon" => "improvements", "Mufit Eribol" => "Turkish translation", "Peter Farr" => "zophImport.pl patches", "Pontus Fröding" => "Bugfixes", "Geonames project" => "Coordinates to timezone lookup", "Donald Gover" => "bugfixes", "Michael Hanke" => "fixes", "Christian Hoenig" => "improvements", "Raimund Hook" => "Bugfix", "Jason (JiCit)" => "Bugfixes", "Neels Jordaan" => "Afrikaans translation", "Francisco Javier Félix" => "Updated Spanish translation, various bugfixes", "Samuel Keim" => "email notification, last login & ip, PHP validation, improvements", "Ian Kerr" => "fixes, Canadian English translation", "Krzysztof Kajkowski" => "Polish translation", "Sławomir Kubiak" => "Updated Polish translation", "David Kulp" => "bugfixes", "Pekka Kutinlati" => "Finnish Translation, bugfix", "Tetsuji Kyan" => "bugfixes", "Patrick Lam" => "Various improvements", "Asheesh Laroia" => "htpasswd authentication, fixes", "Johan Linder" => "Updated Swedish translation", "John Lines" => "location lookup feature", "Mat Lee" => "Traditional Chinese translation", "Haavard Leonardo Lund" => "Norwegian translation", "Matthew MacIntyre" => "bugfixes", "Mikael Magnusson" => "Swedish translation", "Leaflet project" => "Leaflet mapping project", "Iván Sánchez Ortega" => "Leaflet Google Plugin", "Neil McBride" => "Adding multiple people at once", "Jan Miczaika" => "importer, multiple ratings", "Francisco J. Montilla" => "bugfixes and improvements", "Giles Morant" => "Movie import script", "David Moulton" => "improvements", "Nuflix" => "Icons for rating stars", "Aaron Parecki" => "SSL Login", "Mario Peter" => "German translation", ""Prince01"" => "Hebrew translation", "Curtis Rawls" => "Bugfixes", "Oliver Seidel" => "Hierarchical directories in zophImport.pl", "Eric Seigne" => "internationalization, French translation", "Sergey Chursin" => "Russian translation", "Jesper Skytte" => "Danish Translation", "Alan Shutko" => "improvements", "Jason Taylor" => "Various bugfixes", "Arjen Tebbenhof" => "Dutch translation", "Hans Verbrugge" => "Bugfix" ); } } zoph-v0.9.19/php/classes/zophCode/000077500000000000000000000000001415176210700167635ustar00rootroot00000000000000zoph-v0.9.19/php/classes/zophCode/parser.inc.php000066400000000000000000000110421415176210700215360ustar00rootroot00000000000000allowed = $allowed; $this->message = $message; } /** * Output zophcode parsed to HTML * @return string HTML-ized zophCode */ public function __toString() { // This function parses a message using the replaces, smileys and tags // given in the function call. $message = $this->message; $return=""; // The stack is an array of currently open tags. $stack=array(); $message=replace::processMessage($message); $message=smiley::processMessage($message); // The array $allowed can be used to prevent users from using // certain tags in some positions. // This is used for example to limit the number of options // the user has while writing comments. tag::setAllowed($this->allowed); $return=""; foreach (static::parseMessage($message) as $tag) { if (!$tag instanceof tag) { $return.=$tag; } else if ($tag->isAllowed()) { if (!$tag->isClosing() && $tag->needsClosing()) { array_push($stack, $tag); } else if ($tag->isClosing()) { if (end($stack)->getFind() == $tag->getFind()) { array_pop($stack); } else { // Tried to close a tag that wasn't open // Ignore the tag and go on. continue; } } $return.=$tag; } } while ($tag = array_pop($stack)) { // Now close all tags that have not yet been closed. $tag->setClosing(); $return .= $tag; } return $return; } /** * Parse a zophCode message * This parser will tokenize the message into an array of strings and tag objects * @param string Message with zophCode * @return array Array of tokenized zophCode */ private static function parseMessage($msg) { while (strlen($msg)) { $opentag = strpos($msg, "[", 0); if ($opentag === false) { yield $msg; $msg=""; } else if ($opentag > 0) { yield substr($msg, 0, $opentag); } if (($opentag + 1)<= strlen($msg)) { // This prevents a PHP error when the last char // of the message is a "[" $closetag = strpos($msg, "]", $opentag + 1); } else { $closetag = 0; } $tag=substr($msg, $opentag, $closetag - $opentag + 1); // Does the tag contain " " or another "["? // In that case something is probably wrong... // (such as "[b This is bold[/b]") if (!strpos($tag, "[") || strpos($tag, " ")) { yield tag::getFromString($tag); } $msg = substr($msg, $closetag + 1); } } } ?> zoph-v0.9.19/php/classes/zophCode/replace.inc.php000066400000000000000000000052101415176210700216550ustar00rootroot00000000000000find=$find; $this->replace=$replace; } /** * Run the replaces on a message * @param string Message * @return string Message with problematic code changed */ public static function processMessage($msg) { $find=array(); $replace=array(); foreach (static::getArray() as $repl) { array_push($find, "/" . preg_quote($repl->find) . "/"); array_push($replace, $repl->replace); } return preg_replace($find, $replace, $msg); } /** * Get an array of all replace objects */ private static function getArray() { if (empty(static::$replaces)) { static::createArray(); } return static::$replaces; } /** * Fill the static $replaces. */ private static function createArray() { // Watch the order of these... putting & at the end of the array // will make you end up with things like "&lt;"... static::$replaces=array( // The first two are needed to revert anti SQL injection-code new replace("(", "("), new replace(")", ")"), new replace("&", "&"), new replace("<", "<"), new replace(">", ">"), new replace("\n", "
") ); } } ?> zoph-v0.9.19/php/classes/zophCode/smiley.inc.php000066400000000000000000000127031415176210700215510ustar00rootroot00000000000000smiley=$smiley; $this->file=$file; $this->description=$description; } /** * Get an array of all smiley objects */ public static function getArray() { if (empty(static::$smileys)) { static::createArray(); } return static::$smileys; } /** * Fill the static $smileys. */ private static function createArray() { static::$smileys=array( new smiley(":D", "icon_biggrin.gif", "Very Happy"), new smiley(":-D", "icon_biggrin.gif", "Very Happy"), new smiley(":grin:", "icon_biggrin.gif", "Very Happy"), new smiley(":)", "icon_smile.gif", "Smile"), new smiley(":-)", "icon_smile.gif", "Smile"), new smiley(":smile:", "icon_smile.gif", "Smile"), new smiley(":(", "icon_sad.gif", "Sad"), new smiley(":-(", "icon_sad.gif", "Sad"), new smiley(":sad:", "icon_sad.gif", "Sad"), new smiley(":o", "icon_surprised.gif", "Surprised"), new smiley(":-o", "icon_surprised.gif", "Surprised"), new smiley(":eek:", "icon_surprised.gif", "Surprised"), new smiley(":shock:", "icon_eek.gif", "Shocked"), new smiley(":?", "icon_confused.gif", "Confused"), new smiley(":-?", "icon_confused.gif", "Confused"), new smiley(":???:", "icon_confused.gif", "Confused"), new smiley("8)", "icon_cool.gif", "Cool"), new smiley("8-)", "icon_cool.gif", "Cool"), new smiley(":cool:", "icon_cool.gif", "Cool"), new smiley(":lol:", "icon_lol.gif", "Laughing"), new smiley(":x", "icon_mad.gif", "Mad"), new smiley(":-x", "icon_mad.gif", "Mad"), new smiley(":mad:", "icon_mad.gif", "Mad"), new smiley(":P", "icon_razz.gif", "Razz"), new smiley(":-P", "icon_razz.gif", "Razz"), new smiley(":razz:", "icon_razz.gif", "Razz"), new smiley(":oops:", "icon_redface.gif", "Embarassed"), new smiley(":cry:", "icon_cry.gif", "Crying or Very sad"), new smiley(":evil:", "icon_evil.gif", "Evil or Very Mad"), new smiley(":twisted:", "icon_twisted.gif", "Twisted Evil"), new smiley(":roll:", "icon_rolleyes.gif", "Rolling Eyes"), new smiley(":wink:", "icon_wink.gif", "Wink"), new smiley(";)", "icon_wink.gif", "Wink"), new smiley(";-)", "icon_wink.gif", "Wink"), new smiley(":!:", "icon_exclaim.gif", "Exclamation"), new smiley(":?:", "icon_question.gif", "Question"), new smiley(":idea:", "icon_idea.gif", "Idea"), new smiley(":arrow:", "icon_arrow.gif", "Arrow"), new smiley(":|", "icon_neutral.gif", "Neutral"), new smiley(":-|", "icon_neutral.gif", "Neutral"), new smiley(":neutral:", "icon_neutral.gif", "Neutral"), new smiley(":mrgreen:", "icon_mrgreen.gif", "Mr. Green") ); } /** * Get the smiley */ public function __toString() { return (string) new block("img", array( "src" => template::getImage("smileys/" . $this->file), "alt" => $this->description, "class" => "smiley", "size" => null )); } /** * Replace smileys in a message with image tags * @param string Message * @return string Message with image tags */ public static function processMessage($msg) { $find=array(); $replace=array(); foreach (static::getArray() as $smiley) { array_push($find, "/" . preg_quote($smiley->smiley) . "/"); array_push($replace, (string) $smiley); } return preg_replace($find, $replace, $msg); } /** * Get an overview of all defined smileys */ public static function getOverview() { return new block("smileys", array( "smileys" => static::getArray() )); } } ?> zoph-v0.9.19/php/classes/zophCode/tag.inc.php000066400000000000000000000161271415176210700210260ustar00rootroot00000000000000 */ private $replace; /** @var string How to check the parameter */ private $regexp; /** @var string How to translate parameter */ private $param; /** @var bool True if this tags needs closing, false if it does not */ private $needsClosing=true; /** @var bool Whether or not this is a closing tag */ private $isClosing=false; /** @var array List of allowed tags */ private static $allowed=array(); /** @var string Value of the parameter */ private $paramValue=null; /** @var array List of known tags */ private static $tags=array(); /** * Create a new tag object * * @param string The tag in zophCode, without [ ] * @param string The tag in HTML without < > * @param string How to check the parameter * @param string How to translate parameter * @param bool True if this tags needs closure, false if it does not * @todo regexp check of param not implemented */ public function __construct($find, $replace, $regexp = null, $param = null, $close=true) { $this->find=$find; $this->replace=$replace; $this->regexp=$regexp; $this->param=$param; $this->needsClosing=$close; } /** * Determines whether this tag can be used */ public function isAllowed() { return in_array($this->find, static::$allowed); } /** * Returns the "find" string * This is the zophCode tag */ public function getFind() { return $this->find; } /** * Returns the "replace" string * This is the HTML tag */ public function __toString() { if ($this->isClosing()) { return "replace . ">"; } else { return "<" . $this->replace . $this->getParam() . ">"; } } public function needsClosing() { return $this->needsClosing; } public function isClosing() { return $this->isClosing; } public function setClosing($closing=true) { $this->isClosing=$closing; } /** * Get an array of defined tags */ public static function getArray() { if (empty(static::$tags)) { static::createArray(); } return static::$tags; } /** * Fill static $tags */ private static function createArray() { static::$tags=array( new tag("b", "b"), new tag("i", "i"), new tag("u", "u"), new tag("h1", "h1"), new tag("h2", "h2"), new tag("h3", "h3"), new tag("color", "span", "", "style=\"color: [param];\""), new tag("font", "span", "", "style=\"font-family: [param];\""), new tag("br", "br", null, null, false), new tag("background", "div", "", "class='background' style=\"background: [param];\""), new tag("photo", "a", "", "href=\"photo.php?photo_id=[param]\""), new tag("album", "a", "", "href=\"album.php?album_id=[param]\""), new tag("person", "a", "", "href=\"people.php?person_id=[param]\""), new tag("cat", "a", "", "href=\"category.php?category_id=[param]\""), new tag("link", "a", "", "href=\"[param]\""), new tag("place", "a", "", "href=\"places.php?parent_place_id=[param]\""), new tag("thumb", "img", "", "src=\"image.php?photo_id=[param]&type=thumb\"", false), new tag("mid", "img", "", "src=\"image.php?photo_id=[param]&type=mid\"", false) ); } /** * Fill the array of allowed tags * @param array Array of allowed tags */ public static function setAllowed(array $allowed=null) { static::$allowed=array(); if ($allowed) { static::$allowed=$allowed; } else { foreach (static::getArray() as $tag) { static::$allowed[]=$tag->find; } } } /** * Create tag object from a string * @param string Tag [...], [/...], [...=...] * @return tag found tag */ public static function getFromString($string) { // strip off the [ and ] $string=substr($string, 1, -1); $newtag = explode("=", $string); $tag=$newtag[0]; if (isset($newtag[1])) { $param=$newtag[1]; } $closing=false; if (substr($tag, 0, 1) == "/") { $closing=true; $tag=substr($tag, 1); } // Check if tag is a valid tag. foreach (static::getArray() as $newtag) { if ($newtag->find == $tag) { $tag=clone $newtag; $tag->setClosing($closing); if (isset($param)) { $tag->setParamValue($param); } if ($tag->isClosing() && !$tag->needsClosing()) { // This is a closing tag for a tag that is not supposed to be closed // such as [br], we will just ignore it. $tag=null; } return $tag; } } } /** * Set value of parameter */ private function setParamValue($value) { // params in zophCode do not have spaces, so we cut off at the first space list($value)=explode(" ", $value, 2); $this->paramValue=$value; } /** * Check whether a given value conforms to the requirement * @param string Param value to check * @todo currently not used * @return bool true: validates, false: does not validate */ private function checkParam($value) { if (!empty($this->regexp)) { return preg_match($this->regexp, $value); } else { return true; } } /** * Insert parameter value into tag * @param string value to insert into tag * @return string parameter with value inserted in place of [param] placeholder */ private function getParam() { if (!empty($this->param) && $this->checkParam($this->paramValue)) { return " " . str_replace("[param]", $this->paramValue, $this->param); } } } ?> zoph-v0.9.19/php/classes/zophTable.inc.php000066400000000000000000000575131415176210700204340ustar00rootroot00000000000000set(static::$primaryKeys[0],$id); } /** * Returns the value of a field * @param string name of field to get * @return string value of the field */ public function get($name) { log::msg("GET " . $name, log::DEBUG, log::VARS); log::msg("
" . var_export($this->fields, true) . "
", log::MOREDEBUG, log::VARS); if (isset($this->fields[$name])) { return $this->fields[$name]; } else { return ""; } } /** * Get ID * @return int id * @throws zophException */ public function getId() { if (sizeof(static::$primaryKeys)==1) { return (int) $this->get(static::$primaryKeys[0]); } else { throw new zophException("This class (" . get_class($this) . ") " . "requires a specific getId() implementation, please report a bug"); } } /** * Sets the value of a field. * @param string Name of the field to set * @param string Value to set it to */ public function set($name, $value) { $this->fields[$name] = $value; } /** * Sets fields from the given array. Can be used to set vars * directly from a GET or POST. * @param array Variables to be set (like $_GET) * @param string Prefix to cut off from beginning of key name * @param string Suffic to cut off from end of key name * @param bool Whether or not to process empty fields */ public function setFields(array $vars, $prefix = null, $suffix = null, $null=true) { foreach ($vars as $key => $val) { log::msg("" . $key . " = " . implode(",", (array) $val), log::DEBUG, log::VARS); // ignore empty keys or values unless the field must be set. if ($null) { if ((!in_array($key, static::$notNull)) && (empty($key))) { continue; } } else { if ((!in_array($key, static::$notNull)) && (empty($key) || $val == "")) { continue; } } if ($prefix) { if (strpos($key, $prefix) === 0) { $key = substr($key, strlen($prefix)); } else { continue; } } else if ($key[0] == '_') { // a leading uderscore signals a non-database field continue; } if ($suffix) { $pos = strpos($key, $suffix); if (($pos > 0) && (preg_match("/".$suffix."$/", $key))) { $key = substr($key, 0, $pos); } else { continue; } } // something in ALL CAPS is probably PHP or HTML related if (strtoupper($key) == $key) { continue; } $this->fields[$key] = stripslashes($val); } } /** * Checks to see if the given field is listed as a primary key. * @param string Name of the field * @return bool Whether or not field is listed */ public function isKey($name) { return in_array($name, static::$primaryKeys); } /** * Looks up a record. * @return bool success or fail * @todo Should return something more sensible */ public function lookup() { $qry=new select(array(static::$tableName)); list($qry, $where) = $this->addWhereForKeys($qry); if (!($where instanceof clause)) { log::msg("No constraint found", log::NOTIFY, log::GENERAL); return; } $qry->where($where); return $this->lookupFromSQL($qry); } /** * Looks up a record using supplied SQL query * @param select SQL query to use * @return bool success or fail */ public function lookupFromSQL(select $qry) { try { $result = db::query($qry); } catch (PDOException $e) { log::msg("Lookup failed", log::FATAL, log::DB); } $results=$result->fetchAll(PDO::FETCH_ASSOC); $rows=count($results); if ($rows == 1) { $row=array_pop($results); $this->fields = array(); $this->fields = array_merge($this->fields, $row); return true; } return false; } /** * Inserts a record. * The default behavior is to ignore the * primary key field(s) with the assumption that these will * be generated by the db (auto_increment). Passing a non null * parameter causes these fields to be manually inserted. */ public function insert() { $qry=new insert(array(static::$tableName)); reset($this->fields); foreach ($this->fields as $name => $value) { if (!static::$keepKeys && $this->isKey($name)) { continue; } if ($value === "now()") { /* Lastnotify is normally set to "now()" and should not be escaped */ $qry->addSet($name, "now()"); } else { $qry=$this->processValues($name, $value, $qry); } } $id=$qry->execute(); if (count(static::$primaryKeys) == 1 && !static::$keepKeys) { $this->fields[static::$primaryKeys[0]] = $id; } return $id; } /** * Retrieving a the selectarray can take a long time in some cases * pages that use it multiple times can cache it, so it only needs * to be retrieved once per page request. * @param array selectArray; */ public static function setSAcache(array $sa=null) { if (!$sa) { $sa=static::getSelectArray(); } static::$sacache=$sa; } /** * Deletes a record. If extra tables are specified, entries from * those tables this match the keys are removed as well. * @var $extra_tables array Tables to delete referencing objects from */ public function delete() { // simulate overloading if (func_num_args()>=1) { $extra_tables = func_get_arg(0); } else { $extra_tables = null; } $qry=new delete(array(static::$tableName)); list($qry, $where) = $this->addWhereForKeys($qry); if (!($where instanceof clause)) { log::msg("No constraint found", log::NOTIFY, log::GENERAL); return; } $qry->where($where); try { $qry->execute(); } catch (PDOException $e) { log::msg("Delete failed", log::FATAL, log::DB); } if ($extra_tables) { foreach ($extra_tables as $table) { $qry=new delete(array($table)); list($qry, $where) = $this->addWhereForKeys($qry); $qry->where($where); try { $qry->execute(); } catch (PDOException $e) { log::msg("Delete from " . $table . " failed", log::FATAL, log::DB); } } } } /** * Updates a record. */ public function update() { $qry=new update(array(static::$tableName)); list($qry, $where) = $this->addWhereForKeys($qry); reset($this->fields); foreach ($this->fields as $name => $value) { if ($this->isKey($name)) { continue; } if ($value === "now()") { /* Lastnotify is normally set to "now()" and should not be escaped */ $qry->addSetFunction($name . "=now()"); } else { $qry=$this->processValues($name, $value, $qry); $qry->addSet($name, $name); } } if (sizeof($qry->getParams()) === 0 || sizeof($qry->getSet()) === 0) { return; } $qry->where($where); try { $qry->execute(); } catch (PDOException $e) { log::msg("Update failed: " . $e->getMessage(), log::FATAL, log::DB); } } protected function processValues($name, $value, $qry) { if ((is_null($value) || $value==="") && in_array($name, static::$notNull)) { throw new notNullValueIsNullDataException(e($name) . "may not be empty"); } else { if (in_array($name, static::$isFloat) && empty($value)) { $value = null; } if (in_array($name, static::$isInteger)) { if (is_null($value) || $value==="") { $qry->addParam(new param(":" . $name, null, PDO::PARAM_NULL)); } else { $qry->addParam(new param(":" . $name, (int) $value, PDO::PARAM_INT)); } } else { if (is_null($value)) { $qry->addParam(new param(":" . $name, null, PDO::PARAM_NULL)); } else { $qry->addParam(new param(":" . $name, $value, PDO::PARAM_STR)); } } } return $qry; } /** * Creates an alphabetized array of field names and values. * @return array Array for displaying object */ public function getDisplayArray() { if (!$this->fields) { return; } $keys = array_keys($this->fields); sort($keys); reset($keys); $da=array(); foreach ($keys as $k) { if ($this->isKey($k)) { continue; } $title = ucfirst(str_replace("_", " ", $k)); $da[$title] = $this->fields[$k]; } return $da; } /** * Get an URL for the current object * @return string URL */ public function getURL() { return static::$url . $this->getId(); } /** * Turn the array from @see getDetails() into XML * @param array Don't fetch details, but use the given array */ public function getDetailsXML(array $details=null) { if (!isset($details)) { $details=$this->getDetails(); } if (isset($details["title"])) { $display["title"]=$details["title"]; } if (array_key_exists("count", $details) && $details["count"] > 0) { // Remove timezone identifiers from time format // Because in the current way Zoph works, they do not make sense // It's not completely correct this way, because the data comes // from the database where it is not yet timezone-corrected. $timezone=array("e", "I", "O", "P", "T", "Z"); $timeformat=str_replace($timezone, "", conf::get("date.timeformat")); $timeformat=trim(preg_replace("/\s\s+/", "", $timeformat)); $format=conf::get("date.format") . " " . $timeformat; $oldest=new Time($details["oldest"]); $disp_oldest=$oldest->format($format); $newest=new Time($details["newest"]); $disp_newest=$newest->format($format); $first=new Time($details["first"]); $disp_first=$first->format($format); $last=new Time($details["last"]); $disp_last=$last->format($format); $display["count"]=$details["count"] . " " . translate("photos"); $display["taken"]=sprintf(translate("taken between %s and %s",false), $disp_oldest, $disp_newest); $display["modified"]=sprintf(translate("last changed from %s to %s",false), $disp_first, $disp_last); if (isset($details["lowest"]) && isset($details["highest"]) && isset($details["average"])) { $display["rated"]=sprintf( translate("rated between %s and %s and an average of %s",false), $details["lowest"], $details["highest"], $details["average"]); } else { $display["rated"]=translate("no rating", false); } } else { $display["count"]=translate("no photos", false); } if (isset($details["children"])) { $count=$details["children"]; if ($count==0) { $display["children"]=""; $no="no "; } else { $display["children"]=$count . " "; $no=""; } if ($this instanceof album) { $text=translate($no . "sub-albums", false); } else if ($this instanceof category) { $text=translate($no . "sub-categories", false); } else if ($this instanceof place) { $text=translate($no . "sub-places", false); } else { $text=translate($no . "children", false); } $display["children"].=$text; } $xml = new DOMDocument('1.0','UTF-8'); $rootnode=$xml->createElement("details"); $request=$xml->createElement("request"); $class=$xml->createElement("class"); $class->appendChild($xml->createTextNode(get_class($this))); $id=$xml->createElement("id"); $id->appendChild($xml->createTextNode($this->getId())); $request->appendChild($class); $request->appendChild($id); $rootnode->appendChild($request); $response=$xml->createElement("response"); foreach ($display as $subj => $data) { $detail=$xml->createElement("detail"); $subject=$xml->createElement("subject"); $subject->appendChild($xml->createTextNode($subj)); $xmldata=$xml->createElement("data"); $xmldata->appendChild($xml->createTextNode($data)); $detail->appendChild($subject); $detail->appendChild($xmldata); $response->appendChild($detail); } $rootnode->appendChild($response); $xml->appendChild($rootnode); return $xml->saveXML(); } /** * Return object from Id * @param int id * @return mixed object */ public static function getFromId($id) { if (!is_null($id) && $id!=0) { $class=get_called_class(); $obj=new $class($id); $obj->lookup(); return $obj; } } /** * Gets the total count of records in the table for the given class. * @return int count */ public static function getCount() { $qry=new select(array(static::$tableName)); $qry->addFunction(array("count" => "COUNT(*)")); return $qry->getCount(); } /** * Generates an array for Top N albums/cat/.. * Executes a query and returns an array in which each record's * link is mapped to its count (dirived by a group by clause). * @param string query SQL query to use * @return array Table of Top N most popular $class */ protected static function getTopNfromSQL($query) { $pop_array=array(); $records = static::getRecordsFromQuery($query); foreach ($records as $rec) { $pop_array[] = array( "id" => $rec->getId(), "url" => $rec->getURL(), "count" => $rec->get("count"), "title" => $rec->getName() ); } return $pop_array; } /** * Gets an array of the records for a table by doing a * "select *" * and storing the results in classes of the given type. * @param string Sort order * @param array Constraints, conditions that the records must comply to * @param array Conjunctions, and/or * @param array Operators =, !=, >, <, >= or <= * @return array records * @todo This should be an internal (protected) function */ public static function getRecords($order = null, $constraints = null, $conj = "AND", $ops = null) { $qry = new select(static::$tableName); if (is_array($constraints)) { $qry->addWhereFromConstraints($constraints, $conj, $ops); } if ($order) { $qry->addOrder($order); } return static::getRecordsFromQuery($qry); } /** * Return all * @return array Array of objects */ public static function getAll() { return static::getRecords(); } /** * Extract a specific class from vars * @param array vars (like $_GET or $_POST) * @param string suffix to add to var key (e.g. _id) * @return array vars for specific class. */ public static function getFromVars(array $vars, $suffix="") { $class=get_called_class(); $return=array(); $key="_" . $class . $suffix; if (isset($vars[$key])) { if (is_array($vars[$key])) { foreach ($vars[$key] as $id=>$var) { if (!empty($var)) { $return[$id]=$var; } } } else { $return=(array) $vars[$key]; } } return $return; } /** * Stores the results the the given query in an array of objects of * this given type. * @param select SQL query */ public static function getRecordsFromQuery(select $qry) { $class=get_called_class(); try { $result = db::query($qry); } catch (PDOException $e) { log::msg("Unable to get records: " . $e->getMessage(), log::FATAL, log::DB); } $objs=array(); while ($row = $result->fetch(PDO::FETCH_ASSOC)) { $obj = new $class; $obj->setFields($row); $objs[] = $obj; } return $objs; } /** * Creates a constraint clause based on the given keys */ private function addWhereForKeys(query $query, clause $where = null) { foreach (static::$primaryKeys as $key) { $value = $this->fields[$key]; if (!$value) { continue; } $clause = new clause($key . "=:" . $key); $query->addParam(new param(":" . $key, $value, PDO::PARAM_INT)); if ($where instanceof clause) { $where->addAnd($clause); } else { $where = $clause; } } return array($query, $where); } /** * Get coverphoto. * @return photo coverphoto */ public function getCoverphoto() { if ($this->get("coverphoto")) { $coverphoto=new photo($this->get("coverphoto")); if ($coverphoto->lookup()) { return $coverphoto; } } return false; } /** * Lookup an autocover and create template to display * @param how to select the autocover (olders, newest, first, last, random, highest [default]) * @return block thumb img */ public function displayAutoCover($autocover=null) { $cover=$this->getAutoCover($autocover); if ($cover instanceof photo) { return $cover->getImageTag(THUMB_PREFIX); } } /** * Lookup cover and create template to display * @return block thumb img */ public function displayCoverPhoto() { $cover=$this->getCoverphoto(); if ($cover instanceof photo) { return $cover->getImageTag(THUMB_PREFIX); } } /** * Get XML from a database table * This is a wrapper around several objects which will call a method from * those objects * @param string Search string */ public static function getXML($search) { $search=strtolower($search); $xml = new DOMDocument('1.0','UTF-8'); $rootnode=$xml->createElement(static::XMLROOT); $newchild=$xml->createElement(static::XMLNODE); $key=$xml->createElement("key"); $title=$xml->createElement("title"); $key->appendChild($xml->createTextNode("null")); $title->appendChild($xml->createTextNode(" ")); $newchild->appendChild($key); $newchild->appendChild($title); $rootnode->appendChild($newchild); return static::getXMLdata($search, $xml, $rootnode); } /** * Create a pulldown menu for this object * @param string name for this pulldown * @param int|string id of value */ public static function createPulldown($name, $value=null) { if (static::getAutocompPref()) { return static::createAutoCompPulldown($name, $value); } else { if (isset(static::$sacache)) { $sa=static::$sacache; } else { $sa=static::getSelectArray(); } return template::createPulldown($name, $value, $sa); } } public static function createAutoCompPulldown($name, $value=null) { $id=preg_replace("/^_+/", "", $name); $text=""; if ($value) { $obj=static::getFromId($value); $obj->lookup(); $text=$obj->getName(); } $tpl=new block("autocomplete", array( "id" => $id, "name" => $name, "value" => $value, "text" => $text )); return $tpl; } /** * Get an array of id => name to build a non-hierarchical array * this function does NOT check user permissions * @return array */ public static function getSelectArray() { $records=static::getRecords(); $selectArray=array(null => ""); foreach ($records as $record) { $selectArray[(string) $record->getId()] = $record->getName(); } return $selectArray; } } ?> zoph-v0.9.19/php/classes/zophTreeTable.inc.php000066400000000000000000000245231415176210700212470ustar00rootroot00000000000000set("createdby", (int) user::getCurrent()->getId()); return parent::insert(); } /** * Deletes a record along with all of its descendants. * @param array Names of tables from which entries also should be deleted. */ public function delete() { // simulate overloading if (func_num_args()>=1) { $extra_tables = func_get_arg(0); } else { $extra_tables = null; } if ($this->getId()==0) { return; } $this->getChildren(); if ($this->children) { foreach ($this->children as $child) { $child->delete(); } } parent::delete($extra_tables); } /** * Updates a record * first check if there are no circular references created */ public function update() { reset($this->fields); foreach ($this->fields as $name => $value) { if (substr($name, 0, 7) == "parent_") { $children=array(); $this->getBranchIdArray($children); if (in_array($value, $children)) { throw new circularReferenceException("You cannot set the parent to a child of the current selection!"); } } } parent::update(); } /** * Check whether this organizer is the root of the tree * At this moment the root always has id 1 but this may * change in the future, so to be safe we'll make a function for * this * @return bool */ public function isRoot() { $root=static::getRoot(); return ($this->getId() == $root->getId()); } /** * Get the parent node for this node * @return zophTreeTable parent node */ public function getParent() { if ($this->isRoot()) { return null; } $key = static::$primaryKeys[0]; $pid = $this->get("parent_" . $key); if (!$pid) { $this->lookup(); $pid = $this->get("parent_" . $key); } $parent = new static($pid); $parent->lookup(); return $parent; } /** * Gets the ancestors of this record. * @param array ancestors * @return array ancestors */ public function getAncestors($anc = array()) { $parent=$this->getParent(); if ($parent) { array_push($anc, $parent); return $parent->getAncestors($anc); } else { return $anc; } } /** * Get all ancestors of this a list of records, in order to get * all viewable records * * We now have a list of records this person can see, (that is, albums, * categories or places that contain photos this user can see). However, * sometimes it may be neededi to have access to a category, album or * place with no viewable photos, in order to reach a viewable * album, category or place. Therefore, we are going to backtrack up to * the root for each. */ public static function getAllAncestors(array $records) { $ids=array(); foreach ($records as $record) { $ids[$record->getId()]=$record->getId(); $parents=$record->getAncestors(); foreach ($parents as $parent) { $ids[$parent->getId()]=$parent->getId(); } } return $ids; } /* * Gets a list of the id of this record along with the ids of * all of its descendants. * @param array id_array add values to this array * @todo refactor the pass by reference out */ public function getBranchIdArray(array &$id_array=null) { if (!is_array($id_array)) { $id_array=array(); } $id_array[] = (int) $this->getId(); $this->getChildren(); if ($this->children) { foreach ($this->children as $c) { $c->getBranchIdArray($id_array); } } return $id_array; } /* * Gets a comma separated string of this record's id along with * all of its descendant's ids. Useful to make "record_id in * (id_list)" clauses. */ public function getBranchIds() { return implode(",", $this->getBranchIdArray()); } /** * Create an XML tree from this object * @param DOMDocument XML document to insert the new node in * @param Only include nodes that begin with this string */ private function getXMLtree(DOMDocument $xml, $search) { $rootname=static::XMLROOT; $nodename=static::XMLNODE; $idname=static::$primaryKeys[0]; $newchild=$xml->createElement($nodename); $title=$this->getName(); $titleshort=strtolower(substr($title, 0, strlen($search))); if ($titleshort == strtolower($search)) { $key=$this->get($idname); $newchildkey=$xml->createElement("key"); $newchildkey->appendChild($xml->createTextNode($key)); $newchildtitle=$xml->createElement("title"); $newchildtitle->appendChild($xml->createTextNode($title)); $newchild->appendChild($newchildkey); $newchild->appendChild($newchildtitle); } $order = user::getCurrent()->prefs->get("child_sortorder"); $children=$this->getChildren($order); if ($children) { $childset=$xml->createElement($rootname); foreach ($children as $child) { $newnode=$child->getXMLtree($xml, $search); if (isset($newnode)) { $childset->appendChild($newnode); } } $newchild->appendChild($childset); } return $newchild; } /** * Turn the array from @see getDetails() into XML * @param array Don't fetch details, but use the given array */ public function getDetailsXML(array $details=null) { if (!isset($details)) { $details=$this->getDetails(); } $children=$this->getChildren(); if (is_array($children)) { $details["children"]=count($children); } return parent::getDetailsXML($details); } /** * Return the root of the tree * @return album|category|place */ public static function getRoot() { return new static(1); } /** * Search for an object by hierarchical name * @example If you have an album "Vacation" with subalbums "2010" * and "2012", both with a subalbum named "France" * album::getByNameHierarchical("Vacation/2010/France"); * will match the "France" album in 2010, but not in 2012, even * if they are both called "France" * @param string name to search for * @return zophTreeTable found object */ public static function getByNameHierarchical($name) { if (strpos($name, "/") === false) { return static::getByName($name, true); } $found=0; $searchString=explode("/", $name); $depth=sizeof($searchString); foreach ($searchString as $namePart) { $objs = static::getByName($namePart, true); foreach ($objs as $obj) { $obj->lookup(); if (!isset($parentObj)) { $found++; $parentObj=$obj; } else { $nextObjId=$obj->getId(); $children=$parentObj->getChildren(); foreach ($children as $child) { $child->lookup(); if ($child->getId()==$nextObjId) { $parentObj=$obj; $found++; break; } } } } } // Only report success if we have traversed the full depth of the search. if ($depth == $found) { return $obj; } else { return false; } } public static function getXMLdata($search, DOMDocument $xml, DOMElement $rootnode) { $obj = static::getRoot(); $obj->lookup(); $tree=$obj->getXMLtree($xml, $search); $rootnode->appendChild($tree); $xml->appendChild($rootnode); return $xml; } public static function getTreeSelectArray($rec = null, $select_array = null, $depth=0) { $user=user::getCurrent(); $user->lookupPrefs(); $order = $user->prefs->get("child_sortorder"); if (!$rec) { $rec = static::getRoot(); $rec->lookup(); $select_array[""] = ""; } $select_array[$rec->getId()] = str_repeat(" ", $depth * 3) . e($rec->getName()); $children = $rec->getChildren($order); if ($children) { $depth++; foreach ($children as $child) { $select_array = static::getTreeSelectArray($child, $select_array, $depth); } } return $select_array; } } ?> zoph-v0.9.19/php/color_scheme.php000066400000000000000000000032061415176210700167270ustar00rootroot00000000000000getObject(); switch ($controller->getView()) { case "confirm": $view=new template\colorScheme\view\confirm($request, $colorScheme); break; case "display": $view=new template\colorScheme\view\display($request, $colorScheme); break; case "insert": case "update": $view=new template\colorScheme\view\update($request, $colorScheme); break; case "redirect": $view=new template\colorScheme\view\redirect($request, $colorScheme); $view->setRedirect("color_schemes.php"); break; } $title = $view->getTitle(); require_once "header.inc.php"; echo $view->view(); require_once "footer.inc.php"; zoph-v0.9.19/php/color_schemes.php000066400000000000000000000024431415176210700171140ustar00rootroot00000000000000isAdmin()) { redirect("zoph.php"); } $title = translate("color schemes"); require_once "header.inc.php"; $tpl = new template("displayColorSchemes", array( "title" => translate("Color Schemes"), "cs" => colorScheme::getRecords("name") )); $tpl->addActionlinks(array( translate("new") => "color_scheme.php?_action=new" )); echo $tpl; require_once "footer.inc.php"; ?> zoph-v0.9.19/php/comment.php000066400000000000000000000036331415176210700157330ustar00rootroot00000000000000 language::getCurrentISO(), "title" => "Security Error", "message" => $e->getMessage(), )); $tpl->addActionlinks(array("return" => "zoph.php")); echo $tpl; exit(99); } switch ($controller->getView()) { case "confirm": $view=new comment\view\confirm($request); break; case "display": $view=new comment\view\display($request); break; case "insert": case "update": $view=new comment\view\update($request); break; case "redirect": $view=new comment\view\redirect($request); $view->setRedirect($controller->redirect); echo $view->view(); end; break; } $title=$view->getTitle(); require_once("header.inc.php"); $title = $view->getTitle(); echo $view->view(); require_once("footer.inc.php"); ?> zoph-v0.9.19/php/comments.php000066400000000000000000000023721415176210700161150ustar00rootroot00000000000000 translate("Comments") )); $comments=comment::getRecords(); foreach ($comments as $comment) { $photo=$comment->getPhoto(); if ($user->getPhotoPermissions($photo) || $user->canSeeAllPhotos()) { $tpl->addBlock($comment->toHTML(true)); } } echo $tpl; ?> zoph-v0.9.19/php/config.inc.php000066400000000000000000000042721415176210700163060ustar00rootroot00000000000000 zoph-v0.9.19/php/config.php000066400000000000000000000027561415176210700155430ustar00rootroot00000000000000isAdmin()) { redirect("zoph.php"); } // Configuration setting depends on POST if (!empty($_GET)) { redirect("config.php"); } $_action=getvar("_action"); if ($_action == "setconfig") { conf::loadFromRequestVars($request_vars); } $tpl=new template("config", array( "title" => $title, )); // this doesn't work yet, because the page is not fully template-generated // it is also included in header.inc.php, but header.inc.php should be // phased out soon. $tpl->js[]="js/conf.js"; foreach (conf::getAll() as $name=>$item) { $tpl->addBlock($item->display()); } echo $tpl; ?> zoph-v0.9.19/php/css.php000066400000000000000000000023361415176210700150600ustar00rootroot00000000000000 zoph-v0.9.19/php/download.php000066400000000000000000000157061415176210700161040ustar00rootroot00000000000000getRequestVarsClean(); $_action=getvar("_action"); if (!conf::get("feature.download") || (!$user->get("download") && !$user->isAdmin())) { redirect("zoph.php"); } if ($_action=="getfile" || $_action=="download") { $filename=getvar("_filename"); if (!$filename) { $filename="zoph"; } if (!preg_match("/^[a-zA-Z0-9_-]+$/", $filename)) { die("Invalid filename"); } $filenum=(int) getvar("_filenum"); if (!$filenum) { $filenum=1; } } if ($_action=="download") { $zipfile="/tmp/zoph_" . $user->get("user_id") . "_" . $filename ."_" . $filenum . ".zip"; if (file_exists($zipfile)) { header("Content-Length: " . filesize($zipfile)); header("Content-Disposition: inline; filename=" . $filename . $filenum . ".zip"); header("Content-type: application/zip"); readfile($zipfile); unlink($zipfile); } else { echo sprintf(translate("Could not read %s."), $zipfile) . "
\n"; } flush(); exit; } $title=translate("Download zipfile"); require_once "header.inc.php"; ?>

subset($offset, $maxfiles); $totalPhotoCount = sizeof($photoCollection); $downloadCount = sizeof($photos); if ($_action=="getfile") { $maxsize=getvar("_maxsize"); if (!$maxsize) { $maxsize=-1; } if (!is_numeric($maxsize)) { die("Maximum size must be numeric"); } $dateddirs=getvar("dateddirs"); if ($downloadCount) { echo translate("The zipfile is being created...") . "
"; flush(); $zip = new archive(archive::ZIP, $filename, $filenum); $zip->setMaxSize($maxsize); $number=$zip->addPhotos($photos); $newoffset=$offset + $number; echo ""; $new_qs=str_replace("_off=$offset", "_off=$newoffset", $_SERVER["QUERY_STRING"]); if ($new_qs==$_SERVER["QUERY_STRING"]) { $new_qs=$new_qs . "&_off=$newoffset"; } $qs=$new_qs; $new_qs=str_replace("_filenum=$filenum", "_filenum=" . ($filenum + 1), $qs); if ($new_qs==$qs) { $new_qs=$new_qs . "&_filenum=" . ($filenum + 1); } if ($newoffset < $totalPhotoCount) { echo sprintf(translate("Downloaded %s of %s photos."), $newoffset, $totalPhotoCount); ?>


10, 25 => 25, 50 => 50, 75 => 75, 100 => 100, 150 => 150, 200 => 200, 300 => 300, 400 => 400, 500 => 500) ) ?>
"5MiB", "10000000" => "10MiB", "25000000" => "25MiB", "50000000" => "50MiB", "75000000" => "75MiB", "100000000" => "100MiB", "150000000" => "150MiB", "250000000" => "250MiB", "500000000" => "500MiB", "650000000" => "650MiB", "1000000000" => "1GiB", "2000000000" => "2GiB", "4200000000" => "4.2GiB") ) ?>
">


zoph-v0.9.19/php/edit_photos.php000066400000000000000000000432321415176210700166110ustar00rootroot00000000000000prefs->get("num_cols"); } if (!$_rows) { $_rows = $user->prefs->get("num_rows"); } if (!$_off) { $_off = 0; } if (!$_order) { $_order = conf::get("interface.sort.order"); } if (!$_dir) { $_dir = conf::get("interface.sort.dir"); } $cells = $_cols * $_rows; $offset = $_off; $thumbnails; $clean_vars=$request->getRequestVarsClean(); $_qs=getvar("_qs"); $qs = preg_replace('/_crumb=\d+&?/', '', $_SERVER["QUERY_STRING"]); $qs = preg_replace('/_action=\w+&?/', '', $qs); $encoded_qs = urlencode(htmlentities($_qs)); if (empty($encoded_qs)) { $encoded_qs = urlencode(htmlentities($qs)); } /* if page is called via a HTTP POST, the $QUERY_STRING variable is empty so we need to fill $qs differently... */ if (empty($qs)) { $qs=$_qs; } $tplActionlinks=new block("actionlinks", array( "actionlinks" => array(translate("return") => "photos.php?" . $qs), )); $photoCollection = collection::createFromRequest(request::create()); $toDisplay = $photoCollection->subset($offset, $cells); $photoCount=sizeof($photoCollection); $displayCount=sizeof($toDisplay); if ($displayCount) { $pageCount = ceil($photoCount / $cells); $currentPage = floor($offset / $cells) + 1; $num = min($cells, $displayCount); $title = sprintf(translate("Edit Photos (Page %s/%s)", 0), $currentPage, $pageCount); $title_bar = sprintf(translate("edit photos %s to %s of %s"), ($offset + 1), ($offset + $num), $photoCount); } else { $title = translate("No Photos Found"); $title_bar = translate("edit photos"); } require_once "header.inc.php"; ?>

prefs->get("autocomp_categories")) { category::setSAcache(); } if (!$user->prefs->get("autocomp_albums")) { album::setSAcache(); } if (!$user->prefs->get("autocomp_places")) { place::setSAcache(); } if (!$user->prefs->get("autocomp_people")) { person::setSAcache(); } } // used to create hidden fields for recreating the results query $queryIgnoreArray[] = '_action'; $queryIgnoreArray[] = '_overwrite'; $queryIgnoreArray[] = '__location_id__all'; $queryIgnoreArray[] = '_rating__all'; $queryIgnoreArray[] = '_album__all'; $queryIgnoreArray[] = '_category__all'; ?> ">

YYYY-MM-DD
HH:MM:SS





lookup(); $photo_id = $photo->getId(); unset($request_vars["___location_id__" . $photo_id]); unset($request_vars["___photographer_id__" . $photo_id]); unset($request_vars["__album__" . $photo_id]); unset($request_vars["__category__" . $photo_id]); unset($request_vars["__person__" . $photo_id]); $permissions = $user->getPhotoPermissions($photo); if (!$user->isAdmin() && !$permissions) { continue; } $can_edit = false; $action=""; $can_edit = $user->isAdmin() || $permissions->get("writable"); if (array_key_exists("_action__" . $photo_id, $request_vars)) { $action = $request_vars["_action__" . $photo_id]; } if ($can_edit && $action == 'update') { $rating = null; if ($request_vars['_overwrite']) { // set any specific fields $photo->setFields($request_vars, '__', "__$photo_id"); // set "apply to all" fields $photo->setFields($request_vars, '__', '__all', false); $rating = $request_vars["_rating__$photo_id"]; if ($request_vars["_rating__all"]) { $rating = $request_vars["_rating__all"]; } } else { // reverse order $photo->setFields($request_vars, '__', '__all'); $photo->setFields($request_vars, '__', "__$photo_id", false); $rating = $request_vars["_rating__all"]; if ($request_vars["_rating__$photo_id"]) { $rating = $request_vars["_rating__$photo_id"]; } } if ($rating != "0") { if (conf::get("feature.rating")) { $photo->rate($rating); } } // this will update any specific albums, cats & people $photo->update(); $photo->updateRelations($request_vars, '__' . $photo_id); // update "apply to all" albums, cats & people $photo->updateRelations($request_vars, '__all'); if ($can_edit && conf::get("rotate.enable") && ($user->isAdmin() || $permissions->get("writable"))) { $deg = $request_vars["_deg__$photo_id"]; if ($deg && $deg != 0) { $photo->lookup(); try { $photo->rotate($deg); } catch (Exception $e) { echo $e->getMessage(); die; } } } } else if ($can_edit && $action == 'delete') { $photo->delete(); continue; } if ($action == "update") { $request_vars["_action"]="display"; } $photo->lookup(); $queryIgnoreArray[] = "__photo_id__$photo_id"; $queryIgnoreArray[] = "__location_id__$photo_id"; $queryIgnoreArray[] = "__photographer_id__$photo_id"; $queryIgnoreArray[] = "__title__$photo_id"; $queryIgnoreArray[] = "__description__$photo_id"; $queryIgnoreArray[] = "_rating__$photo_id"; $queryIgnoreArray[] = "_album__$photo_id"; $queryIgnoreArray[] = "_remove_album__$photo_id"; $queryIgnoreArray[] = "_category__$photo_id"; $queryIgnoreArray[] = "_remove_category__$photo_id"; $queryIgnoreArray[] = "_remove_person__$photo_id"; $queryIgnoreArray[] = "_person__" . $photo_id; $queryIgnoreArray[] = "_deg__$photo_id"; $queryIgnoreArray[] = "_action__$photo_id"; ?>
get('name')?>


getThumbnailLink() . "\n" ?>
isAdmin() || $permissions->get("writable"))) { ?>
get('name') ?>:
.
get("title"), 30, 64) ?>
get("date") , 12, 10, "date") ?> YYYY-MM-DD
get("time"), 10, 8, "time") ?> HH:MM:SS
get("location_id")) ?>
get("photographer_id")) ?>
getRatingForUser($user); ?>

getAlbums($user); if ($albums) { $append = ""; foreach ($albums as $album) { ?> "> getLink() ?> "; } echo "
\n"; } ?>


getCategories($user); if ($categories) { $append = ""; foreach ($categories as $category) { ?> "> getLink(); $append = "
\n"; } echo "
\n"; } ?>


getAll(); if ($people) { $append = ""; foreach ($people as $person) { ?> "> getLink() ?> \n"; } echo "
\n"; } ?>


">

$value) { if (in_array($key, $queryIgnoreArray)) { continue; } $pager_vars[$key] = $value; } $request_vars = $pager_vars; echo new pager($offset, $photoCount, $pageCount, $cells, $user->prefs->get("max_pager_size"), $request_vars, "_off", "edit_photos.php"); } // if photos ?>
zoph-v0.9.19/php/edit_place.inc.php000066400000000000000000000115571415176210700171360ustar00rootroot00000000000000 translate($_action . " " . "place"), "actionlinks" => $actionlinks )); $tpl->addBlock(template::showJSwarning()); if ($place->isRoot()) { $parentPlace=translate("places"); } else { $parentPlace=place::createPulldown("parent_place_id", $place->get("parent_place_id")); } $form=new form("form", array( "formAction" => "place.php", "onsubmit" => null, "action" => $action, "submit" => translate($action, 0) )); $form->addInputHidden("place_id", $place->getId()); $form->addInputText("title", $place->get("title"), translate("title"), sprintf(translate("%s chars max"), "64"), 64, 40); if (!$place->isRoot()) { $parentPlace=place::createPulldown("parent_place_id", $place->get("parent_place_id")); $form->addPulldown("parent_place_id", $parentPlace, translate("parent location")); } $form->addInputText("address", $place->get("address"), translate("address"), sprintf(translate("%s chars max"), "64"), 64, 40); $form->addInputText("address2", $place->get("address2"), translate("address continued"), sprintf(translate("%s chars max"), "64"), 64, 40); $form->addInputText("city", $place->get("city"), translate("city"), sprintf(translate("%s chars max"), "32"), 32); $form->addInputText("state", $place->get("state"), translate("state"), sprintf(translate("%s chars max"), "32"), 32, 16); $form->addInputText("zip", $place->get("zip"), translate("zip"), translate("zip or zip+4"), 10); $form->addInputText("country", $place->get("country"), translate("country"), sprintf(translate("%s chars max"), "32"), 32); $form->addInputText("url", $place->get("url"), translate("url"), sprintf(translate("%s chars max"), "1024"), 1024, 32); $form->addInputText("urldesc", $place->get("urldesc"), translate("url description"), sprintf(translate("%s chars max"), "32"), 32); $pageset=template::createPulldown("pageset", $place->get("pageset"), template::createSelectArray(pageset::getRecords("title"), array("title"), true)); $form->addPulldown("pageset", $pageset, translate("pageset")); $fieldset=new fieldset("formFieldset", array( "class" => "map", "legend" => translate("map") )); $fieldset->addInputText("lat", $place->get("lat"), translate("latitude"), null, 10); $fieldset->addInputText("lon", $place->get("lon"), translate("longitude"), null, 10); $mapzoom=place::createZoomPulldown($place->get("mapzoom")); $fieldset->addPulldown("mapzoom", $mapzoom, translate("zoom level")); $desc = translate("Paste a location in this field to lookup, supported are currently: decimal GPS coordinates (e.g. 50.5,-5.2), Open Location Codes (also known as pluscodes e.g. 7GXHX4HM+MM), Openstreetmap URLs and Zoph URLs for a photo or place."); $fieldset->addInputText("_locationLookup", "", translate("location lookup"), $desc, 256, 40); if (conf::get("maps.geocode")) { $fieldset->addBlock(new block("geocode")); } $form->addBlock($fieldset); $tzActionlinks=array(); if (conf::get("date.guesstz")) { $tz=e($place->guessTZ()); if (!empty($tz)) { $tzActionlinks[$tz] = "place.php?_action=update&place_id=" . $place->getId() . "&timezone=" . $tz; } } if ($place->get("timezone")) { $tzActionlinks[sprintf(translate("set %s for children"), $place->get("timezone"))] = "place.php?_action=settzchildren&place_id=" . $place->getId(); } if (!empty($tzActionlinks)) { $form->addBlock(new block("actionlinks", array( "actionlinks" => $tzActionlinks ))); } $timezone=TimeZone::createPulldown("timezone_id", $place->get("timezone")); $form->addPulldown("timezone_id", $timezone, translate("timezone")); $form->addTextarea("notes", $place->get("notes"), translate("notes"), 40, 4); $tpl->addBlock($form); echo $tpl; zoph-v0.9.19/php/exception.inc.php000066400000000000000000000315411415176210700170360ustar00rootroot00000000000000 zoph-v0.9.19/php/exif.inc.php000066400000000000000000000202231415176210700157660ustar00rootroot00000000000000getMime(); if ($mime == "image/jpeg") { $exif = exif_read_data($image); } else { $exif = false; } if ($exif === false) { echo "" . basename($image) . "" . ": "; echo translate("No EXIF header found.") . "
\n"; // Set date and time to file date/time list($exifdata["date"],$exifdata["time"])= explode(" ",date("Y-m-d H:i:s", filemtime($image))); return $exifdata; } if (isset($exif["DateTimeOriginal"])) { $datetime = $exif["DateTimeOriginal"]; } else if (isset($exif["DateTimeDigitized"])) { $datetime = $exif["DateTimeDigitized"]; } else if (isset($exif["DateTime"])) { $datetime = $exif["DateTime"]; } if (!isset($datetime)) { $datetime = date ("Y-m-d H:i:s", filemtime($image)); } list($date, $time) = explode(' ', $datetime); $date = str_replace(':', '-', $date); $exifdata["date"] = $date; $exifdata["time"] = $time; if (isset($exif["Make"])) { $exifdata["camera_make"] = ucwords(strtolower($exif["Make"])); } if (isset($exif["Model"])) { $exifdata["camera_model"] = ucwords(strtolower($exif["Model"])); } if (isset($exif["Flash"])) { /* bug#671023 from mail2061 deys org "The code in exif.inc.php that tests $exif["Flash"] in order to determine whether or not the flash was fired is getting wrong values. My FujiFilm S602 is returning '9' for 'Fired' and '16' for 'Not Fired(compulsory)'. I reworked the boolean test into a switch statement that handles this. However, I suspect that this field can have additional values besides the two I've identified." */ //$exifdata["flash_used"] = $exif["Flash"] ? "Yes" : "No"; // Revamped to handled more expressive flash indications $fYN="N"; switch ($exif["Flash"]) { // Flash Not Fired case 16: case 0: $fYN="N"; break; // Flash Fired case 9: default: $fYN="Y"; break; } $exifdata["flash_used"] = $fYN; } if (isset($exif["FocalLength"])) { list($a, $b) = explode('/', $exif["FocalLength"]); if ($b>0) { $exifdata["focal_length"] = sprintf("%.1fmm", $a / $b); } } $exifdata["exposure"]=""; if (isset($exif["ExposureTime"])) { list($a, $b) = explode('/', $exif["ExposureTime"]); if ($b>0) { $val = $a / $b; $exifdata["exposure"] = sprintf("%.3f s", $val); if ($val <= 0.5) { $exifdata["exposure"] .= sprintf(" (1/%d)", (int)(0.5 + 1 / $val)); } } } if (isset($exif["ExposureProgram"])) { $ep = $exif["ExposureProgram"]; switch ($ep) { case 2: $exifdata["exposure"] .= " [program (auto)]"; break; case 3: $exifdata["exposure"] .= " [aperture priority (semi-auto)]"; break; case 4: $exifdata["exposure"] .= " [shutter priority (semi-auto)]"; break; } } if (isset($exif["FNumber"])) { list($a, $b) = explode('/', $exif["FNumber"]); if ($b>0) { $exifdata["aperture"] = sprintf("f/%.1f", $a / $b); } } else if (isset($exif["ApertureValue"])) { list($a, $b) = explode('/', $exif["ApertureValue"]); if ($b>0) { $exifdata["aperture"] = sprintf("f/%.1f", pow(2,($a / $b)/2)); } } else if (isset($exif["MaxApertureValue"])) { list($a, $b) = explode('/', $exif["MaxApertureValue"]); if ($b>0) { $exifdata["aperture"] = sprintf("f/%.1f", pow(2,($a / $b)/2)); } } if (isset($exif["FocusDistance"])) { $exifdata["focus_dist"] = $exif["FocusDistance"]; } if (isset($exif["MeteringMode"])) { $mm = $exif["MeteringMode"]; switch ($mm) { case 2: $exifdata["metering_mode"] = "center weight"; break; case 3: $exifdata["metering_mode"] = "spot"; break; case 5: $exifdata["metering_mode"] = "matrix"; break; } } if (isset($exif["ISOSpeedRatings"])) { $a = $exif["ISOSpeedRatings"]; if ($a < 50) { $a *= 200; } $exifdata["iso_equiv"] = $a; } /* something is not quite right here if ($exif["FocalPlaneXResolution"] && $exif["FocalPlaneResolutionUnit"]) { $width = $exif["ExifImageWidth"]; list($a, $b) = explode('/', $exif["FocalPlaneXResolution"]); $fpxr = $a / $b; $fpru = $exif["FocalPlaneResolutionUnit"]; $exifdata["ccd_width"] = sprintf("%.2fmm", $width * $fpru / $fpxr); } */ if (isset($exif["CompressedBitsPerPixel"])) { list($a, $b) = explode('/', $exif["CompressedBitsPerPixel"]); if ($b>0) { $val = round($a / $b); switch ($val) { case 1: $exifdata["compression"] = "jpeg quality: basic"; break; case 2: $exifdata["compression"] = "jpeg quality: normal"; break; case 4: $exifdata["compression"] = "jpeg quality: fine"; break; } } } if (isset($exif["Comment"])) { $exifdata["comment"] = $exif["Comment"]; } if (isset($exif["GPSLatitudeRef"]) && isset($exif["GPSLatitude"]) && isset($exif["GPSLongitudeRef"]) && isset($exif["GPSLongitude"])) { $latarray=$exif["GPSLatitude"]; // This is an array that looks like this // array(3) { // [0]=>string(5) "150/1" (degrees) // [1]=>string(4) "47/1" (minutes) // [2]=>string(8) "1239/100" (seconds) $latdegarray=explode("/", $latarray[0]); $latminarray=explode("/", $latarray[1]); $latsecarray=explode("/", $latarray[2]); $latdeg=$latdegarray[0] / $latdegarray[1]; $latmin=$latminarray[0] / $latminarray[1]; $latsec=$latsecarray[0] / $latsecarray[1]; $lat=$latdeg + ($latmin / 60) + ($latsec / 3600); if ($exif["GPSLatitudeRef"] == "S") { $lat = $lat * -1; } $exifdata["lat"]=$lat; $lonarray=$exif["GPSLongitude"]; $londegarray=explode("/", $lonarray[0]); $lonminarray=explode("/", $lonarray[1]); $lonsecarray=explode("/", $lonarray[2]); $londeg=$londegarray[0] / $londegarray[1]; $lonmin=$lonminarray[0] / $lonminarray[1]; $lonsec=$lonsecarray[0] / $lonsecarray[1]; $lon=$londeg + ($lonmin / 60) + ($lonsec / 3600); if ($exif["GPSLongitudeRef"] == "W") { $lon = $lon * -1; } $exifdata["lon"]=$lon; /* // No alt in db yet if (isset($exif["GPSAltitude"])) { $altarray=explode("/", $exif["GPSAltitude"]); $alt=$altarray[0] / $altarray[1]; $exifdata["alt"]=$alt; } */ } return $exifdata; } ?> zoph-v0.9.19/php/footer.inc.php000066400000000000000000000014371415176210700163370ustar00rootroot00000000000000 zoph-v0.9.19/php/getxmldata.php000066400000000000000000000030371415176210700164210ustar00rootroot00000000000000getDetailsXML(); } else { if ($object=="location" || $object=="home" || $object=="work") { $object="place"; } else if ($object=="father" || $object=="mother" || $object=="spouse") { $object="person"; } else if ($object=="timezone") { $object="TimeZone"; } else if ($object=="import_thumbs") { $object="import\web"; $search="thumbs"; } echo $object::getXML($search)->SaveXML(); } ?> zoph-v0.9.19/php/group.php000066400000000000000000000026321415176210700154230ustar00rootroot00000000000000isAdmin()) { redirect("zoph.php"); } $controller = new groupController(request::create()); $view = $controller->getView(); $headers = $view->getHeaders(); if (is_array($headers)) { foreach ($headers as $header) { header($header); } echo $view->view(); } else { $title = $view->getTitle(); require_once("header.inc.php"); echo $view->view(); require_once("footer.inc.php"); } zoph-v0.9.19/php/groups.php000066400000000000000000000034211415176210700156030ustar00rootroot00000000000000isAdmin()) { redirect("zoph.php"); } $title = translate("Groups"); require_once "header.inc.php"; ?>

"; foreach ($groups as $group) { ?>
getName() ?>
get("description") . "
"; echo implode(" ", $group->getMemberLinks()); ?>

"; } ?>
zoph-v0.9.19/php/header.inc.php000066400000000000000000000115231415176210700162660ustar00rootroot00000000000000 template::getImage("icons/photo.png"), "taken" => template::getImage("icons/date.png"), "modified" => template::getImage("icons/modified.png"), "rated" => template::getImage("icons/rating.png"), "children" => template::getImage("icons/folder.png"), "geo-photo" => template::getImage("icons/geo-photo.png"), "geo-place" => template::getImage("icons/geo-place.png"), "pause" => template::getImage("icons/pause.png"), "play" => template::getImage("icons/play.png"), "resize" => template::getImage("icons/resize.png"), "unpack" => template::getImage("icons/unpack.png"), "remove" => template::getImage("icons/remove.png"), "down2" => template::getImage("down2.gif"), "pleasewait"=> template::getImage("pleasewait.gif") ); $javascript=array(); $scripts=array( "js/util.js", "js/xml.js", "js/thumbview.js", "js/translate.js.php", "js/error.js" ); switch(basename($_SERVER["SCRIPT_NAME"])) { case "import.php": $scripts[]="js/import.js"; $scripts[]="js/formhelper.js"; $scripts[]="js/rating.js"; if(conf::get("import.upload")) { $scripts[]="js/upload.js"; } break; case "config.php": $scripts[]="js/conf.js"; break; case "photo.php": $scripts[]="js/photoPeople.js"; $scripts[]="js/rating.js"; case "place.php": $scripts[]="js/json.js"; $scripts[]="js/locationLookup.js"; break; case "slideshow.php": $scripts[]="js/json.js"; $scripts[]="js/slideshow.js"; break; } if (conf::get("interface.autocomplete")) { $scripts[]="js/autocomplete.js"; } if (conf::get("maps.provider")) { $scripts[]="js/leaflet-src.js"; $scripts[]="js/maps.js"; if (conf::get("maps.provider") == "mapbox") { $javascript[]="var mapbox_api_key = '" . conf::get("maps.mapbox.apikey") . "';"; } if (conf::get("maps.geocode")) { $scripts[]="js/geocode.js"; } } $html_title=conf::get("interface.title"); if (isset($title)) { $html_title.=" - " . $title; } ?> $icons, "scripts" => $scripts, "javascript" => $javascript, "extrastyle" => isset($extrastyle) ? $extrastyle : null, "title" => $html_title ); if (isset($prev_url)) { $hdrParams["next"] = $prev_url; } if (isset($next_url)) { $hdrParams["next"] = $next_url; } $tpl=new block("header", $hdrParams); echo $tpl; ?> "zoph.php", translate("albums", 0) => "albums.php", translate("categories", 0) => "categories.php" ); if ($user->canBrowsePeople()) { $tabs[translate("people", 0)] = "people.php"; } if ($user->canBrowsePlaces()) { $tabs[translate("places", 0)] = "places.php"; } $tabs[translate("photos", 0)] = "photos.php"; if ($user->get("lightbox_id")) { $tabs[translate("lightbox", 0)] = "photos.php?album_id=" . $user->get("lightbox_id"); } $tabs[translate("search",0)] = "search.php"; if (conf::get("import.enable") && ($user->isAdmin() || $user->get("import"))) { $tabs[translate("import", 0)] = "import.php"; } if ($user->isAdmin()) { $tabs[translate("admin", 0)] = "admin.php"; } $tabs += array( translate("reports", 0) => "reports.php", translate("prefs", 0) => "prefs.php", translate("about", 0) => "info.php" ); if ($user->get("user_id") == conf::get("interface.user.default")) { $tabs[translate("logon", 0)] = "zoph.php?_action=logout"; } else { $tabs[translate("logout", 0)] = "zoph.php?_action=logout"; } if (strpos($_SERVER["PHP_SELF"], "/") === false) { $self = $_SERVER["PHP_SELF"]; } else { $self = substr(strrchr($_SERVER['PHP_SELF'], "/"), 1); } $tpl=new block("menu", array( "tabs" => $tabs, "self" => $self )); echo $tpl; require_once "breadcrumbs.inc.php"; ?> zoph-v0.9.19/php/image.php000066400000000000000000000116201415176210700153460ustar00rootroot00000000000000isAdmin() || $user->get("import"))) { $md5 = getvar("file"); $file = file::getFromMD5(conf::get("path.images") . "/" . conf::get("path.upload"), $md5); $photo = new photo(); $photo->set("name", basename($file)); $photo->set("path", conf::get("path.upload")); if ($type=="import_thumb") { $type="thumb"; } else if ($type=="import_mid") { $type="mid"; } $found=true; } else if (conf::get("share.enable") && !empty($hash)) { try { $photo=photo::getFromHash($hash, "full"); $photo->lookup(); $found = true; } catch(photoNotFoundException $e) { try { $photo=photo::getFromHash($hash, "mid"); $photo->lookup(); $type="mid"; $found = true; } catch(photoNotFoundException $e) { header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found", true, 404); $tpl=new template("error", array( "lang" => language::getCurrentISO(), "title" => "Not Found", "message" => $e->getMessage() )); $tpl->addActionLinks(array( "return" => "zoph.php" )); echo $tpl; exit; } } } else if ($type==MID_PREFIX || $type==THUMB_PREFIX || empty($type)) { $photo = new photo($photo_id); $found = $photo->lookup(); } else if ($type=="background") { if (conf::get("interface.logon.background.album")) { $album=new album(conf::get("interface.logon.background.album")); $photos=$album->getPhotos(); $photo=$photos[array_rand($photos)]; $photo->lookup(); redirect("image.php?hash=" . $photo->getHash("full")); } else { $templates=array( conf::get("interface.template"), "default" ); foreach ($templates as $template) { $bgs=glob(settings::$php_loc . "/templates/" . $template . "/images/backgrounds/*.{jpg,JPG}", GLOB_BRACE); if (sizeof($bgs) > 0) { $image=$bgs[array_rand($bgs)]; redirect("templates/" . $template . "/images/backgrounds/" . basename($image)); } } } exit; } else { die("Illegal type"); } if ($found) { $watermark_file=""; if (!$user->isAdmin() && conf::get("watermark.enable")) { $permissions = $user->getPhotoPermissions($photo); $watermark = $permissions->get("watermark_level"); $photolevel=$photo->get("level"); if ($photolevel > $watermark) { $photo=new watermarkedPhoto($photo_id); $photo->lookup(); } } try { list($headers, $image)=$photo->display($type); } catch(photoNotFoundException $e) { header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found", true, 404); $tpl=new template("error", array( "lang" => language::getCurrentISO(), "title" => "Not Found", "message" => $e->getMessage() )); $tpl->addActionLinks(array( "return" => "zoph.php" )); echo $tpl; exit; } foreach ($headers as $label=>$value) { if ($label=="http_status") { // http status codes do not have a label header($value); } else { header($label . ": " . $value); } } if (!is_null($image)) { echo $image; } exit; } header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found", true, 404); require_once "header.inc.php"; ?>

zoph-v0.9.19/php/import.php000066400000000000000000000052731415176210700156050ustar00rootroot00000000000000isAdmin() && !$user->get("import"))) { redirect("zoph.php"); } // Detect upload larger than upload_max_filesize. //if (isset($_GET["upload"]) && $_GET["upload"]==1 && $_POST==null) { // echo import\web::handleUploadErrors(UPLOAD_ERR_INI_SIZE); // die(); //} $_action=getvar("_action"); $title = translate("Import"); if (empty($_action)) { require_once "header.inc.php"; } session_write_close(); if (empty($_action)) { $javascript= "translate=new Array();\n" . "translate['retry']='" .trim(translate("retry", false)) . "';\n" . "translate['delete']='" .trim(translate("delete", false)) . "';\n" . "translate['import']='" .trim(translate("import", false)) . "';\n" . "parallel=" . (int) conf::get("import.parallel") . ";\n"; if (conf::get("import.upload")) { $upload=new block("uploadform", array( "action" => "import.php?_action=upload", )); } else { $upload=translate("Uploading photos has been disabled in configuration."); } $tpl=new template("import", array( "upload" => $upload, "javascript" => $javascript, )); echo $tpl; include "footer.inc.php"; } else if ($_action=="upload") { if (conf::get("import.upload") && $_FILES["file"]) { $file=$_FILES["file"]; import\web::processUpload($file); } } else if ($_action=="process") { $file=getvar("file"); import\web::processFile($file); } else if ($_action=="retry") { $file=getvar("file"); import\web::retryFile($file); } else if ($_action=="delete") { $file=getvar("file"); import\web::deleteFile($file); } else if ($_action=="import") { $files=import\web::getFileList($request_vars["_import_image"]); import\web::photos($files, $request_vars); } zoph-v0.9.19/php/include.inc.php000066400000000000000000000023161415176210700164610ustar00rootroot00000000000000 zoph-v0.9.19/php/index.html000066400000000000000000000016521415176210700155540ustar00rootroot00000000000000 zoph redirect Redirecting you to zoph.php. zoph-v0.9.19/php/info.php000066400000000000000000000022011415176210700152120ustar00rootroot00000000000000 report::getInfoArray(), "title" => $title, "mailaddr" => template::getImage("mailaddr.png"), "credits" => zoph::getCredits() )); echo $tpl; ?> zoph-v0.9.19/php/interfaces/000077500000000000000000000000001415176210700156765ustar00rootroot00000000000000zoph-v0.9.19/php/interfaces/Organizer.inc.php000066400000000000000000000027231415176210700211230ustar00rootroot000000000000000) { id = id.substring(0,underscore); } else { // prevent duplicate id el.id=id + "_id"; } var text=el; text.id=id; text.addEventListener("mousedown", show); text.addEventListener("keyup", change); text.addEventListener("focus", focus); text.addEventListener("blur", unfocus); text.addEventListener("mouseup", change); text.addEventListener("keydown", handleKeys); text.setAttribute("autocomplete", "off"); text.className=text.className.replace("autocomplete", "autocompinput"); text.style.width="200px"; var dropdown=document.createElement("ul"); dropdown.className="autocompdropdown"; dropdown.id=id + "dropdown"; dropdown.style.position="absolute"; dropdown.style.display="none"; el.parentNode.insertBefore(dropdown,el.nextSibling); if(el.parentNode.className.indexOf("multiple")>=0 && el.id.indexOf("[")) { // This is a field that can appear multiple times, so we add a // 'remove' link var imgRemove=document.createElement("img"); imgRemove.setAttribute("onClick", "autocomplete.remove(this); return false"); imgRemove.setAttribute("src", icons["remove"]); imgRemove.className="actionlink"; el.parentNode.insertBefore(imgRemove,el.nextSibling.nextSibling); } } function remove(obj) { obj.parentNode.removeChild(obj.previousSibling); // remove dropdown obj.parentNode.removeChild(obj.previousSibling); // remove input obj.parentNode.removeChild(obj.previousSibling); // remove hidden input obj.parentNode.removeChild(obj); // remove icon } function httpResponse(object, xml) { var dropdown=document.getElementById(object + "dropdown"); var text=document.getElementById(object); var root=[]; removeChildren(dropdown); text.style.backgroundImage=icons["down2"]; var xmlobj=object.split("_"); if(xmlobj[1]=="parent") { xmlobj.shift(); } var node=xmlobj[1]; root=xml.getElementsByTagName(XML.rootnode[node]); // These will be rebuilt during the XML processing // dataarray=[]; keyarray=[]; selectedvalue[dropdown.id]=0; build_tree(root[0], dropdown, XML.rootnode[node], XML.node[node]); } function setpos() { var input=getElementsByClass("autocompinput"); var dropdown=getElementsByClass("autocompdropdown"); for (var i=0; i 0 && children[i].childNodes[0].nodeName!=branchname) { li=document.createElement("li"); span=document.createElement("span"); key=children[i].childNodes[0].firstChild.nodeValue; keyarray.push(parseInt(key,10)); name=children[i].childNodes[1].firstChild.nodeValue; dataarray.push(name); li.appendChild(span); li.onclick=clickli; if (nodename=="place") { li.className="location"; } else { li.className=nodename; } span.appendChild(document.createTextNode(key)); span.style.display="none"; if (name==" ") { // You cannot use   in a textnode // However, to minimize cross site scripting attacks // I don't want to use innerHTML for all elements li.innerHTML=(" "); } else { li.appendChild(document.createTextNode(name)); } parent.appendChild(li); } parent=build_tree(children[i], parent, branchname, nodename); } if (i==selectedvalue[dropdown.id] && i!==0) { li.id="selected"; } } return parent; } function change() { update(this.id); } function update(objid) { var obj=document.getElementById(objid); var dropdown = document.getElementById(obj.id + "dropdown"); if(dropdown.style.display!="none") { // if the dropdown is invisible, don't bother updating it. var value=obj.value; if(oldtext!=value) { XML.getData(obj.id, value); oldtext=value; } var selectedli=document.getElementById("selected"); if (selectedli) { selectedli.scrollIntoView(true); } } } function focus() { var dropdown=document.getElementById(this.id + "dropdown"); if (dropdown.style.display=="none") { showdropdown(this); } } function unfocus() { // Whenever a selection from a list is made, the textbox will // also lose focus, this delay is made to give the browser // time to process the click, before the dropdown is destroyed. var obj=this; setTimeout(function() { autocomplete.hidedropdown(obj); } , 200); } function clickli() { var li=this; var key=li.firstChild.innerHTML; var newvalue=li.lastChild.nodeValue; oldvalue[open.previousSibling.id]=null; selectli(open.id, key, newvalue); open=false; } function selectli(dropdownid, key, newvalue) { var dropdown = document.getElementById(dropdownid); var field = dropdown.previousSibling; var orig_field = field.previousSibling; field.value = newvalue; orig_field.value = key; hidedropdown(field); if(field.parentNode.className.indexOf("multiple")>=0 && field.id.indexOf("[")) { // if a dropdown field is inside a fieldset with class 'multiple' // we will automatically generate a new dropdown for this field. createNewInput(field); } } function createNewInput(after) { var input=after.cloneNode(true); var hidden=after.previousSibling.cloneNode(true); input.id=increaseValueInBrackets(after.id); if(!document.getElementById(input.id)) { input.name=increaseValueInBrackets(after.name); hidden.id=increaseValueInBrackets(after.previousSibling.id); hidden.name=increaseValueInBrackets(after.previousSibling.name); input.value=""; hidden.value=""; after.parentNode.insertBefore(input,after.nextSibling.nextSibling.nextSibling); after.parentNode.insertBefore(hidden,input); inputToAutocomplete(input); } } function handleKeys(event) { var obj=this; var dropdown = document.getElementById(obj.id + "dropdown"); var keycode=event.code; var maxlength=0; var key; var match=[]; var nowselected; if(keycode=="Tab") { event.preventDefault(); var constraint=obj.value; match = dataarray.filter(data => data.substring(0,constraint.length).toUpperCase() == constraint.toUpperCase()); maxlength = match.reduce((max, current) => Math.max(current.length, max), 0); if(match.length>1) { for (var m=0; m<=maxlength; m++) { if (!match.every((matchval) => matchval.substring(0,m).toUpperCase() == match[0].substring(0,m).toUpperCase())) { break; } } obj.value=trim(match[0].substring(0,m - 1)); } else if (match.length==1) { obj.value=trim(match[0]); } } else if (keycode=="ArrowUp") { event.preventDefault(); selectedvalue[dropdown.id]--; // First deselect the currently selected nowselected=document.getElementById("selected"); if(nowselected) { nowselected.id=""; } var flattree=flattentree(dropdown, "LI"); if (selectedvalue[dropdown.id] < 0) { selectedvalue[dropdown.id]=0; } flattree[selectedvalue[dropdown.id]].id="selected"; } else if (keycode=="ArrowDown") { event.preventDefault(); selectedvalue[dropdown.id]++; nowselected=document.getElementById("selected"); if(nowselected) { nowselected.id=""; } flattree=flattentree(dropdown, "LI"); if (selectedvalue[dropdown.id] > (flattree.length - 1)) { selectedvalue[dropdown.id] =flattree.length - 1; } flattree[selectedvalue[dropdown.id]].id="selected"; } else if (keycode=="Enter") { event.preventDefault(); var nextTab; flattree=flattentree(dropdown, "LI"); if(flattree.length==2) { // If there's only one element (+ empty entry) in the list // we suppose one will select that on pressing enter flattree[1].id="selected"; } nowselected=document.getElementById("selected"); if(nowselected) { oldvalue[open.previousSibling.id]=null; key=parseInt(nowselected.firstChild.innerHTML, 10); var newvalue=nowselected.lastChild.nodeValue; selectli(dropdown.id, key, newvalue); } var inputfields=document.getElementsByTagName("input"); for (var f=0; f0) { flattree=flattentree(node, element, flattree); } } return flattree; } return { setpos:setpos, init:init, hidedropdown:hidedropdown, httpResponse:httpResponse, remove:remove }; }(); if(window.addEventListener) { window.addEventListener("load",autocomplete.init,false); window.addEventListener("resize",autocomplete.setpos, false); } zoph-v0.9.19/php/js/conf.js000066400000000000000000000017661415176210700154640ustar00rootroot00000000000000// This file is part of Zoph. // // Zoph is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // Zoph 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 Zoph; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA var zConf=function() { function genSalt(id) { var input=document.getElementById(id); input.value=""; for(var i=0; i<40; i++) { input.value+=Math.floor(Math.random() * 16).toString(16) } } return { genSalt:genSalt }; }(); zoph-v0.9.19/php/js/error.js000066400000000000000000000037041415176210700156620ustar00rootroot00000000000000 // This file is part of Zoph. // // Zoph is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // Zoph 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 Zoph; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA var zError=function() { var errors = []; function push(error) { zError.errors.push(error); zError.update(); } function update() { var ulError = document.getElementById("errors"); if(!ulError) { ulError = document.createElement("ul"); ulError.id = "errors"; document.body.insertBefore(ulError, document.body.firstChild); } while(error=zError.errors.pop()) { var liError = document.createElement("li"); liError.id = "error_" + (((1+Math.random())*0x10000)|0).toString(16); setTimeout(zError.animateError.bind(null, liError.id), 5000); setTimeout(zError.deleteError.bind(null, liError.id), 7000); liError.innerHTML = error; ulError.insertBefore(liError, ulError.firstChild); } } function animateError(id) { var li=document.getElementById(id); li.className="deleted"; } function deleteError(id) { var li=document.getElementById(id); li.parentElement.removeChild(li); } return { push:push, update:update, errors:errors, animateError:animateError, deleteError:deleteError }; }(); zoph-v0.9.19/php/js/formhelper.js000066400000000000000000000063621415176210700166770ustar00rootroot00000000000000// This file is part of Zoph. // // Zoph is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // Zoph 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 Zoph; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // Formhelper contains functions used in forms, currently, it contains functions // to automatically create an extra field whenever a certain field is changed. // The current autocomplete code contains a similar function, but only for // autocomplete fields. Eventually that code must be integrated with this code. var formhelper=function() { function init() { var multi = document.getElementsByClassName("formhelper-multiple"); for (var i = 0; i < multi.length; i++) { for (var c = 0; c < multi[i].childNodes.length; c++) { addOnChange(multi[i].childNodes[c]); } } } function addOnChange(el) { if(el.tagName=="FIELDSET") { for (var c = 0; c < el.childNodes.length; c++) { el.childNodes[c].addEventListener("change", formhelper.addParentField); el.childNodes[c].addEventListener("keyup", formhelper.addParentField); } } else { el.addEventListener("change", formhelper.addCurrentField); el.addEventListener("keyup", formhelper.addCurrentField); } } function addParentField() { addField(this.parentNode); } function addCurrentField() { addField(this); } function addField(el) { var last = el.parentNode.lastElementChild; var fieldset=false; if(last.tagName=="FIELDSET") { last = last.lastElementChild; fieldset=true; } if(last.value!="") { var remove = document.createElement("img"); remove.addEventListener("click", removeField); remove.setAttribute("src", "templates/default/images/icons/remove.png"); remove.className="actionlink icon"; el.parentNode.insertBefore(remove, null); var newfield=el.parentNode.firstElementChild.cloneNode(true); addOnChange(newfield); if(fieldset) { for (var c = 0; c < newfield.childNodes.length; c++) { newfield.childNodes[c].value=""; } } newfield.value=""; el.parentNode.appendChild(newfield); } } function removeField() { this.parentNode.removeChild(this.previousElementSibling); this.parentNode.removeChild(this); } return { init:init, removeField:removeField, addParentField:addParentField, addCurrentField:addCurrentField }; }(); window.addEventListener("load", formhelper.init, false); zoph-v0.9.19/php/js/geocode.js000066400000000000000000000222041415176210700161320ustar00rootroot00000000000000// This file is part of Zoph. // // Zoph is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // Zoph 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 Zoph; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA var zGeocode=function() { var geourl="https://secure.geonames.org/search?style=SHORT&username=zoph&q="; var wikiurl="https://secure.geonames.org/wikipediaSearch?username=zoph&q="; var url; var geotag="geoname"; var wikitag="entry" var xmltag; function checkGeocode() { // To prevent overwrite of tediously set lat & lon // you need to click the 'find' button twice, if a lat&lon have // already been set. var lat=document.getElementById("lat").value; var lon=document.getElementById("lon").value; if (lat==0 && lon==0) { enableGeocode(); } else { disableGeocode(); } } function enableGeocode() { var button=document.getElementById("geocode"); button.className=button.className.replace("geo_disabled", "geocode"); button.onclick=zGeocode.startGeocode; } function disableGeocode() { var button=document.getElementById("geocode"); button.className=button.className.replace("geocode", "geo_disabled"); button.onclick=zGeocode.enableGeocode; } function startGeocode() { url=geourl; xmltag=geotag; var objQuery={ title: document.getElementById("title").value, address: document.getElementById("address").value, address2: document.getElementById("address2").value, city: document.getElementById("city").value, state: document.getElementById("state").value, zip: document.getElementById("zip").value, country: document.getElementById("country").value }; // remove empty items for (var i in objQuery) { if (trim(objQuery[i])==="") { delete objQuery[i]; } } geocode(objQuery); } function geocode(objQuery) { var divResult=document.getElementById("geocoderesults"); var query=""; for (var i in objQuery) { if (trim(query)!=="") { query += ", "; } query+=objQuery[i]; } divResult.innerHTML="searching for...
" + query; var http=new XMLHttpRequest(); http.open("GET", url + encodeURI(query), true); http.onreadystatechange=function() { zGeocode.handleGeocode(http, objQuery); }; http.send(null); } function handleGeocode(http, objQuery) { var divResult=document.getElementById("geocoderesults"); var b; if (http.readyState == 4) { if (http.status == 200) { var response=http.responseXML; var geonames=response.getElementsByTagName(xmltag); if (geonames.length > 0) { displayGeocode(geonames, divResult, 0); } else { // No results, let's try again with some less fields if (objQuery.zip) { delete (objQuery.zip); } else if (objQuery.address2) { delete objQuery.address2; } else if (objQuery.title) { delete objQuery.title; } else if (objQuery.address) { delete objQuery.address; } else if (objQuery.state) { delete objQuery.state; } else if (objQuery.country) { delete objQuery.country; } if ((Object.keys(objQuery).length == 0) && (url != wikiurl)) { objQuery={ title: document.getElementById("title").value } url=wikiurl; xmltag=wikitag; } if (Object.keys(objQuery).length > 0) { geocode(objQuery); } else { divResult.innerHTML=""; b=document.createElement("b"); b.innerHTML=translate['Nothing found']; divResult.appendChild(b); return; } } } else if (http.status == 0) { divResult.innerHTML=""; b=document.createElement("b"); b.innerHTML=translate['An error occurred']; divResult.appendChild(b); } } } function displayGeocode(geonames, divResult, result) { var total=geonames.length; var titlefield=document.getElementById("title"); var title, lat, lon; // Define zoomlevels for different kinds of respones // see http://www.geonames.org/export/codes.html var zoomlevels= { "A": 6, // Country, state, region "H": 8, // Stream, lake "L": 15, // Parks, area "P": 12, // City, village "R": 17, // Road, railroad "S": 18, // Spot, building, farm "T": 12, // Mountain, hill, rock "U": 5, // Undersea "V": 14 // Forest, heath }; //define zoomlevels for different "features" in Wikipedia // see http://www.geonames.org/wikipedia/wikipedia_features.html var features={ "city": 12, "railwaystation": 18, "edu": 17, "waterbody": 8, "landmark": 18, "adm2nd": 13, "mountain": 12, "adm3rd": 10, "airport": 16, "river": 8, "isle": 14, "event": 17, "adm1st": 15, "glacier": 16, "country": 6, "forest": 14, "pass": 17, "church": 18 }; var zoomlevel=12; for (var tag of geonames[result].childNodes) { var content=tag.textContent; switch(tag.nodeName) { case "title": title=content; break; case "toponymName": title=content; break; case "lat": lat=content; break; case "lng": case "lon": lon=content; break; case "fcl": zoomlevel=zoomlevels[content]; break; case "feature": zoomlevel=features[content]; break; } } if (lat && lon) { document.getElementById("lat").value=lat; document.getElementById("lon").value=lon; document.getElementById("mapzoom").value=zoomlevel; zMaps.updateMap(); } var left=document.createElement("input"); left.setAttribute("type", "button"); left.className="leftright"; left.setAttribute("value","<"); var right=document.createElement("input"); right.setAttribute("type", "button"); right.setAttribute("value",">"); right.className="leftright"; if (result===0) { left.disabled=true; } else if ((result + 1) == total) { right.disabled=true; } right.onclick=function() { displayGeocode(geonames, divResult, result + 1); }; left.onclick=function() { displayGeocode(geonames, divResult, result - 1); }; // This is a little bit of a hidden feature, click the title of the // found place to set this place's title. var b=document.createElement("b"); b.onclick=function() { titlefield.value=title; }; b.innerHTML=title; var text=document.createTextNode((result + 1) + " / " + total); divResult.innerHTML=""; divResult.appendChild(b); divResult.appendChild(document.createElement("br")); divResult.appendChild(text); divResult.appendChild(document.createElement("br")); divResult.appendChild(left); divResult.appendChild(right); disableGeocode(); } return { checkGeocode:checkGeocode, enableGeocode:enableGeocode, startGeocode:startGeocode, handleGeocode:handleGeocode }; }(); zoph-v0.9.19/php/js/import.js000066400000000000000000000513371415176210700160500ustar00rootroot00000000000000// This file is part of Zoph. // // Zoph is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // Zoph 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 Zoph; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA var zImport=function() { var categories = new Array(); function startUpload(form, id, num) { form.style.display="none"; updateProgressbar(id + "_" + num); var div=document.getElementById("prog_" + id + "_" + num); div.style.display="block"; num=parseInt(num, 10) + 1; createUploadIframe(frameElement, id, num); } function createUploadIframe(frame, id, num) { var iframe=document.createElement("iframe"); iframe.src="import.php?_action=browse&num=" + num + "&upload_id=" + id; iframe.className="upload"; iframe.id="upload_" + num; iframe.setAttribute("frameBorder", 0); iframe.setAttribute("allowTransparency", 1); frame.parentNode.insertBefore(iframe, frame); } function deleteIframe(frame_id) { var frame=top.document.getElementById(frame_id); frameparent=frame.parentNode; setTimeout('frameparent.removeChild(frame)', 10000); } function updateProgressbar(id) { setTimeout("zImport.updateProgressbar('" + id + "')", 1000); progress=XML.getData("import_progress", id); } function httpResponse(object, xml) { var root=xml.getElementsByTagName('importprogress'); var importnode=root[0].firstChild; var id=importnode.firstChild.firstChild.nodeValue; var currentnode=importnode.childNodes[1]; var current, total, filename, percent; if(currentnode.childNodes.length===0) { current=0; total=0; filename="unknown"; } else { current=currentnode.firstChild.nodeValue; total=importnode.childNodes[2].firstChild.nodeValue; filename=importnode.childNodes[3].firstChild.nodeValue; } var fn=document.getElementById("fn_" + id); fn.innerHTML=filename; var MB = parseInt(parseInt(total, 10) / 1024 / 102.4, 10) / 10; var size=document.getElementById("sz_" + id); size.innerHTML=MB.toString() + " MiB"; var progressdiv=document.getElementById("pb_" + id + "_inner"); if(total > 0) { percent = parseInt( parseInt(current, 10) / parseInt(total,10) * 100,10); } else { percent=0; } progressdiv.innerHTML=percent + "%"; progressdiv.style.width=percent.toString() + "%"; } function httpResponseCategories(object, xml) { if (xml.firstChild.tagName == "categories") { makeCategorylist(xml.firstChild); } else { console.log("Error with XML"); } } function makeCategorylist(nodes) { var c = nodes.childNodes; var key; var name; for (var i=0; i < c.length; i++) { if ((c[i].nodeName=="categories") || (c[i].nodeName=="category")) { makeCategorylist(c[i]); } else if (c[i].nodeName=="key") { key=c[i].textContent; } else if (c[i].nodeName=="title") { name=c[i].textContent; } if (typeof name != "undefined" && typeof key != "undefined") { categories.push({ name: name, key: parseInt(key,10)}); } } } function getCategories() { XML.getData("categories", ""); zImport.getThumbs(false); } function getThumbs(notimer) { var http=new XMLHttpRequest(); http.open("GET", "getxmldata.php?object=import_thumbs", true); http.onreadystatechange=function() { zImport.showThumbs(http); }; http.send(null); if(!notimer) { setTimeout(function() { zImport.getThumbs(false); }, 15000); } } function showThumbs(http) { var content; var status; var submit; var imgsrc; var importli; var xmlimport; if (http.readyState == 4) { if(http.status == 200) { var response=http.responseXML; var thumbswindow=document.getElementById("import_thumbs"); var thumbs=document.getElementById("import_thumbnails"); var files=response.getElementsByTagName("file"); var ids=[]; if(files.length>0) { thumbswindow.style.display="block"; for(var i=0; i ctg.name.toLowerCase() == subjects[subject].toLowerCase() ); if (category.length == 0) { categoryli.className="unknown"; } else { var input=document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("id", "cat_" + md5 + "[" + cat + "]"); input.value = category[0].key; div.appendChild(input); cat++; } categoryul.appendChild(categoryli); } img.setAttribute("src", imgsrc); div.appendChild(actionlinks); div.appendChild(img); div.appendChild(filename); if (rating) { div.appendChild(zRating.rating(rating, "rating_" + md5)); } div.appendChild(categoryul); thumbs.appendChild(div); } } } else { thumbswindow.style.display="none"; } // Remove all thumbs for which the file no longer // exists for(var thumb of thumbs.childNodes) { if(thumb.className=="thumbnail") { if(findInArray(ids,thumb.id)==-1) { thumbs.removeChild(thumb); setTimeout(function() { zImport.getThumbs(true); }, 500); } } } // Sort the nodes by Filename var names=getElementsByClass("filename"); var oldfile=""; for(var f=0; f 0 && busy.length < parallel) { busy=waiting[0]; busy.className="busy"; var thumbs=top.document.getElementById("import_thumbs"); thumbs.style.display="block"; var md5=busy.parentNode.id; var filename=busy.nextSibling.innerHTML; switch(getFileType(filename)) { case "image": busy.src=icons["resize"]; break; case "archive": busy.src=icons["unpack"]; break; } doAction("process", md5); } else { } } function processDone(html) { if(html) { var output=top.document.getElementById("import_details_text"); var p=document.createElement("p"); var t=document.createElement("p"); t.innerHTML=html; output.appendChild(p); p.innerHTML=t.innerHTML; output.parentNode.style.display="block"; } } function doAction(action,md5) { var http=new XMLHttpRequest(); http.open("GET", "import.php?_action=" + action + "&file=" + md5, true); var thumb=document.getElementById(md5); if(action=="delete" || action=="retry") { deleteNode(thumb); } http.onreadystatechange=function() { XML.httpResponse(http,'action'); }; http.send(null); setTimeout(function() { zImport.getThumbs(true); }, 500); } function deleteSelected() { var images=getElementsByClass("thumb_checkbox"); var toDelete=[]; for(var i=0; i0) { XML.submitForm(form, "import.php?_action=import"); } else { alert("You need to select at least one photo"); } } function createPreviewDiv(md5) { var div=document.createElement("div"); var img=document.createElement("img"); var body=document.getElementsByTagName("body")[0]; div.className="preview"; div.id="preview"+md5; img.setAttribute("src", "image.php?type=import_mid" + "&file=" + md5); div.appendChild(img); body.appendChild(div); } function destroyPreviewDiv(md5) { var div=document.getElementById("preview" + md5); deleteNode(div); } return { getThumbs:getThumbs, getCategories:getCategories, showThumbs:showThumbs, startUpload:startUpload, deleteSelected:deleteSelected, selectAll:selectAll, toggleSelection:toggleSelection, updateProgressbar:updateProgressbar, deleteIframe:deleteIframe, doAction:doAction, httpResponse:httpResponse, httpResponseCategories:httpResponseCategories, processDone:processDone, importPhotos:importPhotos, createPreviewDiv:createPreviewDiv, destroyPreviewDiv:destroyPreviewDiv }; }(); if(window == top) { window.addEventListener("load",function(){ zImport.getCategories(); },false); } zoph-v0.9.19/php/js/json.js000066400000000000000000000037571415176210700155120ustar00rootroot00000000000000 // This file is part of Zoph. // // Zoph is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // Zoph 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 Zoph; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA var zJSON=function() { var retry; var resp; function getData(object, search, response) { var http=new XMLHttpRequest(); resp = response; if (object=='locationLookup') { var url="service/locationLookup.php?search=" + escape(search); } else if (object=='photoData') { var url="service/photoData.php?photoId=" + escape(search); } else if (object=='search') { var url="service/search.php" + search; } else if (object=='photoPeople') { var url="service/photoPeople.php?" + search; } else { return; } if (http) { http.open("GET", url, true); http.onreadystatechange=function() { httpResponse(http, object); }; http.send(null); } else { // try again in 500 ms clearTimeout(retry); retry=setTimeout("JSON.getData('" + object + "','" + search + "')", 500); } } function httpResponse(http, object) { if (http.readyState == 4) { if(http.status == 200) { resp.httpResponse(object, http.response); } } } return { getData:getData, httpResponse:httpResponse }; }(); zoph-v0.9.19/php/js/leaflet-src.js000066400000000000000000015121261415176210700167360ustar00rootroot00000000000000/* @preserve * Leaflet 1.6.0+Detached: bd88f73e8ddb90eb945a28bc1de9eb07f7386118.bd88f73, a JS library for interactive maps. http://leafletjs.com * (c) 2010-2019 Vladimir Agafonkin, (c) 2010-2011 CloudMade */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global.L = {}))); }(this, (function (exports) { 'use strict'; var version = "1.6.0"; /* * @namespace Util * * Various utility functions, used by Leaflet internally. */ // @function extend(dest: Object, src?: Object): Object // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut. function extend(dest) { var i, j, len, src; for (j = 1, len = arguments.length; j < len; j++) { src = arguments[j]; for (i in src) { dest[i] = src[i]; } } return dest; } // @function create(proto: Object, properties?: Object): Object // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create) var create = Object.create || (function () { function F() {} return function (proto) { F.prototype = proto; return new F(); }; })(); // @function bind(fn: Function, …): Function // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). // Has a `L.bind()` shortcut. function bind(fn, obj) { var slice = Array.prototype.slice; if (fn.bind) { return fn.bind.apply(fn, slice.call(arguments, 1)); } var args = slice.call(arguments, 2); return function () { return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); }; } // @property lastId: Number // Last unique ID used by [`stamp()`](#util-stamp) var lastId = 0; // @function stamp(obj: Object): Number // Returns the unique ID of an object, assigning it one if it doesn't have it. function stamp(obj) { /*eslint-disable */ obj._leaflet_id = obj._leaflet_id || ++lastId; return obj._leaflet_id; /* eslint-enable */ } // @function throttle(fn: Function, time: Number, context: Object): Function // Returns a function which executes function `fn` with the given scope `context` // (so that the `this` keyword refers to `context` inside `fn`'s code). The function // `fn` will be called no more than one time per given amount of `time`. The arguments // received by the bound function will be any arguments passed when binding the // function, followed by any arguments passed when invoking the bound function. // Has an `L.throttle` shortcut. function throttle(fn, time, context) { var lock, args, wrapperFn, later; later = function () { // reset lock and call if queued lock = false; if (args) { wrapperFn.apply(context, args); args = false; } }; wrapperFn = function () { if (lock) { // called too soon, queue to call later args = arguments; } else { // call and lock until later fn.apply(context, arguments); setTimeout(later, time); lock = true; } }; return wrapperFn; } // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number // Returns the number `num` modulo `range` in such a way so it lies within // `range[0]` and `range[1]`. The returned value will be always smaller than // `range[1]` unless `includeMax` is set to `true`. function wrapNum(x, range, includeMax) { var max = range[1], min = range[0], d = max - min; return x === max && includeMax ? x : ((x - min) % d + d) % d + min; } // @function falseFn(): Function // Returns a function which always returns `false`. function falseFn() { return false; } // @function formatNum(num: Number, digits?: Number): Number // Returns the number `num` rounded to `digits` decimals, or to 6 decimals by default. function formatNum(num, digits) { var pow = Math.pow(10, (digits === undefined ? 6 : digits)); return Math.round(num * pow) / pow; } // @function trim(str: String): String // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) function trim(str) { return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); } // @function splitWords(str: String): String[] // Trims and splits the string on whitespace and returns the array of parts. function splitWords(str) { return trim(str).split(/\s+/); } // @function setOptions(obj: Object, options: Object): Object // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut. function setOptions(obj, options) { if (!Object.prototype.hasOwnProperty.call(obj, 'options')) { obj.options = obj.options ? create(obj.options) : {}; } for (var i in options) { obj.options[i] = options[i]; } return obj.options; } // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}` // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will // be appended at the end. If `uppercase` is `true`, the parameter names will // be uppercased (e.g. `'?A=foo&B=bar'`) function getParamString(obj, existingUrl, uppercase) { var params = []; for (var i in obj) { params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i])); } return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); } var templateRe = /\{ *([\w_-]+) *\}/g; // @function template(str: String, data: Object): String // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'` // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string // `('Hello foo, bar')`. You can also specify functions instead of strings for // data values — they will be evaluated passing `data` as an argument. function template(str, data) { return str.replace(templateRe, function (str, key) { var value = data[key]; if (value === undefined) { throw new Error('No value provided for variable ' + str); } else if (typeof value === 'function') { value = value(data); } return value; }); } // @function isArray(obj): Boolean // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) var isArray = Array.isArray || function (obj) { return (Object.prototype.toString.call(obj) === '[object Array]'); }; // @function indexOf(array: Array, el: Object): Number // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) function indexOf(array, el) { for (var i = 0; i < array.length; i++) { if (array[i] === el) { return i; } } return -1; } // @property emptyImageUrl: String // Data URI string containing a base64-encoded empty GIF image. // Used as a hack to free memory from unused images on WebKit-powered // mobile devices (by setting image `src` to this string). var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; // inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/ function getPrefixed(name) { return window['webkit' + name] || window['moz' + name] || window['ms' + name]; } var lastTime = 0; // fallback for IE 7-8 function timeoutDefer(fn) { var time = +new Date(), timeToCall = Math.max(0, 16 - (time - lastTime)); lastTime = time + timeToCall; return window.setTimeout(fn, timeToCall); } var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer; var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number // Schedules `fn` to be executed when the browser repaints. `fn` is bound to // `context` if given. When `immediate` is set, `fn` is called immediately if // the browser doesn't have native support for // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame), // otherwise it's delayed. Returns a request ID that can be used to cancel the request. function requestAnimFrame(fn, context, immediate) { if (immediate && requestFn === timeoutDefer) { fn.call(context); } else { return requestFn.call(window, bind(fn, context)); } } // @function cancelAnimFrame(id: Number): undefined // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame). function cancelAnimFrame(id) { if (id) { cancelFn.call(window, id); } } var Util = ({ extend: extend, create: create, bind: bind, lastId: lastId, stamp: stamp, throttle: throttle, wrapNum: wrapNum, falseFn: falseFn, formatNum: formatNum, trim: trim, splitWords: splitWords, setOptions: setOptions, getParamString: getParamString, template: template, isArray: isArray, indexOf: indexOf, emptyImageUrl: emptyImageUrl, requestFn: requestFn, cancelFn: cancelFn, requestAnimFrame: requestAnimFrame, cancelAnimFrame: cancelAnimFrame }); // @class Class // @aka L.Class // @section // @uninheritable // Thanks to John Resig and Dean Edwards for inspiration! function Class() {} Class.extend = function (props) { // @function extend(props: Object): Function // [Extends the current class](#class-inheritance) given the properties to be included. // Returns a Javascript function that is a class constructor (to be called with `new`). var NewClass = function () { // call the constructor if (this.initialize) { this.initialize.apply(this, arguments); } // call all constructor hooks this.callInitHooks(); }; var parentProto = NewClass.__super__ = this.prototype; var proto = create(parentProto); proto.constructor = NewClass; NewClass.prototype = proto; // inherit parent's statics for (var i in this) { if (Object.prototype.hasOwnProperty.call(this, i) && i !== 'prototype' && i !== '__super__') { NewClass[i] = this[i]; } } // mix static properties into the class if (props.statics) { extend(NewClass, props.statics); delete props.statics; } // mix includes into the prototype if (props.includes) { checkDeprecatedMixinEvents(props.includes); extend.apply(null, [proto].concat(props.includes)); delete props.includes; } // merge options if (proto.options) { props.options = extend(create(proto.options), props.options); } // mix given properties into the prototype extend(proto, props); proto._initHooks = []; // add method for calling all hooks proto.callInitHooks = function () { if (this._initHooksCalled) { return; } if (parentProto.callInitHooks) { parentProto.callInitHooks.call(this); } this._initHooksCalled = true; for (var i = 0, len = proto._initHooks.length; i < len; i++) { proto._initHooks[i].call(this); } }; return NewClass; }; // @function include(properties: Object): this // [Includes a mixin](#class-includes) into the current class. Class.include = function (props) { extend(this.prototype, props); return this; }; // @function mergeOptions(options: Object): this // [Merges `options`](#class-options) into the defaults of the class. Class.mergeOptions = function (options) { extend(this.prototype.options, options); return this; }; // @function addInitHook(fn: Function): this // Adds a [constructor hook](#class-constructor-hooks) to the class. Class.addInitHook = function (fn) { // (Function) || (String, args...) var args = Array.prototype.slice.call(arguments, 1); var init = typeof fn === 'function' ? fn : function () { this[fn].apply(this, args); }; this.prototype._initHooks = this.prototype._initHooks || []; this.prototype._initHooks.push(init); return this; }; function checkDeprecatedMixinEvents(includes) { if (typeof L === 'undefined' || !L || !L.Mixin) { return; } includes = isArray(includes) ? includes : [includes]; for (var i = 0; i < includes.length; i++) { if (includes[i] === L.Mixin.Events) { console.warn('Deprecated include of L.Mixin.Events: ' + 'this property will be removed in future releases, ' + 'please inherit from L.Evented instead.', new Error().stack); } } } /* * @class Evented * @aka L.Evented * @inherits Class * * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event). * * @example * * ```js * map.on('click', function(e) { * alert(e.latlng); * } ); * ``` * * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function: * * ```js * function onClick(e) { ... } * * map.on('click', onClick); * map.off('click', onClick); * ``` */ var Events = { /* @method on(type: String, fn: Function, context?: Object): this * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`). * * @alternative * @method on(eventMap: Object): this * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` */ on: function (types, fn, context) { // types can be a map of types/handlers if (typeof types === 'object') { for (var type in types) { // we don't process space-separated events here for performance; // it's a hot path since Layer uses the on(obj) syntax this._on(type, types[type], fn); } } else { // types can be a string of space-separated words types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { this._on(types[i], fn, context); } } return this; }, /* @method off(type: String, fn?: Function, context?: Object): this * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener. * * @alternative * @method off(eventMap: Object): this * Removes a set of type/listener pairs. * * @alternative * @method off: this * Removes all listeners to all events on the object. This includes implicitly attached events. */ off: function (types, fn, context) { if (!types) { // clear all listeners if called without arguments delete this._events; } else if (typeof types === 'object') { for (var type in types) { this._off(type, types[type], fn); } } else { types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { this._off(types[i], fn, context); } } return this; }, // attach listener (without syntactic sugar now) _on: function (type, fn, context) { this._events = this._events || {}; /* get/init listeners for type */ var typeListeners = this._events[type]; if (!typeListeners) { typeListeners = []; this._events[type] = typeListeners; } if (context === this) { // Less memory footprint. context = undefined; } var newListener = {fn: fn, ctx: context}, listeners = typeListeners; // check if fn already there for (var i = 0, len = listeners.length; i < len; i++) { if (listeners[i].fn === fn && listeners[i].ctx === context) { return; } } listeners.push(newListener); }, _off: function (type, fn, context) { var listeners, i, len; if (!this._events) { return; } listeners = this._events[type]; if (!listeners) { return; } if (!fn) { // Set all removed listeners to noop so they are not called if remove happens in fire for (i = 0, len = listeners.length; i < len; i++) { listeners[i].fn = falseFn; } // clear all listeners for a type if function isn't specified delete this._events[type]; return; } if (context === this) { context = undefined; } if (listeners) { // find fn and remove it for (i = 0, len = listeners.length; i < len; i++) { var l = listeners[i]; if (l.ctx !== context) { continue; } if (l.fn === fn) { // set the removed listener to noop so that's not called if remove happens in fire l.fn = falseFn; if (this._firingCount) { /* copy array in case events are being fired */ this._events[type] = listeners = listeners.slice(); } listeners.splice(i, 1); return; } } } }, // @method fire(type: String, data?: Object, propagate?: Boolean): this // Fires an event of the specified type. You can optionally provide an data // object — the first argument of the listener function will contain its // properties. The event can optionally be propagated to event parents. fire: function (type, data, propagate) { if (!this.listens(type, propagate)) { return this; } var event = extend({}, data, { type: type, target: this, sourceTarget: data && data.sourceTarget || this }); if (this._events) { var listeners = this._events[type]; if (listeners) { this._firingCount = (this._firingCount + 1) || 1; for (var i = 0, len = listeners.length; i < len; i++) { var l = listeners[i]; l.fn.call(l.ctx || this, event); } this._firingCount--; } } if (propagate) { // propagate the event to parents (set with addEventParent) this._propagateEvent(event); } return this; }, // @method listens(type: String): Boolean // Returns `true` if a particular event type has any listeners attached to it. listens: function (type, propagate) { var listeners = this._events && this._events[type]; if (listeners && listeners.length) { return true; } if (propagate) { // also check parents for listeners if event propagates for (var id in this._eventParents) { if (this._eventParents[id].listens(type, propagate)) { return true; } } } return false; }, // @method once(…): this // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed. once: function (types, fn, context) { if (typeof types === 'object') { for (var type in types) { this.once(type, types[type], fn); } return this; } var handler = bind(function () { this .off(types, fn, context) .off(types, handler, context); }, this); // add a listener that's executed once and removed after that return this .on(types, fn, context) .on(types, handler, context); }, // @method addEventParent(obj: Evented): this // Adds an event parent - an `Evented` that will receive propagated events addEventParent: function (obj) { this._eventParents = this._eventParents || {}; this._eventParents[stamp(obj)] = obj; return this; }, // @method removeEventParent(obj: Evented): this // Removes an event parent, so it will stop receiving propagated events removeEventParent: function (obj) { if (this._eventParents) { delete this._eventParents[stamp(obj)]; } return this; }, _propagateEvent: function (e) { for (var id in this._eventParents) { this._eventParents[id].fire(e.type, extend({ layer: e.target, propagatedFrom: e.target }, e), true); } } }; // aliases; we should ditch those eventually // @method addEventListener(…): this // Alias to [`on(…)`](#evented-on) Events.addEventListener = Events.on; // @method removeEventListener(…): this // Alias to [`off(…)`](#evented-off) // @method clearAllEventListeners(…): this // Alias to [`off()`](#evented-off) Events.removeEventListener = Events.clearAllEventListeners = Events.off; // @method addOneTimeEventListener(…): this // Alias to [`once(…)`](#evented-once) Events.addOneTimeEventListener = Events.once; // @method fireEvent(…): this // Alias to [`fire(…)`](#evented-fire) Events.fireEvent = Events.fire; // @method hasEventListeners(…): Boolean // Alias to [`listens(…)`](#evented-listens) Events.hasEventListeners = Events.listens; var Evented = Class.extend(Events); /* * @class Point * @aka L.Point * * Represents a point with `x` and `y` coordinates in pixels. * * @example * * ```js * var point = L.point(200, 300); * ``` * * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent: * * ```js * map.panBy([200, 300]); * map.panBy(L.point(200, 300)); * ``` * * Note that `Point` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ function Point(x, y, round) { // @property x: Number; The `x` coordinate of the point this.x = (round ? Math.round(x) : x); // @property y: Number; The `y` coordinate of the point this.y = (round ? Math.round(y) : y); } var trunc = Math.trunc || function (v) { return v > 0 ? Math.floor(v) : Math.ceil(v); }; Point.prototype = { // @method clone(): Point // Returns a copy of the current point. clone: function () { return new Point(this.x, this.y); }, // @method add(otherPoint: Point): Point // Returns the result of addition of the current and the given points. add: function (point) { // non-destructive, returns a new point return this.clone()._add(toPoint(point)); }, _add: function (point) { // destructive, used directly for performance in situations where it's safe to modify existing point this.x += point.x; this.y += point.y; return this; }, // @method subtract(otherPoint: Point): Point // Returns the result of subtraction of the given point from the current. subtract: function (point) { return this.clone()._subtract(toPoint(point)); }, _subtract: function (point) { this.x -= point.x; this.y -= point.y; return this; }, // @method divideBy(num: Number): Point // Returns the result of division of the current point by the given number. divideBy: function (num) { return this.clone()._divideBy(num); }, _divideBy: function (num) { this.x /= num; this.y /= num; return this; }, // @method multiplyBy(num: Number): Point // Returns the result of multiplication of the current point by the given number. multiplyBy: function (num) { return this.clone()._multiplyBy(num); }, _multiplyBy: function (num) { this.x *= num; this.y *= num; return this; }, // @method scaleBy(scale: Point): Point // Multiply each coordinate of the current point by each coordinate of // `scale`. In linear algebra terms, multiply the point by the // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation) // defined by `scale`. scaleBy: function (point) { return new Point(this.x * point.x, this.y * point.y); }, // @method unscaleBy(scale: Point): Point // Inverse of `scaleBy`. Divide each coordinate of the current point by // each coordinate of `scale`. unscaleBy: function (point) { return new Point(this.x / point.x, this.y / point.y); }, // @method round(): Point // Returns a copy of the current point with rounded coordinates. round: function () { return this.clone()._round(); }, _round: function () { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; }, // @method floor(): Point // Returns a copy of the current point with floored coordinates (rounded down). floor: function () { return this.clone()._floor(); }, _floor: function () { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; }, // @method ceil(): Point // Returns a copy of the current point with ceiled coordinates (rounded up). ceil: function () { return this.clone()._ceil(); }, _ceil: function () { this.x = Math.ceil(this.x); this.y = Math.ceil(this.y); return this; }, // @method trunc(): Point // Returns a copy of the current point with truncated coordinates (rounded towards zero). trunc: function () { return this.clone()._trunc(); }, _trunc: function () { this.x = trunc(this.x); this.y = trunc(this.y); return this; }, // @method distanceTo(otherPoint: Point): Number // Returns the cartesian distance between the current and the given points. distanceTo: function (point) { point = toPoint(point); var x = point.x - this.x, y = point.y - this.y; return Math.sqrt(x * x + y * y); }, // @method equals(otherPoint: Point): Boolean // Returns `true` if the given point has the same coordinates. equals: function (point) { point = toPoint(point); return point.x === this.x && point.y === this.y; }, // @method contains(otherPoint: Point): Boolean // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values). contains: function (point) { point = toPoint(point); return Math.abs(point.x) <= Math.abs(this.x) && Math.abs(point.y) <= Math.abs(this.y); }, // @method toString(): String // Returns a string representation of the point for debugging purposes. toString: function () { return 'Point(' + formatNum(this.x) + ', ' + formatNum(this.y) + ')'; } }; // @factory L.point(x: Number, y: Number, round?: Boolean) // Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values. // @alternative // @factory L.point(coords: Number[]) // Expects an array of the form `[x, y]` instead. // @alternative // @factory L.point(coords: Object) // Expects a plain object of the form `{x: Number, y: Number}` instead. function toPoint(x, y, round) { if (x instanceof Point) { return x; } if (isArray(x)) { return new Point(x[0], x[1]); } if (x === undefined || x === null) { return x; } if (typeof x === 'object' && 'x' in x && 'y' in x) { return new Point(x.x, x.y); } return new Point(x, y, round); } /* * @class Bounds * @aka L.Bounds * * Represents a rectangular area in pixel coordinates. * * @example * * ```js * var p1 = L.point(10, 10), * p2 = L.point(40, 60), * bounds = L.bounds(p1, p2); * ``` * * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: * * ```js * otherBounds.intersects([[10, 10], [40, 60]]); * ``` * * Note that `Bounds` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ function Bounds(a, b) { if (!a) { return; } var points = b ? [a, b] : a; for (var i = 0, len = points.length; i < len; i++) { this.extend(points[i]); } } Bounds.prototype = { // @method extend(point: Point): this // Extends the bounds to contain the given point. extend: function (point) { // (Point) point = toPoint(point); // @property min: Point // The top left corner of the rectangle. // @property max: Point // The bottom right corner of the rectangle. if (!this.min && !this.max) { this.min = point.clone(); this.max = point.clone(); } else { this.min.x = Math.min(point.x, this.min.x); this.max.x = Math.max(point.x, this.max.x); this.min.y = Math.min(point.y, this.min.y); this.max.y = Math.max(point.y, this.max.y); } return this; }, // @method getCenter(round?: Boolean): Point // Returns the center point of the bounds. getCenter: function (round) { return new Point( (this.min.x + this.max.x) / 2, (this.min.y + this.max.y) / 2, round); }, // @method getBottomLeft(): Point // Returns the bottom-left point of the bounds. getBottomLeft: function () { return new Point(this.min.x, this.max.y); }, // @method getTopRight(): Point // Returns the top-right point of the bounds. getTopRight: function () { // -> Point return new Point(this.max.x, this.min.y); }, // @method getTopLeft(): Point // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)). getTopLeft: function () { return this.min; // left, top }, // @method getBottomRight(): Point // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)). getBottomRight: function () { return this.max; // right, bottom }, // @method getSize(): Point // Returns the size of the given bounds getSize: function () { return this.max.subtract(this.min); }, // @method contains(otherBounds: Bounds): Boolean // Returns `true` if the rectangle contains the given one. // @alternative // @method contains(point: Point): Boolean // Returns `true` if the rectangle contains the given point. contains: function (obj) { var min, max; if (typeof obj[0] === 'number' || obj instanceof Point) { obj = toPoint(obj); } else { obj = toBounds(obj); } if (obj instanceof Bounds) { min = obj.min; max = obj.max; } else { min = max = obj; } return (min.x >= this.min.x) && (max.x <= this.max.x) && (min.y >= this.min.y) && (max.y <= this.max.y); }, // @method intersects(otherBounds: Bounds): Boolean // Returns `true` if the rectangle intersects the given bounds. Two bounds // intersect if they have at least one point in common. intersects: function (bounds) { // (Bounds) -> Boolean bounds = toBounds(bounds); var min = this.min, max = this.max, min2 = bounds.min, max2 = bounds.max, xIntersects = (max2.x >= min.x) && (min2.x <= max.x), yIntersects = (max2.y >= min.y) && (min2.y <= max.y); return xIntersects && yIntersects; }, // @method overlaps(otherBounds: Bounds): Boolean // Returns `true` if the rectangle overlaps the given bounds. Two bounds // overlap if their intersection is an area. overlaps: function (bounds) { // (Bounds) -> Boolean bounds = toBounds(bounds); var min = this.min, max = this.max, min2 = bounds.min, max2 = bounds.max, xOverlaps = (max2.x > min.x) && (min2.x < max.x), yOverlaps = (max2.y > min.y) && (min2.y < max.y); return xOverlaps && yOverlaps; }, isValid: function () { return !!(this.min && this.max); } }; // @factory L.bounds(corner1: Point, corner2: Point) // Creates a Bounds object from two corners coordinate pairs. // @alternative // @factory L.bounds(points: Point[]) // Creates a Bounds object from the given array of points. function toBounds(a, b) { if (!a || a instanceof Bounds) { return a; } return new Bounds(a, b); } /* * @class LatLngBounds * @aka L.LatLngBounds * * Represents a rectangular geographical area on a map. * * @example * * ```js * var corner1 = L.latLng(40.712, -74.227), * corner2 = L.latLng(40.774, -74.125), * bounds = L.latLngBounds(corner1, corner2); * ``` * * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: * * ```js * map.fitBounds([ * [40.712, -74.227], * [40.774, -74.125] * ]); * ``` * * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range. * * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[]) if (!corner1) { return; } var latlngs = corner2 ? [corner1, corner2] : corner1; for (var i = 0, len = latlngs.length; i < len; i++) { this.extend(latlngs[i]); } } LatLngBounds.prototype = { // @method extend(latlng: LatLng): this // Extend the bounds to contain the given point // @alternative // @method extend(otherBounds: LatLngBounds): this // Extend the bounds to contain the given bounds extend: function (obj) { var sw = this._southWest, ne = this._northEast, sw2, ne2; if (obj instanceof LatLng) { sw2 = obj; ne2 = obj; } else if (obj instanceof LatLngBounds) { sw2 = obj._southWest; ne2 = obj._northEast; if (!sw2 || !ne2) { return this; } } else { return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this; } if (!sw && !ne) { this._southWest = new LatLng(sw2.lat, sw2.lng); this._northEast = new LatLng(ne2.lat, ne2.lng); } else { sw.lat = Math.min(sw2.lat, sw.lat); sw.lng = Math.min(sw2.lng, sw.lng); ne.lat = Math.max(ne2.lat, ne.lat); ne.lng = Math.max(ne2.lng, ne.lng); } return this; }, // @method pad(bufferRatio: Number): LatLngBounds // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction. // For example, a ratio of 0.5 extends the bounds by 50% in each direction. // Negative values will retract the bounds. pad: function (bufferRatio) { var sw = this._southWest, ne = this._northEast, heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; return new LatLngBounds( new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); }, // @method getCenter(): LatLng // Returns the center point of the bounds. getCenter: function () { return new LatLng( (this._southWest.lat + this._northEast.lat) / 2, (this._southWest.lng + this._northEast.lng) / 2); }, // @method getSouthWest(): LatLng // Returns the south-west point of the bounds. getSouthWest: function () { return this._southWest; }, // @method getNorthEast(): LatLng // Returns the north-east point of the bounds. getNorthEast: function () { return this._northEast; }, // @method getNorthWest(): LatLng // Returns the north-west point of the bounds. getNorthWest: function () { return new LatLng(this.getNorth(), this.getWest()); }, // @method getSouthEast(): LatLng // Returns the south-east point of the bounds. getSouthEast: function () { return new LatLng(this.getSouth(), this.getEast()); }, // @method getWest(): Number // Returns the west longitude of the bounds getWest: function () { return this._southWest.lng; }, // @method getSouth(): Number // Returns the south latitude of the bounds getSouth: function () { return this._southWest.lat; }, // @method getEast(): Number // Returns the east longitude of the bounds getEast: function () { return this._northEast.lng; }, // @method getNorth(): Number // Returns the north latitude of the bounds getNorth: function () { return this._northEast.lat; }, // @method contains(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle contains the given one. // @alternative // @method contains (latlng: LatLng): Boolean // Returns `true` if the rectangle contains the given point. contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) { obj = toLatLng(obj); } else { obj = toLatLngBounds(obj); } var sw = this._southWest, ne = this._northEast, sw2, ne2; if (obj instanceof LatLngBounds) { sw2 = obj.getSouthWest(); ne2 = obj.getNorthEast(); } else { sw2 = ne2 = obj; } return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); }, // @method intersects(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common. intersects: function (bounds) { bounds = toLatLngBounds(bounds); var sw = this._southWest, ne = this._northEast, sw2 = bounds.getSouthWest(), ne2 = bounds.getNorthEast(), latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); return latIntersects && lngIntersects; }, // @method overlaps(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area. overlaps: function (bounds) { bounds = toLatLngBounds(bounds); var sw = this._southWest, ne = this._northEast, sw2 = bounds.getSouthWest(), ne2 = bounds.getNorthEast(), latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat), lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng); return latOverlaps && lngOverlaps; }, // @method toBBoxString(): String // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data. toBBoxString: function () { return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); }, // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number. equals: function (bounds, maxMargin) { if (!bounds) { return false; } bounds = toLatLngBounds(bounds); return this._southWest.equals(bounds.getSouthWest(), maxMargin) && this._northEast.equals(bounds.getNorthEast(), maxMargin); }, // @method isValid(): Boolean // Returns `true` if the bounds are properly initialized. isValid: function () { return !!(this._southWest && this._northEast); } }; // TODO International date line? // @factory L.latLngBounds(corner1: LatLng, corner2: LatLng) // Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle. // @alternative // @factory L.latLngBounds(latlngs: LatLng[]) // Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds). function toLatLngBounds(a, b) { if (a instanceof LatLngBounds) { return a; } return new LatLngBounds(a, b); } /* @class LatLng * @aka L.LatLng * * Represents a geographical point with a certain latitude and longitude. * * @example * * ``` * var latlng = L.latLng(50.5, 30.5); * ``` * * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent: * * ``` * map.panTo([50, 30]); * map.panTo({lon: 30, lat: 50}); * map.panTo({lat: 50, lng: 30}); * map.panTo(L.latLng(50, 30)); * ``` * * Note that `LatLng` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ function LatLng(lat, lng, alt) { if (isNaN(lat) || isNaN(lng)) { throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); } // @property lat: Number // Latitude in degrees this.lat = +lat; // @property lng: Number // Longitude in degrees this.lng = +lng; // @property alt: Number // Altitude in meters (optional) if (alt !== undefined) { this.alt = +alt; } } LatLng.prototype = { // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number. equals: function (obj, maxMargin) { if (!obj) { return false; } obj = toLatLng(obj); var margin = Math.max( Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng)); return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); }, // @method toString(): String // Returns a string representation of the point (for debugging purposes). toString: function (precision) { return 'LatLng(' + formatNum(this.lat, precision) + ', ' + formatNum(this.lng, precision) + ')'; }, // @method distanceTo(otherLatLng: LatLng): Number // Returns the distance (in meters) to the given `LatLng` calculated using the [Spherical Law of Cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines). distanceTo: function (other) { return Earth.distance(this, toLatLng(other)); }, // @method wrap(): LatLng // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees. wrap: function () { return Earth.wrapLatLng(this); }, // @method toBounds(sizeInMeters: Number): LatLngBounds // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`. toBounds: function (sizeInMeters) { var latAccuracy = 180 * sizeInMeters / 40075017, lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); return toLatLngBounds( [this.lat - latAccuracy, this.lng - lngAccuracy], [this.lat + latAccuracy, this.lng + lngAccuracy]); }, clone: function () { return new LatLng(this.lat, this.lng, this.alt); } }; // @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng // Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude). // @alternative // @factory L.latLng(coords: Array): LatLng // Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead. // @alternative // @factory L.latLng(coords: Object): LatLng // Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead. function toLatLng(a, b, c) { if (a instanceof LatLng) { return a; } if (isArray(a) && typeof a[0] !== 'object') { if (a.length === 3) { return new LatLng(a[0], a[1], a[2]); } if (a.length === 2) { return new LatLng(a[0], a[1]); } return null; } if (a === undefined || a === null) { return a; } if (typeof a === 'object' && 'lat' in a) { return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); } if (b === undefined) { return null; } return new LatLng(a, b, c); } /* * @namespace CRS * @crs L.CRS.Base * Object that defines coordinate reference systems for projecting * geographical points into pixel (screen) coordinates and back (and to * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See * [spatial reference system](http://en.wikipedia.org/wiki/Coordinate_reference_system). * * Leaflet defines the most usual CRSs by default. If you want to use a * CRS not defined by default, take a look at the * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin. * * Note that the CRS instances do not inherit from Leaflet's `Class` object, * and can't be instantiated. Also, new classes can't inherit from them, * and methods can't be added to them with the `include` function. */ var CRS = { // @method latLngToPoint(latlng: LatLng, zoom: Number): Point // Projects geographical coordinates into pixel coordinates for a given zoom. latLngToPoint: function (latlng, zoom) { var projectedPoint = this.projection.project(latlng), scale = this.scale(zoom); return this.transformation._transform(projectedPoint, scale); }, // @method pointToLatLng(point: Point, zoom: Number): LatLng // The inverse of `latLngToPoint`. Projects pixel coordinates on a given // zoom into geographical coordinates. pointToLatLng: function (point, zoom) { var scale = this.scale(zoom), untransformedPoint = this.transformation.untransform(point, scale); return this.projection.unproject(untransformedPoint); }, // @method project(latlng: LatLng): Point // Projects geographical coordinates into coordinates in units accepted for // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services). project: function (latlng) { return this.projection.project(latlng); }, // @method unproject(point: Point): LatLng // Given a projected coordinate returns the corresponding LatLng. // The inverse of `project`. unproject: function (point) { return this.projection.unproject(point); }, // @method scale(zoom: Number): Number // Returns the scale used when transforming projected coordinates into // pixel coordinates for a particular zoom. For example, it returns // `256 * 2^zoom` for Mercator-based CRS. scale: function (zoom) { return 256 * Math.pow(2, zoom); }, // @method zoom(scale: Number): Number // Inverse of `scale()`, returns the zoom level corresponding to a scale // factor of `scale`. zoom: function (scale) { return Math.log(scale / 256) / Math.LN2; }, // @method getProjectedBounds(zoom: Number): Bounds // Returns the projection's bounds scaled and transformed for the provided `zoom`. getProjectedBounds: function (zoom) { if (this.infinite) { return null; } var b = this.projection.bounds, s = this.scale(zoom), min = this.transformation.transform(b.min, s), max = this.transformation.transform(b.max, s); return new Bounds(min, max); }, // @method distance(latlng1: LatLng, latlng2: LatLng): Number // Returns the distance between two geographical coordinates. // @property code: String // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`) // // @property wrapLng: Number[] // An array of two numbers defining whether the longitude (horizontal) coordinate // axis wraps around a given range and how. Defaults to `[-180, 180]` in most // geographical CRSs. If `undefined`, the longitude axis does not wrap around. // // @property wrapLat: Number[] // Like `wrapLng`, but for the latitude (vertical) axis. // wrapLng: [min, max], // wrapLat: [min, max], // @property infinite: Boolean // If true, the coordinate space will be unbounded (infinite in both axes) infinite: false, // @method wrapLatLng(latlng: LatLng): LatLng // Returns a `LatLng` where lat and lng has been wrapped according to the // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds. wrapLatLng: function (latlng) { var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat, alt = latlng.alt; return new LatLng(lat, lng, alt); }, // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds // Returns a `LatLngBounds` with the same size as the given one, ensuring // that its center is within the CRS's bounds. // Only accepts actual `L.LatLngBounds` instances, not arrays. wrapLatLngBounds: function (bounds) { var center = bounds.getCenter(), newCenter = this.wrapLatLng(center), latShift = center.lat - newCenter.lat, lngShift = center.lng - newCenter.lng; if (latShift === 0 && lngShift === 0) { return bounds; } var sw = bounds.getSouthWest(), ne = bounds.getNorthEast(), newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift), newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift); return new LatLngBounds(newSw, newNe); } }; /* * @namespace CRS * @crs L.CRS.Earth * * Serves as the base for CRS that are global such that they cover the earth. * Can only be used as the base for other CRS and cannot be used directly, * since it does not have a `code`, `projection` or `transformation`. `distance()` returns * meters. */ var Earth = extend({}, CRS, { wrapLng: [-180, 180], // Mean Earth Radius, as recommended for use by // the International Union of Geodesy and Geophysics, // see http://rosettacode.org/wiki/Haversine_formula R: 6371000, // distance between two geographical points using spherical law of cosines approximation distance: function (latlng1, latlng2) { var rad = Math.PI / 180, lat1 = latlng1.lat * rad, lat2 = latlng2.lat * rad, sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2), sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2), a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon, c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return this.R * c; } }); /* * @namespace Projection * @projection L.Projection.SphericalMercator * * Spherical Mercator projection — the most common projection for online maps, * used by almost all free and commercial tile providers. Assumes that Earth is * a sphere. Used by the `EPSG:3857` CRS. */ var earthRadius = 6378137; var SphericalMercator = { R: earthRadius, MAX_LATITUDE: 85.0511287798, project: function (latlng) { var d = Math.PI / 180, max = this.MAX_LATITUDE, lat = Math.max(Math.min(max, latlng.lat), -max), sin = Math.sin(lat * d); return new Point( this.R * latlng.lng * d, this.R * Math.log((1 + sin) / (1 - sin)) / 2); }, unproject: function (point) { var d = 180 / Math.PI; return new LatLng( (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, point.x * d / this.R); }, bounds: (function () { var d = earthRadius * Math.PI; return new Bounds([-d, -d], [d, d]); })() }; /* * @class Transformation * @aka L.Transformation * * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d` * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing * the reverse. Used by Leaflet in its projections code. * * @example * * ```js * var transformation = L.transformation(2, 5, -1, 10), * p = L.point(1, 2), * p2 = transformation.transform(p), // L.point(7, 8) * p3 = transformation.untransform(p2); // L.point(1, 2) * ``` */ // factory new L.Transformation(a: Number, b: Number, c: Number, d: Number) // Creates a `Transformation` object with the given coefficients. function Transformation(a, b, c, d) { if (isArray(a)) { // use array properties this._a = a[0]; this._b = a[1]; this._c = a[2]; this._d = a[3]; return; } this._a = a; this._b = b; this._c = c; this._d = d; } Transformation.prototype = { // @method transform(point: Point, scale?: Number): Point // Returns a transformed point, optionally multiplied by the given scale. // Only accepts actual `L.Point` instances, not arrays. transform: function (point, scale) { // (Point, Number) -> Point return this._transform(point.clone(), scale); }, // destructive transform (faster) _transform: function (point, scale) { scale = scale || 1; point.x = scale * (this._a * point.x + this._b); point.y = scale * (this._c * point.y + this._d); return point; }, // @method untransform(point: Point, scale?: Number): Point // Returns the reverse transformation of the given point, optionally divided // by the given scale. Only accepts actual `L.Point` instances, not arrays. untransform: function (point, scale) { scale = scale || 1; return new Point( (point.x / scale - this._b) / this._a, (point.y / scale - this._d) / this._c); } }; // factory L.transformation(a: Number, b: Number, c: Number, d: Number) // @factory L.transformation(a: Number, b: Number, c: Number, d: Number) // Instantiates a Transformation object with the given coefficients. // @alternative // @factory L.transformation(coefficients: Array): Transformation // Expects an coefficients array of the form // `[a: Number, b: Number, c: Number, d: Number]`. function toTransformation(a, b, c, d) { return new Transformation(a, b, c, d); } /* * @namespace CRS * @crs L.CRS.EPSG3857 * * The most common CRS for online maps, used by almost all free and commercial * tile providers. Uses Spherical Mercator projection. Set in by default in * Map's `crs` option. */ var EPSG3857 = extend({}, Earth, { code: 'EPSG:3857', projection: SphericalMercator, transformation: (function () { var scale = 0.5 / (Math.PI * SphericalMercator.R); return toTransformation(scale, 0.5, -scale, 0.5); }()) }); var EPSG900913 = extend({}, EPSG3857, { code: 'EPSG:900913' }); // @namespace SVG; @section // There are several static functions which can be called without instantiating L.SVG: // @function create(name: String): SVGElement // Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement), // corresponding to the class name passed. For example, using 'line' will return // an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement). function svgCreate(name) { return document.createElementNS('http://www.w3.org/2000/svg', name); } // @function pointsToPath(rings: Point[], closed: Boolean): String // Generates a SVG path string for multiple rings, with each ring turning // into "M..L..L.." instructions function pointsToPath(rings, closed) { var str = '', i, j, len, len2, points, p; for (i = 0, len = rings.length; i < len; i++) { points = rings[i]; for (j = 0, len2 = points.length; j < len2; j++) { p = points[j]; str += (j ? 'L' : 'M') + p.x + ' ' + p.y; } // closes the ring for polygons; "x" is VML syntax str += closed ? (svg ? 'z' : 'x') : ''; } // SVG complains about empty path strings return str || 'M0 0'; } /* * @namespace Browser * @aka L.Browser * * A namespace with static properties for browser/feature detection used by Leaflet internally. * * @example * * ```js * if (L.Browser.ielt9) { * alert('Upgrade your browser, dude!'); * } * ``` */ var style$1 = document.documentElement.style; // @property ie: Boolean; `true` for all Internet Explorer versions (not Edge). var ie = 'ActiveXObject' in window; // @property ielt9: Boolean; `true` for Internet Explorer versions less than 9. var ielt9 = ie && !document.addEventListener; // @property edge: Boolean; `true` for the Edge web browser. var edge = 'msLaunchUri' in navigator && !('documentMode' in document); // @property webkit: Boolean; // `true` for webkit-based browsers like Chrome and Safari (including mobile versions). var webkit = userAgentContains('webkit'); // @property android: Boolean // `true` for any browser running on an Android platform. var android = userAgentContains('android'); // @property android23: Boolean; `true` for browsers running on Android 2 or Android 3. var android23 = userAgentContains('android 2') || userAgentContains('android 3'); /* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */ var webkitVer = parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit // @property androidStock: Boolean; `true` for the Android stock browser (i.e. not Chrome) var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window); // @property opera: Boolean; `true` for the Opera browser var opera = !!window.opera; // @property chrome: Boolean; `true` for the Chrome browser. var chrome = !edge && userAgentContains('chrome'); // @property gecko: Boolean; `true` for gecko-based browsers like Firefox. var gecko = userAgentContains('gecko') && !webkit && !opera && !ie; // @property safari: Boolean; `true` for the Safari browser. var safari = !chrome && userAgentContains('safari'); var phantom = userAgentContains('phantom'); // @property opera12: Boolean // `true` for the Opera browser supporting CSS transforms (version 12 or later). var opera12 = 'OTransition' in style$1; // @property win: Boolean; `true` when the browser is running in a Windows platform var win = navigator.platform.indexOf('Win') === 0; // @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms. var ie3d = ie && ('transition' in style$1); // @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms. var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23; // @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms. var gecko3d = 'MozPerspective' in style$1; // @property any3d: Boolean // `true` for all browsers supporting CSS transforms. var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom; // @property mobile: Boolean; `true` for all browsers running in a mobile device. var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile'); // @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device. var mobileWebkit = mobile && webkit; // @property mobileWebkit3d: Boolean // `true` for all webkit-based browsers in a mobile device supporting CSS transforms. var mobileWebkit3d = mobile && webkit3d; // @property msPointer: Boolean // `true` for browsers implementing the Microsoft touch events model (notably IE10). var msPointer = !window.PointerEvent && window.MSPointerEvent; // @property pointer: Boolean // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx). var pointer = !!(window.PointerEvent || msPointer); // @property touch: Boolean // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events). // This does not necessarily mean that the browser is running in a computer with // a touchscreen, it only means that the browser is capable of understanding // touch events. var touch = !window.L_NO_TOUCH && (pointer || 'ontouchstart' in window || (window.DocumentTouch && document instanceof window.DocumentTouch)); // @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device. var mobileOpera = mobile && opera; // @property mobileGecko: Boolean // `true` for gecko-based browsers running in a mobile device. var mobileGecko = mobile && gecko; // @property retina: Boolean // `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%. var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1; // @property passiveEvents: Boolean // `true` for browsers that support passive events. var passiveEvents = (function () { var supportsPassiveOption = false; try { var opts = Object.defineProperty({}, 'passive', { get: function () { // eslint-disable-line getter-return supportsPassiveOption = true; } }); window.addEventListener('testPassiveEventSupport', falseFn, opts); window.removeEventListener('testPassiveEventSupport', falseFn, opts); } catch (e) { // Errors can safely be ignored since this is only a browser support test. } return supportsPassiveOption; }()); // @property canvas: Boolean // `true` when the browser supports [``](https://developer.mozilla.org/docs/Web/API/Canvas_API). var canvas = (function () { return !!document.createElement('canvas').getContext; }()); // @property svg: Boolean // `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG). var svg = !!(document.createElementNS && svgCreate('svg').createSVGRect); // @property vml: Boolean // `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language). var vml = !svg && (function () { try { var div = document.createElement('div'); div.innerHTML = ''; var shape = div.firstChild; shape.style.behavior = 'url(#default#VML)'; return shape && (typeof shape.adj === 'object'); } catch (e) { return false; } }()); function userAgentContains(str) { return navigator.userAgent.toLowerCase().indexOf(str) >= 0; } var Browser = ({ ie: ie, ielt9: ielt9, edge: edge, webkit: webkit, android: android, android23: android23, androidStock: androidStock, opera: opera, chrome: chrome, gecko: gecko, safari: safari, phantom: phantom, opera12: opera12, win: win, ie3d: ie3d, webkit3d: webkit3d, gecko3d: gecko3d, any3d: any3d, mobile: mobile, mobileWebkit: mobileWebkit, mobileWebkit3d: mobileWebkit3d, msPointer: msPointer, pointer: pointer, touch: touch, mobileOpera: mobileOpera, mobileGecko: mobileGecko, retina: retina, passiveEvents: passiveEvents, canvas: canvas, svg: svg, vml: vml }); /* * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. */ var POINTER_DOWN = msPointer ? 'MSPointerDown' : 'pointerdown'; var POINTER_MOVE = msPointer ? 'MSPointerMove' : 'pointermove'; var POINTER_UP = msPointer ? 'MSPointerUp' : 'pointerup'; var POINTER_CANCEL = msPointer ? 'MSPointerCancel' : 'pointercancel'; var _pointers = {}; var _pointerDocListener = false; // Provides a touch events wrapper for (ms)pointer events. // ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 function addPointerListener(obj, type, handler, id) { if (type === 'touchstart') { _addPointerStart(obj, handler, id); } else if (type === 'touchmove') { _addPointerMove(obj, handler, id); } else if (type === 'touchend') { _addPointerEnd(obj, handler, id); } return this; } function removePointerListener(obj, type, id) { var handler = obj['_leaflet_' + type + id]; if (type === 'touchstart') { obj.removeEventListener(POINTER_DOWN, handler, false); } else if (type === 'touchmove') { obj.removeEventListener(POINTER_MOVE, handler, false); } else if (type === 'touchend') { obj.removeEventListener(POINTER_UP, handler, false); obj.removeEventListener(POINTER_CANCEL, handler, false); } return this; } function _addPointerStart(obj, handler, id) { var onDown = bind(function (e) { // IE10 specific: MsTouch needs preventDefault. See #2000 if (e.MSPOINTER_TYPE_TOUCH && e.pointerType === e.MSPOINTER_TYPE_TOUCH) { preventDefault(e); } _handlePointer(e, handler); }); obj['_leaflet_touchstart' + id] = onDown; obj.addEventListener(POINTER_DOWN, onDown, false); // need to keep track of what pointers and how many are active to provide e.touches emulation if (!_pointerDocListener) { // we listen document as any drags that end by moving the touch off the screen get fired there document.addEventListener(POINTER_DOWN, _globalPointerDown, true); document.addEventListener(POINTER_MOVE, _globalPointerMove, true); document.addEventListener(POINTER_UP, _globalPointerUp, true); document.addEventListener(POINTER_CANCEL, _globalPointerUp, true); _pointerDocListener = true; } } function _globalPointerDown(e) { _pointers[e.pointerId] = e; } function _globalPointerMove(e) { if (_pointers[e.pointerId]) { _pointers[e.pointerId] = e; } } function _globalPointerUp(e) { delete _pointers[e.pointerId]; } function _handlePointer(e, handler) { e.touches = []; for (var i in _pointers) { e.touches.push(_pointers[i]); } e.changedTouches = [e]; handler(e); } function _addPointerMove(obj, handler, id) { var onMove = function (e) { // don't fire touch moves when mouse isn't down if ((e.pointerType === (e.MSPOINTER_TYPE_MOUSE || 'mouse')) && e.buttons === 0) { return; } _handlePointer(e, handler); }; obj['_leaflet_touchmove' + id] = onMove; obj.addEventListener(POINTER_MOVE, onMove, false); } function _addPointerEnd(obj, handler, id) { var onUp = function (e) { _handlePointer(e, handler); }; obj['_leaflet_touchend' + id] = onUp; obj.addEventListener(POINTER_UP, onUp, false); obj.addEventListener(POINTER_CANCEL, onUp, false); } /* * Extends the event handling code with double tap support for mobile browsers. */ var _touchstart = msPointer ? 'MSPointerDown' : pointer ? 'pointerdown' : 'touchstart'; var _touchend = msPointer ? 'MSPointerUp' : pointer ? 'pointerup' : 'touchend'; var _pre = '_leaflet_'; // inspired by Zepto touch code by Thomas Fuchs function addDoubleTapListener(obj, handler, id) { var last, touch$$1, doubleTap = false, delay = 250; function onTouchStart(e) { if (pointer) { if (!e.isPrimary) { return; } if (e.pointerType === 'mouse') { return; } // mouse fires native dblclick } else if (e.touches.length > 1) { return; } var now = Date.now(), delta = now - (last || now); touch$$1 = e.touches ? e.touches[0] : e; doubleTap = (delta > 0 && delta <= delay); last = now; } function onTouchEnd(e) { if (doubleTap && !touch$$1.cancelBubble) { if (pointer) { if (e.pointerType === 'mouse') { return; } // work around .type being readonly with MSPointer* events var newTouch = {}, prop, i; for (i in touch$$1) { prop = touch$$1[i]; newTouch[i] = prop && prop.bind ? prop.bind(touch$$1) : prop; } touch$$1 = newTouch; } touch$$1.type = 'dblclick'; touch$$1.button = 0; handler(touch$$1); last = null; } } obj[_pre + _touchstart + id] = onTouchStart; obj[_pre + _touchend + id] = onTouchEnd; obj[_pre + 'dblclick' + id] = handler; obj.addEventListener(_touchstart, onTouchStart, passiveEvents ? {passive: false} : false); obj.addEventListener(_touchend, onTouchEnd, passiveEvents ? {passive: false} : false); // On some platforms (notably, chrome<55 on win10 + touchscreen + mouse), // the browser doesn't fire touchend/pointerup events but does fire // native dblclicks. See #4127. // Edge 14 also fires native dblclicks, but only for pointerType mouse, see #5180. obj.addEventListener('dblclick', handler, false); return this; } function removeDoubleTapListener(obj, id) { var touchstart = obj[_pre + _touchstart + id], touchend = obj[_pre + _touchend + id], dblclick = obj[_pre + 'dblclick' + id]; obj.removeEventListener(_touchstart, touchstart, passiveEvents ? {passive: false} : false); obj.removeEventListener(_touchend, touchend, passiveEvents ? {passive: false} : false); obj.removeEventListener('dblclick', dblclick, false); return this; } /* * @namespace DomUtil * * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model) * tree, used by Leaflet internally. * * Most functions expecting or returning a `HTMLElement` also work for * SVG elements. The only difference is that classes refer to CSS classes * in HTML and SVG classes in SVG. */ // @property TRANSFORM: String // Vendor-prefixed transform style name (e.g. `'webkitTransform'` for WebKit). var TRANSFORM = testProp( ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform']); // webkitTransition comes first because some browser versions that drop vendor prefix don't do // the same for the transitionend event, in particular the Android 4.1 stock browser // @property TRANSITION: String // Vendor-prefixed transition style name. var TRANSITION = testProp( ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); // @property TRANSITION_END: String // Vendor-prefixed transitionend event name. var TRANSITION_END = TRANSITION === 'webkitTransition' || TRANSITION === 'OTransition' ? TRANSITION + 'End' : 'transitionend'; // @function get(id: String|HTMLElement): HTMLElement // Returns an element given its DOM id, or returns the element itself // if it was passed directly. function get(id) { return typeof id === 'string' ? document.getElementById(id) : id; } // @function getStyle(el: HTMLElement, styleAttrib: String): String // Returns the value for a certain style attribute on an element, // including computed values or values set through CSS. function getStyle(el, style) { var value = el.style[style] || (el.currentStyle && el.currentStyle[style]); if ((!value || value === 'auto') && document.defaultView) { var css = document.defaultView.getComputedStyle(el, null); value = css ? css[style] : null; } return value === 'auto' ? null : value; } // @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement // Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element. function create$1(tagName, className, container) { var el = document.createElement(tagName); el.className = className || ''; if (container) { container.appendChild(el); } return el; } // @function remove(el: HTMLElement) // Removes `el` from its parent element function remove(el) { var parent = el.parentNode; if (parent) { parent.removeChild(el); } } // @function empty(el: HTMLElement) // Removes all of `el`'s children elements from `el` function empty(el) { while (el.firstChild) { el.removeChild(el.firstChild); } } // @function toFront(el: HTMLElement) // Makes `el` the last child of its parent, so it renders in front of the other children. function toFront(el) { var parent = el.parentNode; if (parent && parent.lastChild !== el) { parent.appendChild(el); } } // @function toBack(el: HTMLElement) // Makes `el` the first child of its parent, so it renders behind the other children. function toBack(el) { var parent = el.parentNode; if (parent && parent.firstChild !== el) { parent.insertBefore(el, parent.firstChild); } } // @function hasClass(el: HTMLElement, name: String): Boolean // Returns `true` if the element's class attribute contains `name`. function hasClass(el, name) { if (el.classList !== undefined) { return el.classList.contains(name); } var className = getClass(el); return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); } // @function addClass(el: HTMLElement, name: String) // Adds `name` to the element's class attribute. function addClass(el, name) { if (el.classList !== undefined) { var classes = splitWords(name); for (var i = 0, len = classes.length; i < len; i++) { el.classList.add(classes[i]); } } else if (!hasClass(el, name)) { var className = getClass(el); setClass(el, (className ? className + ' ' : '') + name); } } // @function removeClass(el: HTMLElement, name: String) // Removes `name` from the element's class attribute. function removeClass(el, name) { if (el.classList !== undefined) { el.classList.remove(name); } else { setClass(el, trim((' ' + getClass(el) + ' ').replace(' ' + name + ' ', ' '))); } } // @function setClass(el: HTMLElement, name: String) // Sets the element's class. function setClass(el, name) { if (el.className.baseVal === undefined) { el.className = name; } else { // in case of SVG element el.className.baseVal = name; } } // @function getClass(el: HTMLElement): String // Returns the element's class. function getClass(el) { // Check if the element is an SVGElementInstance and use the correspondingElement instead // (Required for linked SVG elements in IE11.) if (el.correspondingElement) { el = el.correspondingElement; } return el.className.baseVal === undefined ? el.className : el.className.baseVal; } // @function setOpacity(el: HTMLElement, opacity: Number) // Set the opacity of an element (including old IE support). // `opacity` must be a number from `0` to `1`. function setOpacity(el, value) { if ('opacity' in el.style) { el.style.opacity = value; } else if ('filter' in el.style) { _setOpacityIE(el, value); } } function _setOpacityIE(el, value) { var filter = false, filterName = 'DXImageTransform.Microsoft.Alpha'; // filters collection throws an error if we try to retrieve a filter that doesn't exist try { filter = el.filters.item(filterName); } catch (e) { // don't set opacity to 1 if we haven't already set an opacity, // it isn't needed and breaks transparent pngs. if (value === 1) { return; } } value = Math.round(value * 100); if (filter) { filter.Enabled = (value !== 100); filter.Opacity = value; } else { el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')'; } } // @function testProp(props: String[]): String|false // Goes through the array of style names and returns the first name // that is a valid style name for an element. If no such name is found, // it returns false. Useful for vendor-prefixed styles like `transform`. function testProp(props) { var style = document.documentElement.style; for (var i = 0; i < props.length; i++) { if (props[i] in style) { return props[i]; } } return false; } // @function setTransform(el: HTMLElement, offset: Point, scale?: Number) // Resets the 3D CSS transform of `el` so it is translated by `offset` pixels // and optionally scaled by `scale`. Does not have an effect if the // browser doesn't support 3D CSS transforms. function setTransform(el, offset, scale) { var pos = offset || new Point(0, 0); el.style[TRANSFORM] = (ie3d ? 'translate(' + pos.x + 'px,' + pos.y + 'px)' : 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : ''); } // @function setPosition(el: HTMLElement, position: Point) // Sets the position of `el` to coordinates specified by `position`, // using CSS translate or top/left positioning depending on the browser // (used by Leaflet internally to position its layers). function setPosition(el, point) { /*eslint-disable */ el._leaflet_pos = point; /* eslint-enable */ if (any3d) { setTransform(el, point); } else { el.style.left = point.x + 'px'; el.style.top = point.y + 'px'; } } // @function getPosition(el: HTMLElement): Point // Returns the coordinates of an element previously positioned with setPosition. function getPosition(el) { // this method is only used for elements previously positioned using setPosition, // so it's safe to cache the position for performance return el._leaflet_pos || new Point(0, 0); } // @function disableTextSelection() // Prevents the user from generating `selectstart` DOM events, usually generated // when the user drags the mouse through a page with text. Used internally // by Leaflet to override the behaviour of any click-and-drag interaction on // the map. Affects drag interactions on the whole document. // @function enableTextSelection() // Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection). var disableTextSelection; var enableTextSelection; var _userSelect; if ('onselectstart' in document) { disableTextSelection = function () { on(window, 'selectstart', preventDefault); }; enableTextSelection = function () { off(window, 'selectstart', preventDefault); }; } else { var userSelectProperty = testProp( ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); disableTextSelection = function () { if (userSelectProperty) { var style = document.documentElement.style; _userSelect = style[userSelectProperty]; style[userSelectProperty] = 'none'; } }; enableTextSelection = function () { if (userSelectProperty) { document.documentElement.style[userSelectProperty] = _userSelect; _userSelect = undefined; } }; } // @function disableImageDrag() // As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but // for `dragstart` DOM events, usually generated when the user drags an image. function disableImageDrag() { on(window, 'dragstart', preventDefault); } // @function enableImageDrag() // Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection). function enableImageDrag() { off(window, 'dragstart', preventDefault); } var _outlineElement, _outlineStyle; // @function preventOutline(el: HTMLElement) // Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline) // of the element `el` invisible. Used internally by Leaflet to prevent // focusable elements from displaying an outline when the user performs a // drag interaction on them. function preventOutline(element) { while (element.tabIndex === -1) { element = element.parentNode; } if (!element.style) { return; } restoreOutline(); _outlineElement = element; _outlineStyle = element.style.outline; element.style.outline = 'none'; on(window, 'keydown', restoreOutline); } // @function restoreOutline() // Cancels the effects of a previous [`L.DomUtil.preventOutline`](). function restoreOutline() { if (!_outlineElement) { return; } _outlineElement.style.outline = _outlineStyle; _outlineElement = undefined; _outlineStyle = undefined; off(window, 'keydown', restoreOutline); } // @function getSizedParentNode(el: HTMLElement): HTMLElement // Finds the closest parent node which size (width and height) is not null. function getSizedParentNode(element) { do { element = element.parentNode; } while ((!element.offsetWidth || !element.offsetHeight) && element !== document.body); return element; } // @function getScale(el: HTMLElement): Object // Computes the CSS scale currently applied on the element. // Returns an object with `x` and `y` members as horizontal and vertical scales respectively, // and `boundingClientRect` as the result of [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). function getScale(element) { var rect = element.getBoundingClientRect(); // Read-only in old browsers. return { x: rect.width / element.offsetWidth || 1, y: rect.height / element.offsetHeight || 1, boundingClientRect: rect }; } var DomUtil = ({ TRANSFORM: TRANSFORM, TRANSITION: TRANSITION, TRANSITION_END: TRANSITION_END, get: get, getStyle: getStyle, create: create$1, remove: remove, empty: empty, toFront: toFront, toBack: toBack, hasClass: hasClass, addClass: addClass, removeClass: removeClass, setClass: setClass, getClass: getClass, setOpacity: setOpacity, testProp: testProp, setTransform: setTransform, setPosition: setPosition, getPosition: getPosition, disableTextSelection: disableTextSelection, enableTextSelection: enableTextSelection, disableImageDrag: disableImageDrag, enableImageDrag: enableImageDrag, preventOutline: preventOutline, restoreOutline: restoreOutline, getSizedParentNode: getSizedParentNode, getScale: getScale }); /* * @namespace DomEvent * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally. */ // Inspired by John Resig, Dean Edwards and YUI addEvent implementations. // @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this // Adds a listener function (`fn`) to a particular DOM event type of the // element `el`. You can optionally specify the context of the listener // (object the `this` keyword will point to). You can also pass several // space-separated types (e.g. `'click dblclick'`). // @alternative // @function on(el: HTMLElement, eventMap: Object, context?: Object): this // Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` function on(obj, types, fn, context) { if (typeof types === 'object') { for (var type in types) { addOne(obj, type, types[type], fn); } } else { types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { addOne(obj, types[i], fn, context); } } return this; } var eventsKey = '_leaflet_events'; // @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this // Removes a previously added listener function. // Note that if you passed a custom context to on, you must pass the same // context to `off` in order to remove the listener. // @alternative // @function off(el: HTMLElement, eventMap: Object, context?: Object): this // Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` function off(obj, types, fn, context) { if (typeof types === 'object') { for (var type in types) { removeOne(obj, type, types[type], fn); } } else if (types) { types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { removeOne(obj, types[i], fn, context); } } else { for (var j in obj[eventsKey]) { removeOne(obj, j, obj[eventsKey][j]); } delete obj[eventsKey]; } return this; } function browserFiresNativeDblClick() { // See https://github.com/w3c/pointerevents/issues/171 if (pointer) { return !(edge || safari); } } var mouseSubst = { mouseenter: 'mouseover', mouseleave: 'mouseout', wheel: !('onwheel' in window) && 'mousewheel' }; function addOne(obj, type, fn, context) { var id = type + stamp(fn) + (context ? '_' + stamp(context) : ''); if (obj[eventsKey] && obj[eventsKey][id]) { return this; } var handler = function (e) { return fn.call(context || obj, e || window.event); }; var originalHandler = handler; if (pointer && type.indexOf('touch') === 0) { // Needs DomEvent.Pointer.js addPointerListener(obj, type, handler, id); } else if (touch && (type === 'dblclick') && !browserFiresNativeDblClick()) { addDoubleTapListener(obj, handler, id); } else if ('addEventListener' in obj) { if (type === 'touchstart' || type === 'touchmove' || type === 'wheel' || type === 'mousewheel') { obj.addEventListener(mouseSubst[type] || type, handler, passiveEvents ? {passive: false} : false); } else if (type === 'mouseenter' || type === 'mouseleave') { handler = function (e) { e = e || window.event; if (isExternalTarget(obj, e)) { originalHandler(e); } }; obj.addEventListener(mouseSubst[type], handler, false); } else { obj.addEventListener(type, originalHandler, false); } } else if ('attachEvent' in obj) { obj.attachEvent('on' + type, handler); } obj[eventsKey] = obj[eventsKey] || {}; obj[eventsKey][id] = handler; } function removeOne(obj, type, fn, context) { var id = type + stamp(fn) + (context ? '_' + stamp(context) : ''), handler = obj[eventsKey] && obj[eventsKey][id]; if (!handler) { return this; } if (pointer && type.indexOf('touch') === 0) { removePointerListener(obj, type, id); } else if (touch && (type === 'dblclick') && !browserFiresNativeDblClick()) { removeDoubleTapListener(obj, id); } else if ('removeEventListener' in obj) { obj.removeEventListener(mouseSubst[type] || type, handler, false); } else if ('detachEvent' in obj) { obj.detachEvent('on' + type, handler); } obj[eventsKey][id] = null; } // @function stopPropagation(ev: DOMEvent): this // Stop the given event from propagation to parent elements. Used inside the listener functions: // ```js // L.DomEvent.on(div, 'click', function (ev) { // L.DomEvent.stopPropagation(ev); // }); // ``` function stopPropagation(e) { if (e.stopPropagation) { e.stopPropagation(); } else if (e.originalEvent) { // In case of Leaflet event. e.originalEvent._stopped = true; } else { e.cancelBubble = true; } skipped(e); return this; } // @function disableScrollPropagation(el: HTMLElement): this // Adds `stopPropagation` to the element's `'wheel'` events (plus browser variants). function disableScrollPropagation(el) { addOne(el, 'wheel', stopPropagation); return this; } // @function disableClickPropagation(el: HTMLElement): this // Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`, // `'mousedown'` and `'touchstart'` events (plus browser variants). function disableClickPropagation(el) { on(el, 'mousedown touchstart dblclick', stopPropagation); addOne(el, 'click', fakeStop); return this; } // @function preventDefault(ev: DOMEvent): this // Prevents the default action of the DOM Event `ev` from happening (such as // following a link in the href of the a element, or doing a POST request // with page reload when a `
` is submitted). // Use it inside listener functions. function preventDefault(e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } return this; } // @function stop(ev: DOMEvent): this // Does `stopPropagation` and `preventDefault` at the same time. function stop(e) { preventDefault(e); stopPropagation(e); return this; } // @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point // Gets normalized mouse position from a DOM event relative to the // `container` (border excluded) or to the whole page if not specified. function getMousePosition(e, container) { if (!container) { return new Point(e.clientX, e.clientY); } var scale = getScale(container), offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y) return new Point( // offset.left/top values are in page scale (like clientX/Y), // whereas clientLeft/Top (border width) values are the original values (before CSS scale applies). (e.clientX - offset.left) / scale.x - container.clientLeft, (e.clientY - offset.top) / scale.y - container.clientTop ); } // Chrome on Win scrolls double the pixels as in other platforms (see #4538), // and Firefox scrolls device pixels, not CSS pixels var wheelPxFactor = (win && chrome) ? 2 * window.devicePixelRatio : gecko ? window.devicePixelRatio : 1; // @function getWheelDelta(ev: DOMEvent): Number // Gets normalized wheel delta from a wheel DOM event, in vertical // pixels scrolled (negative if scrolling down). // Events from pointing devices without precise scrolling are mapped to // a best guess of 60 pixels. function getWheelDelta(e) { return (edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta (e.deltaY && e.deltaMode === 0) ? -e.deltaY / wheelPxFactor : // Pixels (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages (e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages 0; } var skipEvents = {}; function fakeStop(e) { // fakes stopPropagation by setting a special event flag, checked/reset with skipped(e) skipEvents[e.type] = true; } function skipped(e) { var events = skipEvents[e.type]; // reset when checking, as it's only used in map container and propagates outside of the map skipEvents[e.type] = false; return events; } // check if element really left/entered the event target (for mouseenter/mouseleave) function isExternalTarget(el, e) { var related = e.relatedTarget; if (!related) { return true; } try { while (related && (related !== el)) { related = related.parentNode; } } catch (err) { return false; } return (related !== el); } var DomEvent = ({ on: on, off: off, stopPropagation: stopPropagation, disableScrollPropagation: disableScrollPropagation, disableClickPropagation: disableClickPropagation, preventDefault: preventDefault, stop: stop, getMousePosition: getMousePosition, getWheelDelta: getWheelDelta, fakeStop: fakeStop, skipped: skipped, isExternalTarget: isExternalTarget, addListener: on, removeListener: off }); /* * @class PosAnimation * @aka L.PosAnimation * @inherits Evented * Used internally for panning animations, utilizing CSS3 Transitions for modern browsers and a timer fallback for IE6-9. * * @example * ```js * var fx = new L.PosAnimation(); * fx.run(el, [300, 500], 0.5); * ``` * * @constructor L.PosAnimation() * Creates a `PosAnimation` object. * */ var PosAnimation = Evented.extend({ // @method run(el: HTMLElement, newPos: Point, duration?: Number, easeLinearity?: Number) // Run an animation of a given element to a new position, optionally setting // duration in seconds (`0.25` by default) and easing linearity factor (3rd // argument of the [cubic bezier curve](http://cubic-bezier.com/#0,0,.5,1), // `0.5` by default). run: function (el, newPos, duration, easeLinearity) { this.stop(); this._el = el; this._inProgress = true; this._duration = duration || 0.25; this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2); this._startPos = getPosition(el); this._offset = newPos.subtract(this._startPos); this._startTime = +new Date(); // @event start: Event // Fired when the animation starts this.fire('start'); this._animate(); }, // @method stop() // Stops the animation (if currently running). stop: function () { if (!this._inProgress) { return; } this._step(true); this._complete(); }, _animate: function () { // animation loop this._animId = requestAnimFrame(this._animate, this); this._step(); }, _step: function (round) { var elapsed = (+new Date()) - this._startTime, duration = this._duration * 1000; if (elapsed < duration) { this._runFrame(this._easeOut(elapsed / duration), round); } else { this._runFrame(1); this._complete(); } }, _runFrame: function (progress, round) { var pos = this._startPos.add(this._offset.multiplyBy(progress)); if (round) { pos._round(); } setPosition(this._el, pos); // @event step: Event // Fired continuously during the animation. this.fire('step'); }, _complete: function () { cancelAnimFrame(this._animId); this._inProgress = false; // @event end: Event // Fired when the animation ends. this.fire('end'); }, _easeOut: function (t) { return 1 - Math.pow(1 - t, this._easeOutPower); } }); /* * @class Map * @aka L.Map * @inherits Evented * * The central class of the API — it is used to create a map on a page and manipulate it. * * @example * * ```js * // initialize the map on the "map" div with a given center and zoom * var map = L.map('map', { * center: [51.505, -0.09], * zoom: 13 * }); * ``` * */ var Map = Evented.extend({ options: { // @section Map State Options // @option crs: CRS = L.CRS.EPSG3857 // The [Coordinate Reference System](#crs) to use. Don't change this if you're not // sure what it means. crs: EPSG3857, // @option center: LatLng = undefined // Initial geographic center of the map center: undefined, // @option zoom: Number = undefined // Initial map zoom level zoom: undefined, // @option minZoom: Number = * // Minimum zoom level of the map. // If not specified and at least one `GridLayer` or `TileLayer` is in the map, // the lowest of their `minZoom` options will be used instead. minZoom: undefined, // @option maxZoom: Number = * // Maximum zoom level of the map. // If not specified and at least one `GridLayer` or `TileLayer` is in the map, // the highest of their `maxZoom` options will be used instead. maxZoom: undefined, // @option layers: Layer[] = [] // Array of layers that will be added to the map initially layers: [], // @option maxBounds: LatLngBounds = null // When this option is set, the map restricts the view to the given // geographical bounds, bouncing the user back if the user tries to pan // outside the view. To set the restriction dynamically, use // [`setMaxBounds`](#map-setmaxbounds) method. maxBounds: undefined, // @option renderer: Renderer = * // The default method for drawing vector layers on the map. `L.SVG` // or `L.Canvas` by default depending on browser support. renderer: undefined, // @section Animation Options // @option zoomAnimation: Boolean = true // Whether the map zoom animation is enabled. By default it's enabled // in all browsers that support CSS3 Transitions except Android. zoomAnimation: true, // @option zoomAnimationThreshold: Number = 4 // Won't animate zoom if the zoom difference exceeds this value. zoomAnimationThreshold: 4, // @option fadeAnimation: Boolean = true // Whether the tile fade animation is enabled. By default it's enabled // in all browsers that support CSS3 Transitions except Android. fadeAnimation: true, // @option markerZoomAnimation: Boolean = true // Whether markers animate their zoom with the zoom animation, if disabled // they will disappear for the length of the animation. By default it's // enabled in all browsers that support CSS3 Transitions except Android. markerZoomAnimation: true, // @option transform3DLimit: Number = 2^23 // Defines the maximum size of a CSS translation transform. The default // value should not be changed unless a web browser positions layers in // the wrong place after doing a large `panBy`. transform3DLimit: 8388608, // Precision limit of a 32-bit float // @section Interaction Options // @option zoomSnap: Number = 1 // Forces the map's zoom level to always be a multiple of this, particularly // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom. // By default, the zoom level snaps to the nearest integer; lower values // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0` // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom. zoomSnap: 1, // @option zoomDelta: Number = 1 // Controls how much the map's zoom level will change after a // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+` // or `-` on the keyboard, or using the [zoom controls](#control-zoom). // Values smaller than `1` (e.g. `0.5`) allow for greater granularity. zoomDelta: 1, // @option trackResize: Boolean = true // Whether the map automatically handles browser window resize to update itself. trackResize: true }, initialize: function (id, options) { // (HTMLElement or String, Object) options = setOptions(this, options); // Make sure to assign internal flags at the beginning, // to avoid inconsistent state in some edge cases. this._handlers = []; this._layers = {}; this._zoomBoundLayers = {}; this._sizeChanged = true; this._initContainer(id); this._initLayout(); // hack for https://github.com/Leaflet/Leaflet/issues/1980 this._onResize = bind(this._onResize, this); this._initEvents(); if (options.maxBounds) { this.setMaxBounds(options.maxBounds); } if (options.zoom !== undefined) { this._zoom = this._limitZoom(options.zoom); } if (options.center && options.zoom !== undefined) { this.setView(toLatLng(options.center), options.zoom, {reset: true}); } this.callInitHooks(); // don't animate on browsers without hardware-accelerated transitions or old Android/Opera this._zoomAnimated = TRANSITION && any3d && !mobileOpera && this.options.zoomAnimation; // zoom transitions run with the same duration for all layers, so if one of transitionend events // happens after starting zoom animation (propagating to the map pane), we know that it ended globally if (this._zoomAnimated) { this._createAnimProxy(); on(this._proxy, TRANSITION_END, this._catchTransitionEnd, this); } this._addLayers(this.options.layers); }, // @section Methods for modifying map state // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this // Sets the view of the map (geographical center and zoom) with the given // animation options. setView: function (center, zoom, options) { zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom); center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds); options = options || {}; this._stop(); if (this._loaded && !options.reset && options !== true) { if (options.animate !== undefined) { options.zoom = extend({animate: options.animate}, options.zoom); options.pan = extend({animate: options.animate, duration: options.duration}, options.pan); } // try animating pan or zoom var moved = (this._zoom !== zoom) ? this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : this._tryAnimatedPan(center, options.pan); if (moved) { // prevent resize handler call, the view will refresh after animation anyway clearTimeout(this._sizeTimer); return this; } } // animation didn't start, just reset the map view this._resetView(center, zoom); return this; }, // @method setZoom(zoom: Number, options?: Zoom/pan options): this // Sets the zoom of the map. setZoom: function (zoom, options) { if (!this._loaded) { this._zoom = zoom; return this; } return this.setView(this.getCenter(), zoom, {zoom: options}); }, // @method zoomIn(delta?: Number, options?: Zoom options): this // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). zoomIn: function (delta, options) { delta = delta || (any3d ? this.options.zoomDelta : 1); return this.setZoom(this._zoom + delta, options); }, // @method zoomOut(delta?: Number, options?: Zoom options): this // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). zoomOut: function (delta, options) { delta = delta || (any3d ? this.options.zoomDelta : 1); return this.setZoom(this._zoom - delta, options); }, // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this // Zooms the map while keeping a specified geographical point on the map // stationary (e.g. used internally for scroll zoom and double-click zoom). // @alternative // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary. setZoomAround: function (latlng, zoom, options) { var scale = this.getZoomScale(zoom), viewHalf = this.getSize().divideBy(2), containerPoint = latlng instanceof Point ? latlng : this.latLngToContainerPoint(latlng), centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); return this.setView(newCenter, zoom, {zoom: options}); }, _getBoundsCenterZoom: function (bounds, options) { options = options || {}; bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds); var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]), zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom; if (zoom === Infinity) { return { center: bounds.getCenter(), zoom: zoom }; } var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), swPoint = this.project(bounds.getSouthWest(), zoom), nePoint = this.project(bounds.getNorthEast(), zoom), center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); return { center: center, zoom: zoom }; }, // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this // Sets a map view that contains the given geographical bounds with the // maximum zoom level possible. fitBounds: function (bounds, options) { bounds = toLatLngBounds(bounds); if (!bounds.isValid()) { throw new Error('Bounds are not valid.'); } var target = this._getBoundsCenterZoom(bounds, options); return this.setView(target.center, target.zoom, options); }, // @method fitWorld(options?: fitBounds options): this // Sets a map view that mostly contains the whole world with the maximum // zoom level possible. fitWorld: function (options) { return this.fitBounds([[-90, -180], [90, 180]], options); }, // @method panTo(latlng: LatLng, options?: Pan options): this // Pans the map to a given center. panTo: function (center, options) { // (LatLng) return this.setView(center, this._zoom, {pan: options}); }, // @method panBy(offset: Point, options?: Pan options): this // Pans the map by a given number of pixels (animated). panBy: function (offset, options) { offset = toPoint(offset).round(); options = options || {}; if (!offset.x && !offset.y) { return this.fire('moveend'); } // If we pan too far, Chrome gets issues with tiles // and makes them disappear or appear in the wrong place (slightly offset) #2602 if (options.animate !== true && !this.getSize().contains(offset)) { this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom()); return this; } if (!this._panAnim) { this._panAnim = new PosAnimation(); this._panAnim.on({ 'step': this._onPanTransitionStep, 'end': this._onPanTransitionEnd }, this); } // don't fire movestart if animating inertia if (!options.noMoveStart) { this.fire('movestart'); } // animate pan unless animate: false specified if (options.animate !== false) { addClass(this._mapPane, 'leaflet-pan-anim'); var newPos = this._getMapPanePos().subtract(offset).round(); this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity); } else { this._rawPanBy(offset); this.fire('move').fire('moveend'); } return this; }, // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this // Sets the view of the map (geographical center and zoom) performing a smooth // pan-zoom animation. flyTo: function (targetCenter, targetZoom, options) { options = options || {}; if (options.animate === false || !any3d) { return this.setView(targetCenter, targetZoom, options); } this._stop(); var from = this.project(this.getCenter()), to = this.project(targetCenter), size = this.getSize(), startZoom = this._zoom; targetCenter = toLatLng(targetCenter); targetZoom = targetZoom === undefined ? startZoom : targetZoom; var w0 = Math.max(size.x, size.y), w1 = w0 * this.getZoomScale(startZoom, targetZoom), u1 = (to.distanceTo(from)) || 1, rho = 1.42, rho2 = rho * rho; function r(i) { var s1 = i ? -1 : 1, s2 = i ? w1 : w0, t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1, b1 = 2 * s2 * rho2 * u1, b = t1 / b1, sq = Math.sqrt(b * b + 1) - b; // workaround for floating point precision bug when sq = 0, log = -Infinite, // thus triggering an infinite loop in flyTo var log = sq < 0.000000001 ? -18 : Math.log(sq); return log; } function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } function tanh(n) { return sinh(n) / cosh(n); } var r0 = r(0); function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); } function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; } function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); } var start = Date.now(), S = (r(1) - r0) / rho, duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8; function frame() { var t = (Date.now() - start) / duration, s = easeOut(t) * S; if (t <= 1) { this._flyToFrame = requestAnimFrame(frame, this); this._move( this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom), this.getScaleZoom(w0 / w(s), startZoom), {flyTo: true}); } else { this ._move(targetCenter, targetZoom) ._moveEnd(true); } } this._moveStart(true, options.noMoveStart); frame.call(this); return this; }, // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto), // but takes a bounds parameter like [`fitBounds`](#map-fitbounds). flyToBounds: function (bounds, options) { var target = this._getBoundsCenterZoom(bounds, options); return this.flyTo(target.center, target.zoom, options); }, // @method setMaxBounds(bounds: LatLngBounds): this // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option). setMaxBounds: function (bounds) { bounds = toLatLngBounds(bounds); if (!bounds.isValid()) { this.options.maxBounds = null; return this.off('moveend', this._panInsideMaxBounds); } else if (this.options.maxBounds) { this.off('moveend', this._panInsideMaxBounds); } this.options.maxBounds = bounds; if (this._loaded) { this._panInsideMaxBounds(); } return this.on('moveend', this._panInsideMaxBounds); }, // @method setMinZoom(zoom: Number): this // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option). setMinZoom: function (zoom) { var oldZoom = this.options.minZoom; this.options.minZoom = zoom; if (this._loaded && oldZoom !== zoom) { this.fire('zoomlevelschange'); if (this.getZoom() < this.options.minZoom) { return this.setZoom(zoom); } } return this; }, // @method setMaxZoom(zoom: Number): this // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option). setMaxZoom: function (zoom) { var oldZoom = this.options.maxZoom; this.options.maxZoom = zoom; if (this._loaded && oldZoom !== zoom) { this.fire('zoomlevelschange'); if (this.getZoom() > this.options.maxZoom) { return this.setZoom(zoom); } } return this; }, // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this // Pans the map to the closest view that would lie inside the given bounds (if it's not already), controlling the animation using the options specific, if any. panInsideBounds: function (bounds, options) { this._enforcingBounds = true; var center = this.getCenter(), newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds)); if (!center.equals(newCenter)) { this.panTo(newCenter, options); } this._enforcingBounds = false; return this; }, // @method panInside(latlng: LatLng, options?: options): this // Pans the map the minimum amount to make the `latlng` visible. Use // `padding`, `paddingTopLeft` and `paddingTopRight` options to fit // the display to more restricted bounds, like [`fitBounds`](#map-fitbounds). // If `latlng` is already within the (optionally padded) display bounds, // the map will not be panned. panInside: function (latlng, options) { options = options || {}; var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]), center = this.getCenter(), pixelCenter = this.project(center), pixelPoint = this.project(latlng), pixelBounds = this.getPixelBounds(), halfPixelBounds = pixelBounds.getSize().divideBy(2), paddedBounds = toBounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]); if (!paddedBounds.contains(pixelPoint)) { this._enforcingBounds = true; var diff = pixelCenter.subtract(pixelPoint), newCenter = toPoint(pixelPoint.x + diff.x, pixelPoint.y + diff.y); if (pixelPoint.x < paddedBounds.min.x || pixelPoint.x > paddedBounds.max.x) { newCenter.x = pixelCenter.x - diff.x; if (diff.x > 0) { newCenter.x += halfPixelBounds.x - paddingTL.x; } else { newCenter.x -= halfPixelBounds.x - paddingBR.x; } } if (pixelPoint.y < paddedBounds.min.y || pixelPoint.y > paddedBounds.max.y) { newCenter.y = pixelCenter.y - diff.y; if (diff.y > 0) { newCenter.y += halfPixelBounds.y - paddingTL.y; } else { newCenter.y -= halfPixelBounds.y - paddingBR.y; } } this.panTo(this.unproject(newCenter), options); this._enforcingBounds = false; } return this; }, // @method invalidateSize(options: Zoom/pan options): this // Checks if the map container size changed and updates the map if so — // call it after you've changed the map size dynamically, also animating // pan by default. If `options.pan` is `false`, panning will not occur. // If `options.debounceMoveend` is `true`, it will delay `moveend` event so // that it doesn't happen often even if the method is called many // times in a row. // @alternative // @method invalidateSize(animate: Boolean): this // Checks if the map container size changed and updates the map if so — // call it after you've changed the map size dynamically, also animating // pan by default. invalidateSize: function (options) { if (!this._loaded) { return this; } options = extend({ animate: false, pan: true }, options === true ? {animate: true} : options); var oldSize = this.getSize(); this._sizeChanged = true; this._lastCenter = null; var newSize = this.getSize(), oldCenter = oldSize.divideBy(2).round(), newCenter = newSize.divideBy(2).round(), offset = oldCenter.subtract(newCenter); if (!offset.x && !offset.y) { return this; } if (options.animate && options.pan) { this.panBy(offset); } else { if (options.pan) { this._rawPanBy(offset); } this.fire('move'); if (options.debounceMoveend) { clearTimeout(this._sizeTimer); this._sizeTimer = setTimeout(bind(this.fire, this, 'moveend'), 200); } else { this.fire('moveend'); } } // @section Map state change events // @event resize: ResizeEvent // Fired when the map is resized. return this.fire('resize', { oldSize: oldSize, newSize: newSize }); }, // @section Methods for modifying map state // @method stop(): this // Stops the currently running `panTo` or `flyTo` animation, if any. stop: function () { this.setZoom(this._limitZoom(this._zoom)); if (!this.options.zoomSnap) { this.fire('viewreset'); } return this._stop(); }, // @section Geolocation methods // @method locate(options?: Locate options): this // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound) // event with location data on success or a [`locationerror`](#map-locationerror) event on failure, // and optionally sets the map view to the user's location with respect to // detection accuracy (or to the world view if geolocation failed). // Note that, if your page doesn't use HTTPS, this method will fail in // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins)) // See `Locate options` for more details. locate: function (options) { options = this._locateOptions = extend({ timeout: 10000, watch: false // setView: false // maxZoom: // maximumAge: 0 // enableHighAccuracy: false }, options); if (!('geolocation' in navigator)) { this._handleGeolocationError({ code: 0, message: 'Geolocation not supported.' }); return this; } var onResponse = bind(this._handleGeolocationResponse, this), onError = bind(this._handleGeolocationError, this); if (options.watch) { this._locationWatchId = navigator.geolocation.watchPosition(onResponse, onError, options); } else { navigator.geolocation.getCurrentPosition(onResponse, onError, options); } return this; }, // @method stopLocate(): this // Stops watching location previously initiated by `map.locate({watch: true})` // and aborts resetting the map view if map.locate was called with // `{setView: true}`. stopLocate: function () { if (navigator.geolocation && navigator.geolocation.clearWatch) { navigator.geolocation.clearWatch(this._locationWatchId); } if (this._locateOptions) { this._locateOptions.setView = false; } return this; }, _handleGeolocationError: function (error) { var c = error.code, message = error.message || (c === 1 ? 'permission denied' : (c === 2 ? 'position unavailable' : 'timeout')); if (this._locateOptions.setView && !this._loaded) { this.fitWorld(); } // @section Location events // @event locationerror: ErrorEvent // Fired when geolocation (using the [`locate`](#map-locate) method) failed. this.fire('locationerror', { code: c, message: 'Geolocation error: ' + message + '.' }); }, _handleGeolocationResponse: function (pos) { var lat = pos.coords.latitude, lng = pos.coords.longitude, latlng = new LatLng(lat, lng), bounds = latlng.toBounds(pos.coords.accuracy * 2), options = this._locateOptions; if (options.setView) { var zoom = this.getBoundsZoom(bounds); this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); } var data = { latlng: latlng, bounds: bounds, timestamp: pos.timestamp }; for (var i in pos.coords) { if (typeof pos.coords[i] === 'number') { data[i] = pos.coords[i]; } } // @event locationfound: LocationEvent // Fired when geolocation (using the [`locate`](#map-locate) method) // went successfully. this.fire('locationfound', data); }, // TODO Appropriate docs section? // @section Other Methods // @method addHandler(name: String, HandlerClass: Function): this // Adds a new `Handler` to the map, given its name and constructor function. addHandler: function (name, HandlerClass) { if (!HandlerClass) { return this; } var handler = this[name] = new HandlerClass(this); this._handlers.push(handler); if (this.options[name]) { handler.enable(); } return this; }, // @method remove(): this // Destroys the map and clears all related event listeners. remove: function () { this._initEvents(true); this.off('moveend', this._panInsideMaxBounds); if (this._containerId !== this._container._leaflet_id) { throw new Error('Map container is being reused by another instance'); } try { // throws error in IE6-8 delete this._container._leaflet_id; delete this._containerId; } catch (e) { /*eslint-disable */ this._container._leaflet_id = undefined; /* eslint-enable */ this._containerId = undefined; } if (this._locationWatchId !== undefined) { this.stopLocate(); } this._stop(); remove(this._mapPane); if (this._clearControlPos) { this._clearControlPos(); } if (this._resizeRequest) { cancelAnimFrame(this._resizeRequest); this._resizeRequest = null; } this._clearHandlers(); if (this._loaded) { // @section Map state change events // @event unload: Event // Fired when the map is destroyed with [remove](#map-remove) method. this.fire('unload'); } var i; for (i in this._layers) { this._layers[i].remove(); } for (i in this._panes) { remove(this._panes[i]); } this._layers = []; this._panes = []; delete this._mapPane; delete this._renderer; return this; }, // @section Other Methods // @method createPane(name: String, container?: HTMLElement): HTMLElement // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already, // then returns it. The pane is created as a child of `container`, or // as a child of the main map pane if not set. createPane: function (name, container) { var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), pane = create$1('div', className, container || this._mapPane); if (name) { this._panes[name] = pane; } return pane; }, // @section Methods for Getting Map State // @method getCenter(): LatLng // Returns the geographical center of the map view getCenter: function () { this._checkIfLoaded(); if (this._lastCenter && !this._moved()) { return this._lastCenter; } return this.layerPointToLatLng(this._getCenterLayerPoint()); }, // @method getZoom(): Number // Returns the current zoom level of the map view getZoom: function () { return this._zoom; }, // @method getBounds(): LatLngBounds // Returns the geographical bounds visible in the current map view getBounds: function () { var bounds = this.getPixelBounds(), sw = this.unproject(bounds.getBottomLeft()), ne = this.unproject(bounds.getTopRight()); return new LatLngBounds(sw, ne); }, // @method getMinZoom(): Number // Returns the minimum zoom level of the map (if set in the `minZoom` option of the map or of any layers), or `0` by default. getMinZoom: function () { return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; }, // @method getMaxZoom(): Number // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers). getMaxZoom: function () { return this.options.maxZoom === undefined ? (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : this.options.maxZoom; }, // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean, padding?: Point): Number // Returns the maximum zoom level on which the given bounds fit to the map // view in its entirety. If `inside` (optional) is set to `true`, the method // instead returns the minimum zoom level on which the map view fits into // the given bounds in its entirety. getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number bounds = toLatLngBounds(bounds); padding = toPoint(padding || [0, 0]); var zoom = this.getZoom() || 0, min = this.getMinZoom(), max = this.getMaxZoom(), nw = bounds.getNorthWest(), se = bounds.getSouthEast(), size = this.getSize().subtract(padding), boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(), snap = any3d ? this.options.zoomSnap : 1, scalex = size.x / boundsSize.x, scaley = size.y / boundsSize.y, scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley); zoom = this.getScaleZoom(scale, zoom); if (snap) { zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap; } return Math.max(min, Math.min(max, zoom)); }, // @method getSize(): Point // Returns the current size of the map container (in pixels). getSize: function () { if (!this._size || this._sizeChanged) { this._size = new Point( this._container.clientWidth || 0, this._container.clientHeight || 0); this._sizeChanged = false; } return this._size.clone(); }, // @method getPixelBounds(): Bounds // Returns the bounds of the current map view in projected pixel // coordinates (sometimes useful in layer and overlay implementations). getPixelBounds: function (center, zoom) { var topLeftPoint = this._getTopLeftPoint(center, zoom); return new Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); }, // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to // the map pane? "left point of the map layer" can be confusing, specially // since there can be negative offsets. // @method getPixelOrigin(): Point // Returns the projected pixel coordinates of the top left point of // the map layer (useful in custom layer and overlay implementations). getPixelOrigin: function () { this._checkIfLoaded(); return this._pixelOrigin; }, // @method getPixelWorldBounds(zoom?: Number): Bounds // Returns the world's bounds in pixel coordinates for zoom level `zoom`. // If `zoom` is omitted, the map's current zoom level is used. getPixelWorldBounds: function (zoom) { return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom); }, // @section Other Methods // @method getPane(pane: String|HTMLElement): HTMLElement // Returns a [map pane](#map-pane), given its name or its HTML element (its identity). getPane: function (pane) { return typeof pane === 'string' ? this._panes[pane] : pane; }, // @method getPanes(): Object // Returns a plain object containing the names of all [panes](#map-pane) as keys and // the panes as values. getPanes: function () { return this._panes; }, // @method getContainer: HTMLElement // Returns the HTML element that contains the map. getContainer: function () { return this._container; }, // @section Conversion Methods // @method getZoomScale(toZoom: Number, fromZoom: Number): Number // Returns the scale factor to be applied to a map transition from zoom level // `fromZoom` to `toZoom`. Used internally to help with zoom animations. getZoomScale: function (toZoom, fromZoom) { // TODO replace with universal implementation after refactoring projections var crs = this.options.crs; fromZoom = fromZoom === undefined ? this._zoom : fromZoom; return crs.scale(toZoom) / crs.scale(fromZoom); }, // @method getScaleZoom(scale: Number, fromZoom: Number): Number // Returns the zoom level that the map would end up at, if it is at `fromZoom` // level and everything is scaled by a factor of `scale`. Inverse of // [`getZoomScale`](#map-getZoomScale). getScaleZoom: function (scale, fromZoom) { var crs = this.options.crs; fromZoom = fromZoom === undefined ? this._zoom : fromZoom; var zoom = crs.zoom(scale * crs.scale(fromZoom)); return isNaN(zoom) ? Infinity : zoom; }, // @method project(latlng: LatLng, zoom: Number): Point // Projects a geographical coordinate `LatLng` according to the projection // of the map's CRS, then scales it according to `zoom` and the CRS's // `Transformation`. The result is pixel coordinate relative to // the CRS origin. project: function (latlng, zoom) { zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.latLngToPoint(toLatLng(latlng), zoom); }, // @method unproject(point: Point, zoom: Number): LatLng // Inverse of [`project`](#map-project). unproject: function (point, zoom) { zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.pointToLatLng(toPoint(point), zoom); }, // @method layerPointToLatLng(point: Point): LatLng // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), // returns the corresponding geographical coordinate (for the current zoom level). layerPointToLatLng: function (point) { var projectedPoint = toPoint(point).add(this.getPixelOrigin()); return this.unproject(projectedPoint); }, // @method latLngToLayerPoint(latlng: LatLng): Point // Given a geographical coordinate, returns the corresponding pixel coordinate // relative to the [origin pixel](#map-getpixelorigin). latLngToLayerPoint: function (latlng) { var projectedPoint = this.project(toLatLng(latlng))._round(); return projectedPoint._subtract(this.getPixelOrigin()); }, // @method wrapLatLng(latlng: LatLng): LatLng // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the // CRS's bounds. // By default this means longitude is wrapped around the dateline so its // value is between -180 and +180 degrees. wrapLatLng: function (latlng) { return this.options.crs.wrapLatLng(toLatLng(latlng)); }, // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds // Returns a `LatLngBounds` with the same size as the given one, ensuring that // its center is within the CRS's bounds. // By default this means the center longitude is wrapped around the dateline so its // value is between -180 and +180 degrees, and the majority of the bounds // overlaps the CRS's bounds. wrapLatLngBounds: function (latlng) { return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng)); }, // @method distance(latlng1: LatLng, latlng2: LatLng): Number // Returns the distance between two geographical coordinates according to // the map's CRS. By default this measures distance in meters. distance: function (latlng1, latlng2) { return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2)); }, // @method containerPointToLayerPoint(point: Point): Point // Given a pixel coordinate relative to the map container, returns the corresponding // pixel coordinate relative to the [origin pixel](#map-getpixelorigin). containerPointToLayerPoint: function (point) { // (Point) return toPoint(point).subtract(this._getMapPanePos()); }, // @method layerPointToContainerPoint(point: Point): Point // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), // returns the corresponding pixel coordinate relative to the map container. layerPointToContainerPoint: function (point) { // (Point) return toPoint(point).add(this._getMapPanePos()); }, // @method containerPointToLatLng(point: Point): LatLng // Given a pixel coordinate relative to the map container, returns // the corresponding geographical coordinate (for the current zoom level). containerPointToLatLng: function (point) { var layerPoint = this.containerPointToLayerPoint(toPoint(point)); return this.layerPointToLatLng(layerPoint); }, // @method latLngToContainerPoint(latlng: LatLng): Point // Given a geographical coordinate, returns the corresponding pixel coordinate // relative to the map container. latLngToContainerPoint: function (latlng) { return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng))); }, // @method mouseEventToContainerPoint(ev: MouseEvent): Point // Given a MouseEvent object, returns the pixel coordinate relative to the // map container where the event took place. mouseEventToContainerPoint: function (e) { return getMousePosition(e, this._container); }, // @method mouseEventToLayerPoint(ev: MouseEvent): Point // Given a MouseEvent object, returns the pixel coordinate relative to // the [origin pixel](#map-getpixelorigin) where the event took place. mouseEventToLayerPoint: function (e) { return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); }, // @method mouseEventToLatLng(ev: MouseEvent): LatLng // Given a MouseEvent object, returns geographical coordinate where the // event took place. mouseEventToLatLng: function (e) { // (MouseEvent) return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); }, // map initialization methods _initContainer: function (id) { var container = this._container = get(id); if (!container) { throw new Error('Map container not found.'); } else if (container._leaflet_id) { throw new Error('Map container is already initialized.'); } on(container, 'scroll', this._onScroll, this); this._containerId = stamp(container); }, _initLayout: function () { var container = this._container; this._fadeAnimated = this.options.fadeAnimation && any3d; addClass(container, 'leaflet-container' + (touch ? ' leaflet-touch' : '') + (retina ? ' leaflet-retina' : '') + (ielt9 ? ' leaflet-oldie' : '') + (safari ? ' leaflet-safari' : '') + (this._fadeAnimated ? ' leaflet-fade-anim' : '')); var position = getStyle(container, 'position'); if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') { container.style.position = 'relative'; } this._initPanes(); if (this._initControlPos) { this._initControlPos(); } }, _initPanes: function () { var panes = this._panes = {}; this._paneRenderers = {}; // @section // // Panes are DOM elements used to control the ordering of layers on the map. You // can access panes with [`map.getPane`](#map-getpane) or // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the // [`map.createPane`](#map-createpane) method. // // Every map has the following default panes that differ only in zIndex. // // @pane mapPane: HTMLElement = 'auto' // Pane that contains all other map panes this._mapPane = this.createPane('mapPane', this._container); setPosition(this._mapPane, new Point(0, 0)); // @pane tilePane: HTMLElement = 200 // Pane for `GridLayer`s and `TileLayer`s this.createPane('tilePane'); // @pane overlayPane: HTMLElement = 400 // Pane for overlay shadows (e.g. `Marker` shadows) this.createPane('shadowPane'); // @pane shadowPane: HTMLElement = 500 // Pane for vectors (`Path`s, like `Polyline`s and `Polygon`s), `ImageOverlay`s and `VideoOverlay`s this.createPane('overlayPane'); // @pane markerPane: HTMLElement = 600 // Pane for `Icon`s of `Marker`s this.createPane('markerPane'); // @pane tooltipPane: HTMLElement = 650 // Pane for `Tooltip`s. this.createPane('tooltipPane'); // @pane popupPane: HTMLElement = 700 // Pane for `Popup`s. this.createPane('popupPane'); if (!this.options.markerZoomAnimation) { addClass(panes.markerPane, 'leaflet-zoom-hide'); addClass(panes.shadowPane, 'leaflet-zoom-hide'); } }, // private methods that modify map state // @section Map state change events _resetView: function (center, zoom) { setPosition(this._mapPane, new Point(0, 0)); var loading = !this._loaded; this._loaded = true; zoom = this._limitZoom(zoom); this.fire('viewprereset'); var zoomChanged = this._zoom !== zoom; this ._moveStart(zoomChanged, false) ._move(center, zoom) ._moveEnd(zoomChanged); // @event viewreset: Event // Fired when the map needs to redraw its content (this usually happens // on map zoom or load). Very useful for creating custom overlays. this.fire('viewreset'); // @event load: Event // Fired when the map is initialized (when its center and zoom are set // for the first time). if (loading) { this.fire('load'); } }, _moveStart: function (zoomChanged, noMoveStart) { // @event zoomstart: Event // Fired when the map zoom is about to change (e.g. before zoom animation). // @event movestart: Event // Fired when the view of the map starts changing (e.g. user starts dragging the map). if (zoomChanged) { this.fire('zoomstart'); } if (!noMoveStart) { this.fire('movestart'); } return this; }, _move: function (center, zoom, data) { if (zoom === undefined) { zoom = this._zoom; } var zoomChanged = this._zoom !== zoom; this._zoom = zoom; this._lastCenter = center; this._pixelOrigin = this._getNewPixelOrigin(center); // @event zoom: Event // Fired repeatedly during any change in zoom level, including zoom // and fly animations. if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530 this.fire('zoom', data); } // @event move: Event // Fired repeatedly during any movement of the map, including pan and // fly animations. return this.fire('move', data); }, _moveEnd: function (zoomChanged) { // @event zoomend: Event // Fired when the map has changed, after any animations. if (zoomChanged) { this.fire('zoomend'); } // @event moveend: Event // Fired when the center of the map stops changing (e.g. user stopped // dragging the map). return this.fire('moveend'); }, _stop: function () { cancelAnimFrame(this._flyToFrame); if (this._panAnim) { this._panAnim.stop(); } return this; }, _rawPanBy: function (offset) { setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); }, _getZoomSpan: function () { return this.getMaxZoom() - this.getMinZoom(); }, _panInsideMaxBounds: function () { if (!this._enforcingBounds) { this.panInsideBounds(this.options.maxBounds); } }, _checkIfLoaded: function () { if (!this._loaded) { throw new Error('Set map center and zoom first.'); } }, // DOM event handling // @section Interaction events _initEvents: function (remove$$1) { this._targets = {}; this._targets[stamp(this._container)] = this; var onOff = remove$$1 ? off : on; // @event click: MouseEvent // Fired when the user clicks (or taps) the map. // @event dblclick: MouseEvent // Fired when the user double-clicks (or double-taps) the map. // @event mousedown: MouseEvent // Fired when the user pushes the mouse button on the map. // @event mouseup: MouseEvent // Fired when the user releases the mouse button on the map. // @event mouseover: MouseEvent // Fired when the mouse enters the map. // @event mouseout: MouseEvent // Fired when the mouse leaves the map. // @event mousemove: MouseEvent // Fired while the mouse moves over the map. // @event contextmenu: MouseEvent // Fired when the user pushes the right mouse button on the map, prevents // default browser context menu from showing if there are listeners on // this event. Also fired on mobile when the user holds a single touch // for a second (also called long press). // @event keypress: KeyboardEvent // Fired when the user presses a key from the keyboard that produces a character value while the map is focused. // @event keydown: KeyboardEvent // Fired when the user presses a key from the keyboard while the map is focused. Unlike the `keypress` event, // the `keydown` event is fired for keys that produce a character value and for keys // that do not produce a character value. // @event keyup: KeyboardEvent // Fired when the user releases a key from the keyboard while the map is focused. onOff(this._container, 'click dblclick mousedown mouseup ' + 'mouseover mouseout mousemove contextmenu keypress keydown keyup', this._handleDOMEvent, this); if (this.options.trackResize) { onOff(window, 'resize', this._onResize, this); } if (any3d && this.options.transform3DLimit) { (remove$$1 ? this.off : this.on).call(this, 'moveend', this._onMoveEnd); } }, _onResize: function () { cancelAnimFrame(this._resizeRequest); this._resizeRequest = requestAnimFrame( function () { this.invalidateSize({debounceMoveend: true}); }, this); }, _onScroll: function () { this._container.scrollTop = 0; this._container.scrollLeft = 0; }, _onMoveEnd: function () { var pos = this._getMapPanePos(); if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) { // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have // a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/ this._resetView(this.getCenter(), this.getZoom()); } }, _findEventTargets: function (e, type) { var targets = [], target, isHover = type === 'mouseout' || type === 'mouseover', src = e.target || e.srcElement, dragging = false; while (src) { target = this._targets[stamp(src)]; if (target && (type === 'click' || type === 'preclick') && !e._simulated && this._draggableMoved(target)) { // Prevent firing click after you just dragged an object. dragging = true; break; } if (target && target.listens(type, true)) { if (isHover && !isExternalTarget(src, e)) { break; } targets.push(target); if (isHover) { break; } } if (src === this._container) { break; } src = src.parentNode; } if (!targets.length && !dragging && !isHover && isExternalTarget(src, e)) { targets = [this]; } return targets; }, _handleDOMEvent: function (e) { if (!this._loaded || skipped(e)) { return; } var type = e.type; if (type === 'mousedown' || type === 'keypress' || type === 'keyup' || type === 'keydown') { // prevents outline when clicking on keyboard-focusable element preventOutline(e.target || e.srcElement); } this._fireDOMEvent(e, type); }, _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'], _fireDOMEvent: function (e, type, targets) { if (e.type === 'click') { // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups). // @event preclick: MouseEvent // Fired before mouse click on the map (sometimes useful when you // want something to happen on click before any existing click // handlers start running). var synth = extend({}, e); synth.type = 'preclick'; this._fireDOMEvent(synth, synth.type, targets); } if (e._stopped) { return; } // Find the layer the event is propagating from and its parents. targets = (targets || []).concat(this._findEventTargets(e, type)); if (!targets.length) { return; } var target = targets[0]; if (type === 'contextmenu' && target.listens(type, true)) { preventDefault(e); } var data = { originalEvent: e }; if (e.type !== 'keypress' && e.type !== 'keydown' && e.type !== 'keyup') { var isMarker = target.getLatLng && (!target._radius || target._radius <= 10); data.containerPoint = isMarker ? this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e); data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint); } for (var i = 0; i < targets.length; i++) { targets[i].fire(type, data, true); if (data.originalEvent._stopped || (targets[i].options.bubblingMouseEvents === false && indexOf(this._mouseEvents, type) !== -1)) { return; } } }, _draggableMoved: function (obj) { obj = obj.dragging && obj.dragging.enabled() ? obj : this; return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()); }, _clearHandlers: function () { for (var i = 0, len = this._handlers.length; i < len; i++) { this._handlers[i].disable(); } }, // @section Other Methods // @method whenReady(fn: Function, context?: Object): this // Runs the given function `fn` when the map gets initialized with // a view (center and zoom) and at least one layer, or immediately // if it's already initialized, optionally passing a function context. whenReady: function (callback, context) { if (this._loaded) { callback.call(context || this, {target: this}); } else { this.on('load', callback, context); } return this; }, // private methods for getting map state _getMapPanePos: function () { return getPosition(this._mapPane) || new Point(0, 0); }, _moved: function () { var pos = this._getMapPanePos(); return pos && !pos.equals([0, 0]); }, _getTopLeftPoint: function (center, zoom) { var pixelOrigin = center && zoom !== undefined ? this._getNewPixelOrigin(center, zoom) : this.getPixelOrigin(); return pixelOrigin.subtract(this._getMapPanePos()); }, _getNewPixelOrigin: function (center, zoom) { var viewHalf = this.getSize()._divideBy(2); return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round(); }, _latLngToNewLayerPoint: function (latlng, zoom, center) { var topLeft = this._getNewPixelOrigin(center, zoom); return this.project(latlng, zoom)._subtract(topLeft); }, _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) { var topLeft = this._getNewPixelOrigin(center, zoom); return toBounds([ this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft), this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft), this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft), this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft) ]); }, // layer point of the current center _getCenterLayerPoint: function () { return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); }, // offset of the specified place to the current center in pixels _getCenterOffset: function (latlng) { return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); }, // adjust center for view to get inside bounds _limitCenter: function (center, zoom, bounds) { if (!bounds) { return center; } var centerPoint = this.project(center, zoom), viewHalf = this.getSize().divideBy(2), viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), offset = this._getBoundsOffset(viewBounds, bounds, zoom); // If offset is less than a pixel, ignore. // This prevents unstable projections from getting into // an infinite loop of tiny offsets. if (offset.round().equals([0, 0])) { return center; } return this.unproject(centerPoint.add(offset), zoom); }, // adjust offset for view to get inside bounds _limitOffset: function (offset, bounds) { if (!bounds) { return offset; } var viewBounds = this.getPixelBounds(), newBounds = new Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); return offset.add(this._getBoundsOffset(newBounds, bounds)); }, // returns offset needed for pxBounds to get inside maxBounds at a specified zoom _getBoundsOffset: function (pxBounds, maxBounds, zoom) { var projectedMaxBounds = toBounds( this.project(maxBounds.getNorthEast(), zoom), this.project(maxBounds.getSouthWest(), zoom) ), minOffset = projectedMaxBounds.min.subtract(pxBounds.min), maxOffset = projectedMaxBounds.max.subtract(pxBounds.max), dx = this._rebound(minOffset.x, -maxOffset.x), dy = this._rebound(minOffset.y, -maxOffset.y); return new Point(dx, dy); }, _rebound: function (left, right) { return left + right > 0 ? Math.round(left - right) / 2 : Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); }, _limitZoom: function (zoom) { var min = this.getMinZoom(), max = this.getMaxZoom(), snap = any3d ? this.options.zoomSnap : 1; if (snap) { zoom = Math.round(zoom / snap) * snap; } return Math.max(min, Math.min(max, zoom)); }, _onPanTransitionStep: function () { this.fire('move'); }, _onPanTransitionEnd: function () { removeClass(this._mapPane, 'leaflet-pan-anim'); this.fire('moveend'); }, _tryAnimatedPan: function (center, options) { // difference between the new and current centers in pixels var offset = this._getCenterOffset(center)._trunc(); // don't animate too far unless animate: true specified in options if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; } this.panBy(offset, options); return true; }, _createAnimProxy: function () { var proxy = this._proxy = create$1('div', 'leaflet-proxy leaflet-zoom-animated'); this._panes.mapPane.appendChild(proxy); this.on('zoomanim', function (e) { var prop = TRANSFORM, transform = this._proxy.style[prop]; setTransform(this._proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1)); // workaround for case when transform is the same and so transitionend event is not fired if (transform === this._proxy.style[prop] && this._animatingZoom) { this._onZoomTransitionEnd(); } }, this); this.on('load moveend', this._animMoveEnd, this); this._on('unload', this._destroyAnimProxy, this); }, _destroyAnimProxy: function () { remove(this._proxy); this.off('load moveend', this._animMoveEnd, this); delete this._proxy; }, _animMoveEnd: function () { var c = this.getCenter(), z = this.getZoom(); setTransform(this._proxy, this.project(c, z), this.getZoomScale(z, 1)); }, _catchTransitionEnd: function (e) { if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) { this._onZoomTransitionEnd(); } }, _nothingToAnimate: function () { return !this._container.getElementsByClassName('leaflet-zoom-animated').length; }, _tryAnimatedZoom: function (center, zoom, options) { if (this._animatingZoom) { return true; } options = options || {}; // don't animate if disabled, not supported or zoom difference is too large if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() || Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; } // offset is the pixel coords of the zoom origin relative to the current center var scale = this.getZoomScale(zoom), offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale); // don't animate if the zoom origin isn't within one screen from the current center, unless forced if (options.animate !== true && !this.getSize().contains(offset)) { return false; } requestAnimFrame(function () { this ._moveStart(true, false) ._animateZoom(center, zoom, true); }, this); return true; }, _animateZoom: function (center, zoom, startAnim, noUpdate) { if (!this._mapPane) { return; } if (startAnim) { this._animatingZoom = true; // remember what center/zoom to set after animation this._animateToCenter = center; this._animateToZoom = zoom; addClass(this._mapPane, 'leaflet-zoom-anim'); } // @section Other Events // @event zoomanim: ZoomAnimEvent // Fired at least once per zoom animation. For continuous zoom, like pinch zooming, fired once per frame during zoom. this.fire('zoomanim', { center: center, zoom: zoom, noUpdate: noUpdate }); // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693 setTimeout(bind(this._onZoomTransitionEnd, this), 250); }, _onZoomTransitionEnd: function () { if (!this._animatingZoom) { return; } if (this._mapPane) { removeClass(this._mapPane, 'leaflet-zoom-anim'); } this._animatingZoom = false; this._move(this._animateToCenter, this._animateToZoom); // This anim frame should prevent an obscure iOS webkit tile loading race condition. requestAnimFrame(function () { this._moveEnd(true); }, this); } }); // @section // @factory L.map(id: String, options?: Map options) // Instantiates a map object given the DOM ID of a `
` element // and optionally an object literal with `Map options`. // // @alternative // @factory L.map(el: HTMLElement, options?: Map options) // Instantiates a map object given an instance of a `
` HTML element // and optionally an object literal with `Map options`. function createMap(id, options) { return new Map(id, options); } /* * @class Control * @aka L.Control * @inherits Class * * L.Control is a base class for implementing map controls. Handles positioning. * All other controls extend from this class. */ var Control = Class.extend({ // @section // @aka Control options options: { // @option position: String = 'topright' // The position of the control (one of the map corners). Possible values are `'topleft'`, // `'topright'`, `'bottomleft'` or `'bottomright'` position: 'topright' }, initialize: function (options) { setOptions(this, options); }, /* @section * Classes extending L.Control will inherit the following methods: * * @method getPosition: string * Returns the position of the control. */ getPosition: function () { return this.options.position; }, // @method setPosition(position: string): this // Sets the position of the control. setPosition: function (position) { var map = this._map; if (map) { map.removeControl(this); } this.options.position = position; if (map) { map.addControl(this); } return this; }, // @method getContainer: HTMLElement // Returns the HTMLElement that contains the control. getContainer: function () { return this._container; }, // @method addTo(map: Map): this // Adds the control to the given map. addTo: function (map) { this.remove(); this._map = map; var container = this._container = this.onAdd(map), pos = this.getPosition(), corner = map._controlCorners[pos]; addClass(container, 'leaflet-control'); if (pos.indexOf('bottom') !== -1) { corner.insertBefore(container, corner.firstChild); } else { corner.appendChild(container); } this._map.on('unload', this.remove, this); return this; }, // @method remove: this // Removes the control from the map it is currently active on. remove: function () { if (!this._map) { return this; } remove(this._container); if (this.onRemove) { this.onRemove(this._map); } this._map.off('unload', this.remove, this); this._map = null; return this; }, _refocusOnMap: function (e) { // if map exists and event is not a keyboard event if (this._map && e && e.screenX > 0 && e.screenY > 0) { this._map.getContainer().focus(); } } }); var control = function (options) { return new Control(options); }; /* @section Extension methods * @uninheritable * * Every control should extend from `L.Control` and (re-)implement the following methods. * * @method onAdd(map: Map): HTMLElement * Should return the container DOM element for the control and add listeners on relevant map events. Called on [`control.addTo(map)`](#control-addTo). * * @method onRemove(map: Map) * Optional method. Should contain all clean up code that removes the listeners previously added in [`onAdd`](#control-onadd). Called on [`control.remove()`](#control-remove). */ /* @namespace Map * @section Methods for Layers and Controls */ Map.include({ // @method addControl(control: Control): this // Adds the given control to the map addControl: function (control) { control.addTo(this); return this; }, // @method removeControl(control: Control): this // Removes the given control from the map removeControl: function (control) { control.remove(); return this; }, _initControlPos: function () { var corners = this._controlCorners = {}, l = 'leaflet-', container = this._controlContainer = create$1('div', l + 'control-container', this._container); function createCorner(vSide, hSide) { var className = l + vSide + ' ' + l + hSide; corners[vSide + hSide] = create$1('div', className, container); } createCorner('top', 'left'); createCorner('top', 'right'); createCorner('bottom', 'left'); createCorner('bottom', 'right'); }, _clearControlPos: function () { for (var i in this._controlCorners) { remove(this._controlCorners[i]); } remove(this._controlContainer); delete this._controlCorners; delete this._controlContainer; } }); /* * @class Control.Layers * @aka L.Control.Layers * @inherits Control * * The layers control gives users the ability to switch between different base layers and switch overlays on/off (check out the [detailed example](http://leafletjs.com/examples/layers-control/)). Extends `Control`. * * @example * * ```js * var baseLayers = { * "Mapbox": mapbox, * "OpenStreetMap": osm * }; * * var overlays = { * "Marker": marker, * "Roads": roadsLayer * }; * * L.control.layers(baseLayers, overlays).addTo(map); * ``` * * The `baseLayers` and `overlays` parameters are object literals with layer names as keys and `Layer` objects as values: * * ```js * { * "": layer1, * "": layer2 * } * ``` * * The layer names can contain HTML, which allows you to add additional styling to the items: * * ```js * {" My Layer": myLayer} * ``` */ var Layers = Control.extend({ // @section // @aka Control.Layers options options: { // @option collapsed: Boolean = true // If `true`, the control will be collapsed into an icon and expanded on mouse hover or touch. collapsed: true, position: 'topright', // @option autoZIndex: Boolean = true // If `true`, the control will assign zIndexes in increasing order to all of its layers so that the order is preserved when switching them on/off. autoZIndex: true, // @option hideSingleBase: Boolean = false // If `true`, the base layers in the control will be hidden when there is only one. hideSingleBase: false, // @option sortLayers: Boolean = false // Whether to sort the layers. When `false`, layers will keep the order // in which they were added to the control. sortLayers: false, // @option sortFunction: Function = * // A [compare function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) // that will be used for sorting the layers, when `sortLayers` is `true`. // The function receives both the `L.Layer` instances and their names, as in // `sortFunction(layerA, layerB, nameA, nameB)`. // By default, it sorts layers alphabetically by their name. sortFunction: function (layerA, layerB, nameA, nameB) { return nameA < nameB ? -1 : (nameB < nameA ? 1 : 0); } }, initialize: function (baseLayers, overlays, options) { setOptions(this, options); this._layerControlInputs = []; this._layers = []; this._lastZIndex = 0; this._handlingClick = false; for (var i in baseLayers) { this._addLayer(baseLayers[i], i); } for (i in overlays) { this._addLayer(overlays[i], i, true); } }, onAdd: function (map) { this._initLayout(); this._update(); this._map = map; map.on('zoomend', this._checkDisabledLayers, this); for (var i = 0; i < this._layers.length; i++) { this._layers[i].layer.on('add remove', this._onLayerChange, this); } return this._container; }, addTo: function (map) { Control.prototype.addTo.call(this, map); // Trigger expand after Layers Control has been inserted into DOM so that is now has an actual height. return this._expandIfNotCollapsed(); }, onRemove: function () { this._map.off('zoomend', this._checkDisabledLayers, this); for (var i = 0; i < this._layers.length; i++) { this._layers[i].layer.off('add remove', this._onLayerChange, this); } }, // @method addBaseLayer(layer: Layer, name: String): this // Adds a base layer (radio button entry) with the given name to the control. addBaseLayer: function (layer, name) { this._addLayer(layer, name); return (this._map) ? this._update() : this; }, // @method addOverlay(layer: Layer, name: String): this // Adds an overlay (checkbox entry) with the given name to the control. addOverlay: function (layer, name) { this._addLayer(layer, name, true); return (this._map) ? this._update() : this; }, // @method removeLayer(layer: Layer): this // Remove the given layer from the control. removeLayer: function (layer) { layer.off('add remove', this._onLayerChange, this); var obj = this._getLayer(stamp(layer)); if (obj) { this._layers.splice(this._layers.indexOf(obj), 1); } return (this._map) ? this._update() : this; }, // @method expand(): this // Expand the control container if collapsed. expand: function () { addClass(this._container, 'leaflet-control-layers-expanded'); this._section.style.height = null; var acceptableHeight = this._map.getSize().y - (this._container.offsetTop + 50); if (acceptableHeight < this._section.clientHeight) { addClass(this._section, 'leaflet-control-layers-scrollbar'); this._section.style.height = acceptableHeight + 'px'; } else { removeClass(this._section, 'leaflet-control-layers-scrollbar'); } this._checkDisabledLayers(); return this; }, // @method collapse(): this // Collapse the control container if expanded. collapse: function () { removeClass(this._container, 'leaflet-control-layers-expanded'); return this; }, _initLayout: function () { var className = 'leaflet-control-layers', container = this._container = create$1('div', className), collapsed = this.options.collapsed; // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released container.setAttribute('aria-haspopup', true); disableClickPropagation(container); disableScrollPropagation(container); var section = this._section = create$1('section', className + '-list'); if (collapsed) { this._map.on('click', this.collapse, this); if (!android) { on(container, { mouseenter: this.expand, mouseleave: this.collapse }, this); } } var link = this._layersLink = create$1('a', className + '-toggle', container); link.href = '#'; link.title = 'Layers'; if (touch) { on(link, 'click', stop); on(link, 'click', this.expand, this); } else { on(link, 'focus', this.expand, this); } if (!collapsed) { this.expand(); } this._baseLayersList = create$1('div', className + '-base', section); this._separator = create$1('div', className + '-separator', section); this._overlaysList = create$1('div', className + '-overlays', section); container.appendChild(section); }, _getLayer: function (id) { for (var i = 0; i < this._layers.length; i++) { if (this._layers[i] && stamp(this._layers[i].layer) === id) { return this._layers[i]; } } }, _addLayer: function (layer, name, overlay) { if (this._map) { layer.on('add remove', this._onLayerChange, this); } this._layers.push({ layer: layer, name: name, overlay: overlay }); if (this.options.sortLayers) { this._layers.sort(bind(function (a, b) { return this.options.sortFunction(a.layer, b.layer, a.name, b.name); }, this)); } if (this.options.autoZIndex && layer.setZIndex) { this._lastZIndex++; layer.setZIndex(this._lastZIndex); } this._expandIfNotCollapsed(); }, _update: function () { if (!this._container) { return this; } empty(this._baseLayersList); empty(this._overlaysList); this._layerControlInputs = []; var baseLayersPresent, overlaysPresent, i, obj, baseLayersCount = 0; for (i = 0; i < this._layers.length; i++) { obj = this._layers[i]; this._addItem(obj); overlaysPresent = overlaysPresent || obj.overlay; baseLayersPresent = baseLayersPresent || !obj.overlay; baseLayersCount += !obj.overlay ? 1 : 0; } // Hide base layers section if there's only one layer. if (this.options.hideSingleBase) { baseLayersPresent = baseLayersPresent && baseLayersCount > 1; this._baseLayersList.style.display = baseLayersPresent ? '' : 'none'; } this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none'; return this; }, _onLayerChange: function (e) { if (!this._handlingClick) { this._update(); } var obj = this._getLayer(stamp(e.target)); // @namespace Map // @section Layer events // @event baselayerchange: LayersControlEvent // Fired when the base layer is changed through the [layers control](#control-layers). // @event overlayadd: LayersControlEvent // Fired when an overlay is selected through the [layers control](#control-layers). // @event overlayremove: LayersControlEvent // Fired when an overlay is deselected through the [layers control](#control-layers). // @namespace Control.Layers var type = obj.overlay ? (e.type === 'add' ? 'overlayadd' : 'overlayremove') : (e.type === 'add' ? 'baselayerchange' : null); if (type) { this._map.fire(type, obj); } }, // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe) _createRadioElement: function (name, checked) { var radioHtml = ''; var radioFragment = document.createElement('div'); radioFragment.innerHTML = radioHtml; return radioFragment.firstChild; }, _addItem: function (obj) { var label = document.createElement('label'), checked = this._map.hasLayer(obj.layer), input; if (obj.overlay) { input = document.createElement('input'); input.type = 'checkbox'; input.className = 'leaflet-control-layers-selector'; input.defaultChecked = checked; } else { input = this._createRadioElement('leaflet-base-layers_' + stamp(this), checked); } this._layerControlInputs.push(input); input.layerId = stamp(obj.layer); on(input, 'click', this._onInputClick, this); var name = document.createElement('span'); name.innerHTML = ' ' + obj.name; // Helps from preventing layer control flicker when checkboxes are disabled // https://github.com/Leaflet/Leaflet/issues/2771 var holder = document.createElement('div'); label.appendChild(holder); holder.appendChild(input); holder.appendChild(name); var container = obj.overlay ? this._overlaysList : this._baseLayersList; container.appendChild(label); this._checkDisabledLayers(); return label; }, _onInputClick: function () { var inputs = this._layerControlInputs, input, layer; var addedLayers = [], removedLayers = []; this._handlingClick = true; for (var i = inputs.length - 1; i >= 0; i--) { input = inputs[i]; layer = this._getLayer(input.layerId).layer; if (input.checked) { addedLayers.push(layer); } else if (!input.checked) { removedLayers.push(layer); } } // Bugfix issue 2318: Should remove all old layers before readding new ones for (i = 0; i < removedLayers.length; i++) { if (this._map.hasLayer(removedLayers[i])) { this._map.removeLayer(removedLayers[i]); } } for (i = 0; i < addedLayers.length; i++) { if (!this._map.hasLayer(addedLayers[i])) { this._map.addLayer(addedLayers[i]); } } this._handlingClick = false; this._refocusOnMap(); }, _checkDisabledLayers: function () { var inputs = this._layerControlInputs, input, layer, zoom = this._map.getZoom(); for (var i = inputs.length - 1; i >= 0; i--) { input = inputs[i]; layer = this._getLayer(input.layerId).layer; input.disabled = (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) || (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom); } }, _expandIfNotCollapsed: function () { if (this._map && !this.options.collapsed) { this.expand(); } return this; }, _expand: function () { // Backward compatibility, remove me in 1.1. return this.expand(); }, _collapse: function () { // Backward compatibility, remove me in 1.1. return this.collapse(); } }); // @factory L.control.layers(baselayers?: Object, overlays?: Object, options?: Control.Layers options) // Creates a layers control with the given layers. Base layers will be switched with radio buttons, while overlays will be switched with checkboxes. Note that all base layers should be passed in the base layers object, but only one should be added to the map during map instantiation. var layers = function (baseLayers, overlays, options) { return new Layers(baseLayers, overlays, options); }; /* * @class Control.Zoom * @aka L.Control.Zoom * @inherits Control * * A basic zoom control with two buttons (zoom in and zoom out). It is put on the map by default unless you set its [`zoomControl` option](#map-zoomcontrol) to `false`. Extends `Control`. */ var Zoom = Control.extend({ // @section // @aka Control.Zoom options options: { position: 'topleft', // @option zoomInText: String = '+' // The text set on the 'zoom in' button. zoomInText: '+', // @option zoomInTitle: String = 'Zoom in' // The title set on the 'zoom in' button. zoomInTitle: 'Zoom in', // @option zoomOutText: String = '−' // The text set on the 'zoom out' button. zoomOutText: '−', // @option zoomOutTitle: String = 'Zoom out' // The title set on the 'zoom out' button. zoomOutTitle: 'Zoom out' }, onAdd: function (map) { var zoomName = 'leaflet-control-zoom', container = create$1('div', zoomName + ' leaflet-bar'), options = this.options; this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle, zoomName + '-in', container, this._zoomIn); this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle, zoomName + '-out', container, this._zoomOut); this._updateDisabled(); map.on('zoomend zoomlevelschange', this._updateDisabled, this); return container; }, onRemove: function (map) { map.off('zoomend zoomlevelschange', this._updateDisabled, this); }, disable: function () { this._disabled = true; this._updateDisabled(); return this; }, enable: function () { this._disabled = false; this._updateDisabled(); return this; }, _zoomIn: function (e) { if (!this._disabled && this._map._zoom < this._map.getMaxZoom()) { this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); } }, _zoomOut: function (e) { if (!this._disabled && this._map._zoom > this._map.getMinZoom()) { this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); } }, _createButton: function (html, title, className, container, fn) { var link = create$1('a', className, container); link.innerHTML = html; link.href = '#'; link.title = title; /* * Will force screen readers like VoiceOver to read this as "Zoom in - button" */ link.setAttribute('role', 'button'); link.setAttribute('aria-label', title); disableClickPropagation(link); on(link, 'click', stop); on(link, 'click', fn, this); on(link, 'click', this._refocusOnMap, this); return link; }, _updateDisabled: function () { var map = this._map, className = 'leaflet-disabled'; removeClass(this._zoomInButton, className); removeClass(this._zoomOutButton, className); if (this._disabled || map._zoom === map.getMinZoom()) { addClass(this._zoomOutButton, className); } if (this._disabled || map._zoom === map.getMaxZoom()) { addClass(this._zoomInButton, className); } } }); // @namespace Map // @section Control options // @option zoomControl: Boolean = true // Whether a [zoom control](#control-zoom) is added to the map by default. Map.mergeOptions({ zoomControl: true }); Map.addInitHook(function () { if (this.options.zoomControl) { // @section Controls // @property zoomControl: Control.Zoom // The default zoom control (only available if the // [`zoomControl` option](#map-zoomcontrol) was `true` when creating the map). this.zoomControl = new Zoom(); this.addControl(this.zoomControl); } }); // @namespace Control.Zoom // @factory L.control.zoom(options: Control.Zoom options) // Creates a zoom control var zoom = function (options) { return new Zoom(options); }; /* * @class Control.Scale * @aka L.Control.Scale * @inherits Control * * A simple scale control that shows the scale of the current center of screen in metric (m/km) and imperial (mi/ft) systems. Extends `Control`. * * @example * * ```js * L.control.scale().addTo(map); * ``` */ var Scale = Control.extend({ // @section // @aka Control.Scale options options: { position: 'bottomleft', // @option maxWidth: Number = 100 // Maximum width of the control in pixels. The width is set dynamically to show round values (e.g. 100, 200, 500). maxWidth: 100, // @option metric: Boolean = True // Whether to show the metric scale line (m/km). metric: true, // @option imperial: Boolean = True // Whether to show the imperial scale line (mi/ft). imperial: true // @option updateWhenIdle: Boolean = false // If `true`, the control is updated on [`moveend`](#map-moveend), otherwise it's always up-to-date (updated on [`move`](#map-move)). }, onAdd: function (map) { var className = 'leaflet-control-scale', container = create$1('div', className), options = this.options; this._addScales(options, className + '-line', container); map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this); map.whenReady(this._update, this); return container; }, onRemove: function (map) { map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this); }, _addScales: function (options, className, container) { if (options.metric) { this._mScale = create$1('div', className, container); } if (options.imperial) { this._iScale = create$1('div', className, container); } }, _update: function () { var map = this._map, y = map.getSize().y / 2; var maxMeters = map.distance( map.containerPointToLatLng([0, y]), map.containerPointToLatLng([this.options.maxWidth, y])); this._updateScales(maxMeters); }, _updateScales: function (maxMeters) { if (this.options.metric && maxMeters) { this._updateMetric(maxMeters); } if (this.options.imperial && maxMeters) { this._updateImperial(maxMeters); } }, _updateMetric: function (maxMeters) { var meters = this._getRoundNum(maxMeters), label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; this._updateScale(this._mScale, label, meters / maxMeters); }, _updateImperial: function (maxMeters) { var maxFeet = maxMeters * 3.2808399, maxMiles, miles, feet; if (maxFeet > 5280) { maxMiles = maxFeet / 5280; miles = this._getRoundNum(maxMiles); this._updateScale(this._iScale, miles + ' mi', miles / maxMiles); } else { feet = this._getRoundNum(maxFeet); this._updateScale(this._iScale, feet + ' ft', feet / maxFeet); } }, _updateScale: function (scale, text, ratio) { scale.style.width = Math.round(this.options.maxWidth * ratio) + 'px'; scale.innerHTML = text; }, _getRoundNum: function (num) { var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1), d = num / pow10; d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1; return pow10 * d; } }); // @factory L.control.scale(options?: Control.Scale options) // Creates an scale control with the given options. var scale = function (options) { return new Scale(options); }; /* * @class Control.Attribution * @aka L.Control.Attribution * @inherits Control * * The attribution control allows you to display attribution data in a small text box on a map. It is put on the map by default unless you set its [`attributionControl` option](#map-attributioncontrol) to `false`, and it fetches attribution texts from layers with the [`getAttribution` method](#layer-getattribution) automatically. Extends Control. */ var Attribution = Control.extend({ // @section // @aka Control.Attribution options options: { position: 'bottomright', // @option prefix: String = 'Leaflet' // The HTML text shown before the attributions. Pass `false` to disable. prefix: 'Leaflet' }, initialize: function (options) { setOptions(this, options); this._attributions = {}; }, onAdd: function (map) { map.attributionControl = this; this._container = create$1('div', 'leaflet-control-attribution'); disableClickPropagation(this._container); // TODO ugly, refactor for (var i in map._layers) { if (map._layers[i].getAttribution) { this.addAttribution(map._layers[i].getAttribution()); } } this._update(); return this._container; }, // @method setPrefix(prefix: String): this // Sets the text before the attributions. setPrefix: function (prefix) { this.options.prefix = prefix; this._update(); return this; }, // @method addAttribution(text: String): this // Adds an attribution text (e.g. `'Vector data © Mapbox'`). addAttribution: function (text) { if (!text) { return this; } if (!this._attributions[text]) { this._attributions[text] = 0; } this._attributions[text]++; this._update(); return this; }, // @method removeAttribution(text: String): this // Removes an attribution text. removeAttribution: function (text) { if (!text) { return this; } if (this._attributions[text]) { this._attributions[text]--; this._update(); } return this; }, _update: function () { if (!this._map) { return; } var attribs = []; for (var i in this._attributions) { if (this._attributions[i]) { attribs.push(i); } } var prefixAndAttribs = []; if (this.options.prefix) { prefixAndAttribs.push(this.options.prefix); } if (attribs.length) { prefixAndAttribs.push(attribs.join(', ')); } this._container.innerHTML = prefixAndAttribs.join(' | '); } }); // @namespace Map // @section Control options // @option attributionControl: Boolean = true // Whether a [attribution control](#control-attribution) is added to the map by default. Map.mergeOptions({ attributionControl: true }); Map.addInitHook(function () { if (this.options.attributionControl) { new Attribution().addTo(this); } }); // @namespace Control.Attribution // @factory L.control.attribution(options: Control.Attribution options) // Creates an attribution control. var attribution = function (options) { return new Attribution(options); }; Control.Layers = Layers; Control.Zoom = Zoom; Control.Scale = Scale; Control.Attribution = Attribution; control.layers = layers; control.zoom = zoom; control.scale = scale; control.attribution = attribution; /* L.Handler is a base class for handler classes that are used internally to inject interaction features like dragging to classes like Map and Marker. */ // @class Handler // @aka L.Handler // Abstract class for map interaction handlers var Handler = Class.extend({ initialize: function (map) { this._map = map; }, // @method enable(): this // Enables the handler enable: function () { if (this._enabled) { return this; } this._enabled = true; this.addHooks(); return this; }, // @method disable(): this // Disables the handler disable: function () { if (!this._enabled) { return this; } this._enabled = false; this.removeHooks(); return this; }, // @method enabled(): Boolean // Returns `true` if the handler is enabled enabled: function () { return !!this._enabled; } // @section Extension methods // Classes inheriting from `Handler` must implement the two following methods: // @method addHooks() // Called when the handler is enabled, should add event hooks. // @method removeHooks() // Called when the handler is disabled, should remove the event hooks added previously. }); // @section There is static function which can be called without instantiating L.Handler: // @function addTo(map: Map, name: String): this // Adds a new Handler to the given map with the given name. Handler.addTo = function (map, name) { map.addHandler(name, this); return this; }; var Mixin = {Events: Events}; /* * @class Draggable * @aka L.Draggable * @inherits Evented * * A class for making DOM elements draggable (including touch support). * Used internally for map and marker dragging. Only works for elements * that were positioned with [`L.DomUtil.setPosition`](#domutil-setposition). * * @example * ```js * var draggable = new L.Draggable(elementToDrag); * draggable.enable(); * ``` */ var START = touch ? 'touchstart mousedown' : 'mousedown'; var END = { mousedown: 'mouseup', touchstart: 'touchend', pointerdown: 'touchend', MSPointerDown: 'touchend' }; var MOVE = { mousedown: 'mousemove', touchstart: 'touchmove', pointerdown: 'touchmove', MSPointerDown: 'touchmove' }; var Draggable = Evented.extend({ options: { // @section // @aka Draggable options // @option clickTolerance: Number = 3 // The max number of pixels a user can shift the mouse pointer during a click // for it to be considered a valid click (as opposed to a mouse drag). clickTolerance: 3 }, // @constructor L.Draggable(el: HTMLElement, dragHandle?: HTMLElement, preventOutline?: Boolean, options?: Draggable options) // Creates a `Draggable` object for moving `el` when you start dragging the `dragHandle` element (equals `el` itself by default). initialize: function (element, dragStartTarget, preventOutline$$1, options) { setOptions(this, options); this._element = element; this._dragStartTarget = dragStartTarget || element; this._preventOutline = preventOutline$$1; }, // @method enable() // Enables the dragging ability enable: function () { if (this._enabled) { return; } on(this._dragStartTarget, START, this._onDown, this); this._enabled = true; }, // @method disable() // Disables the dragging ability disable: function () { if (!this._enabled) { return; } // If we're currently dragging this draggable, // disabling it counts as first ending the drag. if (Draggable._dragging === this) { this.finishDrag(); } off(this._dragStartTarget, START, this._onDown, this); this._enabled = false; this._moved = false; }, _onDown: function (e) { // Ignore simulated events, since we handle both touch and // mouse explicitly; otherwise we risk getting duplicates of // touch events, see #4315. // Also ignore the event if disabled; this happens in IE11 // under some circumstances, see #3666. if (e._simulated || !this._enabled) { return; } this._moved = false; if (hasClass(this._element, 'leaflet-zoom-anim')) { return; } if (Draggable._dragging || e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; } Draggable._dragging = this; // Prevent dragging multiple objects at once. if (this._preventOutline) { preventOutline(this._element); } disableImageDrag(); disableTextSelection(); if (this._moving) { return; } // @event down: Event // Fired when a drag is about to start. this.fire('down'); var first = e.touches ? e.touches[0] : e, sizedParent = getSizedParentNode(this._element); this._startPoint = new Point(first.clientX, first.clientY); // Cache the scale, so that we can continuously compensate for it during drag (_onMove). this._parentScale = getScale(sizedParent); on(document, MOVE[e.type], this._onMove, this); on(document, END[e.type], this._onUp, this); }, _onMove: function (e) { // Ignore simulated events, since we handle both touch and // mouse explicitly; otherwise we risk getting duplicates of // touch events, see #4315. // Also ignore the event if disabled; this happens in IE11 // under some circumstances, see #3666. if (e._simulated || !this._enabled) { return; } if (e.touches && e.touches.length > 1) { this._moved = true; return; } var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e), offset = new Point(first.clientX, first.clientY)._subtract(this._startPoint); if (!offset.x && !offset.y) { return; } if (Math.abs(offset.x) + Math.abs(offset.y) < this.options.clickTolerance) { return; } // We assume that the parent container's position, border and scale do not change for the duration of the drag. // Therefore there is no need to account for the position and border (they are eliminated by the subtraction) // and we can use the cached value for the scale. offset.x /= this._parentScale.x; offset.y /= this._parentScale.y; preventDefault(e); if (!this._moved) { // @event dragstart: Event // Fired when a drag starts this.fire('dragstart'); this._moved = true; this._startPos = getPosition(this._element).subtract(offset); addClass(document.body, 'leaflet-dragging'); this._lastTarget = e.target || e.srcElement; // IE and Edge do not give the element, so fetch it // if necessary if (window.SVGElementInstance && this._lastTarget instanceof window.SVGElementInstance) { this._lastTarget = this._lastTarget.correspondingUseElement; } addClass(this._lastTarget, 'leaflet-drag-target'); } this._newPos = this._startPos.add(offset); this._moving = true; cancelAnimFrame(this._animRequest); this._lastEvent = e; this._animRequest = requestAnimFrame(this._updatePosition, this, true); }, _updatePosition: function () { var e = {originalEvent: this._lastEvent}; // @event predrag: Event // Fired continuously during dragging *before* each corresponding // update of the element's position. this.fire('predrag', e); setPosition(this._element, this._newPos); // @event drag: Event // Fired continuously during dragging. this.fire('drag', e); }, _onUp: function (e) { // Ignore simulated events, since we handle both touch and // mouse explicitly; otherwise we risk getting duplicates of // touch events, see #4315. // Also ignore the event if disabled; this happens in IE11 // under some circumstances, see #3666. if (e._simulated || !this._enabled) { return; } this.finishDrag(); }, finishDrag: function () { removeClass(document.body, 'leaflet-dragging'); if (this._lastTarget) { removeClass(this._lastTarget, 'leaflet-drag-target'); this._lastTarget = null; } for (var i in MOVE) { off(document, MOVE[i], this._onMove, this); off(document, END[i], this._onUp, this); } enableImageDrag(); enableTextSelection(); if (this._moved && this._moving) { // ensure drag is not fired after dragend cancelAnimFrame(this._animRequest); // @event dragend: DragEndEvent // Fired when the drag ends. this.fire('dragend', { distance: this._newPos.distanceTo(this._startPos) }); } this._moving = false; Draggable._dragging = false; } }); /* * @namespace LineUtil * * Various utility functions for polyline points processing, used by Leaflet internally to make polylines lightning-fast. */ // Simplify polyline with vertex reduction and Douglas-Peucker simplification. // Improves rendering performance dramatically by lessening the number of points to draw. // @function simplify(points: Point[], tolerance: Number): Point[] // Dramatically reduces the number of points in a polyline while retaining // its shape and returns a new array of simplified points, using the // [Douglas-Peucker algorithm](http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm). // Used for a huge performance boost when processing/displaying Leaflet polylines for // each zoom level and also reducing visual noise. tolerance affects the amount of // simplification (lesser value means higher quality but slower and with more points). // Also released as a separated micro-library [Simplify.js](http://mourner.github.com/simplify-js/). function simplify(points, tolerance) { if (!tolerance || !points.length) { return points.slice(); } var sqTolerance = tolerance * tolerance; // stage 1: vertex reduction points = _reducePoints(points, sqTolerance); // stage 2: Douglas-Peucker simplification points = _simplifyDP(points, sqTolerance); return points; } // @function pointToSegmentDistance(p: Point, p1: Point, p2: Point): Number // Returns the distance between point `p` and segment `p1` to `p2`. function pointToSegmentDistance(p, p1, p2) { return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true)); } // @function closestPointOnSegment(p: Point, p1: Point, p2: Point): Number // Returns the closest point from a point `p` on a segment `p1` to `p2`. function closestPointOnSegment(p, p1, p2) { return _sqClosestPointOnSegment(p, p1, p2); } // Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm function _simplifyDP(points, sqTolerance) { var len = points.length, ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array, markers = new ArrayConstructor(len); markers[0] = markers[len - 1] = 1; _simplifyDPStep(points, markers, sqTolerance, 0, len - 1); var i, newPoints = []; for (i = 0; i < len; i++) { if (markers[i]) { newPoints.push(points[i]); } } return newPoints; } function _simplifyDPStep(points, markers, sqTolerance, first, last) { var maxSqDist = 0, index, i, sqDist; for (i = first + 1; i <= last - 1; i++) { sqDist = _sqClosestPointOnSegment(points[i], points[first], points[last], true); if (sqDist > maxSqDist) { index = i; maxSqDist = sqDist; } } if (maxSqDist > sqTolerance) { markers[index] = 1; _simplifyDPStep(points, markers, sqTolerance, first, index); _simplifyDPStep(points, markers, sqTolerance, index, last); } } // reduce points that are too close to each other to a single point function _reducePoints(points, sqTolerance) { var reducedPoints = [points[0]]; for (var i = 1, prev = 0, len = points.length; i < len; i++) { if (_sqDist(points[i], points[prev]) > sqTolerance) { reducedPoints.push(points[i]); prev = i; } } if (prev < len - 1) { reducedPoints.push(points[len - 1]); } return reducedPoints; } var _lastCode; // @function clipSegment(a: Point, b: Point, bounds: Bounds, useLastCode?: Boolean, round?: Boolean): Point[]|Boolean // Clips the segment a to b by rectangular bounds with the // [Cohen-Sutherland algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm) // (modifying the segment points directly!). Used by Leaflet to only show polyline // points that are on the screen or near, increasing performance. function clipSegment(a, b, bounds, useLastCode, round) { var codeA = useLastCode ? _lastCode : _getBitCode(a, bounds), codeB = _getBitCode(b, bounds), codeOut, p, newCode; // save 2nd code to avoid calculating it on the next segment _lastCode = codeB; while (true) { // if a,b is inside the clip window (trivial accept) if (!(codeA | codeB)) { return [a, b]; } // if a,b is outside the clip window (trivial reject) if (codeA & codeB) { return false; } // other cases codeOut = codeA || codeB; p = _getEdgeIntersection(a, b, codeOut, bounds, round); newCode = _getBitCode(p, bounds); if (codeOut === codeA) { a = p; codeA = newCode; } else { b = p; codeB = newCode; } } } function _getEdgeIntersection(a, b, code, bounds, round) { var dx = b.x - a.x, dy = b.y - a.y, min = bounds.min, max = bounds.max, x, y; if (code & 8) { // top x = a.x + dx * (max.y - a.y) / dy; y = max.y; } else if (code & 4) { // bottom x = a.x + dx * (min.y - a.y) / dy; y = min.y; } else if (code & 2) { // right x = max.x; y = a.y + dy * (max.x - a.x) / dx; } else if (code & 1) { // left x = min.x; y = a.y + dy * (min.x - a.x) / dx; } return new Point(x, y, round); } function _getBitCode(p, bounds) { var code = 0; if (p.x < bounds.min.x) { // left code |= 1; } else if (p.x > bounds.max.x) { // right code |= 2; } if (p.y < bounds.min.y) { // bottom code |= 4; } else if (p.y > bounds.max.y) { // top code |= 8; } return code; } // square distance (to avoid unnecessary Math.sqrt calls) function _sqDist(p1, p2) { var dx = p2.x - p1.x, dy = p2.y - p1.y; return dx * dx + dy * dy; } // return closest point on segment or distance to that point function _sqClosestPointOnSegment(p, p1, p2, sqDist) { var x = p1.x, y = p1.y, dx = p2.x - x, dy = p2.y - y, dot = dx * dx + dy * dy, t; if (dot > 0) { t = ((p.x - x) * dx + (p.y - y) * dy) / dot; if (t > 1) { x = p2.x; y = p2.y; } else if (t > 0) { x += dx * t; y += dy * t; } } dx = p.x - x; dy = p.y - y; return sqDist ? dx * dx + dy * dy : new Point(x, y); } // @function isFlat(latlngs: LatLng[]): Boolean // Returns true if `latlngs` is a flat array, false is nested. function isFlat(latlngs) { return !isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined'); } function _flat(latlngs) { console.warn('Deprecated use of _flat, please use L.LineUtil.isFlat instead.'); return isFlat(latlngs); } var LineUtil = ({ simplify: simplify, pointToSegmentDistance: pointToSegmentDistance, closestPointOnSegment: closestPointOnSegment, clipSegment: clipSegment, _getEdgeIntersection: _getEdgeIntersection, _getBitCode: _getBitCode, _sqClosestPointOnSegment: _sqClosestPointOnSegment, isFlat: isFlat, _flat: _flat }); /* * @namespace PolyUtil * Various utility functions for polygon geometries. */ /* @function clipPolygon(points: Point[], bounds: Bounds, round?: Boolean): Point[] * Clips the polygon geometry defined by the given `points` by the given bounds (using the [Sutherland-Hodgman algorithm](https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm)). * Used by Leaflet to only show polygon points that are on the screen or near, increasing * performance. Note that polygon points needs different algorithm for clipping * than polyline, so there's a separate method for it. */ function clipPolygon(points, bounds, round) { var clippedPoints, edges = [1, 4, 2, 8], i, j, k, a, b, len, edge, p; for (i = 0, len = points.length; i < len; i++) { points[i]._code = _getBitCode(points[i], bounds); } // for each edge (left, bottom, right, top) for (k = 0; k < 4; k++) { edge = edges[k]; clippedPoints = []; for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { a = points[i]; b = points[j]; // if a is inside the clip window if (!(a._code & edge)) { // if b is outside the clip window (a->b goes out of screen) if (b._code & edge) { p = _getEdgeIntersection(b, a, edge, bounds, round); p._code = _getBitCode(p, bounds); clippedPoints.push(p); } clippedPoints.push(a); // else if b is inside the clip window (a->b enters the screen) } else if (!(b._code & edge)) { p = _getEdgeIntersection(b, a, edge, bounds, round); p._code = _getBitCode(p, bounds); clippedPoints.push(p); } } points = clippedPoints; } return points; } var PolyUtil = ({ clipPolygon: clipPolygon }); /* * @namespace Projection * @section * Leaflet comes with a set of already defined Projections out of the box: * * @projection L.Projection.LonLat * * Equirectangular, or Plate Carree projection — the most simple projection, * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as * latitude. Also suitable for flat worlds, e.g. game maps. Used by the * `EPSG:4326` and `Simple` CRS. */ var LonLat = { project: function (latlng) { return new Point(latlng.lng, latlng.lat); }, unproject: function (point) { return new LatLng(point.y, point.x); }, bounds: new Bounds([-180, -90], [180, 90]) }; /* * @namespace Projection * @projection L.Projection.Mercator * * Elliptical Mercator projection — more complex than Spherical Mercator. Assumes that Earth is an ellipsoid. Used by the EPSG:3395 CRS. */ var Mercator = { R: 6378137, R_MINOR: 6356752.314245179, bounds: new Bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), project: function (latlng) { var d = Math.PI / 180, r = this.R, y = latlng.lat * d, tmp = this.R_MINOR / r, e = Math.sqrt(1 - tmp * tmp), con = e * Math.sin(y); var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2); y = -r * Math.log(Math.max(ts, 1E-10)); return new Point(latlng.lng * d * r, y); }, unproject: function (point) { var d = 180 / Math.PI, r = this.R, tmp = this.R_MINOR / r, e = Math.sqrt(1 - tmp * tmp), ts = Math.exp(-point.y / r), phi = Math.PI / 2 - 2 * Math.atan(ts); for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) { con = e * Math.sin(phi); con = Math.pow((1 - con) / (1 + con), e / 2); dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi; phi += dphi; } return new LatLng(phi * d, point.x * d / r); } }; /* * @class Projection * An object with methods for projecting geographical coordinates of the world onto * a flat surface (and back). See [Map projection](http://en.wikipedia.org/wiki/Map_projection). * @property bounds: Bounds * The bounds (specified in CRS units) where the projection is valid * @method project(latlng: LatLng): Point * Projects geographical coordinates into a 2D point. * Only accepts actual `L.LatLng` instances, not arrays. * @method unproject(point: Point): LatLng * The inverse of `project`. Projects a 2D point into a geographical location. * Only accepts actual `L.Point` instances, not arrays. * Note that the projection instances do not inherit from Leaflet's `Class` object, * and can't be instantiated. Also, new classes can't inherit from them, * and methods can't be added to them with the `include` function. */ var index = ({ LonLat: LonLat, Mercator: Mercator, SphericalMercator: SphericalMercator }); /* * @namespace CRS * @crs L.CRS.EPSG3395 * * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection. */ var EPSG3395 = extend({}, Earth, { code: 'EPSG:3395', projection: Mercator, transformation: (function () { var scale = 0.5 / (Math.PI * Mercator.R); return toTransformation(scale, 0.5, -scale, 0.5); }()) }); /* * @namespace CRS * @crs L.CRS.EPSG4326 * * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. * * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic), * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer` * with this CRS, ensure that there are two 256x256 pixel tiles covering the * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90), * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set. */ var EPSG4326 = extend({}, Earth, { code: 'EPSG:4326', projection: LonLat, transformation: toTransformation(1 / 180, 1, -1 / 180, 0.5) }); /* * @namespace CRS * @crs L.CRS.Simple * * A simple CRS that maps longitude and latitude into `x` and `y` directly. * May be used for maps of flat surfaces (e.g. game maps). Note that the `y` * axis should still be inverted (going from bottom to top). `distance()` returns * simple euclidean distance. */ var Simple = extend({}, CRS, { projection: LonLat, transformation: toTransformation(1, 0, -1, 0), scale: function (zoom) { return Math.pow(2, zoom); }, zoom: function (scale) { return Math.log(scale) / Math.LN2; }, distance: function (latlng1, latlng2) { var dx = latlng2.lng - latlng1.lng, dy = latlng2.lat - latlng1.lat; return Math.sqrt(dx * dx + dy * dy); }, infinite: true }); CRS.Earth = Earth; CRS.EPSG3395 = EPSG3395; CRS.EPSG3857 = EPSG3857; CRS.EPSG900913 = EPSG900913; CRS.EPSG4326 = EPSG4326; CRS.Simple = Simple; /* * @class Layer * @inherits Evented * @aka L.Layer * @aka ILayer * * A set of methods from the Layer base class that all Leaflet layers use. * Inherits all methods, options and events from `L.Evented`. * * @example * * ```js * var layer = L.marker(latlng).addTo(map); * layer.addTo(map); * layer.remove(); * ``` * * @event add: Event * Fired after the layer is added to a map * * @event remove: Event * Fired after the layer is removed from a map */ var Layer = Evented.extend({ // Classes extending `L.Layer` will inherit the following options: options: { // @option pane: String = 'overlayPane' // By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default. pane: 'overlayPane', // @option attribution: String = null // String to be shown in the attribution control, e.g. "© OpenStreetMap contributors". It describes the layer data and is often a legal obligation towards copyright holders and tile providers. attribution: null, bubblingMouseEvents: true }, /* @section * Classes extending `L.Layer` will inherit the following methods: * * @method addTo(map: Map|LayerGroup): this * Adds the layer to the given map or layer group. */ addTo: function (map) { map.addLayer(this); return this; }, // @method remove: this // Removes the layer from the map it is currently active on. remove: function () { return this.removeFrom(this._map || this._mapToAdd); }, // @method removeFrom(map: Map): this // Removes the layer from the given map // // @alternative // @method removeFrom(group: LayerGroup): this // Removes the layer from the given `LayerGroup` removeFrom: function (obj) { if (obj) { obj.removeLayer(this); } return this; }, // @method getPane(name? : String): HTMLElement // Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer. getPane: function (name) { return this._map.getPane(name ? (this.options[name] || name) : this.options.pane); }, addInteractiveTarget: function (targetEl) { this._map._targets[stamp(targetEl)] = this; return this; }, removeInteractiveTarget: function (targetEl) { delete this._map._targets[stamp(targetEl)]; return this; }, // @method getAttribution: String // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution). getAttribution: function () { return this.options.attribution; }, _layerAdd: function (e) { var map = e.target; // check in case layer gets added and then removed before the map is ready if (!map.hasLayer(this)) { return; } this._map = map; this._zoomAnimated = map._zoomAnimated; if (this.getEvents) { var events = this.getEvents(); map.on(events, this); this.once('remove', function () { map.off(events, this); }, this); } this.onAdd(map); if (this.getAttribution && map.attributionControl) { map.attributionControl.addAttribution(this.getAttribution()); } this.fire('add'); map.fire('layeradd', {layer: this}); } }); /* @section Extension methods * @uninheritable * * Every layer should extend from `L.Layer` and (re-)implement the following methods. * * @method onAdd(map: Map): this * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer). * * @method onRemove(map: Map): this * Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer). * * @method getEvents(): Object * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer. * * @method getAttribution(): String * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible. * * @method beforeAdd(map: Map): this * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only. */ /* @namespace Map * @section Layer events * * @event layeradd: LayerEvent * Fired when a new layer is added to the map. * * @event layerremove: LayerEvent * Fired when some layer is removed from the map * * @section Methods for Layers and Controls */ Map.include({ // @method addLayer(layer: Layer): this // Adds the given layer to the map addLayer: function (layer) { if (!layer._layerAdd) { throw new Error('The provided object is not a Layer.'); } var id = stamp(layer); if (this._layers[id]) { return this; } this._layers[id] = layer; layer._mapToAdd = this; if (layer.beforeAdd) { layer.beforeAdd(this); } this.whenReady(layer._layerAdd, layer); return this; }, // @method removeLayer(layer: Layer): this // Removes the given layer from the map. removeLayer: function (layer) { var id = stamp(layer); if (!this._layers[id]) { return this; } if (this._loaded) { layer.onRemove(this); } if (layer.getAttribution && this.attributionControl) { this.attributionControl.removeAttribution(layer.getAttribution()); } delete this._layers[id]; if (this._loaded) { this.fire('layerremove', {layer: layer}); layer.fire('remove'); } layer._map = layer._mapToAdd = null; return this; }, // @method hasLayer(layer: Layer): Boolean // Returns `true` if the given layer is currently added to the map hasLayer: function (layer) { return !!layer && (stamp(layer) in this._layers); }, /* @method eachLayer(fn: Function, context?: Object): this * Iterates over the layers of the map, optionally specifying context of the iterator function. * ``` * map.eachLayer(function(layer){ * layer.bindPopup('Hello'); * }); * ``` */ eachLayer: function (method, context) { for (var i in this._layers) { method.call(context, this._layers[i]); } return this; }, _addLayers: function (layers) { layers = layers ? (isArray(layers) ? layers : [layers]) : []; for (var i = 0, len = layers.length; i < len; i++) { this.addLayer(layers[i]); } }, _addZoomLimit: function (layer) { if (isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) { this._zoomBoundLayers[stamp(layer)] = layer; this._updateZoomLevels(); } }, _removeZoomLimit: function (layer) { var id = stamp(layer); if (this._zoomBoundLayers[id]) { delete this._zoomBoundLayers[id]; this._updateZoomLevels(); } }, _updateZoomLevels: function () { var minZoom = Infinity, maxZoom = -Infinity, oldZoomSpan = this._getZoomSpan(); for (var i in this._zoomBoundLayers) { var options = this._zoomBoundLayers[i].options; minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom); maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom); } this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom; this._layersMinZoom = minZoom === Infinity ? undefined : minZoom; // @section Map state change events // @event zoomlevelschange: Event // Fired when the number of zoomlevels on the map is changed due // to adding or removing a layer. if (oldZoomSpan !== this._getZoomSpan()) { this.fire('zoomlevelschange'); } if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) { this.setZoom(this._layersMaxZoom); } if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) { this.setZoom(this._layersMinZoom); } } }); /* * @class LayerGroup * @aka L.LayerGroup * @inherits Layer * * Used to group several layers and handle them as one. If you add it to the map, * any layers added or removed from the group will be added/removed on the map as * well. Extends `Layer`. * * @example * * ```js * L.layerGroup([marker1, marker2]) * .addLayer(polyline) * .addTo(map); * ``` */ var LayerGroup = Layer.extend({ initialize: function (layers, options) { setOptions(this, options); this._layers = {}; var i, len; if (layers) { for (i = 0, len = layers.length; i < len; i++) { this.addLayer(layers[i]); } } }, // @method addLayer(layer: Layer): this // Adds the given layer to the group. addLayer: function (layer) { var id = this.getLayerId(layer); this._layers[id] = layer; if (this._map) { this._map.addLayer(layer); } return this; }, // @method removeLayer(layer: Layer): this // Removes the given layer from the group. // @alternative // @method removeLayer(id: Number): this // Removes the layer with the given internal ID from the group. removeLayer: function (layer) { var id = layer in this._layers ? layer : this.getLayerId(layer); if (this._map && this._layers[id]) { this._map.removeLayer(this._layers[id]); } delete this._layers[id]; return this; }, // @method hasLayer(layer: Layer): Boolean // Returns `true` if the given layer is currently added to the group. // @alternative // @method hasLayer(id: Number): Boolean // Returns `true` if the given internal ID is currently added to the group. hasLayer: function (layer) { if (!layer) { return false; } var layerId = typeof layer === 'number' ? layer : this.getLayerId(layer); return layerId in this._layers; }, // @method clearLayers(): this // Removes all the layers from the group. clearLayers: function () { return this.eachLayer(this.removeLayer, this); }, // @method invoke(methodName: String, …): this // Calls `methodName` on every layer contained in this group, passing any // additional parameters. Has no effect if the layers contained do not // implement `methodName`. invoke: function (methodName) { var args = Array.prototype.slice.call(arguments, 1), i, layer; for (i in this._layers) { layer = this._layers[i]; if (layer[methodName]) { layer[methodName].apply(layer, args); } } return this; }, onAdd: function (map) { this.eachLayer(map.addLayer, map); }, onRemove: function (map) { this.eachLayer(map.removeLayer, map); }, // @method eachLayer(fn: Function, context?: Object): this // Iterates over the layers of the group, optionally specifying context of the iterator function. // ```js // group.eachLayer(function (layer) { // layer.bindPopup('Hello'); // }); // ``` eachLayer: function (method, context) { for (var i in this._layers) { method.call(context, this._layers[i]); } return this; }, // @method getLayer(id: Number): Layer // Returns the layer with the given internal ID. getLayer: function (id) { return this._layers[id]; }, // @method getLayers(): Layer[] // Returns an array of all the layers added to the group. getLayers: function () { var layers = []; this.eachLayer(layers.push, layers); return layers; }, // @method setZIndex(zIndex: Number): this // Calls `setZIndex` on every layer contained in this group, passing the z-index. setZIndex: function (zIndex) { return this.invoke('setZIndex', zIndex); }, // @method getLayerId(layer: Layer): Number // Returns the internal ID for a layer getLayerId: function (layer) { return stamp(layer); } }); // @factory L.layerGroup(layers?: Layer[], options?: Object) // Create a layer group, optionally given an initial set of layers and an `options` object. var layerGroup = function (layers, options) { return new LayerGroup(layers, options); }; /* * @class FeatureGroup * @aka L.FeatureGroup * @inherits LayerGroup * * Extended `LayerGroup` that makes it easier to do the same thing to all its member layers: * * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip)) * * Events are propagated to the `FeatureGroup`, so if the group has an event * handler, it will handle events from any of the layers. This includes mouse events * and custom events. * * Has `layeradd` and `layerremove` events * * @example * * ```js * L.featureGroup([marker1, marker2, polyline]) * .bindPopup('Hello world!') * .on('click', function() { alert('Clicked on a member of the group!'); }) * .addTo(map); * ``` */ var FeatureGroup = LayerGroup.extend({ addLayer: function (layer) { if (this.hasLayer(layer)) { return this; } layer.addEventParent(this); LayerGroup.prototype.addLayer.call(this, layer); // @event layeradd: LayerEvent // Fired when a layer is added to this `FeatureGroup` return this.fire('layeradd', {layer: layer}); }, removeLayer: function (layer) { if (!this.hasLayer(layer)) { return this; } if (layer in this._layers) { layer = this._layers[layer]; } layer.removeEventParent(this); LayerGroup.prototype.removeLayer.call(this, layer); // @event layerremove: LayerEvent // Fired when a layer is removed from this `FeatureGroup` return this.fire('layerremove', {layer: layer}); }, // @method setStyle(style: Path options): this // Sets the given path options to each layer of the group that has a `setStyle` method. setStyle: function (style) { return this.invoke('setStyle', style); }, // @method bringToFront(): this // Brings the layer group to the top of all other layers bringToFront: function () { return this.invoke('bringToFront'); }, // @method bringToBack(): this // Brings the layer group to the back of all other layers bringToBack: function () { return this.invoke('bringToBack'); }, // @method getBounds(): LatLngBounds // Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children). getBounds: function () { var bounds = new LatLngBounds(); for (var id in this._layers) { var layer = this._layers[id]; bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng()); } return bounds; } }); // @factory L.featureGroup(layers?: Layer[], options?: Object) // Create a feature group, optionally given an initial set of layers and an `options` object. var featureGroup = function (layers, options) { return new FeatureGroup(layers, options); }; /* * @class Icon * @aka L.Icon * * Represents an icon to provide when creating a marker. * * @example * * ```js * var myIcon = L.icon({ * iconUrl: 'my-icon.png', * iconRetinaUrl: 'my-icon@2x.png', * iconSize: [38, 95], * iconAnchor: [22, 94], * popupAnchor: [-3, -76], * shadowUrl: 'my-icon-shadow.png', * shadowRetinaUrl: 'my-icon-shadow@2x.png', * shadowSize: [68, 95], * shadowAnchor: [22, 94] * }); * * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map); * ``` * * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default. * */ var Icon = Class.extend({ /* @section * @aka Icon options * * @option iconUrl: String = null * **(required)** The URL to the icon image (absolute or relative to your script path). * * @option iconRetinaUrl: String = null * The URL to a retina sized version of the icon image (absolute or relative to your * script path). Used for Retina screen devices. * * @option iconSize: Point = null * Size of the icon image in pixels. * * @option iconAnchor: Point = null * The coordinates of the "tip" of the icon (relative to its top left corner). The icon * will be aligned so that this point is at the marker's geographical location. Centered * by default if size is specified, also can be set in CSS with negative margins. * * @option popupAnchor: Point = [0, 0] * The coordinates of the point from which popups will "open", relative to the icon anchor. * * @option tooltipAnchor: Point = [0, 0] * The coordinates of the point from which tooltips will "open", relative to the icon anchor. * * @option shadowUrl: String = null * The URL to the icon shadow image. If not specified, no shadow image will be created. * * @option shadowRetinaUrl: String = null * * @option shadowSize: Point = null * Size of the shadow image in pixels. * * @option shadowAnchor: Point = null * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same * as iconAnchor if not specified). * * @option className: String = '' * A custom class name to assign to both icon and shadow images. Empty by default. */ options: { popupAnchor: [0, 0], tooltipAnchor: [0, 0] }, initialize: function (options) { setOptions(this, options); }, // @method createIcon(oldIcon?: HTMLElement): HTMLElement // Called internally when the icon has to be shown, returns a `` HTML element // styled according to the options. createIcon: function (oldIcon) { return this._createIcon('icon', oldIcon); }, // @method createShadow(oldIcon?: HTMLElement): HTMLElement // As `createIcon`, but for the shadow beneath it. createShadow: function (oldIcon) { return this._createIcon('shadow', oldIcon); }, _createIcon: function (name, oldIcon) { var src = this._getIconUrl(name); if (!src) { if (name === 'icon') { throw new Error('iconUrl not set in Icon options (see the docs).'); } return null; } var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null); this._setIconStyles(img, name); return img; }, _setIconStyles: function (img, name) { var options = this.options; var sizeOption = options[name + 'Size']; if (typeof sizeOption === 'number') { sizeOption = [sizeOption, sizeOption]; } var size = toPoint(sizeOption), anchor = toPoint(name === 'shadow' && options.shadowAnchor || options.iconAnchor || size && size.divideBy(2, true)); img.className = 'leaflet-marker-' + name + ' ' + (options.className || ''); if (anchor) { img.style.marginLeft = (-anchor.x) + 'px'; img.style.marginTop = (-anchor.y) + 'px'; } if (size) { img.style.width = size.x + 'px'; img.style.height = size.y + 'px'; } }, _createImg: function (src, el) { el = el || document.createElement('img'); el.src = src; return el; }, _getIconUrl: function (name) { return retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url']; } }); // @factory L.icon(options: Icon options) // Creates an icon instance with the given options. function icon(options) { return new Icon(options); } /* * @miniclass Icon.Default (Icon) * @aka L.Icon.Default * @section * * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when * no icon is specified. Points to the blue marker image distributed with Leaflet * releases. * * In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options` * (which is a set of `Icon options`). * * If you want to _completely_ replace the default icon, override the * `L.Marker.prototype.options.icon` with your own icon instead. */ var IconDefault = Icon.extend({ options: { iconUrl: 'marker-icon.png', iconRetinaUrl: 'marker-icon-2x.png', shadowUrl: 'marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], tooltipAnchor: [16, -28], shadowSize: [41, 41] }, _getIconUrl: function (name) { if (!IconDefault.imagePath) { // Deprecated, backwards-compatibility only IconDefault.imagePath = this._detectIconPath(); } // @option imagePath: String // `Icon.Default` will try to auto-detect the location of the // blue icon images. If you are placing these images in a non-standard // way, set this option to point to the right path. return (this.options.imagePath || IconDefault.imagePath) + Icon.prototype._getIconUrl.call(this, name); }, _detectIconPath: function () { var el = create$1('div', 'leaflet-default-icon-path', document.body); var path = getStyle(el, 'background-image') || getStyle(el, 'backgroundImage'); // IE8 document.body.removeChild(el); if (path === null || path.indexOf('url') !== 0) { path = ''; } else { path = path.replace(/^url\(["']?/, '').replace(/marker-icon\.png["']?\)$/, ''); } return path; } }); /* * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable. */ /* @namespace Marker * @section Interaction handlers * * Interaction handlers are properties of a marker instance that allow you to control interaction behavior in runtime, enabling or disabling certain features such as dragging (see `Handler` methods). Example: * * ```js * marker.dragging.disable(); * ``` * * @property dragging: Handler * Marker dragging handler (by both mouse and touch). Only valid when the marker is on the map (Otherwise set [`marker.options.draggable`](#marker-draggable)). */ var MarkerDrag = Handler.extend({ initialize: function (marker) { this._marker = marker; }, addHooks: function () { var icon = this._marker._icon; if (!this._draggable) { this._draggable = new Draggable(icon, icon, true); } this._draggable.on({ dragstart: this._onDragStart, predrag: this._onPreDrag, drag: this._onDrag, dragend: this._onDragEnd }, this).enable(); addClass(icon, 'leaflet-marker-draggable'); }, removeHooks: function () { this._draggable.off({ dragstart: this._onDragStart, predrag: this._onPreDrag, drag: this._onDrag, dragend: this._onDragEnd }, this).disable(); if (this._marker._icon) { removeClass(this._marker._icon, 'leaflet-marker-draggable'); } }, moved: function () { return this._draggable && this._draggable._moved; }, _adjustPan: function (e) { var marker = this._marker, map = marker._map, speed = this._marker.options.autoPanSpeed, padding = this._marker.options.autoPanPadding, iconPos = getPosition(marker._icon), bounds = map.getPixelBounds(), origin = map.getPixelOrigin(); var panBounds = toBounds( bounds.min._subtract(origin).add(padding), bounds.max._subtract(origin).subtract(padding) ); if (!panBounds.contains(iconPos)) { // Compute incremental movement var movement = toPoint( (Math.max(panBounds.max.x, iconPos.x) - panBounds.max.x) / (bounds.max.x - panBounds.max.x) - (Math.min(panBounds.min.x, iconPos.x) - panBounds.min.x) / (bounds.min.x - panBounds.min.x), (Math.max(panBounds.max.y, iconPos.y) - panBounds.max.y) / (bounds.max.y - panBounds.max.y) - (Math.min(panBounds.min.y, iconPos.y) - panBounds.min.y) / (bounds.min.y - panBounds.min.y) ).multiplyBy(speed); map.panBy(movement, {animate: false}); this._draggable._newPos._add(movement); this._draggable._startPos._add(movement); setPosition(marker._icon, this._draggable._newPos); this._onDrag(e); this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e)); } }, _onDragStart: function () { // @section Dragging events // @event dragstart: Event // Fired when the user starts dragging the marker. // @event movestart: Event // Fired when the marker starts moving (because of dragging). this._oldLatLng = this._marker.getLatLng(); // When using ES6 imports it could not be set when `Popup` was not imported as well this._marker.closePopup && this._marker.closePopup(); this._marker .fire('movestart') .fire('dragstart'); }, _onPreDrag: function (e) { if (this._marker.options.autoPan) { cancelAnimFrame(this._panRequest); this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e)); } }, _onDrag: function (e) { var marker = this._marker, shadow = marker._shadow, iconPos = getPosition(marker._icon), latlng = marker._map.layerPointToLatLng(iconPos); // update shadow position if (shadow) { setPosition(shadow, iconPos); } marker._latlng = latlng; e.latlng = latlng; e.oldLatLng = this._oldLatLng; // @event drag: Event // Fired repeatedly while the user drags the marker. marker .fire('move', e) .fire('drag', e); }, _onDragEnd: function (e) { // @event dragend: DragEndEvent // Fired when the user stops dragging the marker. cancelAnimFrame(this._panRequest); // @event moveend: Event // Fired when the marker stops moving (because of dragging). delete this._oldLatLng; this._marker .fire('moveend') .fire('dragend', e); } }); /* * @class Marker * @inherits Interactive layer * @aka L.Marker * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`. * * @example * * ```js * L.marker([50.5, 30.5]).addTo(map); * ``` */ var Marker = Layer.extend({ // @section // @aka Marker options options: { // @option icon: Icon = * // Icon instance to use for rendering the marker. // See [Icon documentation](#L.Icon) for details on how to customize the marker icon. // If not specified, a common instance of `L.Icon.Default` is used. icon: new IconDefault(), // Option inherited from "Interactive layer" abstract class interactive: true, // @option keyboard: Boolean = true // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter. keyboard: true, // @option title: String = '' // Text for the browser tooltip that appear on marker hover (no tooltip by default). title: '', // @option alt: String = '' // Text for the `alt` attribute of the icon image (useful for accessibility). alt: '', // @option zIndexOffset: Number = 0 // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively). zIndexOffset: 0, // @option opacity: Number = 1.0 // The opacity of the marker. opacity: 1, // @option riseOnHover: Boolean = false // If `true`, the marker will get on top of others when you hover the mouse over it. riseOnHover: false, // @option riseOffset: Number = 250 // The z-index offset used for the `riseOnHover` feature. riseOffset: 250, // @option pane: String = 'markerPane' // `Map pane` where the markers icon will be added. pane: 'markerPane', // @option shadowPane: String = 'shadowPane' // `Map pane` where the markers shadow will be added. shadowPane: 'shadowPane', // @option bubblingMouseEvents: Boolean = false // When `true`, a mouse event on this marker will trigger the same event on the map // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used). bubblingMouseEvents: false, // @section Draggable marker options // @option draggable: Boolean = false // Whether the marker is draggable with mouse/touch or not. draggable: false, // @option autoPan: Boolean = false // Whether to pan the map when dragging this marker near its edge or not. autoPan: false, // @option autoPanPadding: Point = Point(50, 50) // Distance (in pixels to the left/right and to the top/bottom) of the // map edge to start panning the map. autoPanPadding: [50, 50], // @option autoPanSpeed: Number = 10 // Number of pixels the map should pan by. autoPanSpeed: 10 }, /* @section * * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods: */ initialize: function (latlng, options) { setOptions(this, options); this._latlng = toLatLng(latlng); }, onAdd: function (map) { this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation; if (this._zoomAnimated) { map.on('zoomanim', this._animateZoom, this); } this._initIcon(); this.update(); }, onRemove: function (map) { if (this.dragging && this.dragging.enabled()) { this.options.draggable = true; this.dragging.removeHooks(); } delete this.dragging; if (this._zoomAnimated) { map.off('zoomanim', this._animateZoom, this); } this._removeIcon(); this._removeShadow(); }, getEvents: function () { return { zoom: this.update, viewreset: this.update }; }, // @method getLatLng: LatLng // Returns the current geographical position of the marker. getLatLng: function () { return this._latlng; }, // @method setLatLng(latlng: LatLng): this // Changes the marker position to the given point. setLatLng: function (latlng) { var oldLatLng = this._latlng; this._latlng = toLatLng(latlng); this.update(); // @event move: Event // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`. return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng}); }, // @method setZIndexOffset(offset: Number): this // Changes the [zIndex offset](#marker-zindexoffset) of the marker. setZIndexOffset: function (offset) { this.options.zIndexOffset = offset; return this.update(); }, // @method getIcon: Icon // Returns the current icon used by the marker getIcon: function () { return this.options.icon; }, // @method setIcon(icon: Icon): this // Changes the marker icon. setIcon: function (icon) { this.options.icon = icon; if (this._map) { this._initIcon(); this.update(); } if (this._popup) { this.bindPopup(this._popup, this._popup.options); } return this; }, getElement: function () { return this._icon; }, update: function () { if (this._icon && this._map) { var pos = this._map.latLngToLayerPoint(this._latlng).round(); this._setPos(pos); } return this; }, _initIcon: function () { var options = this.options, classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide'); var icon = options.icon.createIcon(this._icon), addIcon = false; // if we're not reusing the icon, remove the old one and init new one if (icon !== this._icon) { if (this._icon) { this._removeIcon(); } addIcon = true; if (options.title) { icon.title = options.title; } if (icon.tagName === 'IMG') { icon.alt = options.alt || ''; } } addClass(icon, classToAdd); if (options.keyboard) { icon.tabIndex = '0'; } this._icon = icon; if (options.riseOnHover) { this.on({ mouseover: this._bringToFront, mouseout: this._resetZIndex }); } var newShadow = options.icon.createShadow(this._shadow), addShadow = false; if (newShadow !== this._shadow) { this._removeShadow(); addShadow = true; } if (newShadow) { addClass(newShadow, classToAdd); newShadow.alt = ''; } this._shadow = newShadow; if (options.opacity < 1) { this._updateOpacity(); } if (addIcon) { this.getPane().appendChild(this._icon); } this._initInteraction(); if (newShadow && addShadow) { this.getPane(options.shadowPane).appendChild(this._shadow); } }, _removeIcon: function () { if (this.options.riseOnHover) { this.off({ mouseover: this._bringToFront, mouseout: this._resetZIndex }); } remove(this._icon); this.removeInteractiveTarget(this._icon); this._icon = null; }, _removeShadow: function () { if (this._shadow) { remove(this._shadow); } this._shadow = null; }, _setPos: function (pos) { if (this._icon) { setPosition(this._icon, pos); } if (this._shadow) { setPosition(this._shadow, pos); } this._zIndex = pos.y + this.options.zIndexOffset; this._resetZIndex(); }, _updateZIndex: function (offset) { if (this._icon) { this._icon.style.zIndex = this._zIndex + offset; } }, _animateZoom: function (opt) { var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round(); this._setPos(pos); }, _initInteraction: function () { if (!this.options.interactive) { return; } addClass(this._icon, 'leaflet-interactive'); this.addInteractiveTarget(this._icon); if (MarkerDrag) { var draggable = this.options.draggable; if (this.dragging) { draggable = this.dragging.enabled(); this.dragging.disable(); } this.dragging = new MarkerDrag(this); if (draggable) { this.dragging.enable(); } } }, // @method setOpacity(opacity: Number): this // Changes the opacity of the marker. setOpacity: function (opacity) { this.options.opacity = opacity; if (this._map) { this._updateOpacity(); } return this; }, _updateOpacity: function () { var opacity = this.options.opacity; if (this._icon) { setOpacity(this._icon, opacity); } if (this._shadow) { setOpacity(this._shadow, opacity); } }, _bringToFront: function () { this._updateZIndex(this.options.riseOffset); }, _resetZIndex: function () { this._updateZIndex(0); }, _getPopupAnchor: function () { return this.options.icon.options.popupAnchor; }, _getTooltipAnchor: function () { return this.options.icon.options.tooltipAnchor; } }); // factory L.marker(latlng: LatLng, options? : Marker options) // @factory L.marker(latlng: LatLng, options? : Marker options) // Instantiates a Marker object given a geographical point and optionally an options object. function marker(latlng, options) { return new Marker(latlng, options); } /* * @class Path * @aka L.Path * @inherits Interactive layer * * An abstract class that contains options and constants shared between vector * overlays (Polygon, Polyline, Circle). Do not use it directly. Extends `Layer`. */ var Path = Layer.extend({ // @section // @aka Path options options: { // @option stroke: Boolean = true // Whether to draw stroke along the path. Set it to `false` to disable borders on polygons or circles. stroke: true, // @option color: String = '#3388ff' // Stroke color color: '#3388ff', // @option weight: Number = 3 // Stroke width in pixels weight: 3, // @option opacity: Number = 1.0 // Stroke opacity opacity: 1, // @option lineCap: String= 'round' // A string that defines [shape to be used at the end](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linecap) of the stroke. lineCap: 'round', // @option lineJoin: String = 'round' // A string that defines [shape to be used at the corners](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linejoin) of the stroke. lineJoin: 'round', // @option dashArray: String = null // A string that defines the stroke [dash pattern](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dasharray). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility). dashArray: null, // @option dashOffset: String = null // A string that defines the [distance into the dash pattern to start the dash](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dashoffset). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility). dashOffset: null, // @option fill: Boolean = depends // Whether to fill the path with color. Set it to `false` to disable filling on polygons or circles. fill: false, // @option fillColor: String = * // Fill color. Defaults to the value of the [`color`](#path-color) option fillColor: null, // @option fillOpacity: Number = 0.2 // Fill opacity. fillOpacity: 0.2, // @option fillRule: String = 'evenodd' // A string that defines [how the inside of a shape](https://developer.mozilla.org/docs/Web/SVG/Attribute/fill-rule) is determined. fillRule: 'evenodd', // className: '', // Option inherited from "Interactive layer" abstract class interactive: true, // @option bubblingMouseEvents: Boolean = true // When `true`, a mouse event on this path will trigger the same event on the map // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used). bubblingMouseEvents: true }, beforeAdd: function (map) { // Renderer is set here because we need to call renderer.getEvents // before this.getEvents. this._renderer = map.getRenderer(this); }, onAdd: function () { this._renderer._initPath(this); this._reset(); this._renderer._addPath(this); }, onRemove: function () { this._renderer._removePath(this); }, // @method redraw(): this // Redraws the layer. Sometimes useful after you changed the coordinates that the path uses. redraw: function () { if (this._map) { this._renderer._updatePath(this); } return this; }, // @method setStyle(style: Path options): this // Changes the appearance of a Path based on the options in the `Path options` object. setStyle: function (style) { setOptions(this, style); if (this._renderer) { this._renderer._updateStyle(this); if (this.options.stroke && style && Object.prototype.hasOwnProperty.call(style, 'weight')) { this._updateBounds(); } } return this; }, // @method bringToFront(): this // Brings the layer to the top of all path layers. bringToFront: function () { if (this._renderer) { this._renderer._bringToFront(this); } return this; }, // @method bringToBack(): this // Brings the layer to the bottom of all path layers. bringToBack: function () { if (this._renderer) { this._renderer._bringToBack(this); } return this; }, getElement: function () { return this._path; }, _reset: function () { // defined in child classes this._project(); this._update(); }, _clickTolerance: function () { // used when doing hit detection for Canvas layers return (this.options.stroke ? this.options.weight / 2 : 0) + this._renderer.options.tolerance; } }); /* * @class CircleMarker * @aka L.CircleMarker * @inherits Path * * A circle of a fixed size with radius specified in pixels. Extends `Path`. */ var CircleMarker = Path.extend({ // @section // @aka CircleMarker options options: { fill: true, // @option radius: Number = 10 // Radius of the circle marker, in pixels radius: 10 }, initialize: function (latlng, options) { setOptions(this, options); this._latlng = toLatLng(latlng); this._radius = this.options.radius; }, // @method setLatLng(latLng: LatLng): this // Sets the position of a circle marker to a new location. setLatLng: function (latlng) { var oldLatLng = this._latlng; this._latlng = toLatLng(latlng); this.redraw(); // @event move: Event // Fired when the marker is moved via [`setLatLng`](#circlemarker-setlatlng). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`. return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng}); }, // @method getLatLng(): LatLng // Returns the current geographical position of the circle marker getLatLng: function () { return this._latlng; }, // @method setRadius(radius: Number): this // Sets the radius of a circle marker. Units are in pixels. setRadius: function (radius) { this.options.radius = this._radius = radius; return this.redraw(); }, // @method getRadius(): Number // Returns the current radius of the circle getRadius: function () { return this._radius; }, setStyle : function (options) { var radius = options && options.radius || this._radius; Path.prototype.setStyle.call(this, options); this.setRadius(radius); return this; }, _project: function () { this._point = this._map.latLngToLayerPoint(this._latlng); this._updateBounds(); }, _updateBounds: function () { var r = this._radius, r2 = this._radiusY || r, w = this._clickTolerance(), p = [r + w, r2 + w]; this._pxBounds = new Bounds(this._point.subtract(p), this._point.add(p)); }, _update: function () { if (this._map) { this._updatePath(); } }, _updatePath: function () { this._renderer._updateCircle(this); }, _empty: function () { return this._radius && !this._renderer._bounds.intersects(this._pxBounds); }, // Needed by the `Canvas` renderer for interactivity _containsPoint: function (p) { return p.distanceTo(this._point) <= this._radius + this._clickTolerance(); } }); // @factory L.circleMarker(latlng: LatLng, options?: CircleMarker options) // Instantiates a circle marker object given a geographical point, and an optional options object. function circleMarker(latlng, options) { return new CircleMarker(latlng, options); } /* * @class Circle * @aka L.Circle * @inherits CircleMarker * * A class for drawing circle overlays on a map. Extends `CircleMarker`. * * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion). * * @example * * ```js * L.circle([50.5, 30.5], {radius: 200}).addTo(map); * ``` */ var Circle = CircleMarker.extend({ initialize: function (latlng, options, legacyOptions) { if (typeof options === 'number') { // Backwards compatibility with 0.7.x factory (latlng, radius, options?) options = extend({}, legacyOptions, {radius: options}); } setOptions(this, options); this._latlng = toLatLng(latlng); if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); } // @section // @aka Circle options // @option radius: Number; Radius of the circle, in meters. this._mRadius = this.options.radius; }, // @method setRadius(radius: Number): this // Sets the radius of a circle. Units are in meters. setRadius: function (radius) { this._mRadius = radius; return this.redraw(); }, // @method getRadius(): Number // Returns the current radius of a circle. Units are in meters. getRadius: function () { return this._mRadius; }, // @method getBounds(): LatLngBounds // Returns the `LatLngBounds` of the path. getBounds: function () { var half = [this._radius, this._radiusY || this._radius]; return new LatLngBounds( this._map.layerPointToLatLng(this._point.subtract(half)), this._map.layerPointToLatLng(this._point.add(half))); }, setStyle: Path.prototype.setStyle, _project: function () { var lng = this._latlng.lng, lat = this._latlng.lat, map = this._map, crs = map.options.crs; if (crs.distance === Earth.distance) { var d = Math.PI / 180, latR = (this._mRadius / Earth.R) / d, top = map.project([lat + latR, lng]), bottom = map.project([lat - latR, lng]), p = top.add(bottom).divideBy(2), lat2 = map.unproject(p).lat, lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) / (Math.cos(lat * d) * Math.cos(lat2 * d))) / d; if (isNaN(lngR) || lngR === 0) { lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425 } this._point = p.subtract(map.getPixelOrigin()); this._radius = isNaN(lngR) ? 0 : p.x - map.project([lat2, lng - lngR]).x; this._radiusY = p.y - top.y; } else { var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0])); this._point = map.latLngToLayerPoint(this._latlng); this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x; } this._updateBounds(); } }); // @factory L.circle(latlng: LatLng, options?: Circle options) // Instantiates a circle object given a geographical point, and an options object // which contains the circle radius. // @alternative // @factory L.circle(latlng: LatLng, radius: Number, options?: Circle options) // Obsolete way of instantiating a circle, for compatibility with 0.7.x code. // Do not use in new applications or plugins. function circle(latlng, options, legacyOptions) { return new Circle(latlng, options, legacyOptions); } /* * @class Polyline * @aka L.Polyline * @inherits Path * * A class for drawing polyline overlays on a map. Extends `Path`. * * @example * * ```js * // create a red polyline from an array of LatLng points * var latlngs = [ * [45.51, -122.68], * [37.77, -122.43], * [34.04, -118.2] * ]; * * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map); * * // zoom the map to the polyline * map.fitBounds(polyline.getBounds()); * ``` * * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape: * * ```js * // create a red polyline from an array of arrays of LatLng points * var latlngs = [ * [[45.51, -122.68], * [37.77, -122.43], * [34.04, -118.2]], * [[40.78, -73.91], * [41.83, -87.62], * [32.76, -96.72]] * ]; * ``` */ var Polyline = Path.extend({ // @section // @aka Polyline options options: { // @option smoothFactor: Number = 1.0 // How much to simplify the polyline on each zoom level. More means // better performance and smoother look, and less means more accurate representation. smoothFactor: 1.0, // @option noClip: Boolean = false // Disable polyline clipping. noClip: false }, initialize: function (latlngs, options) { setOptions(this, options); this._setLatLngs(latlngs); }, // @method getLatLngs(): LatLng[] // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline. getLatLngs: function () { return this._latlngs; }, // @method setLatLngs(latlngs: LatLng[]): this // Replaces all the points in the polyline with the given array of geographical points. setLatLngs: function (latlngs) { this._setLatLngs(latlngs); return this.redraw(); }, // @method isEmpty(): Boolean // Returns `true` if the Polyline has no LatLngs. isEmpty: function () { return !this._latlngs.length; }, // @method closestLayerPoint(p: Point): Point // Returns the point closest to `p` on the Polyline. closestLayerPoint: function (p) { var minDistance = Infinity, minPoint = null, closest = _sqClosestPointOnSegment, p1, p2; for (var j = 0, jLen = this._parts.length; j < jLen; j++) { var points = this._parts[j]; for (var i = 1, len = points.length; i < len; i++) { p1 = points[i - 1]; p2 = points[i]; var sqDist = closest(p, p1, p2, true); if (sqDist < minDistance) { minDistance = sqDist; minPoint = closest(p, p1, p2); } } } if (minPoint) { minPoint.distance = Math.sqrt(minDistance); } return minPoint; }, // @method getCenter(): LatLng // Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the polyline. getCenter: function () { // throws error when not yet added to map as this center calculation requires projected coordinates if (!this._map) { throw new Error('Must add layer to map before using getCenter()'); } var i, halfDist, segDist, dist, p1, p2, ratio, points = this._rings[0], len = points.length; if (!len) { return null; } // polyline centroid algorithm; only uses the first ring if there are multiple for (i = 0, halfDist = 0; i < len - 1; i++) { halfDist += points[i].distanceTo(points[i + 1]) / 2; } // The line is so small in the current view that all points are on the same pixel. if (halfDist === 0) { return this._map.layerPointToLatLng(points[0]); } for (i = 0, dist = 0; i < len - 1; i++) { p1 = points[i]; p2 = points[i + 1]; segDist = p1.distanceTo(p2); dist += segDist; if (dist > halfDist) { ratio = (dist - halfDist) / segDist; return this._map.layerPointToLatLng([ p2.x - ratio * (p2.x - p1.x), p2.y - ratio * (p2.y - p1.y) ]); } } }, // @method getBounds(): LatLngBounds // Returns the `LatLngBounds` of the path. getBounds: function () { return this._bounds; }, // @method addLatLng(latlng: LatLng, latlngs?: LatLng[]): this // Adds a given point to the polyline. By default, adds to the first ring of // the polyline in case of a multi-polyline, but can be overridden by passing // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)). addLatLng: function (latlng, latlngs) { latlngs = latlngs || this._defaultShape(); latlng = toLatLng(latlng); latlngs.push(latlng); this._bounds.extend(latlng); return this.redraw(); }, _setLatLngs: function (latlngs) { this._bounds = new LatLngBounds(); this._latlngs = this._convertLatLngs(latlngs); }, _defaultShape: function () { return isFlat(this._latlngs) ? this._latlngs : this._latlngs[0]; }, // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way _convertLatLngs: function (latlngs) { var result = [], flat = isFlat(latlngs); for (var i = 0, len = latlngs.length; i < len; i++) { if (flat) { result[i] = toLatLng(latlngs[i]); this._bounds.extend(result[i]); } else { result[i] = this._convertLatLngs(latlngs[i]); } } return result; }, _project: function () { var pxBounds = new Bounds(); this._rings = []; this._projectLatlngs(this._latlngs, this._rings, pxBounds); if (this._bounds.isValid() && pxBounds.isValid()) { this._rawPxBounds = pxBounds; this._updateBounds(); } }, _updateBounds: function () { var w = this._clickTolerance(), p = new Point(w, w); this._pxBounds = new Bounds([ this._rawPxBounds.min.subtract(p), this._rawPxBounds.max.add(p) ]); }, // recursively turns latlngs into a set of rings with projected coordinates _projectLatlngs: function (latlngs, result, projectedBounds) { var flat = latlngs[0] instanceof LatLng, len = latlngs.length, i, ring; if (flat) { ring = []; for (i = 0; i < len; i++) { ring[i] = this._map.latLngToLayerPoint(latlngs[i]); projectedBounds.extend(ring[i]); } result.push(ring); } else { for (i = 0; i < len; i++) { this._projectLatlngs(latlngs[i], result, projectedBounds); } } }, // clip polyline by renderer bounds so that we have less to render for performance _clipPoints: function () { var bounds = this._renderer._bounds; this._parts = []; if (!this._pxBounds || !this._pxBounds.intersects(bounds)) { return; } if (this.options.noClip) { this._parts = this._rings; return; } var parts = this._parts, i, j, k, len, len2, segment, points; for (i = 0, k = 0, len = this._rings.length; i < len; i++) { points = this._rings[i]; for (j = 0, len2 = points.length; j < len2 - 1; j++) { segment = clipSegment(points[j], points[j + 1], bounds, j, true); if (!segment) { continue; } parts[k] = parts[k] || []; parts[k].push(segment[0]); // if segment goes out of screen, or it's the last one, it's the end of the line part if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) { parts[k].push(segment[1]); k++; } } } }, // simplify each clipped part of the polyline for performance _simplifyPoints: function () { var parts = this._parts, tolerance = this.options.smoothFactor; for (var i = 0, len = parts.length; i < len; i++) { parts[i] = simplify(parts[i], tolerance); } }, _update: function () { if (!this._map) { return; } this._clipPoints(); this._simplifyPoints(); this._updatePath(); }, _updatePath: function () { this._renderer._updatePoly(this); }, // Needed by the `Canvas` renderer for interactivity _containsPoint: function (p, closed) { var i, j, k, len, len2, part, w = this._clickTolerance(); if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; } // hit detection for polylines for (i = 0, len = this._parts.length; i < len; i++) { part = this._parts[i]; for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { if (!closed && (j === 0)) { continue; } if (pointToSegmentDistance(p, part[k], part[j]) <= w) { return true; } } } return false; } }); // @factory L.polyline(latlngs: LatLng[], options?: Polyline options) // Instantiates a polyline object given an array of geographical points and // optionally an options object. You can create a `Polyline` object with // multiple separate lines (`MultiPolyline`) by passing an array of arrays // of geographic points. function polyline(latlngs, options) { return new Polyline(latlngs, options); } // Retrocompat. Allow plugins to support Leaflet versions before and after 1.1. Polyline._flat = _flat; /* * @class Polygon * @aka L.Polygon * @inherits Polyline * * A class for drawing polygon overlays on a map. Extends `Polyline`. * * Note that points you pass when creating a polygon shouldn't have an additional last point equal to the first one — it's better to filter out such points. * * * @example * * ```js * // create a red polygon from an array of LatLng points * var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]]; * * var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map); * * // zoom the map to the polygon * map.fitBounds(polygon.getBounds()); * ``` * * You can also pass an array of arrays of latlngs, with the first array representing the outer shape and the other arrays representing holes in the outer shape: * * ```js * var latlngs = [ * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole * ]; * ``` * * Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape. * * ```js * var latlngs = [ * [ // first polygon * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole * ], * [ // second polygon * [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]] * ] * ]; * ``` */ var Polygon = Polyline.extend({ options: { fill: true }, isEmpty: function () { return !this._latlngs.length || !this._latlngs[0].length; }, getCenter: function () { // throws error when not yet added to map as this center calculation requires projected coordinates if (!this._map) { throw new Error('Must add layer to map before using getCenter()'); } var i, j, p1, p2, f, area, x, y, center, points = this._rings[0], len = points.length; if (!len) { return null; } // polygon centroid algorithm; only uses the first ring if there are multiple area = x = y = 0; for (i = 0, j = len - 1; i < len; j = i++) { p1 = points[i]; p2 = points[j]; f = p1.y * p2.x - p2.y * p1.x; x += (p1.x + p2.x) * f; y += (p1.y + p2.y) * f; area += f * 3; } if (area === 0) { // Polygon is so small that all points are on same pixel. center = points[0]; } else { center = [x / area, y / area]; } return this._map.layerPointToLatLng(center); }, _convertLatLngs: function (latlngs) { var result = Polyline.prototype._convertLatLngs.call(this, latlngs), len = result.length; // remove last point if it equals first one if (len >= 2 && result[0] instanceof LatLng && result[0].equals(result[len - 1])) { result.pop(); } return result; }, _setLatLngs: function (latlngs) { Polyline.prototype._setLatLngs.call(this, latlngs); if (isFlat(this._latlngs)) { this._latlngs = [this._latlngs]; } }, _defaultShape: function () { return isFlat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0]; }, _clipPoints: function () { // polygons need a different clipping algorithm so we redefine that var bounds = this._renderer._bounds, w = this.options.weight, p = new Point(w, w); // increase clip padding by stroke width to avoid stroke on clip edges bounds = new Bounds(bounds.min.subtract(p), bounds.max.add(p)); this._parts = []; if (!this._pxBounds || !this._pxBounds.intersects(bounds)) { return; } if (this.options.noClip) { this._parts = this._rings; return; } for (var i = 0, len = this._rings.length, clipped; i < len; i++) { clipped = clipPolygon(this._rings[i], bounds, true); if (clipped.length) { this._parts.push(clipped); } } }, _updatePath: function () { this._renderer._updatePoly(this, true); }, // Needed by the `Canvas` renderer for interactivity _containsPoint: function (p) { var inside = false, part, p1, p2, i, j, k, len, len2; if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; } // ray casting algorithm for detecting if point is in polygon for (i = 0, len = this._parts.length; i < len; i++) { part = this._parts[i]; for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { p1 = part[j]; p2 = part[k]; if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { inside = !inside; } } } // also check if it's on polygon stroke return inside || Polyline.prototype._containsPoint.call(this, p, true); } }); // @factory L.polygon(latlngs: LatLng[], options?: Polyline options) function polygon(latlngs, options) { return new Polygon(latlngs, options); } /* * @class GeoJSON * @aka L.GeoJSON * @inherits FeatureGroup * * Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse * GeoJSON data and display it on the map. Extends `FeatureGroup`. * * @example * * ```js * L.geoJSON(data, { * style: function (feature) { * return {color: feature.properties.color}; * } * }).bindPopup(function (layer) { * return layer.feature.properties.description; * }).addTo(map); * ``` */ var GeoJSON = FeatureGroup.extend({ /* @section * @aka GeoJSON options * * @option pointToLayer: Function = * * A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally * called when data is added, passing the GeoJSON point feature and its `LatLng`. * The default is to spawn a default `Marker`: * ```js * function(geoJsonPoint, latlng) { * return L.marker(latlng); * } * ``` * * @option style: Function = * * A `Function` defining the `Path options` for styling GeoJSON lines and polygons, * called internally when data is added. * The default value is to not override any defaults: * ```js * function (geoJsonFeature) { * return {} * } * ``` * * @option onEachFeature: Function = * * A `Function` that will be called once for each created `Feature`, after it has * been created and styled. Useful for attaching events and popups to features. * The default is to do nothing with the newly created layers: * ```js * function (feature, layer) {} * ``` * * @option filter: Function = * * A `Function` that will be used to decide whether to include a feature or not. * The default is to include all features: * ```js * function (geoJsonFeature) { * return true; * } * ``` * Note: dynamically changing the `filter` option will have effect only on newly * added data. It will _not_ re-evaluate already included features. * * @option coordsToLatLng: Function = * * A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s. * The default is the `coordsToLatLng` static method. * * @option markersInheritOptions: Boolean = false * Whether default Markers for "Point" type Features inherit from group options. */ initialize: function (geojson, options) { setOptions(this, options); this._layers = {}; if (geojson) { this.addData(geojson); } }, // @method addData( data ): this // Adds a GeoJSON object to the layer. addData: function (geojson) { var features = isArray(geojson) ? geojson : geojson.features, i, len, feature; if (features) { for (i = 0, len = features.length; i < len; i++) { // only add this if geometry or geometries are set and not null feature = features[i]; if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { this.addData(feature); } } return this; } var options = this.options; if (options.filter && !options.filter(geojson)) { return this; } var layer = geometryToLayer(geojson, options); if (!layer) { return this; } layer.feature = asFeature(geojson); layer.defaultOptions = layer.options; this.resetStyle(layer); if (options.onEachFeature) { options.onEachFeature(geojson, layer); } return this.addLayer(layer); }, // @method resetStyle( layer? ): this // Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events. // If `layer` is omitted, the style of all features in the current layer is reset. resetStyle: function (layer) { if (layer === undefined) { return this.eachLayer(this.resetStyle, this); } // reset any custom styles layer.options = extend({}, layer.defaultOptions); this._setLayerStyle(layer, this.options.style); return this; }, // @method setStyle( style ): this // Changes styles of GeoJSON vector layers with the given style function. setStyle: function (style) { return this.eachLayer(function (layer) { this._setLayerStyle(layer, style); }, this); }, _setLayerStyle: function (layer, style) { if (layer.setStyle) { if (typeof style === 'function') { style = style(layer.feature); } layer.setStyle(style); } } }); // @section // There are several static functions which can be called without instantiating L.GeoJSON: // @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer // Creates a `Layer` from a given GeoJSON feature. Can use a custom // [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng) // functions if provided as options. function geometryToLayer(geojson, options) { var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, coords = geometry ? geometry.coordinates : null, layers = [], pointToLayer = options && options.pointToLayer, _coordsToLatLng = options && options.coordsToLatLng || coordsToLatLng, latlng, latlngs, i, len; if (!coords && !geometry) { return null; } switch (geometry.type) { case 'Point': latlng = _coordsToLatLng(coords); return _pointToLayer(pointToLayer, geojson, latlng, options); case 'MultiPoint': for (i = 0, len = coords.length; i < len; i++) { latlng = _coordsToLatLng(coords[i]); layers.push(_pointToLayer(pointToLayer, geojson, latlng, options)); } return new FeatureGroup(layers); case 'LineString': case 'MultiLineString': latlngs = coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng); return new Polyline(latlngs, options); case 'Polygon': case 'MultiPolygon': latlngs = coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng); return new Polygon(latlngs, options); case 'GeometryCollection': for (i = 0, len = geometry.geometries.length; i < len; i++) { var layer = geometryToLayer({ geometry: geometry.geometries[i], type: 'Feature', properties: geojson.properties }, options); if (layer) { layers.push(layer); } } return new FeatureGroup(layers); default: throw new Error('Invalid GeoJSON object.'); } } function _pointToLayer(pointToLayerFn, geojson, latlng, options) { return pointToLayerFn ? pointToLayerFn(geojson, latlng) : new Marker(latlng, options && options.markersInheritOptions && options); } // @function coordsToLatLng(coords: Array): LatLng // Creates a `LatLng` object from an array of 2 numbers (longitude, latitude) // or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points. function coordsToLatLng(coords) { return new LatLng(coords[1], coords[0], coords[2]); } // @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array // Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array. // `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default). // Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function. function coordsToLatLngs(coords, levelsDeep, _coordsToLatLng) { var latlngs = []; for (var i = 0, len = coords.length, latlng; i < len; i++) { latlng = levelsDeep ? coordsToLatLngs(coords[i], levelsDeep - 1, _coordsToLatLng) : (_coordsToLatLng || coordsToLatLng)(coords[i]); latlngs.push(latlng); } return latlngs; } // @function latLngToCoords(latlng: LatLng, precision?: Number): Array // Reverse of [`coordsToLatLng`](#geojson-coordstolatlng) function latLngToCoords(latlng, precision) { precision = typeof precision === 'number' ? precision : 6; return latlng.alt !== undefined ? [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision), formatNum(latlng.alt, precision)] : [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision)]; } // @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean): Array // Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs) // `closed` determines whether the first point should be appended to the end of the array to close the feature, only used when `levelsDeep` is 0. False by default. function latLngsToCoords(latlngs, levelsDeep, closed, precision) { var coords = []; for (var i = 0, len = latlngs.length; i < len; i++) { coords.push(levelsDeep ? latLngsToCoords(latlngs[i], levelsDeep - 1, closed, precision) : latLngToCoords(latlngs[i], precision)); } if (!levelsDeep && closed) { coords.push(coords[0]); } return coords; } function getFeature(layer, newGeometry) { return layer.feature ? extend({}, layer.feature, {geometry: newGeometry}) : asFeature(newGeometry); } // @function asFeature(geojson: Object): Object // Normalize GeoJSON geometries/features into GeoJSON features. function asFeature(geojson) { if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') { return geojson; } return { type: 'Feature', properties: {}, geometry: geojson }; } var PointToGeoJSON = { toGeoJSON: function (precision) { return getFeature(this, { type: 'Point', coordinates: latLngToCoords(this.getLatLng(), precision) }); } }; // @namespace Marker // @section Other methods // @method toGeoJSON(precision?: Number): Object // `precision` is the number of decimal places for coordinates. // The default value is 6 places. // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the marker (as a GeoJSON `Point` Feature). Marker.include(PointToGeoJSON); // @namespace CircleMarker // @method toGeoJSON(precision?: Number): Object // `precision` is the number of decimal places for coordinates. // The default value is 6 places. // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the circle marker (as a GeoJSON `Point` Feature). Circle.include(PointToGeoJSON); CircleMarker.include(PointToGeoJSON); // @namespace Polyline // @method toGeoJSON(precision?: Number): Object // `precision` is the number of decimal places for coordinates. // The default value is 6 places. // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the polyline (as a GeoJSON `LineString` or `MultiLineString` Feature). Polyline.include({ toGeoJSON: function (precision) { var multi = !isFlat(this._latlngs); var coords = latLngsToCoords(this._latlngs, multi ? 1 : 0, false, precision); return getFeature(this, { type: (multi ? 'Multi' : '') + 'LineString', coordinates: coords }); } }); // @namespace Polygon // @method toGeoJSON(precision?: Number): Object // `precision` is the number of decimal places for coordinates. // The default value is 6 places. // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the polygon (as a GeoJSON `Polygon` or `MultiPolygon` Feature). Polygon.include({ toGeoJSON: function (precision) { var holes = !isFlat(this._latlngs), multi = holes && !isFlat(this._latlngs[0]); var coords = latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true, precision); if (!holes) { coords = [coords]; } return getFeature(this, { type: (multi ? 'Multi' : '') + 'Polygon', coordinates: coords }); } }); // @namespace LayerGroup LayerGroup.include({ toMultiPoint: function (precision) { var coords = []; this.eachLayer(function (layer) { coords.push(layer.toGeoJSON(precision).geometry.coordinates); }); return getFeature(this, { type: 'MultiPoint', coordinates: coords }); }, // @method toGeoJSON(precision?: Number): Object // `precision` is the number of decimal places for coordinates. // The default value is 6 places. // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the layer group (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`). toGeoJSON: function (precision) { var type = this.feature && this.feature.geometry && this.feature.geometry.type; if (type === 'MultiPoint') { return this.toMultiPoint(precision); } var isGeometryCollection = type === 'GeometryCollection', jsons = []; this.eachLayer(function (layer) { if (layer.toGeoJSON) { var json = layer.toGeoJSON(precision); if (isGeometryCollection) { jsons.push(json.geometry); } else { var feature = asFeature(json); // Squash nested feature collections if (feature.type === 'FeatureCollection') { jsons.push.apply(jsons, feature.features); } else { jsons.push(feature); } } } }); if (isGeometryCollection) { return getFeature(this, { geometries: jsons, type: 'GeometryCollection' }); } return { type: 'FeatureCollection', features: jsons }; } }); // @namespace GeoJSON // @factory L.geoJSON(geojson?: Object, options?: GeoJSON options) // Creates a GeoJSON layer. Optionally accepts an object in // [GeoJSON format](https://tools.ietf.org/html/rfc7946) to display on the map // (you can alternatively add it later with `addData` method) and an `options` object. function geoJSON(geojson, options) { return new GeoJSON(geojson, options); } // Backward compatibility. var geoJson = geoJSON; /* * @class ImageOverlay * @aka L.ImageOverlay * @inherits Interactive layer * * Used to load and display a single image over specific bounds of the map. Extends `Layer`. * * @example * * ```js * var imageUrl = 'http://www.lib.utexas.edu/maps/historical/newark_nj_1922.jpg', * imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]]; * L.imageOverlay(imageUrl, imageBounds).addTo(map); * ``` */ var ImageOverlay = Layer.extend({ // @section // @aka ImageOverlay options options: { // @option opacity: Number = 1.0 // The opacity of the image overlay. opacity: 1, // @option alt: String = '' // Text for the `alt` attribute of the image (useful for accessibility). alt: '', // @option interactive: Boolean = false // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered. interactive: false, // @option crossOrigin: Boolean|String = false // Whether the crossOrigin attribute will be added to the image. // If a String is provided, the image will have its crossOrigin attribute set to the String provided. This is needed if you want to access image pixel data. // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values. crossOrigin: false, // @option errorOverlayUrl: String = '' // URL to the overlay image to show in place of the overlay that failed to load. errorOverlayUrl: '', // @option zIndex: Number = 1 // The explicit [zIndex](https://developer.mozilla.org/docs/Web/CSS/CSS_Positioning/Understanding_z_index) of the overlay layer. zIndex: 1, // @option className: String = '' // A custom class name to assign to the image. Empty by default. className: '' }, initialize: function (url, bounds, options) { // (String, LatLngBounds, Object) this._url = url; this._bounds = toLatLngBounds(bounds); setOptions(this, options); }, onAdd: function () { if (!this._image) { this._initImage(); if (this.options.opacity < 1) { this._updateOpacity(); } } if (this.options.interactive) { addClass(this._image, 'leaflet-interactive'); this.addInteractiveTarget(this._image); } this.getPane().appendChild(this._image); this._reset(); }, onRemove: function () { remove(this._image); if (this.options.interactive) { this.removeInteractiveTarget(this._image); } }, // @method setOpacity(opacity: Number): this // Sets the opacity of the overlay. setOpacity: function (opacity) { this.options.opacity = opacity; if (this._image) { this._updateOpacity(); } return this; }, setStyle: function (styleOpts) { if (styleOpts.opacity) { this.setOpacity(styleOpts.opacity); } return this; }, // @method bringToFront(): this // Brings the layer to the top of all overlays. bringToFront: function () { if (this._map) { toFront(this._image); } return this; }, // @method bringToBack(): this // Brings the layer to the bottom of all overlays. bringToBack: function () { if (this._map) { toBack(this._image); } return this; }, // @method setUrl(url: String): this // Changes the URL of the image. setUrl: function (url) { this._url = url; if (this._image) { this._image.src = url; } return this; }, // @method setBounds(bounds: LatLngBounds): this // Update the bounds that this ImageOverlay covers setBounds: function (bounds) { this._bounds = toLatLngBounds(bounds); if (this._map) { this._reset(); } return this; }, getEvents: function () { var events = { zoom: this._reset, viewreset: this._reset }; if (this._zoomAnimated) { events.zoomanim = this._animateZoom; } return events; }, // @method setZIndex(value: Number): this // Changes the [zIndex](#imageoverlay-zindex) of the image overlay. setZIndex: function (value) { this.options.zIndex = value; this._updateZIndex(); return this; }, // @method getBounds(): LatLngBounds // Get the bounds that this ImageOverlay covers getBounds: function () { return this._bounds; }, // @method getElement(): HTMLElement // Returns the instance of [`HTMLImageElement`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement) // used by this overlay. getElement: function () { return this._image; }, _initImage: function () { var wasElementSupplied = this._url.tagName === 'IMG'; var img = this._image = wasElementSupplied ? this._url : create$1('img'); addClass(img, 'leaflet-image-layer'); if (this._zoomAnimated) { addClass(img, 'leaflet-zoom-animated'); } if (this.options.className) { addClass(img, this.options.className); } img.onselectstart = falseFn; img.onmousemove = falseFn; // @event load: Event // Fired when the ImageOverlay layer has loaded its image img.onload = bind(this.fire, this, 'load'); img.onerror = bind(this._overlayOnError, this, 'error'); if (this.options.crossOrigin || this.options.crossOrigin === '') { img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin; } if (this.options.zIndex) { this._updateZIndex(); } if (wasElementSupplied) { this._url = img.src; return; } img.src = this._url; img.alt = this.options.alt; }, _animateZoom: function (e) { var scale = this._map.getZoomScale(e.zoom), offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min; setTransform(this._image, offset, scale); }, _reset: function () { var image = this._image, bounds = new Bounds( this._map.latLngToLayerPoint(this._bounds.getNorthWest()), this._map.latLngToLayerPoint(this._bounds.getSouthEast())), size = bounds.getSize(); setPosition(image, bounds.min); image.style.width = size.x + 'px'; image.style.height = size.y + 'px'; }, _updateOpacity: function () { setOpacity(this._image, this.options.opacity); }, _updateZIndex: function () { if (this._image && this.options.zIndex !== undefined && this.options.zIndex !== null) { this._image.style.zIndex = this.options.zIndex; } }, _overlayOnError: function () { // @event error: Event // Fired when the ImageOverlay layer fails to load its image this.fire('error'); var errorUrl = this.options.errorOverlayUrl; if (errorUrl && this._url !== errorUrl) { this._url = errorUrl; this._image.src = errorUrl; } } }); // @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options) // Instantiates an image overlay object given the URL of the image and the // geographical bounds it is tied to. var imageOverlay = function (url, bounds, options) { return new ImageOverlay(url, bounds, options); }; /* * @class VideoOverlay * @aka L.VideoOverlay * @inherits ImageOverlay * * Used to load and display a video player over specific bounds of the map. Extends `ImageOverlay`. * * A video overlay uses the [`