dkopp-6.5/0000775000175000017500000000000012343020444011137 5ustar micomicodkopp-6.5/dkopp-6.5.spec0000644000175000017500000000172112343020444013435 0ustar micomico# RPM spec file for dkopp # Name: dkopp Version: 6.5 Release: 1 Summary: full and incremental backup to DVD Group: utils Vendor: kornelix Packager: kornelix@posteo.de License: GPL3 Source: %{name}-%{version}.tar.gz URL: http://kornelix.com %description Back-up files to DVD or Blue-Ray disc. Full or incremental backup with full or incremental verification. Choose files and directories to include or exclude at any level. Incremental backup updates the same disc from a prior full backup. Recover files using Dkopp, or drag and drop using a file browser. Dkopp is a graphical front end for growisofs and genisoimage. %prep %setup -q %build make %install make install PREFIX=$RPM_BUILD_ROOT/usr %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root) /usr/bin/%{name} /usr/share/%{name} /usr/share/doc/%{name} /usr/share/applications/%{name}.desktop /usr/share/man/man1/%{name}.1.gz dkopp-6.5/doc/0000755000175000017500000000000012343020444011702 5ustar micomicodkopp-6.5/doc/changelog0000644000175000017500000004464312343020444013567 0ustar micomicodkopp Change Log ================ 2014.06.01 v.6.5 + Bugfix: find command fails when embedded blanks in filespec are represented as "\040" (as in /etc/mtab). Therefore replace with " ". 2014.02.10 v.6.4 + Rely on desktop manager (e.g. Gnome) and user to perform disc mounting (discontinue internal mount commmand causing a fight with Gnome). + Update method used to find DVD/BlueRay devices. 2013.04.01 v.6.3.1 + Display online help file from menu Help > contents. 2012.11.01 v.6.3 + Replaced deprecated GTK functions with new versions. + Improved clarity of some GUI and report texts. 2012.03.17 v.6.2 + Bugfix: Some DVDs have a top directory with embedded blanks. These would not work because "\040" was being substituted. Fixed. 2012.02.01 v.6.1 + Dkopp was converted to use GTK3 and Cairo. It will no longer build or install on older Linux distros lacking these libraries. + Bugfix: /etc/mtab is no longer reliable for DVD mount status. Using /proc/mounts has improved the reliability of DVD mounting. + RPM packages are built using Fedora and rpmbuild instead of alien. 2011.10.20 v.6.0 + Code cleanup for compiler warnings from GCC 4.6 (Ubuntu 11.10). + Removed the use of GTK functions within threads. + DEB package is now made by dkpg and RPM is made from DEB via alien. 2011.08.17 v.5.9.2 + Compensation added for hung DVD/BD drive after growisofs is done. The drive can become unresponsive (ignores mount commands), but ejecting and reloading cures the problem and allows the verify operation to proceed. 2011.08.08 v.5.9.1 + The option was added to abort/retry/ignore errors from growisofs. Sometimes nothing is really wrong, or the error is temporary and will not recurr if the last operation is repeated after a short delay. Interference by Gnome auto-mount cannot always be stopped. These error statuses vary with new releases of the kernel, Gnome, and growisofs, and can also vary by DVD/BD hardware. 2011.08.01 v.5.9 + Mount logic changed to deal better with automatically mounted media. Gnome will mount the DVD/BD even if dkopp issues the mount command. Dkopp gets "broken pipe" status. This wierdness is simply accepted. 2011.05.01 v.5.8 + Packages are now built with Ubuntu 10.04 instead of 8.10. The default install location is now /usr/ instead of /usr/local/. + Growisofs always returns a bad status for Blue-Ray media, even though they are always OK (so far). The user message was changed to state that the media is likely OK and give the option to continue accordingly. The verify phase will determine the true media status afterwards. 2011.02.20 v.5.7 + For full backups, do not mount BD/DVD (stop meaningless error messages). + For incremental backup or verify: if mount fails, eject and mount again (try to recover from Gnome mount contention, "broken pipe" etc.) 2010.12.05 v.5.6 + Bugfix: quote DVD label to prevent failure from imbedded blanks. + Big backup job with multiple DVDs: add recovery from write or verify error: repeat a failed DVD in-sequence before starting the next one. + Workaround: "Unknown error 18446744073709551615" from the stat() function turns out not to be a real error and can be ignored. + An elementary man page was added. Details are in the user guide. 2010.09.13 v.5.5 + Bugfix: -script and -nogui failed to work if script is verify only. + Improved diagnostics and progress monitoring for -nogui option. 2010.08.21 v.5.4 + A button [root] was added to the toolbar. This restarts the application with root privileges if the queried password (sudo) is correct. 2010.07.15 v.5.3 + Dkopp was tested with Blue-Ray media, recordable (BD-R) and re-writeable (BD-RE). It works without modification (i.e. growisofs works). The job size reasonableness check was raised to 50 GB. The menus and docs were revised to indicate that BD as well as DVD media can be used. + Setting the DVD/BD mount point was removed from backup job edit and restore job edit. This is now done only in the dialog to select the DVD/BD drive and override the default mount point. 2010.05.24 v.5.2 Output formats for several reports were slightly improved. 2010.03.01 v.5.1 + Simplify how DVD labels and mount points are handled, in an attempt to play better with various DVD auto-mount environments. Ubuntu 9.10 auto-mounts DVDs that dkopp requests to mount, causing a small war. Ubuntu 10.04 creates /media/xxxx when a DVD with label xxxx auto mounts: the location is therefore variable. Dkopp now looks only for the DVD device and uses an existing mount point. If DVD auto-mounting is enabled, start dkopp after the auto-mount completes. Mount points in existing dkopp job files are now ignored and can be removed to avoid an innocuous diagnostic. You can use gconf-editor to prevent auto-mounting: keys are: /apps/nautilus/preferences/media_automount. + The mod time resolution on DVD files has recently changed from 0.001 to 1.0 seconds, and apparently only for hidden files (genisoimage?). Dkopp now regards mod time differences under 1 second as equivalent. 2009.12.26 v.5.0 + Support non-GUI mode for command-line / deferred execution (cron). (e.g. $ dkopp -nogui -run /.../my-dkopp.job) 2009.10.30 v.4.8 + Set file exclusion date to 1970.01.01 by default (older files cannot be date compared with disk files). + Avoid dialog to request DVD mount if a script file is running and verify that the DVD is present before continuing. + If DVD mount point is missing, create it and continue. + Unmount DVD: use mount point instead of device in umount command. 2009.07.13 v.4.7.1 fix compile errors introduced by gcc 4.4 2009.04.25 v.4.7 Another change in DVD mounting to get around newest Linux issue: DVD mount command can lock-up (hang) if a DVD is blank or newly formatted. Therefore, in the case of a full backup, a mount attempt is no longer made (this was done to show the label and any prior backup usage data). For incremental backups the mount is still required, so do not try an incremental backup with a blank DVD (which makes no sense anyway). 2009.04.12 v.4.6 + An additional file selection method is provided: include only files having a creation or modification date on or after a given date. + Inconsistencies in the handling of symlinks have been cleaned up (see the technical note in the user guide). + Bugfix: an excluded file that is also inaccessible or exceeds 4 GB in size caused a segment fault. 2009.03.30 v.4.5 + DVD speed factor was added to the job edit. The user may elect a lower speed than the default for the DVD medium, to increase DVD reliability and longevity. Leave blank or zero to use the default speed. + The Linux program "udevinfo" has recently become "udevadm info". Dkopp was modified to use whichever variant works. 2008.11.03 v.4.4 Detect if the DVD is already mounted and use as-is (set backup directory to match mount point). The dkopp backup job should use the same device name that Gnome (or other desktop) uses, e.g. /dev/scdo instead of /dev/sr0. Otherwise dkopp will try to redundantly mount the device. It is best to let the auto-mount complete before starting a backup job, to avoid a tug-of-war as dkopp tries to mount the device in parallel. 2008.06.01 v 4.3 + change from build script to conventional makefile + name change from dkop to dkopp 2008.04.20 v.42 + code changes for compatibility with recent gcc, c-lib, and GTK2 + changes to DVD mount logic to get around the latest driver flakeyness and mount interference from Gnome 2008.03.11 v.41 + added DVD eject and re-mount when DVD mount fails after growisofs - growisofs can leave the DVD totally unresponsive, causing verify after backup to fail. The backup is usually OK and verify will work normally after the DVD has been ejected and reloaded. - happens with DVD+R and -R, but not with DVD+RW (on my 2 computers) - this workaround works for my desktop but not for my notebook (but it does work if the DVD tray is manually pushed back in) 2008.02.04 v.40 + replace DVD eject between backup and verify with sleep(3) + mount. 2008.01.29 v.39 + separate build scripts for downloaders and package builders + code changes to get application directories from build script 2008.01.20 v.38 + ignore mount error if full backup (likely a blank DVD) + ignore failed eject command - some systems cannot do software eject 2008.01.01 v.37 + made several changes for 64-bit architecture compatibility, including sequence sensitivity of #include statements leading to runtime crash in gtk_dialog_new_with_buttons() + build script allows user choice of install location and desktop launcher 2007.12.11 v.36 + workaround for defective lib function strerror_r() and BSD compatibility 2007.12.10 v.35 + fix compile errors for 64-bit architecture + code cleanup for newest gcc compiler warnings + changed logic of verify thorough - now works independently of backup job + error messages to stderr are now captured and put on the screen 2007.11.20 v.34 + trap mkisofs / genisoimage errors not reported in growisofs status (can make a bad DVD look good) 2007.11.15 v.33 + new GTK requirement: if (! g_thread_supported()) g_thread_init(0); 2007.11.12 v.32 + stop propagation of DVD label suffix (dkop:A:A:A...) if same DVD is re-used for multiple full backups + locale related library changes 2007.10.15 v.31 + fix significant bug in thorough verify: files not compared 100% + list include and exclude records at the start of the history log file + don't lose comment records in job file + eliminate "end" record in job file (end is EOF) 2007.08.29 v.30 + move DVD label setting from job edit to separate menu item + DVD mount no longer overwrites a previously set backup job label 2007.08.23 v.29 + stop exotic files names with special characters like \r from trashing output formats + optional user input of DVD label + provide searchable backup log file (time/date, label, list of files) + query to find all backup DVDs containing desired files, using wildcards 2007.07.13 v.28 + accept job file on command line (without -job) (per Linux convention) + allow retry of a failed DVD within a multiple-DVD backup job + correct progress tracking for multiple-DVD jobs 2007.07.04 v.27 .27b + back to using bash for DVD mount (undocumented black magic works better) + follow Linux convention for application files in /home/user/.dkop/ or /root/.dkop/ (move your job files here for convenience) + improve clarity of command outputs and reports + allow show / hide of hidden files in job edit file chooser dialog 2007.05.28 v.26 + add file load and save convenience buttons to job edit dialog + increase file limit from 100K to 200K files (memory usage +15 MB) 2007.05.13 v.25 + replace menu and toolbar macros with new zfuncs functions 2007.04.25 v.24 + minor report improvements + add report: disk:DVD differences by directory + allow file size exceeding 2 GB 2007.04.08 v.23 + fix bug: thorough verify was checking files created after last DVD update + progress monitoring: backup displays file names being written to DVD + set margin for comparing file mod times to 1 ms instead of 1 sec. + remove new owner option from restore (don't duplicate Linux capability) + use c-lib functions for file restores instead of shell commands 2007.03.26 v.22 + detect mount EROFS status (read-only file system) and call it success 2007.03.26 v.21 + unmount DVD before eject (avoid drive hung busy with some Linux kernels) + use $ wodim -abort to reset hung-up DVD drives (newer Linux systems only) 2007.03.19 v.20 + clarify output from menu: DVD and mount point + use mount() function for DVD instead of shell - better status information 2007.02.02 v.19 + job editor: select DVD and mount point from list (instead of typing in) + incremental backup: eliminate duplicate differences report 2007.01.10 v.18 + use generic monospace font instead of Courier 10 (not always available) + rationalized menus and toolbar buttons for more conventional interface + removed Joliet file system from DVD: Windows XP can read standard DVD + converted dialogs to zdialog functions + added icons to toolbar buttons 2006.11.30 v.17 + added tool tips to buttons 2006.11.28 v.16 + added 3 sec. delay between backup and verify and between mount retries (DVD drive may still be busy after growisofs completes, causing following mount command to fail) (why is the driver so flakey?) + reorganized library functions into zfuncs.cpp and zfuncs.h + added "edit" button for faster access to backup job editor + corrected bug causing verify error for file names with trailing blanks 2006.11.01 v.15 + added menu: set DVD device and mount point independently of job file. + bug fix: report of disk-DVD differences was ignoring a manual DVD change. + GTK 2.6.10 issue: hidden files not shown in the file-chooser dialog. Button was added to allow user to view these files if desired. 2006.09.26 v.14 + added support for large backup jobs using multiple DVDs + corrected GTK coding error that was slowing down report output + added button to clear window in backup job editor + added command to set DVD device independently of backup job file + verify (thorough) reports files modified during run (not a compare error) + default backup job changed to: include /home/username/* + made minor improvements in output formats and user guide 2006.09.12 v.13 + exit script file if a command fails + fix bug in thorough verify: DVD file compared to itself (ouch) + detect growisofs failure from its log messages (may not emit bad status) 2006.09.05 v.12 + reorganize menu names and groups + add menu command for quick-formatting DVDs + add menu command for drive hardware reset (may unlock hung drive) + do not retry failed mount for full backup (likely a blank DVD) + add growisofs undocumented parameters to improve robustness (avoid rejecting DVD already having ISO-9660 file system) + improve disk-DVD differences report: report file names by category: unmatched disk files, unmatched DVD files, files with different content 2006.08.23 v.11 + improve error reporting (DVD mount failures) + DVD mount status 8192: meaning? insert extra eject and mount retry + build script: check that GCC compiler and GTK+ library are installed + workaround for GTK multi-thread seg. faults + fix bug: restore owner failure for filenames containing single quote + additions to user guide, technical notes 2006.08.09 v.10 + correct zlock() thread problems (add mutex lock for global variables) + suppress error messages (e.g. broken links) for excluded files + ignore status from shell "find" command (bad status can be OK) + errors are now visible in terminal window, e.g. "permission denied" + minor additions to user guide 2006.08.08 v.09 + added script file capability: all menu commands can be scripted + improved robustness in the use of GTK within threads + command line parameters revised as follows: -backup command -job filename -script filename + exclude statements pertain to ALL prior include statements + improve diagnostics for problem files (stat() function returns error) 2006.07.13 v.08 + minor improvements to user guide / help file + added an option to change ownership for files restored from DVD + removed option to restore directory permissions and made this automatic (revising permissions is outside dkop scope - see user guide) + eject is now unmount + eject (eject alone sometimes does not work) + made restore more impervious to special chars in file names [ $ " = ] + /tmp/dkop-xxxx files now have PID in name, allowing parallel use + /tmp/dkop-xxxx files are now deleted automatically when dkop exits 2006.07.10 v.07 + add index and hyperlinks to help file, dkop-guide.pdf + make help function run as subprocess (view help and run dkop in parallel) + use acroread | gpdf | evince (first one available) to display help file + speed up include files processing (use find instead of readdir) + report byte counts for included and excluded files + if command line job file is relative, make absolute /pathname/filename (gtk_file_chooser_set_filename() does not apply cwd) + fix bug: restore of files having '$' in pathname failed (use cp 'path' instead of "path") + detect duplicate files (overlapping includes) (avoid growisofs failure) + better formatting for report > in/ex file counts + added verification of DVD device and mount point 2006.07.01 v.06 + added toolbar with buttons for clear, kill, pause, resume + minor tightening of job verify logic after loading or editing + added menu function: show job file + removed auto mount of DVD (confusing) + avoid redundant remounts of DVD (detect when no change, avoid time waste) 2006.06.24 v.05 + new menus: full backup + verify, incremental backup + verify, accumulate backup + verify + remove autoload of file default.job - unnecessary and confusing + skip special files - pipes, sockets, devices + fix bug: restore directory owner & permissions omitted some directories + symlinks are backed-up and restored, not target files (see user guide) + fix SearchWild() bug: search for file without wildcards failed (should find 0 or 1 file) 2006.06.15 v.04 + save on DVD: backup job file, backup date-time, DVD usage counter + new feature: run backup job file stored on DVD itself + detect broken symlink files, diagnose and ignore (message was "error: success") + revised GTK menu macros to use "string" instead of #string 2006.06.10 v.03 + finished escaping of '=' in file names (mkisofs requirement) + disk and DVD file names are equal if disk with '=' matches DVD with "\=" + file restore function replaces "\=" with '=' + user sees "\=" in restore function file-chooser dialog + file restore bug: invalid copy_from diagnostic naming copy_to location + auto locate help file in same directory as dkop executable + fixed bug in zmainloop(): menu lockup from use of pause function + simplified lock and pause/resume logic to make more robust 2006.06.01 v.02 + added file restore capability + added directory owner and permissions recovery + corrected bug in byte count calculation (1 file and 0 bytes to restore) + eliminated trailing blanks in filespecs (caused failures) + corrected bad detection of open() error (fid < 0) + replace '=' in file names with "\\=" (mkisofs requirement) (caused failures with strange gnome file names) + stopped use of fprintf() for writes to scratch files ('%' in filename interpreted as format) + bug: open of /tmp files fails if they do not exist beforehand 2006.05.01 v.01 initial release + backup job: open, edit, save + backup: full, incremental, accumulate + verify: full, incremental, thorough + disk / DVD differences reports dkopp-6.5/doc/README0000644000175000017500000000305412343020444012564 0ustar micomicoInstallation of dkopp from source tarball Building dkopp requires the following packages: g++ the Gnu C++ compiler and linker libgtk3.0-dev GTK graphics library (GUI base) Build and install dkopp as follows: 1. Download the tar file (dkopp-N.N.tar.gz) to Desktop 2. Open a terminal window 3. $ cd Desktop # go to Desktop 4. $ tar -xzf dkopp-N.N.tar.gz # unpack to ./dkopp 5. $ cd dkopp # go there 6. $ make # build program 7. $ sudo make install # install program Missing dependencies will cause error messages in step 6. Install these from your repository and repeat step 6. Step 7 moves all files to the following locations: /usr/bin/dkopp binary executable /usr/share/dkopp/ icons, translations ... /usr/share/doc/dkopp/ user guide, README ... For step 7, use "sudo" or "su -c" to get root privileges. To run dkopp, the programs growisofs and either mkisofs or genisoimage are needed. These are normally included in Linux distributions. If needed, use your package manager to install them. The program wodim is also recommended (currently used only to reset a hung DVD drive). Normally dkopp is run as root so that DVDs can be mounted. Use "sudo dkopp" or "gksu dkopp". Please review the user guide (Help menu) before trying dkopp. NOTES FOR PACKAGE BUILDERS: If $PREFIX is defined, files go there instead of /usr. If $DESTDIR is also defined, files go to $DESTDIR$PREFIX. dkopp-6.5/doc/dkopp.man0000644000175000017500000000424712343020444013523 0ustar micomico.TH DKOPP 1 2010-10-01 "Linux" "Dkopp man page" .SH NAME Dkopp - copy files to DVD or BD (Blue-ray) media .SH SYNOPSIS \fBdkopp\fR [ \fB-job\fR | \fB-run\fR ] \fIjobfile\fR \fBdkopp\fR [ \fB-nogui\fR ] \fB-script\fR \fIscriptfile\fR .SH DESCRIPTION Dkopp copies files to backup DVD or BD media. It supports full or incremental backups and full or incremental media verification. .SH OVERVIEW Dkopp is a menu-driven GUI (GTK) program operating in its own window. Dkopp copies files and directories specified in a job file to DVD or BD media. Dkopp can copy all files to empty media (full copy), or only new and modified files to previously used media (incremental). Files and directories to include or exclude can be selected from the file system hierarchy using a GUI navigator. Specifications are saved in a job file which can be re-edited and re-used. Script files can be run in batch mode using the \-nogui option. Dkopp can be used to select and restore files previously copied, and owner and permission data is also restored. The DVD/BD media can also be accessed with file system tools like Nautilus. Dkopp supports the following functionalities: - Three backup modes: full, incremental, accumulate. - Three media verification modes: full, incremental, thorough. - Use write-once or re-writable DVD or BD media (but not CD). - Report disk:backup differences in detail or summary form. - Select and restore files from a backup copy (or use drag and drop). - Search log files to find media where specified files are saved. .SH OPTIONS Command line options: [ \fB-job\fR ] \fIjobfile\fR open job file for editing \fB-run\fR \fIjobfile\fR execute a job file [ \fB-nogui\fR ] \fB-script\fR \fIscriptfile\fR execute a script file .SH SEE ALSO The online user manual is available using the menu Help > contents. This manual explains Dkopp operation in great detail. Dkopp uses the batch programs \fBgrwoisofs\fR and \fBgenisoimage\fR. Dkopp is essentially a GUI front-end for these programs. .SH AUTHORS Written by Mike Cornelison dkopp-6.5/doc/copyright0000644000175000017500000000100012343020444013624 0ustar micomicoAuthor: Michael Cornelison Copyright: Copyright 2007-2014 Michael Cornelison The source program code can be found here: http://kornelix.com/tarballs License: You are free to distribute this software under the terms of the GNU General Public Licensel, either version 3 of the License, or (at your option) any later version. The complete text of the license can be found here: http://www.gnu.org/licenses/gpl-3.0.html See "/usr/share/common-licenses/GPL-3". dkopp-6.5/zfuncs.h0000644000175000017500000013437212343020444012630 0ustar micomico/************************************************************************** zfuncs.h include file for zfuncs functions Copyright 2006-2014 Michael Cornelison source URL: kornelix.com contact: kornelix@posteo.de This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***************************************************************************/ // zfuncs.h version v.5.8 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VERTICAL GTK_ORIENTATION_VERTICAL // GTK shortcuts #define HORIZONTAL GTK_ORIENTATION_HORIZONTAL #define int8 char // number types #define int16 short #define int32 long #define int64 long long #define uint8 unsigned char #define uint16 unsigned short #define uint32 unsigned long #define uint64 unsigned long long #define uchar unsigned char #define cchar const char #define wstrerror(err) strerror(WEXITSTATUS(err)) // get text status for child process #define mutex_tp pthread_mutex_t // abbreviations #define mutex_init pthread_mutex_init #define mutex_lock pthread_mutex_lock #define mutex_trylock pthread_mutex_trylock #define mutex_unlock pthread_mutex_unlock #define mutex_destroy pthread_mutex_destroy #define maxfcc 1000 // max. file pathname cc tolerated #define null NULL #define true 1 #define false 0 // trace execution: output file, function, line no, caller address #define TRACE \ trace(__FILE__,__FUNCTION__,__LINE__,__builtin_return_address(0)); // system functions ====================================================== void *zmalloc(size_t cc); // malloc() with counter v.5.8 void zfree(void *pp); // free() with counter char *zstrdup(cchar *string, int addcc = 0); // strdup() with counter void printz(cchar *format, ...); // printf() with immediate fflush() v.5.8 void zpopup_message(cchar *format, ...); // popup message, thread safe void zappcrash(cchar *format, ...); // crash with popup message in text window void catch_signals(); // catch signals and do backtrace dump void trace(cchar *file, cchar *func, int line, void *addr); // implements TRACE macro void tracedump(); // dump program trace data void beroot(int argc = 0, char *argv[] = 0); // restart image as root if password OK double get_seconds(); // seconds since 2000.01.01 void start_timer(double &time0); // start a timer double get_timer(double &time0); // get elapsed time in seconds void start_CPUtimer(double &time0); // start a process CPU timer double get_CPUtimer(double &time0); // get elapsed CPU time, seconds double CPUtime(); // get elapsed process CPU time for main() double CPUtime2(); // " include all threads double jobtime(); // " include all threads + subprocesses void compact_time(const time_t fileDT, char *compactDT); // time_t date/time to yyyymmddhhmmss int parseprocfile(cchar *pfile, cchar *pname, double *value, ...); // get data from /proc file int parseprocrec(char *prec, int field, double *value, ...); // get data from /proc file record void zsleep(double dsecs); // sleep specified seconds int global_lock(cchar *lockfile); // obtain exclusive lock, multi-process int global_unlock(int fd, cchar *lockfile); // release the lock void start_detached_thread(void * tfunc(void *), void * arg); // start detached thread function void synch_threads(int NT = 0); // synchronize NT threads int zget_locked(int ¶m); // lock and get multi-thread parameter void zput_locked(int ¶m, int value); // put and unlock multi-thread parameter int zadd_locked(int ¶m, int incr); // increment multi-thread parameter int shell_quiet(cchar *command, ...); // format/run shell command, return status int shell_ack(cchar *command, ...); // "" + popup an error message if error int shell_asynch(cchar *command, ...); // start shell command, return immediately int shell_asynch_status(int handle); // get status of asynch shell command char * command_output(int &contx, cchar *command, ...); // get shell command output int command_status(int contx); // get exit status of command int command_kill(int contx); // kill command before completion int signalProc(cchar * pname, cchar * signal); // pause/resume/kill subprocess int runroot(cchar *sucomm, cchar *command); // run command as root via su or sudo char * fgets_trim(char * buff, int maxcc, FILE *, int bf = 0); // fgets + trim trailing \n \r (blanks) int samedirk(cchar *file1, cchar *file2); // returns 1 if files in same directory int parsefile(cchar *path, char **dir, char **file, char **ext); // parse a filespec int renamez(cchar *file1, cchar *file2); // rename, also across file systems // measure CPU time spent in a function or code block within a function extern volatile double cpu_profile_timer; // internal data tables extern volatile double cpu_profile_table[100]; extern volatile double cpu_profile_elapsed; void cpu_profile_init(); // initialize at start of test void cpu_profile_report(); // report CPU time per function inline void cpu_profile_enter(int fnum) // at entry to measured code block { cpu_profile_timer = cpu_profile_elapsed; } inline void cpu_profile_exit(int fnum) // at exit from measured code block { cpu_profile_table[fnum] += cpu_profile_elapsed - cpu_profile_timer; } int pagefaultrate(); // monitor own process hard fault rate // string macros and functions =========================================== #define strEqu(str1,str2) (! strcmp((str1),(str2))) // TRUE if strings compare equal #define strcaseEqu(str1,str2) (! strcasecmp((str1),(str2))) // TRUE if strings equal, ignoring case #define strNeq(str1,str2) (strcmp((str1),(str2))) // TRUE if strings compare not-equal #define strnEqu(str1,str2,cc) (! strncmp((str1),(str2),(cc))) // TRUE if strings[cc] compare equal #define strncaseEqu(str1,str2,cc) (! strncasecmp((str1),(str2),(cc))) // TRUE if strings[cc] equal, ignoring case #define strnNeq(str1,str2,cc) (strncmp((str1),(str2),(cc))) // TRUE if strings[cc] compare not-equal cchar * strField(cchar *string, cchar *delims, int Nth); // get Nth delimited field in string cchar * strField(cchar *string, cchar delim, int Nth); // get Nth delimited field in string int strParms(int &bf, cchar *inp, char *pname, int maxcc, double &pval); // parse string: name1=val1 | name2 ... int strHash(cchar *string, int max); // string --> random int 0 to max-1 int strncpy0(char *dest, cchar *source, uint cc); // strncpy, insure null, return 0 if fit void strnPad(char *dest, cchar *source, int cc); // strncpy with blank padding to cc int strTrim(char *dest, cchar *source); // remove trailing blanks int strTrim(char *string); // remove trailing blanks int strTrim2(char *dest, cchar *source); // remove leading and trailing blanks int strTrim2(char *string); // remove leading and trailing blanks int strCompress(char *dest, cchar *source); // remove all blanks incl. imbedded int strCompress(char *string); // remove all blanks int strncatv(char *dest, int maxcc, cchar *source, ...); // catenate strings (last = null) int strcmpv(cchar *string, ...); // compare to N strings, return 1-N or 0 void strToUpper(char *dest, cchar *source); // move and conv. string to upper case void strToUpper(char *string); // conv. string to upper case void strToLower(char *dest, cchar *source); // move and conv. string to lower case void strToLower(char *string); // conv. string to lower case int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout); // copy string and replace 1 substring int repl_Nstrs(cchar *strin, char *strout, ...); // copy string and replace N substrings void strncpyx(char *out, cchar *in, int ccin); // conv. string to hex format void StripZeros(char *pNum); // 1.230000E+12 --> 1.23E+12 int blank_null(cchar *string); // test for blank/null string char * strdupz(cchar *string, int more = 0); // duplicate string and add space at end int clean_escapes(char *string); // replace \x escapes with real characters int utf8len(cchar *utf8string); // get graphic cc for UTF8 string int utf8substring(char *utf8out, cchar *utf8in, int pos, int cc); // get graphic substring from UTF8 string int utf8_check(cchar *string); // check utf8 string for encoding errors int utf8_position(cchar *utf8in, int Nth); // get byte position of Nth graphic char. // number conversion ===================================================== int convSI (cchar *inp, int &inum, cchar **delm = 0); // string to int int convSI (cchar *inp, int &inum, int low, int hi, cchar **delm = 0); // (with low/high limit checking) int convSD (cchar *inp, double &dnum, cchar **delm = 0); // string to double int convSD (cchar *inp, double &dnum, double low, double hi, cchar **delm = 0); // (with low/high limit checking) int convSF (cchar *inp, float &fnum, cchar **delm = 0); // string to double int convSF (cchar *inp, float &fnum, float low, float hi, cchar **delm = 0); // (with low/high limit checking) int convIS (int iin, char *outp, int *cc = 0); // int to string, returned cc int convDS (double din, int prec, char *outp, int *cc = 0); // double to string, precision, ret. cc char * formatKBMB(double fnum, int prec); // format nnn B, nn.n KB, n.nn MB, etc. // wildcard functions ==================================================== int MatchWild(cchar * wildstr, cchar * str); // wildcard string match (match = 0) int MatchWildIgnoreCase(cchar * wildstr, cchar * str); // wildcard string match, ignoring case cchar * SearchWild(cchar *wpath, int &flag); // wildcard file search cchar * SearchWildIgnoreCase(cchar *wpath, int &flag); // wildcard file search, ignoring case // search and sort functions ============================================= int bsearch(int seekint, int nn, int list[]); // binary search sorted list[nn] int bsearch(char *seekrec, char *allrecs, int recl, int nrecs); // binary search sorted records int bsearch(char *seekrec, char **allrecs, int N, int nrecs); // binary search of sorted pointers to recs typedef int HeapSortUcomp(cchar *rec1, cchar *rec2); // return -1/0/+1 if rec1 rec2 void HeapSort(int vv[], int nn); // Heap Sort - integer void HeapSort(float vv[], int nn); // Heap Sort - float void HeapSort(double vv[], int nn); // Heap Sort - double void HeapSort(char *vv[], int nn); // Heap Sort - char *, ascending order void HeapSort(char *vv[], int nn, HeapSortUcomp); // Heap Sort - char *, user-defined order void HeapSort(char *recs, int RL, int NR, HeapSortUcomp); // Heap Sort - records, user-defined order int MemSort(char * RECS, int RL, int NR, int KEYS[][3], int NK); // memory sort, records with multiple keys // variable string list functions ======================================== struct pvlist { int max; // max. entries int act; // actual entries char **list; // entries }; pvlist * pvlist_create(int max); // create pvlist void pvlist_free(pvlist *pv); // free pvlist int pvlist_append(pvlist *pv, cchar *entry, int unique = 0); // append new entry (opt. if unique) int pvlist_prepend(pvlist *pv, cchar *entry, int unique = 0); // prepend new entry (opt. if unique) int pvlist_find(pvlist *pv, cchar *entry); // find entry by name int pvlist_remove(pvlist *pv, cchar *entry); // remove entry by name int pvlist_remove(pvlist *pv, int Nth); // remove entry by number (0...) int pvlist_count(pvlist *pv); // return entry count int pvlist_replace(pvlist *pv, int Nth, cchar *entry); // replace Nth entry (0...) char * pvlist_get(pvlist *pv, int Nth); // return Nth entry (0...) int pvlist_sort(pvlist *pv); // sort list, ascending // random number functions =============================================== int lrandz(int64 * seed); // returns 0 to 0x7fffffff int lrandz(); // built-in seed double drandz(int64 * seed); // returns 0.0 to 0.99999... double drandz(); // built-in seed // spline curve-fitting functions ======================================== void spline1(int nn, float *dx, float *dy); // define a curve using nn data points float spline2(float x); // return y-value for given x-value // FIFO queue for text strings, single or dual-thread access ============= v.5.7 typedef struct { int qcap; // queue capacity int qnewest; // newest entry position, circular int qoldest; // oldest entry position, circular int qdone; // flag, last entry added to queue char **qtext; // up to qcap text strings } Qtext; void Qtext_open(Qtext *qtext, int cap); // initialize Qtext queue, empty void Qtext_put(Qtext *qtext, cchar *format, ...); // add text string to Qtext queue char * Qtext_get(Qtext *qtext); // remove text string from Qtext queue void Qtext_close(Qtext *qtext); // free() leftover strings if any // application initialization and administration ========================= int zinitapp(cchar *appname, ...); // initz. app directories and files v.4.1 cchar * get_zprefix(); // get /usr or /usr/local ... v.4.1 cchar * get_zuserdir(); // get /home/user/.appname/ cchar * get_zdatadir(); // get data directory cchar * get_zdocdir(); // get document directory cchar * get_zicondir(); // get icon directory v.5.7 int locale_filespec(cchar *ftype, cchar *fname, char *filespec); // get a locale dependent file v.5.5 void showz_userguide(cchar *context = 0); // show user guide in new process void showz_logfile(); // show log file in popup window v.5.2 void showz_textfile(cchar *type, cchar *file); // show text file [.gz] in popup window void showz_html(cchar *url); // show html via preferred browser void zmake_menu_launcher(cchar *command, cchar *cats, cchar *generic); // add desktop menu and launcher v.4.1 /**************************************************************************/ // translation functions #define ZTXmaxent 2000 // max. translation strings #define ZTXmaxcc 4000 // max. cc per string void ZTXinit(cchar *lang); // setup for message translation cchar * ZTX(cchar *english); // get translation for English message cchar * ZTX_missing(int &ftf); // get missing translations, one per call /************************************************************************** GTK utility functions ***************************************************************************/ void zmainloop(int skip = 0); // do main loop, process menu events void zthreadcrash(); // crash if thread is not main() thread // text window print and read utilities void wprintx(GtkWidget *Win, int line, cchar *mess, cchar *font = 0); // write text to line, optional font void wprintf(GtkWidget *Win, int line, cchar *format, ...); // "printf" version void wprintf(GtkWidget *Win, cchar *format, ... ); // "printf" to next line, scroll up void wscroll(GtkWidget *mLog, int line); // scroll window to put line on screen void wclear(GtkWidget *Win); // clear window void wclear(GtkWidget *Win, int line); // clear from line to end char * wscanf(GtkWidget *Win, int &ftf); // get text lines from edit widget int wfiledump(GtkWidget *Win, char *filespec); // dump text window to file void wfilesave(GtkWidget *Win); // wfiledump() via file-chooser dialog void wprintp(GtkWidget *Win); // print text window to default printer // intercept text window mouse click functions typedef void clickfunc_t(GtkWidget *widget, int line, int pos); // function to get clicked text position void textwidget_set_clickfunc(GtkWidget *widget, clickfunc_t clickfunc); // (wrapped lines are one logical line) char * textwidget_get_line(GtkWidget *widget, int line, int hilite); // get entire line at clicked position char * textwidget_get_word(char *line, int pos, cchar *dlims, char &end); // get delimited word at clicked position // get the screen and mouse for a given widget int get_mouse_device(GtkWidget *, GdkScreen **screen, GdkDevice **mouse); // return pointers to screen and mouse // functions to simplify building menus, tool bars, status bars #define G_SIGNAL(window,event,func,arg) \ g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg) #define zdcbmax 100 // max. combo box drop-down list typedef void cbFunc(GtkWidget *, cchar *mname); // menu or button response function GtkWidget * create_menubar(GtkWidget *vbox); // create menubar in packing box GtkWidget * add_menubar_item(GtkWidget *mbar, cchar *mname, cbFunc func = 0); // add menu item to menubar GtkWidget * add_submenu_item(GtkWidget *mitem, cchar *subname, // add submenu item to menu item cbFunc func = 0, cchar *mtip = 0); // with opt. function and popup tip GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize = 24); // toolbar in packing box (no vert gtk3) GtkWidget * add_toolbar_button(GtkWidget *tbar, cchar *lab, cchar *tip, // add button with label, tool-tip, icon cchar *icon, cbFunc func); GtkWidget * create_stbar(GtkWidget *vbox); // create status bar in packing box int stbar_message(GtkWidget *stbar, cchar *message); // display message in status bar /**************************************************************************/ GtkWidget * create_popmenu(); // create an empty popup menu GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, // add menu item to popup menu cbFunc func, cchar *arg, cchar *mtip = 0); void popup_menu(GtkWidget *, GtkWidget *popmenu); // pop-up menu at current mouse posn. /**************************************************************************/ // user editable graphic menu in popup window // menus can be added and arranged using the mouse typedef void gmenuz_cbfunc(cchar *menu); // caller-supplied callback function void gmenuz(GtkWidget *parent, cchar *title, cchar *ufile, gmenuz_cbfunc); // show window, handle mouse drag/click /**************************************************************************/ // create vertical menu/toolbar in vertical packing box // v.5.5 struct vmenuent { // menu data from caller cchar *name; // menu name, text cchar *icon; // opt. icon file name cchar *desc; // description (mouse hover popup) cbFunc *func; // callback func (GtkWidget *, cchar *arg) cchar *arg; // callback arg GdkPixbuf *pixbuf; // icon pixbuf or null PangoLayout *playout1, *playout2; // normal and bold menu text int namex, namey; // menu name position in layout int iconx, icony; // menu icon position int ylo, yhi; // menu height limits int iconww, iconhh; // icon width and height }; struct Vmenu { GtkWidget *vbox; // parent window GtkWidget *layout; // drawing window int xmax, ymax; // layout size v.5.8 int mcount; // menu entry count vmenuent menu[100]; }; Vmenu *Vmenu_new(GtkWidget *vbox); // create new menu in parent vbox void Vmenu_add(Vmenu *vbm, cchar *name, cchar *icon, // add menu item with response function int iconww, int iconhh, cchar *desc, cbFunc func, cchar *arg); // function may be popup_menu() /**************************************************************************/ // functions to implement GTK dialogs with less complexity // widget types: dialog, hbox, vbox, hsep, vsep, frame, scrwin, label, entry, edit, radio, // check, button, togbutt, spin, combo, comboE, hscale, vscale, colorbutt #define zdmaxwidgets 300 // v.5.8 #define zdmaxbutts 10 #define zdsentinel 0x97530000 struct zwidget { cchar *type; // dialog, hbox, vbox, label, entry ... cchar *name; // widget name cchar *pname; // parent (container) name char *data; // widget data, initial / returned pvlist *cblist; // combo box drop-down list int scc; // entry widget: text cc width int homog; // hbox/vbox: equal spacing flag int expand; // expandable flag int space; // extra padding space (pixels) int wrap; // wrap mode for edit widget int stopKB; // stop propagation of KB events to parent GtkWidget *widget; // GTK widget pointer }; struct zdialog { int sentinel1, sentinel2; // validity sentinels void *eventCB; // widget event user callback function int zstat; // dialog status (from completion button) int disabled; // widget signals/events are disabled int saveposn; // save and recall window position each use int saveinputs; // save and recall user inputs each use int stopKB; // flag, next KB event will be ignored GtkWidget *parent; // parent window or null GtkWidget *compbutt[zdmaxbutts]; // dialog completion buttons zwidget widget[zdmaxwidgets]; // dialog widgets (EOF = type = 0) }; zdialog *zdialog_new(cchar *title, GtkWidget *parent, ...); // create a zdialog with opt. buttons int zdialog_add_widget(zdialog *zd, // add widget to zdialog cchar *type, cchar *name, cchar *pname, // required args cchar *data = 0, int scc = 0, int homog = 0, // optional args int expand = 0, int space = 0, int wrap = 0); int zdialog_add_widget(zdialog *zd, // add widget to zdialog cchar *type, cchar *name, cchar *pname, // (alternative form) cchar *data, cchar *options); // "scc=nn|homog|expand|space=nn|wrap" int zdialog_valid(zdialog *zd); // return 1/0 if zdialog valid/invalid GtkWidget * zdialog_widget(zdialog *zd, cchar *name); // GTK widget from zdialog widget name int zdialog_resize(zdialog *zd, int width, int height); // set size > widget sizes int zdialog_put_data(zdialog *zd, cchar *name, cchar *data); // put data in widget (entry, spin ...) cchar * zdialog_get_data(zdialog *zd, cchar *name); // get widget data int zdialog_set_limits(zdialog *zd, cchar *name, double min, double max); // set new widget limits (spin, scale) typedef int zdialog_event(zdialog *zd, cchar *name); // widget event callback function int zdialog_run(zdialog *zd, zdialog_event = 0, cchar *posn = 0); // run dialog, handle events void KBstate(GdkEventKey *event, int state); // extern: pass KB events to main app int zdialog_send_event(zdialog *zd, cchar *event); // send an event to an active dialog int zdialog_send_response(zdialog *zd, int zstat); // complete a dialog, set status int zdialog_show(zdialog *zd, int flag); // show or hide a dialog v.5.2 int zdialog_destroy(zdialog *zd); // destroy dialog (caller resp.) int zdialog_free(zdialog *&zd); // free zdialog memory int zdialog_wait(zdialog *zd); // wait for dialog completion int zdialog_goto(zdialog *zd, cchar *name); // put cursor at named widget void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor); // set cursor for dialog window int zdialog_stuff(zdialog *zd, cchar *name, cchar *data); // stuff string data into widget int zdialog_stuff(zdialog *zd, cchar *name, int data); // stuff int data int zdialog_stuff(zdialog *zd, cchar *name, double data); // stuff double data int zdialog_fetch(zdialog *zd, cchar *name, char *data, int maxcc); // get string data from widget int zdialog_fetch(zdialog *zd, cchar *name, int &data); // get int data int zdialog_fetch(zdialog *zd, cchar *name, double &data); // get double data int zdialog_fetch(zdialog *zd, cchar *name, float &data); // get float data int zdialog_cb_app(zdialog *zd, cchar *name, cchar *data); // append entry to combo drop down list int zdialog_cb_prep(zdialog *zd, cchar *name, cchar *data); // prepend entry to combo drop down list char * zdialog_cb_get(zdialog *zd, cchar *name, int Nth); // get combo drop down list Nth entry int zdialog_cb_delete(zdialog *zd, cchar *name, cchar *data); // delete combo drop down list entry int zdialog_cb_clear(zdialog *zd, cchar *name); // clear all combo box entries int zdialog_cb_popup(zdialog *zd, cchar *name); // show all combo box list entries int zdialog_positions(cchar *action); // load or save zdialog window positions void zdialog_set_position(zdialog *zd, cchar *posn); // set initial/new zdialog window position void zdialog_save_position(zdialog *zd); // save zdialog window position int zdialog_inputs(cchar *action); // load or save zdialog input fields int zdialog_save_inputs(zdialog *zd); // save zdialog input fields when finished int zdialog_restore_inputs(zdialog *zd); // restore zdialog inputs from prior use // write text to popup window, shell command to popup window GtkWidget * write_popup_text(cchar *action, cchar *text = 0, int ww = 0, int hh = 0, GtkWidget *parent = 0); int popup_command(cchar *command, int ww = 400, int hh = 300, GtkWidget *parent = 0); // popup message dialogs void zmessageACK(GtkWidget *parent, cchar *title, cchar *format, ... ); // display message, wait for OK void zmessLogACK(GtkWidget *parent, cchar *format, ... ); // same, with log to STDOUT int zmessageYN(GtkWidget *parent, cchar *format, ... ); // display message, wait for YES/NO zdialog * zmessage_post(GtkWidget *parent, int secs, cchar *format, ...); // show message, timeout or cancel char * zdialog_text(GtkWidget *parent, cchar * title, cchar * initext); // get short text input from user int zdialog_choose(cchar *title, GtkWidget *parent, cchar *format, ...); // show message, return choice void poptext_mouse(cchar *text, GdkDevice *, int dx, int dy, float s1, float s2); // show popup text at mouse posn void poptext_window(cchar *text, GtkWindow *, int mx, int my, float s1, float s2); // show popup text at window posn int poptext_killnow(); // kill current popup window int popup_image(cchar *imagefile, GtkWindow *parent, int size); // show image in a small popup window int move_pointer(GtkWidget *, int px, int py); // move the mouse pointer to px, py // file chooser dialogs for one file or multiple files char * zgetfile(cchar *title, cchar *action, cchar *file, cchar *butt = 0); char ** zgetfiles(cchar *title, cchar *action, cchar *file, cchar *butt = 0); // print an image file, choosing printer, paper, orientation, margins, and scale void print_image_file(GtkWidget *parent, cchar *imagefile); // drag and drop functions typedef void drag_drop_func(int x, int y, char *text); // user function, get drag_drop text void drag_drop_connect(GtkWidget *window, drag_drop_func); // connect window to user function // miscellaneous GDK/GTK functions GdkPixbuf * get_thumbnail(char *fpath, int size); // get sized thumbnail for image file GdkCursor * zmakecursor(cchar *iconfile); // make a cursor from an image file GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *, float deg, int alfa = 0); // rotate pixbuf through any angle /**************************************************************************/ // parameter management functions int initParmlist(int max); // initz. parameter list int initz_userParms(); // load or initialize user parms int loadParms(cchar * filename); // load parameters from a file int loadParms(); // + user file select dialog int saveParms(cchar * filename); // save parameters to a file int saveParms(); // + user file select dialog int setParm(cchar * parmname, double parmval); // set parameter value double getParm(cchar * parmname); // get parameter value char * getParm(int Nth); // get Nth parameter name (0-based) int listParms(GtkWidget *textWin); // list all parameters in given window int editParms(GtkWidget *textWin = 0, int addp = 0); // parameter editor /************************************************************************** C++ classes ***************************************************************************/ // dynamic string class ================================================== class xstring { static int tcount; // total xstring count static int tmem; // total memory used int wmi; // internal ID int xcc; // actual cc (excl. NULL) int xmem; // memory allocated cc char * xpp; // memory pointer public: xstring(int cc = 0); // default constructor xstring(cchar * ); // string constructor xstring(const xstring &); // copy constructor ~xstring(); // destructor operator cchar * () const { return xpp; } // conversion operator (cchar *) xstring operator= (const xstring &); // operator = xstring operator= (cchar *); // operator = friend xstring operator+ (const xstring &, const xstring &); // operator + friend xstring operator+ (const xstring &, cchar *); // operator + friend xstring operator+ (cchar *, const xstring &); // operator + void insert(int pos, cchar * string, int cc = 0); // insert substring at position (expand) void overlay(int pos, cchar * string, int cc = 0); // overlay substring (possibly expand) static void getStats(int & tcount2, int & tmem2); // get statistics void validate() const; // verify integrity int getcc() const { return xcc; } // return string length }; // vector (array) of xstring ============================================= class Vxstring { int nd; // count xstring * pdata; // xstring[nd] public: Vxstring(int = 0); // constructor ~Vxstring(); // destructor Vxstring(const Vxstring &); // copy constructor Vxstring operator= (const Vxstring &); // operator = xstring & operator[] (int); // operator [] const xstring & operator[] (int) const; // operator [] (const) int search(cchar * string); // find element in unsorted Vxstring int bsearch(cchar * string); // find element in sorted Vxstring int sort(int nkeys, int keys[][3]); // sort by designated subfields int sort(int pos = 0, int cc = 0); // sort by 1 subfield (cc 0 = all) int getCount() const { return nd; } // get current count }; // hash table class ====================================================== class HashTab { static int trys1; // insert trys static int trys2; // find/delete trys int cap; // table capacity int count; // strings contained int cc; // string length char * table; // table[cc][cap] public: HashTab(int cc, int cap); // constructor ~HashTab(); // destructor int Add(cchar * string); // add a new string int Del(cchar * string); // delete a string int Find(cchar * string); // find a string int GetCount() { return count; }; // get string count int GetNext(int & first, char * string); // get first/next string int Dump(); // dump hash table }; // Queue class, FIFO, LIFO or mixed ====================================== class Queue { char wmi[8]; Vxstring * vd; // vector of xstrings mutex_tp qmutex; // for multi-thread access int qcap; // queue capacity int qcount; // curr. queue count int ent1; // first entry pointer int entN; // last entry pointer char * lastent; // last entry retrieved int lcc; // last entry cc private: void lock(); // auto locking and unlocking void unlock(); // (for multi-thread access) public: Queue(int cap); // create queue with capacity ~Queue(); // destroy queue int getCount(); // get current entry count int push(const xstring * entry, double secs); // add new entry with max. wait time xstring * pop1(); // get 1st entry (oldest) xstring * popN(); // get Nth entry (newest) }; /* ======================================================================= Tree class - sparse array indexed by names or numbers - every element of a Tree is a Tree put(): cc is data length to store get(): cc is max. data length to retrieve actual length is returned, = 0 if not found nn is array count for nodes[] arguments */ class Tree { int wmi; // for ID checking char *tname; // tree name int tmem; // tree data memory void *tdata; // tree data[tmem] int nsub; // no. sub-nodes (Trees) Tree **psub; // pointer to sub-nodes public: Tree(cchar * name); // create Tree ~Tree(); // destroy Tree int put(void * data, int cc, char * nodes[], int nn); // put data by node names[] int put(void * data, int cc, int nodes[], int nn); // put data by node numbers[] int get(void * data, int cc, char * nodes[], int nn); // get data by node names[] int get(void * data, int cc, int nodes[], int nn); // get data by node numbers[] void stats(int nnodes[], int ndata[]); // get nodes and data per level void dump(int level = 0); // diagnostic private: Tree * find(char * nodes[], int nn); // find a sub-node by names[] Tree * find(int nodes[], int nn); // find a sub-node by numbers[] Tree * make(char * nodes[], int nn); // find/create a sub-node by names[] Tree * make(int nodes[], int nn); // find/create a sub-node by numbers[] }; dkopp-6.5/icons/0000755000175000017500000000000012343020444012250 5ustar micomicodkopp-6.5/icons/kill.png0000664000175000017500000001431612343020444013720 0ustar micomico‰PNG  IHDR00Wù‡ =iCCPiccxÚSgTSé=÷ÞôBKˆ€”KoR RB‹€Ti¢’¡„@숨Àˆ¢"‚qÀѱ"Š…A±÷y(ãà(6TÞÞ}³æ½7oö¯½ö9gïœ}>F`°Dš…ªdJòˆ<6.'w T €@˜- ‰ôàûñðìˆøàÍm@n؆á8üPÊä $ ¦‹ÄÙB¤2r22 ì¤t™%[€j;e’OvÒ$÷¶(S*@£@&ʉÐX—£‹°`(Ê‘ˆs°›`’¡Ì”`ï€)d`¢ SöÀGEð3(Œ”¯xÒW\!ÎSð²d‹å’”Tn!´Ä\]¹x 87C¬PØ„ „é¹çeeÊÒÅ“3€FvD€Î÷ã9;¸:;Û8Ú:|µ¨ÿü‹ˆ‹ÿ—?¯Â„ÓõEû³¼¬î¶ñ‹–´ e €Öý/šÉÕB€æ«_ÍÃáûñðT…Bæfg—››k+ m…©_õùŸ _õ³åûñðß׃ûŠ“Ê àƒ ³2²”r<[&Šq›?ñß.üówL‹'‹åb©PŒGKĹi ÎË’Š$ I–—Hÿ“‰³ì˜¼k`Õ~öB[P»Êì—. °è€%ìäwß‚©Ñ1ƒ“w0ù›ÿh Ù’€… •òœÉ€4P6hƒ>ƒØ€#¸€;x̆Pˆ‚8XBH…LC.,…UP%°¶Bì†Z¨‡F8-pθ×à<€^€ç0 o`A2ÂDXˆ6b€˜"Öˆ#ÂEf!~H0Ä!‰H "E”ÈRd5R‚”#UÈ^¤ù9ŽœE.!=È=¤F~C> Ê@Ù¨j†Ú¡\Ô B£Ðùh ºÍG Ñ h%ZƒB›Ñ³èôÚ‹>GÇ0Àè3Äl0.ÆÃB±x,“c˱b¬«Á±6¬»õb#Ø{‰À"à‚;!0— $,",'”ªÍ„ Ba”ð™È$ê­‰nD>1–˜BÌ%+ˆuÄcÄóÄ[Äâ‰Ä!™“\H¤8Ri ©”´“ÔD:Cê!õ“ÆÈd²6ÙšìA% È ry;ùù4ù:y€üŽB§P)þ”xŠ”R@© ¤œ¢\§ RÆ©jTSª5”*¢.¦–Qk©mÔ«Ôê8MfNó EÑÒh«h•´FÚyÚCÚ+:nDw¥‡Ó%ô•ôJúaúEzý=CƒaÅà1JÆÆ~ÆÆ=Æ+&“iÆôbÆ3Ì Ìzæ9æcæ;–Š­ _E¤²B¥Z¥YåºÊ Uªª©ª·êÕ|Õ Õ£ªWUGÔ¨jfj<5ÚrµjµãjwÔÆÔYêê¡ê™ê¥êÕ/©i5Ì4ü4D…û4Îiô³0–1‹Ç²V³jYçYlÛœÍg§±KØß±»Ù£šš34£5ó4«5Ojör0އÏÉà”qŽpns>LÑ›â=Ej¨kh¨4ÜkØm8ndn4רÀ¨Éè‘1͘kœl¼Å¸ÝxÔÄÀ$Äd©IƒÉ}Sª)×4Õt›i§é[3s³³µf-fCæZæ|ó|óó‡L O‹E57-I–\Ët˖׬P+'«T«j««Ö¨µ³µÄz§uÏ4â4×iÒi5ÓîØ0l¼mrllúl9¶Á¶¶-¶/ìLìâí6ÙuÚ}¶w²Ï°¯µà á0Û¡À¡Íá7G+G¡cµãÍéÌéþÓWLoþr†õ ñŒ]3î:±œBœÖ:µ;}rvq–;7:»˜¸$ºìp¹Ãesø¥Ü‹®DW×®'\ß»9»)ÜŽ¸ýênãžî~Ð}h¦ùLñÌÚ™ýF½½³ðY‰³öÌêõ4ôxÖx>ñ2öyÕy z[z§yò~ácï#÷9æó–çÆ[Æ;ã‹ùøûvûiøÍõ«ò{ìoäŸâßà?à°$àL 10(pSà¾_ȯçÎv™½lvG#(2¨*èI°U°<¸- ™²9äáÓ9Ò9-¡ÊÝú(Ìy‰¶ˆ†Åârñ`²GryòPŠGÊæ”áTÏÔŠÔ OR%y™˜¶;ímzhúþô‰Œ˜Œ¦LJfbæq©†4]Ú‘¥Ÿ•—Õ#³–Éz¹-ÚºhT$¯ËF²çg·*Ø ™¢Ki¡\£ìË™•Só.7:÷hžzž4¯k±Õâõ‹óýó¿]BX"\Ò¾Ôp骥}˼—í]Ž,OZÞ¾ÂxEን+¬¢­J_õS}AyÁëÕ1«Û õ Wö¯ XÓP¤R$/º³Ö}íîu„u’uÝ맯߾þs±¨ør‰}IEÉÇRaéåo¾©üfbCò†î2ç²]I¥ooòÜt \½<¿¼sÈææ-ø–â-¯·.Üz©bFÅîm´mÊm½•Á•­ÛM¶oÜþ±*µêVµOuÓÝëw¼Ý)Úy}—×®ÆÝz»KvØ#ÙswoÀÞæ³šŠ}¤}9ûžÖF×v~Ëý¶¾N§®¤îÓ~éþÞ:ê]êëê,k@” Ç]ûÎ÷»ÖF›Æ½Mœ¦’ÃpXyøÙ÷‰ßß>t¤ý(÷hã¦?ì8Æ:VÜŒ4/nmImémkí9>ûx{›{Û±mÜÂðDõIÍ“e§h§ OMœÎ?=vFvfälÊÙþö…íÎÅž»ÙÞÑ}>èüÅ þÎuzwž¾èqñÄ%·KÇ/s/·\q¾ÒÜåÔuì'§ŸŽu;w7_u¹ÚzÍõZ[ÏÌžS×=¯Ÿ½á{ãÂMþÍ+·æÜê¹=÷öÝ; wzïŠîÝ˸÷ò~Îýñ+?R{TñX÷qÍÏ–?7õ:÷žìóíëzùäA¿°ÿù?²ÿñq ð)óiÅ Á`ýãЉaÿákÏæ=x.{>>Rô‹ú/;^X¼øáW¯_»FcG^Ê_NüVúJûÕþ×3^·…=~“ùfümñ;íwÞsßw~ˆù08žû‘ü±ò“å§¶ÏAŸNdNLü˜óü%c3¢ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFbKGDÿÿÿ ½§“ pHYs.#.#x¥?v vpAg00ÎîŒW ‚IDAThÞÍY[Œ]ÕyþÖÚkßÏÙç>÷ñxìâNÇ—ú‚¨…+Ài¢@êÆUûP¥jÕ*­Šxp*U®à!(/Q¡mÚ‡4(Rª@"”6…@´ÁÔƒ!{˜a<õxÆc{ngŽÏuß÷Ú«>ût0ÁÆý¤¥sÎÖÖZÿ·þÛ·Ö!ø\¼x„!Ç1†‡‡ñÿdýþþ~\¹r…pàÀÌ}÷ÝwüرcÍ©©)ŒŽŽÞn»;èx@’$pÎÕ­[·~ppðDQß÷!IRKÓ´Ÿ§Óé—J¥—wïÞ=3??>ñÄ „üò«ßL;vì@E;ººº^aŒõ!qƒsŽ0 DZ`Œ-§R©×J¥Òs™Læõ½{÷.¯¬¬ˆcÇŽÝ(Š‚ÅÅE´Z­¯>ôÐC‡MÓ„ëºp]BH’ÆdY&q§ëõúEQ‡aøàÜÜÜpµZõ;hšY–ïôôôÀó<ÌÌÌ R© Ñh`bbKKKÃŒ1‹EìÙ³º®ÏDQôÃÕÕÕÞÿý³<ð€ûðÃߦiBÓ4!þsþ/žçÎ9TUE6›E>ŸGoo/¶oßþoÄqŒz½ŽóçÏc||ÓÓÓ¨ÕjP}}}B¡P¨BÞj4?zûí·/(öõõ½}Ï=÷,,//ó£GÞªª"Žã¿’$隦ÇqÐjµ©T ù|ù|ƒƒƒØµkÆÆÆ°eˆ0 ±²²‚©©)œ9s ð<–ea``|bb»xñ¢&IÒÅT*õŸÙlöù¾¾¾“?þxõÕW_Å¡C‡~9©T QÒ#„øŠ,ˇ ÃøuÓ4ÓŒ1xž‡Z­Çq Ë2,ËB©TBOO†††°ÿ~ìÞ½ÝÝÝ`ŒÁq\ºt “““˜™™ÁÚÚ‚ @Çß÷†!!®¦igMÓ|!—ËýôŽ;™™ñŸ|òI˜¦ùÉr Á¶mÛpþüùŒ¢(»c¿«iÚƒ¦in6M“ !P©T°ººŠÑÑQô÷÷w’ز,ŒŽŽâ®»îÂÈÈ,Ë‚õz 8}ú4*• Êå2lÛF†ˆ¢aB!ckº®ÿw:~®T*?xðà•ÅÅÅø‘Gùd®•$i@×õûu]ÿ²a¿ ³´´„‘‘ôôô ŽãN³k{½½½Ø³göíÛ‡¡¡!PJQ­VÁÃÜÜ&&&pîÜ9\½zQuÄb[gE²,_0MógÃÃÃ/oÚ´éÍ£G®8q÷Þ{ï'&°, ®ëªétúAJé?ø¾?ÐßßÞÞ^0Æ@)…ªª¦—Ñ4 [·nÅèè(vî܉͛7C’$ø¾ååeLNNb||—.]‚ëº×Œ!œs4›MPJýb±8iYÖ Åbñù»ï¾ûŽ?ž}饗tkªKKK7&…B¥R •JåÏòùü?A DQ$2™LP,Y©T’jµ@Ñ‘áQuB,©bÛ¶mƒeY€f³‰ .àÌ™3˜˜˜@µZç”R¤R)4›M´Z-H’´šÉd&Ç韞ž¶!_BücìZ#»\×E†pg~ppðóétº»^¯§Z­>Újµ^¯×ëˆã¸@)Õ%I"‰À£”‚1†0 ±°°€“'Oâ­·ÞB£Ñ€®ëÈçóÀöíÛA¥²,wB«P( «« Q™•JeØó¼B)ÏóVWWW_䜋 Æ\× 2™ÌoÕj5çÊ•+L)}s~zuuõ9ß÷_v]÷²çy)Îy·®ë”1Ö‘’$Aññq¼þúë8qâfgg!„c årº®£X,¢P(@UU´Z-ÔëõÎFhšUUA À‹Œ±Ê† hš† `šæC¾ï7›ÍæÓ™LFT*pÎ#Û¶—ƒ ø¯r¹|¥V«aŒ)®ë"ŽcÄq Û¶ÁC6›…,˨T*8{ö,^{í5ŒÃ²,†EQ`r¹ …²Ù,(¥àœ#H’J)s]÷ßEY`%Ð &„ÈB.–J¥¸-A>+aÃ04’¸|ù2Â0„ªª0 º®Ã²,är9ø¾jµŠµµ5¼ûî»P™Lýýý±˜J¥’yaš&Î;‡©©)¬­­JéÆr ñ€išiMÓ朿Çñ†a Z­vŒ_^^–¶lÙòç–eýªçyß,‹—!ïÕëõXÑí8<σïûàœƒ1†t:L&àÚªÙlbqq—/_N¦i¢¿¿÷ß?öíÛÃ0jo¼ñÆ·$IZÝR©êõ:¶mÛö€,Ë#­VˉãŽã$IÆ …BqppðLÓü=ÆØw(¥3;;_ºt)9Öß߿˶m4 8Žƒf³ ß÷ÁC¡P@’/„Bàyæææ0??L&ƒÑÑQär9lÚ´ BˆÓóóóÒéôÆÊh:FE›‡‡‡œÉdvÎÏÏÿG£ÑøåL&Ó'„гÙìe³ÙŒ±W‚ ø.¥´ùæ›oâÀ¨V«GwíÚõwIHt–ã8àœCÓ4èºÞItž’H¶lG±XŒ9ç±wïÞo?öØcŸˆ€–ÏçïËf³_ò}ÿïûMÏóÊQýš®ë?Ëår_*•¦ßyçððáÃxæ™gÍfQ«ÕÈwÞùÏ###_UB!:ÇTß÷Ag­ šÍ&‚ ¥”RÄqœè²™z½þ9Jé‚ëº#@–e„aH»»»5Mû!äHEcQåE™’$éë+++?RUÕÓ4 ½½½ð}̲¬Íf³™Lš¦RÚ™3ŽãNè8Žƒr¹Œr¹Œ(ŠbÃ0N˲ü²‚4›ÍÏW*•½¶möí¿6 Žãl<‰“ÅÛ¶ë©TêçFãßTU}EQ”+„_áœE1JihšVÔ4í·Óéô×J¥Ò”RÔj5hšEQ:±žÔøt: ×u±¸¸×u‘N§`YÖŸÖjµç'''‹ÅŸº®{Á÷ýï)ŠÒpÀ´ÐÀCE(•JyEQ~ÃuÝ/«ªºÝ0 Ý0Œ¼a=¥R‰EQ„÷Þ{ÝÝÝèîî†eYÐ4 Œ1†8Ž1==(Šàºîy_ „Ìž•Ëå<Ï»ñ~ZëÑl6€Û¶]:×5ßîëëëâœ?nYVÇàäL$µ®ë¢ÕjA–eaš¦HÂä3#p=‚ Hêû¬aq6›¥ëÏ ”PBˆeÛ¶ º­dYF*•‚ã8;E¡‰ÌæœèJr½ 眧¢(’…îm% „@µZU4MÛ•—4¨»Sm0‚ P7rNoøÆ§„eY€“IW圃sŽ(Š:#y¶Ž€Î9W“.ü‹pK=Ü,!眬õ‰7’qëœsý¶„ Yx¿žÀuï"Žc•snÜvŒ1¨ª Ïó¶ É ÆÇå@"+Â0T|ßßÐåÐ-Ï­[·‚’[/Ê>n$Êó<Ù¶í”mÛ7Þ¤[i|E˜…$I+IKÂ$Aò}}’AÀ‚ Hmd[E ‰R¯×©eYƒ’$!Žã$©?`|ò= ÃÎÀÖ«ÖÏŠÁµ°¤¸ö߃¤ªê$IdYîhû$þ×çAr6h_Wújœs©='ð‘™3 $ÆËT&!d@’¤GdYaŒutPÒÔÖï¾ïûðà‰Ot ¹ñÒ:%!„N©èºɲL|ßgœsF)%ëÃÉqÔëõ¤ 5|ÀrÛè°=n©Äuƒp¬ø¾jiiiJ’$S–圦iݺ®*Š2H)- !Ìf³IÃ0ç\!Ž˜n›Ìû‘;w³@Ú¢´½j€ÖöNRVc,£(Ê&ÆØÎ ABœBü-€Y\ €×öÂ-Íkúð4ЮD1hE4Š"“R:MaBˆ@ñÝöî»mƒ£öˆ?Êø›íziý§„ÿóÀiœ„‹Øè„·ëm¸¡Á×ãdE§RžÕ%tEXtcreate-date2008-03-22T12:13:02+00:00‹Íš%tEXtmodify-date2008-03-22T12:13:02+00:00Ô|ì0IEND®B`‚dkopp-6.5/icons/dkopp.png0000774000175000017500000000541412343020444014103 0ustar micomico‰PNG  IHDR00Wù‡sRGB®ÎébKGDÿÿÿ ½§“ pHYs  ÒÝ~ütIMEÜ ѪlA ŒIDAThÞí™kL”WÇÌU†@faFuÐÙèzi²jF·±F’MWùPÊ61ÛOÕ&ÍÆ¬måÃ6Ùl“‚¥©É¶¦Ò¤ÕØ´‹-Õu³®Ö6"ÊÆ¡ tµrQ¦ Ê0÷ûÙ È t»ýà“œLÞ÷œ9çÿ?ç9Ïí…'òDæ%iÿãù[€Òøo2¹XS?Y@­F£©]±b…¹²²’²²2*++“îììÄf³qåÊgOOO3PÜúX Ô™L¦Ú­[·²}ûvòòòÐjµddd V«BLiáp˜Áa'÷îƒw„Î+ÇÇf³5ĉŒþ²€ý&“©®ººšM›6Q\\L^^Þ4ÀÉšm$Š+,Æfr9=èã›o.ÓØØhu»Ýµ@Ç£—ÍüjµZÝüÒK/•>ýôÓ ²³³S.„`4›E1¬„ìì èt:sccã…žžž-"1ŸØg2™:ĪU«())IøxërFq…Ç&S+¼l(êJLÞeƒïoxØ»w¯Óëõ–ΤN’9‚ÿ ººº¡±±‘mÛ¶¥ ~²x#"`e^ß”þ‚e –æðÆoh晀Hç~ß¾}µ/¾ø"kÖ¬A&“%˜L&“¹íø¢cï—åÞB—áLŒ{R`÷«PæÈQ¡&¥6›­?™*Iæþ¹çžKjSU@DàŽÎ^à¢$ëî4ðã¢1¨Ø¹s'jµºn¾'ðÁ¾}ûjkjj¨¬¬œµ¾On·¼1|QI"ü²°©D$ãö£ˆHõ…µ6›Í \ŸË êò†cc»¿@dYîíià¥á(yC.2þishÕ*V®\©{ü”Ù¾gžyÆüÔSO¡Ñhfû¨ÝÄï÷©Ï-Q…–µúï‘K£SÀ«Gd:ýHbk¤Ie¨ó¨óxzzÑëõÄÃ[©ÈÒëõu/¼ðK–,™•Êøý~¾þúkz{{B T*ðù|ÈUR~QmäAš»_EZ4ÆÂ{”È„eQ¤£)ª #¯dB­23)++0_¥B`ÿž={´F£qVàùä“OÐjµäææ>¬ŽD"Þyû*Û~·•FFFðz½ÎTT­_¿ž¼¼¼”CƒÓ§O³hÑ¢DŸÕj¥©©©ßçó5Î/¿ü²áàÁƒÚqÙÙÙŒŠ òãàƒÁ ~¿?ñ …¦¬èëë#ž½=š@YYY•Édšç<ª=xð»ÝžPŽ9b§‘£ÃÃô¶¶ÛºukbÇÃÐÐ@ éé>œüx<ži'ÌlÞ°aìâú®®®)ö‹/¾÷š“C`§ÏçK<ƒA"‘HRð3Y¶ø t<ŽÀ“É”°û©4»Ýž0•’äµµf³9ñ‡Ñét)¯ …°ÙlRñÄæ²²2´Zí#/¯T*E¡P V«‘H¦N³páBs<ÕœKU‰DRN9…ƒA.]º4^Áx,­V«š¶¥¥!•JQ*•¨Õj²²²ÈÌÌD­V£T*‰ÅbSÆ?ûì³Úøb‡€ó‹åØîÝ»ý‡ƒ7¦dÝÜn7—/_&Yb3í”j4d2iiiH$¤ÒäQw$èÃç¸MA†Àî &ÔÈl6S__o0'Ìçøe6›Ídee¥šÜ¹s‡«W¯Z“%4Óäææ–J$ ÅŒæ3èÆç¸ox`,_šÏ¿O_£¤dÂõ«T*ÊË˧üoxxF3«|ÂívÓÚÚŠÛín˜wUÂë¸Ïq›û>1I.m:Ѭ¬ÙRŠ­uh<àšzR‘‡ƒåË—³víÚY…&N§““'OöM)°ÙlFGGeÀX$ŒÏqÏ¢!?Eª°_n¦hCWíç‰Ê¥dËC¬ú•}¶†––›(D˜ôôtü~?r¹NÇ®]»ÈÌÌœx—ËÅgŸ}ÆÐÐPݬB‰H$B$èÃs§Ÿã6"™ÒïÒ¦¹û8øb•pTŠ7s#–=›)W…éïïG§Ó¡T*ç”øÄb1ìv;'Nœ˜q÷g"`íííÝróJ ÍC× ¤»Ì®ì1ãàº%%ÊBˆi¥–Tdòx‡ÃA}}=§öQÿIfF­6› Ox¢æÕò¯K¬ûÍïùÃ;LØß~…²Å5üåðW8|Ù\»ðöìØÆöíÛ9wîƒ×_M›6qæÌ„,[¶Œºº:, gÏž^±¥¥¥…¶¶¶†É±ʵÎõë׋¿·œöËÍ¢¿ýsQ¶¤D¼}ò-ñöÉ· ®?ø§DNŽFü­åÏâôìâóî;bIy…øè£Ä‡~(***Doo¯DSS“8vì˜0âúõëGï¿ÿ¾0 Âf³‰ÎÎNñí·ßŠÖÖVÑÐÐ 4͵TÀ&;Ѷ¶¶æ¡{Ü“)¸[˜Iì¡ãj“ž®à›kA"1Š4”±LKKK¨D2»>¹or(¢»»›W_}µßívo™OÑv³ÅbgΟ—ï^úkÐ/Ò‰š½; |Ï Î\>%ôE%¢æå?Šö›vqôèQQ^^.ÊËËÅ{ï½'nÞ¼)Q]]- D}}½øî»ï vïÞ-òóóÅ›o¾):;;E{{»8qâ„0#ÀêãûÀù†††-«¶¬DU ¤(ÝGŽ"4áÞƒ*Ú~0ÅQ°\B"bÓv{éҥܸqcÊ»åË—ÓÕÕ5ÅYµ··søðaçãªÑ³)«Ô666:ït;Èz¦€G¥tÜ]šxΓG‘"R².«Õýû÷9þ<´Î|*åõ]‹¥ù•Wö³Ú Ù9c/¯—0àÊŸØýô fWì ƒ8ššš8~üx3P›Ê™ÙÖF¯÷õõõ Ý­ÊÍ1¡P(+µ|ÿ`"ïÕÉ£¨%Ñ”‡Ãa†‡‡¹zõ*‡r^¼xñð2œËeM¥¸ÛÑ××gµZ­¿Î[¸lA()(3(R„RÚ}ǃÓéÄf³ñî»ïräȑ摑‘*àìOõ™u1Ð`±Xªª««Ñ/*F®Ò ÏLG.—“žžžF ‰æõz¹téçΣ­­­˜“ƒú±>1mjFcÕºuë´&“‰ŒŒ Cb@oo/^¯—¾¾>:;;éîÝîcñ„ä?¢Ì÷+åæxåA/ù%¢Œx³Æ3³QžÈùyÊ huužf¥ÃIEND®B`‚dkopp-6.5/icons/run.png0000644000175000017500000001026312343020444013564 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆjIDAThÝYyp×þÞëî¹ÍèFB'q¶§¸]Ž/ˆ“² `DZw½k'åxw‹Ýr’òVÊE6kWùÀ6fmg½&Ž1&Bœá0‡AB tޤ¹gzfzúxûÇ:¬²üµ¯êU÷t¿~ïû~ï÷~×ÜæVW·Ñžl!Å ÄÀ4&‚ @5réàŸßîõÈížpÙÊ>h4?µL˜ž©JŸ«g >·¡áMÏí\¿“£dNIUµVR5‰z|€yN+,è …Þ©ŽÜÎõn;n¡9ËF/]í/(‚ž£°Ù|Èë­Æm&@oçd¦Í´YíH$ŽR$ ¬ö,!¬۽ÜMí@ÝêÍÿ•œšøî_þò¾w´q Ød%q8Mf#UG)IRa³ÛA8Ìg)R·jÓOØFV5xçÀxØÆÜõë×sK×lzwÒäò—/[°Há„Öe«žX1Úx>¦=d±Û$YÖÀ ŽRp…,«°Ù¬ÐTeaíšÍΑ¾]¾|CѲ5OÍ/*ü÷%+WBoÒÿOÝêM›Ç#ÀñŽäÏß=þ¼‡×­¿Ï0yr9-¯¬0¶\n[WT2+7;k^}OO“VW·5§lòìË«jÞ¥÷èÌ9sNo¢¾€˜$@€Ê²|ýx<~¾|RÍÚ’²Ùñò’/º\ ÚâU[ãy~ßœ»î,9·FÏ L6Epww®*,žÖájûúܨ G{±ôž-oÌœY½á{ë0ôûÃPUg–LÓðÉ'Ž5_lé!fƒîÁEu „üÂ"!U%ÐÓD0@@ PcCŽÓ ½ÀÁÝíÆ¥óçÄ`Àcq›Ý–¿xù “Fx´wy¡ª » ÅùVìûÓÞx0yäðWï|6ÎÑΪn]´h>TMƒª2øƒQ˜Œ:<þØZccÓ¥I11\6¿v®‰&pþr®¸ú *@žãRx|xý" :¹Ù6Üsÿw̽î>³i••´»?¯?€ã("¢“9……B8Üö€ ŒªBsN\wu|·vÁ¥ „€£É{Æ1*ÁãÀjµ °0´ux!F%PBA)G(ÊŠ³ôyYãÉÓýœJ]»Ö$Ý×µ³mÅÓ*™F¦Ï˜>‰‹Æå¤4)c@$*!“¡i ݽÄâr0—A)ÉJŸš"D Fð¢`Œ%I¦Ææ:­°[tøóÞ½1U–ï­?°£m4œcZ!Ó={èÐq_ww²¬F’ÔiJ((% „ vNfM)Ïqà¹ëç¯Ãž¥ éçt`¬ÕlÀÄìÿ*ª&´_7xïÄXÇ$ÐÐðzŠ´îÃ]{bOaÐó„&¯Œ1ôzBˆÇep)@ƒ§AòiiÀ)'7x¬A'`JEN76)>Ÿ÷›úE¿ ߸ààW;yý½ÛÝ‹IŸÜ…xB|þÎ7w¡«70DºzžƒÕd„Ù¨ÏH7 ˜´;™gé+OÑÙ~ƒiŠrÛ¶iãá7­[±ñžÂ‰…øÇø‘Ùë¡jZæCB®ºúÐæê!¥•¥¹(›˜ƒ\§±˜Žç ðÜ}!\o÷ÀÝŸ´4 $3GÒÒ&¡äf[Á{vï‰E*Ëó7JPð>t¯9‘P3‡-I iß«Êò I âq‹î®‚×ë×_|%^¸pIÇ‹ªL3ZŒ&¥æÎ9ܲºZý”I8}ö:䄚œ?å'Òæ6Œ¡ª"ÕS«…Ë-—àgcâëå’•g:ö¢)“J F¥!‡˜àʵ^ttûA)Åò…ÕøêËCñ×~·£çÌ™ ?!2É­ß÷vÖ¡/ßÑ#¡©GŸxå—/oo\¿¡-[P ƒ^Hžƒa»0ϽN@MÍl=€MضmLŒc…¨¬ªyvõšK‹‹ h\R2%èê ÂÕáAD”°äîÉøÃ÷E›¾®‹Žþ i°Ýn¿v.èj;ÓP2i懗š¯<”›“m›1­œº{ƒ)Ó:`nKŠœÈ˱B§Ó£¹¥UËk骘X5§súäž«WO©Ã1fÎ@MÍVÁìÐ&ÊTŽ£Õ„ óT%±ö/þ=Ì$IÉ|Axý".\êÄô) ØîßrÆañݽgÏžo-2¸-\µi‚I§;ÿâÏšíjÀë$­ ‚â"&9‹ÉèêêaÇO6F{Ü=j04q”ï!geE>Ž„ö^CÃ{nuk¶\ÑëøÊ²²‰Ñ²’bRP˜gÈË˦9Ù˜Í&x|hÀÐÑå!€ÃnÆùæN¬©›Ž{ù·ápØW{ðËÇŸnËVoÙZ»ð®WïYUgvµ{P<Á —«Îl òl „€i >¿ˆ„¬B¯ãÁC „ÇãÉ“'˜»ßóFýo=ÅcjÅ+¿þgBÍŠ¢BQ5¨Š†xBE4Ie×Û=]~ä:-ðú‚£¢ïfÁ§ÑÝ_oÿÁº{á5`6é0cZQf{¥¸ŒöNÂáxÆb …NÏ£¢¬ V‹|´çÓ uˆž§‚À#ŽÄÉeªÂÐï £µ­c°YŒ);Î#‹>ŸÜù›¼Ô%‹Å Å ©ZÆ¿ø}".·º¥Œãã) K*Âá z0ø•+·ÚH„}(ÇÕë}` ˜=£ñ¸Œ«×ú’Aœ¬BJÈà9 ¤LžŽç‹©´È­H •Hª¢¢¸È Àèë £»; P’1­É°<¹:åæzàÀ›AY–õ^oŠ¢Â À‚.wýžHƼE£T• Ä-”B–5dÙ¬¡·„~Û6ªjš£°À‰l‡”„‚1ôºƒCb&³YG– ùy69P^šƒ‚|;Á KN•qd;ßß™P4Á”““Cs³°Z U züÿz3tzÃ+k¸Ç&@)A„aˆzš:´´^Å®ßïG¹ß*L¹@TÚ¬â-G÷}èOÏÅ@}ýŽnØ‘RÝÆ¬þ>ϹîžÞ›Õ‚HDJécÚý'wÁåêÇý÷ÖšÎ|ýüÒÕ›NÚÿÎc_¾fËË“«ÊkfL­ ÁP šÆ ÅÁqÉ Fz§-®»c /Ôùök£Í7zBã:/¯šcÑéô «§Tò¢˜H©2V2AQ zKß©;Ýt~mqé,cAnå‰ÎÎKÊàË7UN½ûýy53æÇ™bq`@(‡–en‚ÀÁn7a÷§{%U‘^tµµ9f4º|ùæRj䛟öi£Ç+"‘P†X†ô™ „ ¬,6»ÿýÙø‘cU)TUº@@M°dÕÆ* ûì©ÍO˜ ã Å•!%“2 ýV\?øš®õp” ¯ÐƒAÈäJB…œÐ’cR–ˆi ‹ r¹o._Y1qÂ}oº\ #æÇ£˜¾~½Î©X¬{è¾ü’âbÆ2 PJa2 )‰Q2P¼JnnÓõ YR!†%:p¥zƒ02k(ІòÒ"ÒÑÓcö¯¯]iª ç¨*4At¾:cFuÉÜ;fÒp 6D]²²LÈͳ"/ß‡Ó ƒŽRë©d’V/MѠʤ˜ %®"“pî›KÌ`â`¶ê3ë3A KØøÈÃ&MÓ~1š*h…jîßjJÄO¯^¶Ь‚€€§Ç!Ëi‚ÇëÅ˯¾­ò¼Nݰþa]õä ¨Š)® ‘P ™Ì-mÓ5O‡Þ @c*ö7S?Û·?—$¿Ýnw<³ù ã„ü<„ƒ4Uƒ ãÑæriÔ«RÄoZ…zZ›ä’ÊÙ7.µ´®YxW`Ð xv§‡ŸRÞýèã¨Ï×óB àÝwùªëŽ¿6~Íõz!?ÏI²³­0™õЖqN„XlXìzt»{±¿á°üÖí–¿in>uùò©'Ï6îÛ®7;/ž¹py¹^§ç¦U—S½A@¯§¿Ûñ¾èó÷/>~ð£kH*Ú6š#£ø¥«6>Ÿãt¾ôüÓOš@ÞØùA¬£«û\Kóéçº\罩ä§ÝQ· ¿ t=¡º”@÷Òßý”sÚ²‹Ê™ íNvíù4q¼±‘FEÿÛ]W>ïjÿ¦€@ OwNUûzEié´Kv~üG±¯ß½öôÑOŽ¥Æ)ÃIŒI€°`Ù£Oô†×8Ž&B¡à¶“G>þ( |¤^»äûÛä{Ëßy'Ãegž/ýê7bã©úç®·¹˜ý­n0h‹k×ÿ˜òüÏ‚áþegOü©iÐû‰¤ÚhžXK÷ã?Ø5÷®û[¢1°åÂQwê›´È NÐPÐÓØÙÙµ«½Û‘M†Þ^ŸßÐÝy¹IÕÕF`<®9¸k;€ß¤3dlÔ·ÛX¡„’wæÔÞF ñd´.Fý®]Ç 8ާðx}à(ç•$1†¤ô‡÷Á;¡»¦ïo‰”€6¨«#Ü+P!ÙÓ£ÙÝïÕŠL¬Ãqîþ~hj¢€8àá]v?j‘÷fþfMKˆKƒÄP½ç½£‘€G¤Ñh½ 4p<t¥*ƒï_^Ùn,ÎÏ—4€IR]n«¬Ê'1t‡«Ñ¸ ÿV#‘Ühº{ü‹¥¨c–Çëˆ Y¡,D5%xæÄg­)€éCyK`ÿ_¶ÿ’Ò 0'ÜIEND®B`‚dkopp-6.5/icons/quit.png0000664000175000017500000001137512343020444013751 0ustar micomico‰PNG  IHDR00Wù‡æzTXtRaw profile type exifxÚ–a’ì(„ÿû{ ¡ã‚ˆ½Á?»ªzºß¼˜y¦£ì R*3ñµÿ÷ßsý‡+·Ü¯Ú¬««&®êÕeðÐÓëòç7?÷üê*ï!þÿÑ}²üøÜóøÙ_ß›äùËBŸé¿,$ïî xÎõõ¿÷B…Þï }Ô»¥ú×@߯{¬Ï‚%•\jÜï_)$AO湕~qËOzúÅîø?x0Röü½ÿ‰ñyi|Í¿Þ/°XyùNI~ö^þê¿~ |.}:ó3¿õýø×¯èÿøOZß°”ñzºèþ10÷ÏuNôsö3¡ŽªpMß,ú$ÿZˆ‰“8î$©Ô§ž_Oíi ¤ šÚ»4ižVÎéd¡å‹‡û*YóÊÆ½æåï«3Þù/hçi%›ˆT‘¼¥2Ó·_¡\õõµ¯=;ovîi3©²XÍûÏÚõ§×ιÙJzý­µò[îºÍ]öû—iÀž×»fò!áo.2¹H©¿§P–×Ë¥äWEn–Ê~cæ÷ëÅîúé^ÀîÆ8‘æ/|SÏ }Ô*M”C&.ë§=È_4x1ûïÞå½";ç/¾š}‘ô[t×3‘2!f²Êµæ;2"‚‹€n1éyÞ¸)z[ÉzDôBµÜvö©—>1Ž,eE[gÖ–Ø®§nI—çÖ1­§ím³&¯9ŠÏºT—õ½&3çD2ÎhêÓwÊjF™K]E{‰~ ^‹z–»—4V·\gͰžÄZ/²×°¦9Åé}~¶§Ò{(hŒ1uϾ–œÜ.’TñRTµM‹QŽÓ–#Nw]2f‹5XÑû)é ð‹N`9uhìj×$¾FÈeÎ24UÖkÔ‡Pœ4MKõþøòöqƪN)O JÕrÝ®HÄn°v™¤KÂÍu‡÷TÍɹïbK<@µ•é¸ÀËØYS÷\„íåôªùÂN&)-¼!/«3Ž™AaЊ%»eÏÍ6#íݦ®‘k‰N)½Ç6¿¤‘ð±NvÄ€6lKY}ÌЊh?£Jo*¡k ä[|öZ§Ca8)ë#*A’L?¢YÁ×ú¤Ì3`©jÔ¡wÙ>­ªgóÞ™‘[Óˆ™Úµ)gÛóVSÜrœ¤5£ꑭÒMx1†µTƒŒQ·úÐr„j-d~5‡„\bs¢iËØÃ'êæï˜a rrØ)y‰(h¤ÆV˜) ®|iõ¶[3KþšlcGkH#PÉ^ ²šª {/ ÀÙ\Õˆs^V¦Õª 18:L‡°s•R±4ZÚ¥G›€#Q¶vïaÁ«uª¥0š²ø,ÊÓõ(ãØZa¦®ieé ±Y‡–µX}çF¾ÃUI‡ï+I{:ÌÔ—zÜuOý¼å9 `H0Û‰-«¡_0°ßjâÐ2ܳcƒÈW¯ÍÉÏêTÛ&Q` Ĩ5Š1—fQôPÔ>±$,¥Ô} †ìÊÕ`Ÿ‡Î¿Ö–p0i‡"¬ò ‰Zô “Û¸Ñ"{žm˜â¡HdÏ\fÇÍ&u%<8?§û¨ šâÑâÔÅ¡{fFä~F&¬2¢ ôwŠN4-·±^7ràÝØýBbÝR>=ë6Wè1Š3M0–»$j,’¸Öð:±O";ËH¬• ? VÛŽ{ËÚøi÷"»l6ÅS‡4D©¬Áµng^|k–†ž[몛ï@;À,UÛâ¬[2a„Ô´YÆSY>C®½¶ŸãÕ;cà;-ÔŠ]%ìG7~nÒíæ†ÆG‚Ofß”Öü·³ÕÛµqú‹#qðå1¨öZ¶‘ÖÊ9Sõ6ŽãwjÙñNÜ¢c{êÂéà“‰ à ^={~ý•õŠã çesBIT|dˆ ÂIDAThµÚ{ÞUyðÏïò¾ï¾»ÙKî $‚XÁ€õ±ZFíh*¥3ÕÞ ^Zu:ÔÑ¿¦ÒªÃ8Që8ÓÎX¦‚ñBEÄH#—Ê5„$rÛû{ùýNÿ8ïî¾»ÙM"Ð3sæÝÙ÷œ÷|¿Ïynç9'ñÒZŽåx%.Â؈U ý¨tƵ#xc~‰‡°í y‘óa.Ç[2Πٜ"²Z„€Q¼€g±û(†)x w`~-’ü%°oÁRÞ´œ%¯&ùü>Öb‰(úTQ¢‰ ŒávãAüp¸à.|ÿÝáü²Èñ\sÙ:/Æ›±®¸Ö\M¨¥T’ØKQšfÙùìÅÓønÇN޶ùnÕ섪u2ðç [Ééï ¹XT“:ª)½5ê½ÔzÉk$¤ñ×Ë@QR¶)Z´´&hN0ÙŠ¤x®CâVÂ>ž ü³¸#Ã/…À\SáÊÍô¾g¡'¡¯Ê’!Së#Í£¾—(Ë<„øyL/#™Ö“#ŒF2“xÿ‰_1Þäßñ9ÑtæmÙqÀŸ//â}ï¢öþŽÔû{X³šµ§³x9•^’ìXðÇ#R’*y•EIÑfQ]Z•ÊÓü^“Wˆêtðw!°_â²?!ûC f¬]ÉYX¶Œ¼Ò=¨™>Mj.9ã¥ä=T{;äZ¬ >Å™“œ-Úüá“!°ÿ4Àå[ÉÞ€%uÎ[ÏúS©V;‹„™Þ­Óÿ/ Ed0zjÞ\2Sß%)Õ:yN«Éª’A’ÇYßàTÑÞÇŽG Ÿéáƒï¦òF¬äü3Y±Ø´QÎ’bÁ& ò*Gè9Hý½‡¨¡w˜Ú(•I’²³+É»ƒ,§ÚC«ÅÒ65ÒÇØØŽ6{—.ïÔM Á¥|îM,z{Âé‹yí™ öͨËôB%¡M6Lí9êÏR;Hu˜|‚¼AÖ$oÆ¿«“ÔÇX4Ì¢Qj“›R$sˆ IâníHb”lç~+öc¬Á¿¬gã{qæ I}ží.HRÛMmùIAæQÈ9-AVÐÓ`ѽ“´YÇnÂŒ#Hª•‰‚Ôsº+†» ¤øP?}7Ù¦^^¿‘¡¾ÙÀË@˜¤º7öl\ÜšÙ’@µÅÀ8yÁx…–Ù“PÉ Mª%°²3“{¦œŽk_Ū·ç¼a=§ ÍV›PbŒêNòC:îãåiI ¯A½ÉhN#íXˆÆ¦Ôšì ²}¬Â÷q¤/]ÚÃÆ‹pî2N[2øQ*O‘wy”—¹÷O²á‹ÇwO…%5.DOŒ§—Š!È®9³ÞUgËz«³Ýd˜ ²“t–C½.9í4V®Š‚Fã$İl™dÓ&6nad¤£/TJz[­Òè2nÈRÒÙᘟmKqN«ÏÅ«–1Ô1Ú¢ÓËÙn’ÑÙÒröÙÒo”ýèG²ÿXvÓM\v™e K9MÙ²Eöío˶m“Ý|³ì{ß“|àBµ:=®¯Åi#ÑØ»w!MYZ¡9áÕxE†­u.¿¼*}ûº¸UÝÁ(ÙO¾ u§­\)ÿêWeïydéRÉÀ€dýzéß(ìÝ+<úht%Ý-M¥_,ûÊW¤\ ©×%µšdùrÉ…*ï½WعszxOA3áèTš2ì&ZÜÔÚü6ÅëÉÏ` gޝŸ Ýßqkfz²e‹tË–(’níX»V~à ’­[•y>3'M%—\"¿ñFéÙgGÝóV¬^z©2M§ç¬ž §=S–pJÎ`<í½.Å9Ë6ÅßíN ’ƒ˜œ£IºuÔjó«øêÕ*×_/ݺUÈs!ˤ—\"ÿÒ—$6>NJ„~!If­ÕS°¼ÑñHf‚Ü`N8'Çê•9+ûæøûOÛÖL A{ßn•VSRëYDõúë5«UáèQ•뮓žqƼc!4Zïæª–5y¦F+™q«µ”¡Áê+kôVf$_ŒÅ 5Ÿ»oß}‡äîÔóº·Jæ¨Ñ,_ü"í¶dÙ²…Á…‰Ÿßä­·¨#-ê%}mÆóᦠý) ýiB6T.ªìÚ*£Ñˆ»uZ?wísô?fbÇóJmšÄÐÐ Áýt›áø¸t×þy×J­c3ØZ Áµê•ͺ³Ë0±pÀÑ&¿ûa‡>õã;î<ÖãœD EaäŽmŽ|æj•_?M{áõz§Òò©Ôµæòl¶ú”EÌ=æ“ÈÔA%mP½ûa/ü݇lÿéqwâXðm÷ßâЧ®V»o—¤½ðZ%*&³0vêÚa†Ùô Ô§»§Mª¿xÄþO|ØÈ=?9)¡,ùáÍ^øû«Ô81ø)5š:M[‹Íf»…އ õ¬Eýþ§Œßþ#¡8‰[QjÿïCzŸ8 )NniÁv‚kby&P¤áÃí™]˜Þ‰ôä~¼Ìs½¼Õò¿ù˜´R=!þ¤R±ô¯¯Ò÷WRöôœÔ-f„[F¢ÃÎðgËr+/Œ‡ƒéƒöp´ƒãe*=W\aàÚke«WŸXúS$êu• /TŽk>ð€²Ý>î:à Ïfܬ#ý»Kö³3ÖJâ¼w Ñ×åJÃÆ¾~啯½V¶jÕIƒï&Q½è"e£¡yß}ÂqH<Ÿr ÑáÀO£ü<ÚfðÖ7÷ËN©ÌøÙ² Y|ï•WúÂd+W.2tS2_ú€¤§Guófåø¸ÆŽBQ³VgòXõ"°§ä® Ýæ)î+Ü>:³Ee ¨QTæ×É|ófƒŸÿ¼lÅŠÁ—­¦=7Û¾ÛoSÅ‚ãÒÁAýŸü¤üüóç]k,áHø2ðD`2º~‘â·%ül„#í./”ÓÕÓTýzê‚ ŠfÓ®ÿø–§¯þ[O|ôÞ½m›²½°‡ÊW¬Pݼùصp ‰•í)ðcßDGs?~“Чû[™ÐüõØlZ Päs¤’$,]:V‰¢Ñðä7¿f÷§?méîý?¶Ë“W}Ôî[þkA­²Ø‹Û·`8íÌ¿m´ôøMGí:µ«´ûçH¥,~èAe³9/øÇ¿ùu{®¹Æâ}/HY`ñÎ=žøäÇíºå;ó’˜x~Ÿ¶ÿrÖ:m<—ÎÖý‰Àö¨>á6fªÃXô|Ë–³j²uSgbQ•Bì\åÐÞ½’ó^©ýúél´56æ±o|ÍžÏ~ÖòýefƧ¨±{û=ªÕÏ8SšçB&öï÷Èu_ÐÞö=yQNÏ9šðTc@èøþ‡?£ÙŽ÷?@èÖƒ5¸iSÝ…×­bE>ÃÜ8Ù’Žðš Ï­]kéWXqÁkµ''ìýá4¾»Íª£Ãòy%‚ynÉbÕ·½Í’M›´ÇǸã'꿺×òFsºÖ?‰Ç2ŽvIÿPà[Ø‹¼ïÃ^fß$xOÆ¿¾È’,‰·-Ý5¡üPgE%|!MŒUª’²4ÔnYŽ_¯×™~£I"Ã`ui`gÊ¡®Óa#p+îâ`É_Šú8v½§O6½v(•m¬vÒl´óhКñ)úCEaqYªw@/¢†Î˜ºxíÓ/^Ké¬1‘ðtÆá.ð­ÀvÜUçËâ­Í¼Å]/lgü¦á+2éi]'¡"±aªl:“NúD½uþ™4ãTbÙÆ}¸bœ›ðYs®œæÛñ1ì˜Î}¨aýP*9­SÚ2P¤´j±¢Š®÷‹èEGêϧ<—E?%¬¦x-s+åH4ØOˆ7µ³ÚB*{¿6<аéº<–¦·+´ª‘P÷%ÅÉJ|"á`Á'3¾~*X݉ÒwÈÇÎù€Ïæâ®F0ôPÓ9{Ú*«3º ¯eB+§Q¥Y™©õÏêh§Ñs§1ÿ=Ôé£ÉŒ›,CLé÷„ˆx;c þ ŸÂ3 <™kÖAüEÂU«3k/«KÞTcq'v¦/>º?;>¼0;×êîEàp`GàÂ!v†èë¿é·÷'òzDÏv/î †lZsKm<ЛÄ+×é‚ÀTð3;¢.¼x>ðËÀ­û92ÆwD}ß&†„ã¶ßõ©A?.ÆS^¿45tv.9/g]ÆbñÆ> fɦ¤Ü Œ—1(=x4°3(r¨ŒjÿuñíÄÜ:øËF`ª à|ñ±Ç›3Ö÷%ú'Ò%ªYŸÎS•Å88RÆ·Gƒbœá’';€¿+zÌ“þR Lµ+ñ*ñ¹ÍkÄç6+0Äï…h«#Ø'>·¹WŒO〗ðÜæÿ]S†®ø¼,7IEND®B`‚dkopp-6.5/icons/media-play.png0000664000175000017500000000641712343020444015012 0ustar micomico‰PNG  IHDR00Wù‡ ÖIDATxÚÕZ pUÕþÎ[’—å…$$$µˆ ©Ek™ºT«RAj«ÇÁ—A± ¶hÕŽ‘Ä–ÔjÕŽEP¬u-ŠZE FQFÂbÉB²¼äå-§ß¹Ë»÷-I#`ž™?÷ä.çþßù¿9ç>c×Ò)'Q&QÆRr)IƵJ+e?eåJϱx©8ÊçGS.£\L9•’jWÑ÷Q¶QþFyžÒø¿ð]Ê"ÊE”díLr†@ÞD‰¬1@F@J–„Ã¥¿#”èëº thþ ðw™`ü”—)SþõuGyr)ÅA%â³H˜08š>œàÑípÿ‡¥D€¤4 ¡Ž­{H¨w½[ ãm”(wRökÊ ”åP\OL&sÆÏ ¦É2ÉåÀI£½¢¤0CNÈKÃèLðz\ÒíÔ†”ÝýA4vö‹=ͽ²¦¡ Ÿ|Ù-a"ó Ôn–ØI&õ´¨º)K(« PG ÀKyŠ2“”˜2GâäY€ÛƒÂìqþ¤\yzq¼çã}T¿»?$ÞßÛ!_ßÕ*Úû¤øh½@õZIÊ©[6P怎€rRÅÏ)È,~Hëæ#'= ³NÉÇ©c2MÖÄŒ:Ȱ&…bNm;Љõÿn¡«Ö½À›digƒº\ ÝÏuò¡(å7Q&¢hªÀ¹wJx¼âûÅYò’’<$»|XÅl¥0b_ŸÝa÷ÙÌ1ü0^ªiÿ¤UÐÏIßô @ýu/=çb0Š6ô.LÆx:è9‹„+9YÎ,ÉSŠ2¤þz›B‘¾†+-;4@F®kwHQ]ß%7Ô4#è§o¼ñ°Dí»êÂNÊY‰è”€rØu”Ÿà¸©4àRú©³'çaìÈTý ó);UN…y2ž:Q§"ÑI—ým>¬ÛÙLÿ¦_¼¼¨Û¡®¾H™ÇNôÚ›(åÈ*øéJ)R½¸àÄ\Qœ›ª›[¯VÓ,ìÖL‹¡h“À꜔Q×÷¶úÄ«Ÿ¶Jéã¤ÿõæz5ÆBʪ¡ŒÓÌåp¥ã竘˜ÆË)…(-ÊÐõFÂ|R4uú¥“Á)7#9:Úˆ˜È#m}}¶óÒv]›½ÏóÛI§j†\4× üå&z4ZÛòD,€ç53ö àŒ¹ÈNsã¼r †Â–âú\=yšÚûñÇMÈÎò %Ù‰![,u¤­¡‘á)L?oC»ŠNï1’ð4 z_–€*¶bD¾À5U, RÅ™ã²äH‚ÐR«€nÝ&1ÿ”´€E¡DT2¬ðÈ”|\C ‹¢²ëH©eŸ´âK B%´úB¼‰ÿïauZqºãIÀ\ÜX |«T›i•OŽ¥iÓ±èT>9O^3fÄQ[z4/ÖwË_±”®ëöGQˆ  †Ï·K<¦xÊ¢Ðõ¬R'¦+ètÀ£8‘s…=y”,;þèTwú±øãñý¦ÒTº_m¨D¦Îïú@¢â¦…”7,ÁUË€Ó~¬QH…U";Dÿ?•ôãc%¹¸–Ž´µøƒ¸gwžþ¢ ]Áh!eÂ< J=) }ðwàÏ÷ªÇ—›at .`½ä:κCJ ––œ6 Y Äê“råµEÞ¯l?g²¢®KÞ_Û!:é¸f¼·Ó†Êj}–z?¤ØP)ñê5Ì<+‘àŒ0òÓ—Óü¯ªÍ'–6öÈ–V­{¢JÈ{Mé°¡tÈ8¯œü™‡^{*®œÖ4#rßÜ:‹!4 öF‡ÜØRMßZ,*X¹N"3K³HV©öDg…Z{½d_‚FYÀ¶ÊŠ®u¬iX£+ ‚¼~¸ƒŽ;[ ~ï°¶U³6wKÏdµQŨäÑ,‘íq2ÏÙ\‚ZIhí&ˆ$. u‚œñö~†\e~›%edû;jaoîªfm¯O¿¸§\ Õ#].‡(ð¸´m«Ü0"Õp,¶­yÍ™©yöõ²Ù×/pßB‰·_¾âöºÙ¬ß;K`9 ¨¬,‘D*—âBŠË˜Ä¿ -VÈÔŽ}Tº®?¨7ë m–°°üpËà°ƒÐ?1aÞx€‹žI%Z™=št*LviI:žF¶•›éÄ‘º'¦Tæ±%u#i£Êfìªs±r V=yTŸ˜ìtÒ?ò¹“Ên–(£Ÿ{ÓUdEINYÈ%¨—Ž.T½¤zÂÚV(bb¿$练R4p[ÚÂëîaݱJ êQ‰€ú°l>ò™-ú3ëè"`Á­À¬ËÒÓ´Ä–îvŠ\Ò+‹ÔòÒ7RÔÒÔØÁf0‘}JGÒ¥ƒÊ¶’.=Îýž^õÏI<¾‚ó\ÿµ|fµ7•'‚ŠP Tn{Ìè—\ L-˜ô¼a‹N€­,ëÑEv;¶/½ÀØÂ ¦µøš?tǶøŸŒÌ8ušDI ˬñùR ¿*+ûú$6±\¬¨©‘ضh;ôüÔ ¶ýßþØ#QûF~nóe¾þ`w!‡3IEND®B`‚dkopp-6.5/icons/stop.png0000664000175000017500000001011712343020444013745 0ustar micomico‰PNG  IHDR00Wù‡UzTXtRaw profile type exifxÚVk’ô& üÏ)rÐ q¢*7ÈñÓxf¾ìnRILíá)µZ-—üã÷S~ÃÕŒ¬ˆv·aVqÉA¯¯k<÷ö<Û«‹ßCøÿ­¿|}ø<[|ï—÷!mþØè3@þc#z[pÀ{“×ÿï½_7ú¼Øð^å¯Ï×s¯Ï†\¹±pàyïÄp=„we¸v»žÞ ôkÿŒaAm£}íl|ůùå½–òÈ?áTé{ÿ/Àù¯þòmàsÙÓÙžö¥ï_À/?Ñÿ/ð·¾`Iñz+èþ9 _;gû9ùL×ìÍ¢ó¯0qÂŽë„Âyãýõ¦O3@º@Óþn§N´QWkõ4Bk/÷âfmµŽ§´húô9Æÿ6Úy·NDBÔ’ó}ù2¥ÈõunNNœì51I°™´ü­ü߉ÿÔιl…{þÎ5~‚M7.ðæ†ýÞ1M^ÔzbÆ¿þûËÃË #½Þ=žHÜ\ÒgyÀÿz½˜-OÀm@@ ™„qëÚÔF2‰ÙÛAÊAk&ö±Ï»>íËU¾¼ ö¿'ékEkoÛFì ÊÏ«=nË ?WkHLE4›À6œÞ‘¤J°1îäpz_¤Ö³å džɫO±0)'ÖVb¥ðuΦðjš1,x]è¢ÆÖè¢+Ò%ë‡ìå3vJî®ÚÕ0òÃm»ö”ÙÖ©<;£ÏβÇAEŸ óD‘4Q§ªŒaÝ£.m8§O=#k·”ξ8÷›áao4y‹/OX½îvÁ&kï3gÂqY£íU ¹Ç™Kgδ³ïÕ–œîxëkwµ3]C1”Tõ¢Ö¼ÕqYX[FÎŽ9°ƒÔŽ4wYnÕ~êh3ë>7]&Û­³ :FÛHat&àäie•î^Ç^ÒûÖã±ÎÍȽ¬oÄ‚©ï†MÈFWqÏ%c¶±hϵ&6*±úê0‚%†ÛÔzÜe°“*üK6sa?7PP†Ìeàc„®£57D0 (œ8°Î·”¼T Q't¥/­kÿå00Áý˜ \ÈìgË@u½÷›"gtÓBOÂzm¾Äî3OSFÕ9 8º+‚ï–¬»É4{ÐVxSÐ šŒ ½Ù-6â=ŽìÞ¦Sr.†sGµKÞšµëc…F“‰îzfžô ‘,¤Ÿ>÷Ä4îk Ô¿‘Æ ½Bip©â8ÜúE°Ð:rGbmƒÕ@“ ûU›¶dcã[!¯,×¼Ø|;PÒA´ÇÚ¥ƒ¤s¨õëÈC ¸Ár”n¯j Ú0Hàx˜ëL©»çšcùJ•tIŒÖ ºútWBiñs-äŸÅ ÀJX‚à`zT×°!«²˜×31\¥âÌ­÷³Id" Zg_ º&"R"8Û{FÎ9QŸ þ`G$% b+áÜ ó¡[6sœ¹Eù?T‡•÷~ãŸÛC 9 àÓ†Ýâë‡ è.´zy»TB¢3;l’º¤_uºÐÞØ \!©û1<ñ»U#Àä£ÍU5ˆd üH.ˆÂP&]siÈK.I®Þò˜?kC«’Dá$äE5½ï M"³LæêdÇo÷sS~vÌV(dW¤p ¨]lºqZ‡0$rôâ½}‘Ì­—Ým­êdtnB9#ØkÂ|Ë $W Pè!ócPýª<ll¨]7jš6tBiô~ö Æ#dêºrù'a ²– [~Ù 4ðIB×ÕRߦ 8„aK›"{¿klPŸ¨¨ÑìRTx&Xésè®ÁW ¡3Þ㉼ïtÅ‘;&œ SQÐòâ 0ð@qy{ƒGÀ¡Í²è•†3ñ¾ù<íYšå‘wÚÚ-^ø¡ìÙMê F]ÝEc_ s@:ž®©zKñs!&½¬±vÔ÷Qþú8µÝ5úsBIT|dˆ ¥IDAThÕškpTåÆç²×ÛæN. ƒA vG­E¥SdZ±Ei• ÖŠS[Á©mMë´FÇ ~ùP¼àt4­µÚVëXƒ$¹ZÌ’H¹î&»ÙÍî9{Îé‡%K’ÝÜ´@ú|ÚyÏ{yž÷}Þÿyßó_)¢¦¦Æ^RRòxB\Ü#†2Oµ},&Yqõ÷?wìØ±Ê•+Wú§Ôv*•÷ìÙc+++Û”,É[Níx%ÁëlšÓ1_4‡Ùë×z\!¥²®®î…Õ«WN¶í¤œ'¿1I”·6VmOh{篡ÐWc<š„,“sû­ÌÝücO¬:?ú7-¯ï%ÐÕ5VL)K±àW?Ç–‘NÈïG ±Í̤èuèŠBËî·0ͰcNLA@ž‡ ŠhþAtUE äp×É‹2ÿ±Ÿ`MKEé÷ + ÖôT Ö}ÁöN½ºCב¬Vì¹3‘,tUÅ0 Q$./‡¤âùÄæshËS¨ýžñˆ&³îZ…-3ƒ¾#ÇùOÕ6w?³ïþyß^IîªoÑVó7=þ’ÍŠ%5…’ß>5#ÆwÒùñ§ ëøN·"Y,äß½kZ*îCG9öÛgQû<>ðrï¼YkVÑþÁ‡:º"ã+}ýúyHV é×-'÷ÎÛȼñz2>øˆ¶š÷Æ ÇÙ‰/*ÀÐ4Zv¿Iç¿>@õ`Ÿ™…h6#ÇÅÑÿÅ l33Ã^6 ü­gé;r<ÒW|a)K£+ 'w¾Jo]'_ÚEZE)3 óI^X̹Ž#m´`Þºƒ 4·Ð][‡=;‹ôo”“Rz5gÿòþˆý Ž`hZ˜ `NJDÂ/k_Ëê6læ³õám<Ãxш/*À’ê ÐÕCßác‘r_Ë<'œH ŽEWظ£¡z"bÌÉI ޤ% ä÷Ó[ÿ9ë¾KÁ=k0%&`è:ªÇ‹ÚïAŸd¸µçå Ê2¾¶³{]‘r-ÄÛ¾OÇåÏB”cnEd» {nöy1^6ûÃB†¦Ó²ûMR®)Áqu WþâaRË®áËm;è?~bÄòM‹#Åݼ21 Ïu‚a„WyX0Íf‹®Â–™Žh1“Z¶„Ô²%èjˆ¾CÇ¢Æ)Ýwº•†Ÿ=ɼ‡7uÓudݼ‚„yEÿÝsœûûGÚ$DáØ  bƈǚßaˆ&Â0[X’“XX¹ÃÐD Ñ«=ûêèøð“¨ab¯a0ÐtŠÃÿšÞºƒm¸—¸Yy,øåc(ýzöÕO,`8t#ªÈˆñ ÀÐu—;lSÃ@õÐ[×@ó®7tvîf ç¡zhy­¯³‰O>Fâ¼¹þðúŽ|AÈ矴Z €d·EmTÉjAtE±¢A—›ƒlÅw¦ m0€êÀд˜ÃDmbA–ÂËz>úºNÏþƒœ¨z‘ÏOòÂbâò²Ç'?D¦×†%Å4ü +X3ÒÃû£¯Ä1ÆÐ4]Ý ¶w0x®3ü| òQIdνw³dû˜µfÕ…Y3 <_:Qúú‘ì¶p8›|§[ÑUÛÌLÌŽäH¹d6“0·ðB¯qˆ¹XÓÓȺyÙ·Ý„)~Fä‘-; SBçáu6ìîÅ–‘FrIq¤ÖŒtNžÂuèèW&16qïþνÿOrîø&óÞ@þÚÕHV+æ¤D‚.7M;_AéóÄê+ zP¡åµ½¤U”’\²€ÒÏ£+ –GøPøú^‚Ñ'̯% äóqü÷$äó“yÓõ˜““0BîÃÇ8ùÒŸèøÇÇ#–ÜÐ4Û;À0ù¢¿Œ»åøÓÏSôÀ:ìÙYH3Î.Îì­¡õíw#ï-ÀßÚŽêõNiOîŽÎЛŸº>Ù7âh1——‹-3-dàÔéHTÑ$aMKE%—›?ú£² ŠX3Ó±çÌD”M:»ðiCWÕH¡ ¡ë»{'‘~í2æWýF“‘$5¾hŽÔ½¯~D8Óƒ ^gå Mc°£sü:º‹ícÖÑAügÏÛÏY&¾hH’*º½žgg¯_ëɽý–È%d:cèR?{ýZÛëyVºá†>KHK æ,/+U{ݯ³9êÄ7] È29+oaî£z\!µòèÑ£UÒîÝ»Õòòò†¤ŒŒ`îµËJUWŸÅëlšv""YœGôô(ÁÊúúúV­Z5(TWW‡***YYÁœåËJC.·Å3VbÈ6s7oô¸Bje}}}$ù6Z]]*//opdeN+;¶Íè$àˆ¯ÓC"¦‹bÙftò/*?0]ì4žm†#f†ærÛi"Û Ç˜9²Ëe§ÉØf8ÆÍR^j;MÖ6Ã1ažøRÙi*¶ŽIeê/¶¦j›m§2Ð…T”yKËËÿgöBMM½¸¸x‹#1ñ§Óáï6ÿgx—ŒkBšçIEND®B`‚dkopp-6.5/icons/clear.png0000600000175000017500000000532212343020444014036 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆ ‰IDAThí˜ilåÇÿï1³;»ëÛ±c{m'v GI(DT”DŽBJ P¨R¡¶@©Ô¦m*(ýå*>”R¤ÒªÐ¦)Q•ƒp%M£8ÄŽãøˆÛñµ§wwfÞ«Ö9Hâ+î‡ö‘Íìhæyþ¿™çyþo3c SynJÍ”ýú.|ž[Ÿ+c®RRQŸröÇê=üøËp'ƒŸk‘g²çï¶×2nÿâòå+횆‹hÐ ÃË¥»·¿~7mß_¸wL$=×BdzçWÛ–V>v˽ë‚R)ó+Ogw¼ùB–‰8–]½2(…\9ÑX³ðÜü+PÑÚëV=î:ø±Ú³ãõv7[ÒÝÕF³£1@Kc&\³ ðÔ}(°lºáگݡj{?ÜâiåÞ®FŒ% îïµìŽ‰ÆœU€°<ïü¥ÁH¤n²Bú–RȲ`àåhm½¡èhkò„ 'sÖž» Ž1æ¡‹–^éx‰#Þ(æÍk”„ZÊË*®½pñ"Çórh=Ô¦}£_œhÜY…˜mÝSmd Þè0ŒVX´p¾s^c=ŒV ÌÆ¦}ž6dãO7¸];k„еó^v“ý^F+£a´EN¦ÑÒÞ£}¯›LÜYxúû"b…*‹ "pã0J@ŸÀ‚EhúäpN)úÂã›bG'{VãwF£µ–7:á¦óo €ð †Ž ›ž¡d"°lìYІÞ[Q^lyé“壥>nt…Æ­ÏlÈL6ö9xâ¶‚óÁí"‡I¸‰£N”³Âhje…†'6Æ>œJüs@(n¬(‰PwtÂË0j9èû¦/®ã¾o™jüØ»µ¸8È0N©•&”ëlVÅ,¥•Mã¸xB9²>Á¿ºtÎW¸î™7‡ÓSÍ9ãËéCï—©_¼®Žò¤ŸD6uÉáOMw‹ùp» TÆÁÎVŒ¦<òø“{ŸN¾/!mL)ea¸ÉÝàvEàlªnF:ÖŠÁ’0š6Âê^÷ÄÀsÓÍ7c_ছnú¥Öê‡Õsõ…eçÑÆª6\¼8'â`àÈ”U׃ÐB€ÎA.›@_çöœ›Ú%<²ú‹·Ç»ÿ«+V¬àÁ`0·víZ>00€C­-ho?×ÍbÙŠ% qÙ²ÀøÞ(<#Rv9ú:¶ëøÀá$\³èÊÕ£ƒSÉÍfàíõ# v7ÕÝ×ÜÒК ­Ãe—]ªª:ôôlûÈÃ;[5”ôPÀ"R\ÏóÌ`÷ž´4øêUw¥M5÷´¿@ËÎ’±ž+©ø²ÝÖÝÀ?ü¨ZSH¤ êëëQ]]‡p8Œ¡¡!í9Œx<†®-BcÙÖc¹ËoOíšNþ)zèƒò—íàÜ[«Ö„8gÈ&öÃË ‚ÚZZز5†æÎDkÖ"—Ë¡£ý b#鿽ñnátÄOÀÐÖeëC‘·T/¸ß!z"× €R­\¤FzÆ10˜Ážý{> áÈÑêê磪ªïíØ!]Ïs¶mÛ&§0¥¸óKeO:‘yw×4® }0)0n@ÃhÆøà6¥–Eã| 1C]ÕúlìÝ×BùS[¶lÙ2ñÀæ–%wq«ôÁšÆo‡ˆîL~ýe´„ÖÚh-`´@.·)8•¨‹2”»¨ŸÇÐÙÒ”­(Ìmzë­éÊŸä–²u[áåŒ_¬]ø1#€Í‹7^.còÂPÒƒp3 Ìä+Á€Æ¼ê–.i€©{§/m[K£†YïFÖ„9 GZÐ*dýÌ ñF äÒ1°ÏíC Fû¢NA£š\3kÀÖÝ<§æ†H0T¨^€U‚°zdS½0F‚q £óå#¥‹\&+ÀÇ€$„0FB‹8N)„2³`%æ¼)Y²¸¤b97ªàõ t.r£½ð2°œÐ‰ú7F ›ŽÃ²9Ô‰ò1ÒûYP€Í6ÿ¶:tÎZß+ù.·Šn¨¬]0ª„ÕBø>|onj?¸eƒs²þI9|ÏF `Á˜<”ô]h-a´8—QK84YpNZw2çéèÂï…² ¬ ñ¡½PÒE.Ù £2°ÆH-†R•` òo\úPR@+ @Œ!&'õ´WÃghÞQx#aΫµç=âÜHǺހV.´”PþQ0Ë!ùæÕo€—·S'šY Z œœ¯ C I70é=ð„î,ù–eþ¹nÑ#!;X ­îC/¹ÙL,…ÈŒBÀqÆêZ€Z¾„ôºÁ­|C+áA+Úœ2ÙÀ†’Ò*N&Fgààö¢ùF›—çÖß²s!ü$ºZžÍf’m|nÝ*KdÛ¡T”s`ìí^ ßð3£ f0ùIM)JJPZ§hy>!+‚YP‚Ô5cZˈq]ì!ßè?òJ:ø'ºš•ËeB•u7S-¤ß ;„1 !h” 9rÂë*h“ŸÈ”0Fà ]ÊÆËàgc „t°_PLr}6n -¾*¾Y{Ù•½íë“Rdv9¡ú¬ã,¦ÒmƒÑ̲A` @x޶¾€S¨£ DA+%|-Àí ¼Fç÷íÔªDntظžjoŸr<îò봳œ±‰­Hìxo§¿Xi²$^4FÀP° ´– V#†z÷i­´†Q`V*¿&’>Œ–0€9çCÉ4ŒI˜]ƒØPv8avž&ø¸óÓü3AÎ`<ð³lnûûÞÇzÿÑ}¬ûïµ(¼ „UÁÍdÐ×¾••_H•ׯ ´ò¡¤­ìХȤzÃaT „Ø`¼‰Á>¾s—Ø3ŽÐÏòqµŽw‘Ž=Àð‡Ëu®þ~ìú®Î¯uø›NÓĺå8Ü´Ñ#,ìY¹ èÉHJ@k jU‚°2ô¶5nG6 “ì…~Ïï7‹¡±\Ç랎ãdÿ”· §ÓŽÀ¬ßä~`q{EÁ‘ù‰þ=sâCµ—Ùsë®`–ínÅ¡U Òw¡”D ²Ýû}σšSåZ4#X¸}m^[ûàoÞÜêïEþŸ®ÉúŸÍî}jäu7 O¼q2±Â²D+¢ì`˜;E—;x ¯µ¢B ³y),ŠÒŠr‹ X‚¶½ïÊ¿¼íþ¨¹CgŸšõXN=ŽŸ೺œâdƒW«ôs‹íðš[ùUó¢ìúP¬`,ÄCe:)µ†uèD,ÙVU×xAMã¥,*DZ®Ý¢µi÷ŸVý ó“³?ÌØDÆ\Š“ÃÚq€qëôþ[­Ê‹òó Â4Ê) %Ó¦£¶ß´8Ç*ß´Õ»ò¥×üÓÄŸéüŒÂ'púý§Ž §6 9åžãfÆ9w=Îù©×&,h&쬣> ¡O»ö¿kÿ~·“" A®âIEND®B`‚dkopp-6.5/icons/burn.png0000774000175000017500000001012412343020444013726 0ustar micomico‰PNG  IHDR00Wù‡IDAThÕY{Œ\guÿó}÷Ι}y×»özmÇNÖ6ë8Vâ(…áQ§EДRDA Rµ¥­R‘ª­”ЇŠÚBTƒ€D¥j…Tž …-` ŒplÖ8~Ư{wvçußßwúǽw½ë1)Bý¤3÷νwf~¿s~çœïû†DÿŸÿ²¼Øq]†††ª³çÊ·4ÎÔ~Q€®wèëyØ÷ýÊãÿ’ܶãÝé?6Îö¿qpå|{áæÁǨýÕ¿¬ zG[[Ÿ&ш&+`•«¨Bþ‰u^ý8úÖwzÞýtúK! "Zã ¼Nðjÿ1õÁ-ÕV`·…F¿ƒdøÊÆCŠæ«%1šD B°FÙ´ÿLºf]9XýÀÍO¹/¬>Wyçÿ˜C€^h÷÷÷WõÕ×ÿý‡éÑjË¥Mï>ðÉ›[H£÷•àß⊯Xe¯DË.R®Â8•SŠ'F¨þ‘•žœú…õnÞ4sÏãÿL®õ`ÜΓœøÊøO¢séd«ÝÄĦ3¥%À‰¨€¢œL~ V°ºŠÄhTuò×Ã4ýXߟœk^/”ÄFcíý÷Ó½µŒKHN)RçÛ“ˆ[¨wRTbhO@Ä`RPÌ`b0gFÌ`ÅPLP*¿‚¶>ªI}Ðó·u^ÿéémÚü !`­ºi n‘y@fਂ‹™°bÔB¨ô¥PÌPJ•ÊŽœunJk¨â¨³kÌ€cÚÚIæ^×Ñß™ùк­sÏìþ¿!0:ÒßÛ8â½Úu A‘ÌÒÒ–ÆLW {lžH( µÒÐZA;Zg¦”^ ‘ÝÏ-¿î †_ØØ-}Á|÷¾»pô‡/Ž@¥RYñ7uþ@Jfçí«µ>º·g¹4´€žAkojcÛdWYˆ¦SYL]¬tZkhí\þLq®4 ܤ¹6*~böK¿½ñç&P«Õjo~Cük÷ÜCa[8ñ‡¯]õæÖ©Òêà<¾À±=% XBxaHʹl8—C±‚ÎE>p‘¬_Ì 'Ÿ0••lþÝ@åç"†áš·ÿ^çúè½ðc§¶qEù÷ú]‡Ž) i¾1UAªHÖ,8ML ¦°j ‰"É 2 ¤0ÑëMÿ†÷øŸ¼ýš2¿ìæC=D"R*aLfNØw§G‘×Wu0ßt €T@¤~/$­fÀ(§˜ÀD`¦Œ/­J´pN Qù9qö9&‚J» ·vÒœYw]¦§§€¹ -  ç†…}$&À†å~ÞH€8†hPn–.%F’ ¢$A§ˆS ÐEÐTDˆ(/¿ytŠˆ0ÀKyèííϽ•®Fà²FvòäIÜxã·þ×#²só½1 &{.몀hÙu Ãé‰P À°ç{µ üV#BÚ mǬ˜]×sU¹×s+}NÉu3iaIÓË» b€5bòŽÙsûîúóÖ™+¸l.´fÍ8ŽsæÉ§âïNÜ-Û]ƒ’˜ 1åñC€&TçQÄs„$矛•$¼ÀŒ6ÅdÁAô8a0Гè©VªÂp£ ¤&EtÑíúhE´ºêô¿bøÔ ’ø¾_ÿÊù¿p¸:ec€R[€1€4;*càZ"‚îAòÔ.<4£0šJÁ{Yé}ž«8JMõ–ÕA€§Z­ÖÙ¤;ãcòFÃZ‹V»…sÓçpúôiÌÌÎ" CmŒy¾õÞ+Êè²ìÞ½‡¢7½ {ËÍ ?Á˜ž†GIb%£Î€Äù h܈ØOmÐižtzÎUtj}ñþìžsµúkÖE›ïݾ&ŒÓfs~Ž*½ƒëb@A¿ÛEÇy>䀂&!zigÏ¿V{^¹«ó3 $I‚x`ù°oÝòÆé êù!œ{v-úãsè£6H29-$C XÄjXî¤%¶u­TÈLÒå[ïÒ&''/ܲâ̲ ƒ¾©7¥vþüùÑ„¼P$P¬²¼È'D C±¥ôš“sQÿ$pË$´sçNuê+Ê~òqùöî/Ólõ•§1¾9Æ|y-Žûƒ@‚“@ÌP4ˆÓ³ ÔhרÀžb2ŠaËŽDÅ÷×éøZ'˃â7­¨âûÍaŒ¤m”ÄdJ æçZpTµ„0è0±bJ=½€ÿšz´1èI§M‰ÄZ ®”úWöDa”Ë%k^´ÐŠ\Èß³¢$›ô^›€ïû“µ?ƒ@¤€²9%¬}ø Ý—µæ©6`áJKÐvº8[?s¾‰reò±0AJ ¦­Ìõ ¤l)Båe¶ºi²êÒYò<ßX” 9«lô›°MJë…R¹0é[L€òkŒ¥óÚÑs×$055•*¥Zg÷K}}?M¼i›ü'µXS @˜§.žïÄØu¡- ˆÊª)"@BßQ Z3ˆ„È„ýQ¬ûãÔf Ä‚˜ µZ¢÷ýã¢þQLYÒ˜›ÿñÍ7 \q½¼$ˆèø×¿.ûÖÝEº)e´tðž ‹m‰ jlÖÐY @\†¯FR Z]šˆ"É=ª—ê|A:¼ÐV.qwêç/|cÛ_EñÏ$P«Õ¦ûfë{¯——¯Và%T" J3À!u]„+WB*èbfÉ—(À.%G¸èé+ Иo cì»Ê#KûÀÜÜ\÷l[?ùþ/É¿³â£ -@W@²®››Ä€õÀKÁ­¨þÔÞ=Ùÿñgæ®Ø¼^ééé‰ßÝBw­ õÖä Mf©¤‘…ÿc0]³dK÷ú‰°91Ý|n÷÷þÚ‡¾S!Ÿ½,víÚEÏ>û,YcîxËV~u„Ä6·T²Y„ ó¸ÐÜ7T_µÑqh2Ñ%;Ô×NvÐìF­ý?9¼çGG.üûSgÊ_>ÜYÖüôÛÞæ›-[¶È»Þõ.»mÛ¶+~‰>ÿùÏÓ×¾ö5:~ü8ÍÏÏÓÔÔTßK–ùúÙ·ð9§¥lÎ ’9A ÉH '  è*Ô¿qƒþÞ-›nظnÍŠ±ž²[qi² ¨XPç®VÒˆâ4®Ï¶‡Žynßá ßùÑ4sÏi}„H…Ìœj­S¥”)•J¦R©˜+V˜Ûn»MvìØa'''/FàþûïçÇÓüü<µÛm Ó$é® ¢ZU+€ÉÁ'X*'Ỿ'ÚpøèKn>¶ù†aoíŠe=ãÃË*e×Ëš-s§é\³Ó™žm6ž¿œyn:üéOgèÐÑY~>2ÔL/³xâ4McfŽ“$I¢(J»Ý®9{ö¬Ý³g¹çž{ÌÎ;ôþýû©ÙlrEèt:*Š"¥µö÷×ãf˜ ,NÁÖ0ȉÅézWÕ§;2ó­ã²w¨özAéÂPÍC¯Ãâ ?!¿¡ÓðÑší¢¤g_+ªD[k5ik­@‰Hl­ML6ðÄOàÌ™3öcû˜ÕN‡‚  v»­Â0Ôiš*"²gæéøÁSrt›ƒ­Ku›“È#Ó)=c­uóÛÑŒOõŸfCV(Š`å[ùIörÑ7€ˆ‘ ¬µ €ffkŒAh4Ø¿?~øaQƒƒƒªÙlê(ŠTš¦ÚZ«¬µÚZÛ92cÞ°’¶hŽ kò_ÈQ8X`fWƒÿéÐÒ‡\Áì" )€”ˆR,Yegט9]tn˜Ù2³%"QJ‰ã8¢µ†ëº¢jµšE§iªŠ°‰ˆ"¢ôTÛ&’ÊK+4A°]œ¼Ù1x²Mùhƒ¦yÒ2³!"CD‹AÀ‹¦¹%D”0sBD ÆÌiaJ©T)e”RVk-¥RÉ–ËeY¹reF I6Æ8û7R"jîÃþsò“1 U"h$ÌÖS<ó3´ëãst€)¼™(@"J/±ä’ó„ˆâE$b¥TÂ̱R*)ª‘ã8Æqñ<ÏöööÚU«VÉ}÷Ýg53E[ˆHrO‚ˆ$cs·¯¾½»+?ÑÒ3¦Q™³ãb›§E‹$P€*&ß‹GѬ/ýM @r™ØB2¹‰RÊà«Õª¬_¿ÞÞ{ï½rçwB{ž'QYk-[k-eʈÍAˆˆP7”Ö : â¥92sHDaú<—Î0 Ç-rš0³‘ä …™Ek]·CCCrÇwØGydÁ1º` ¤i IÀZ+¹—¬ˆ˜¢¬U¤Ðw.‡˜™CfNòF”(¥,®2rà(ÈäfÇqÄu])•JÒÛÛ+«V­²wß}·ìر㲯+•Š„a(J)ÑZ›|:,Æ!"±ÖZI("Z\±ˆ€aæ¸ð¼Ö:õ’›'È“ ¨ÚH ä¡@D­(J™j‹ÓÂŒo°3Ö±t,:µu¦‚(ÚÂTm±CQ,VE@ ÈCÞF©$DB ¯îcûíÙsï=7¹pRÅ:Ý™Îæœ³{÷ÛïûÿýwO.]I¦]M˧ ¢¥ÓâÌgçi ´ã´Ã´/h­—âGÅ·lŸM»›ö#Úµ´DK¿ò"õvÚÚ¿hoÓjÿ×FѧÝF‹7îħ d^%‘6Hí/&ápéßú%:šs'š+%ê¿:Ï…ÀtÒÞ§½@Ûû]L{Žv'ÍÁAyÅÌ ÌPö¦»¼º]óï ”ðƒ”&êÚp”‚ÚT”ÂÇ×hïÒž »Ô´‡hÏBi=9Aå ™Ä‘ÆË8—Wg§ˆ‚œT943 Ù½="Åã’n§Ñ½ð¤lñúQ{Æ+ŽÖ·É²šsøâëqÞ$²Nò-‡¨¤ÖSªA m!íÔ·B{“6ƒ’(œ%1|&àö §O‚˜–Ÿ.Çå¥!Åã´Ó}T½Å;+šåúà ¢¦©ƒ@¼Àgk®–”œze-mŽ èPNªôYˆÞ9Àd²›ž‡~Éq˜yM®Ø;¤š.½^ Û„ºÜÚSykÔát+ƒUC°‰*=S£„ö³ :ùŨÁo¤]…Ü‘7=!áI×å¥ÉÛ 2ïv°±JÙjÀìˆu=»=®³„úèôñ^Y½ØAVàå¤o|N z¿z—›.âB”lè]!tÐ) W|¼œQ) sS¥þyË€Âu!MbZ†êÒ~n¼!ÅÁêsrmY=üô_(ß®¢Ç’S,Êaß¡Ý#Ià"ú©wÈÄ ¾‰ºE¨•U*1§"t³»t¢n…£“¶ãíxçP=ý›~ñþ"àÄ~õôŸ´»Ðűcýì#´%HËøñb)S0ý‡é"/=QÓ-ÌŸVÓ,¬ ˆÈL‹‹É&êž”QÏ+Úź7HÙÎIÿǮժù´¥0Ø ËáJÆO–ra" sRQ”›ªÇ'Ì("D¨¥9n]ƒ5Úˆ.‘GZêz¶ÍûÒòܘ]çý}”ÓA†\Ô— üýZ Y[Ö‰®Þ6hûSàú9è“äÆÔ+úA8ÌsàÁ€Dg‡vÅÓ+^·Sca¸÷ží´mŸà†CEciz Ûm8Òˆ&>a$ßõ7õš’÷ݱ¨ô`7ze ü|{Kã§É¾a,­Â˜3‘Ÿ‘$Ÿ™0ÀT‘0"I¬úýŽÉSíP‘‘è–¯Þ4(æûÖ{O•œ‡OµEØâÞØæÃ¶cÍí™ œ­SOÇÀL;¬VȦ=&Qxú¥Ä‰"®¬¦\L 1,3Q>7ñr[÷­¯õm>ã¯Ì$—|}ê`[Ol­Ÿ×·KÍ@HNûjΉÓ-ç%rIZÿ¢0•2Ë @Åür$õñà¡·˜{0¬Š!!€EB#˜*,¾q ­fTº6-µ,ö³êæ<Û6 6WâP}[XBdÀ¨7±ŸÏëèÌ**½rÐÖÄ †ÐjCÐþˆ1³¦<(Ìm8û*Ó¡AÍëJH…™IòÀŸc]¹<Ùî7šg%ºä»`ÇÀC›+Åz-!”ú¾"È‚ A7.ؽZÝý%mqÀÚDÌ} ȹ ½ÝÔ'Q¯]("˧ ²Í[ÖUàkí¸Œý}8Ýžy›Žc_]wTä?ÞÔŽ³*xTsa^q¿z}+m’–ÚIÕ!±w"~µVÀé”)qè›g2 ˜>Àüxxc²=SÖUÈZÓ‰³éÄo¶÷û6UнРéAíŠÆ¶óò”ŠFþ€Àó3$ÚϨMQ–0=z'®¼¸÷Æ 3Sã‘ï²0£¯£³’äÊH¨ø#h3$¹eÉ4{÷RBŸÖ…è(ÐÚé—õç::V>)ñÕ'ªÉ8à´˜xS¦y†\Ò™mzâœZ:†|DXNc²’°z²½„Æ­¯@M›ß^N¢ ;§ÙKh%´;$!•0èÁæõÐР*?XN½¡šÌU~Gû5fý(ºÅ˜õ4j6ÎíÐ 8B ¨:ĸÌd¹æFû0:Šë@µ)¡\JhOÖ™›«ÄÎúVi^3 ›ŸfÕŸb`߇«Õ°ñ{àOÊ0w1ç1Æ S=.¸­ÔB¦ëâ:úÀÚIö 7*"7Á%L±0cK•Øå! €€±«3|¹[b… œX®¼®ü.†\Ê(2‘EJ×3 }`¸X) N´;+  „Êz »[·Vá“°„¢  ã|`*ƒ£ÌN—= š¼0¿ ü È˜ißUB&7uÅö©Dþ–JYÕa—“/¸zÛ10½ô„Øb -!nx¤ßTŽì“xÙðfDB2KÍ«êtÀ£8»Ï\hýø\[W–TEø’ íLÛV-¶Š€ƒöªµ©û‡wI,{$,!å ñ³§±·‚Ã§Š¢¢ÿŽXqF>¾!×VC·U£Ê«%4>ud¼}›)Û«Qzª-Z>”Lø¥Ž@JB»>þú[ÕäÙP]Žé ¡·?ÀYwHsЂ)E„͆(æÆfÓu9¶ äí¨‘&Aòè¸l[&逸 š¦º0@¬}UbÝrÕd^d!>{±ÅÙ8•lR æõ:â½â~‰róØl[ƒw×ÊJ¯Î….'€ŠÑ—Ù¸qW­(9Ýn ÔÈ…(!é7ˆÈ KH|V¢šŒ‹¤ÉiIX”Èå ËȸZ%DIMè—€­c²må0hïרôL 9q|Ôe¶m&î®EÉ鎈TL ™3¯¯~ö9ÐÚL­éTB•ÌMÀSo -ÐršQÈaC]‹û&È­£úÛ20ð@¬:ÐNç”Ç 3m˜¸÷¤(mìˆHÅ:ë ¾_^&ðô=ê}EÁ¤ètúæ9÷>à48PFfÙ(«—U&€x–a`tÐtÀô…V¾ ðÑ›ÝÒi½¡éÕσ—6 †\L ñZÜ'%×dÙÊaàᘠ€  2?ݶ̈́ƒu(môFäbX0"¥v& ó§gNwÛШ¢·”óILM_á5ÁÁ†C§yäÖáö |Õ(«|&n2pE{ÊN‰Ò&¯9ë! # lXüyQÌ-¥*zSŸ‘-°„q6)É”M€ö °eXº-€AG›d¥/¨Ã¨Û! I³0éóQÒìö€  áõQ®S µÜÔ«¢Ž,îÄì‡{æ ²  .jFíM=ç†'j…ŽÚøëÑ}êõK¯9Vïö¨–MŠzSN^q?k9/ÎtrÔzñаr‰Äª—UkpcUôÁ–Ë‚ùÞÐ|^û€Û¥Vç~#á m?£NCÛÄn‰ºi>ÀœÇçF|àÈaà±™ ¡>u6zу-UôÑbnžÀâw$z§ €d–j]è"¡¶+ˉ]ødβˊÎu"!Ò\};è;>ðóùÙf:î]Õ=:ZT%r¸[4žÙÆ F%ÁD.H.§u‹Á„ê1tµRÞ¤£[®ceÀÏoR  bÀË`³p.Õ¾MuÒãÃ]U"ÇënžZ"è‘.—CôgZ`H)œn˜‘ª' úGxæ»ä<';ü:mn÷ <3_¢äCà¿<^•ÈŽÑÅÏ2JKq”Ò€\k’gM¹#[Ðnd@À”yíà Oxýú»Y3e³‰å§¥ßø‡„þÄ4ëÆ¸éÉ/0ÒìlÊ)'Þe,ÒÝe9;qøŒ§KªÌkM§µ”J›q¸ x’›•ÊrÕò[}b²ÊIäsÇ Ì}Tb.ý<%YE&‘Ë<'‡+m ]¨|ÉÚô¬‡$¤2Lj¾Å/E ÷¹ÕÁˆ#ÑÒ ¬X*°â% Ÿú°i>ò…JôgÖlnNî ˜9[ 9ÉÈ—’ÝN‘Ny¥QZ)ôµ5Õ Ù¡ÆH¹4s° ”K«/ µßÚ&°f•Äk/rž«¿“Ϭ֢։ç¡"”•žÉWôÛïFn§¹nX¢`I‹ƒ:º¨cÿ>à½w[˜Á4Ôßñ‡î®¥û¿ôí'p퉂¦YCúgI#üª#™vf˜'ë˜.– ”•IìÙ 4žþ^þÕ kù¿ýgXå{ùw›ÿ‹OÉ`8ÿNÔIEND®B`‚dkopp-6.5/icons/editjob.png0000644000175000017500000000501712343020444014401 0ustar micomico‰PNG  IHDR00Wù‡sBIT|dˆtEXtSoftwarewww.inkscape.org›î< ¡IDAThÕš}påÇ¿ÏîÞîÞëîåòÂ%10y•bí8UgŠˆJ‡©­Su¤¥3N‹Š0tpŠÕú2­m¥1­(R’–RhDg,ˆ ØHHä\LîÈq·÷¾w»÷ëIÎ\ H ão晹}™çù|žý=/{³ŒˆðMîr|ÝF»AƘà65¢(*étú(€íDtü¢ê­bŒ¹%Iz*›Í.˜4iRvúôéRIq1ßvèPzûöíL„ñxü1"úxDQÁ €ÉV«µë–[nIøá‡ )S4¥D"AÝÝÝ´jÕª¬,ËiŽãŽ¤î‚§cìFQßlhhî[¸ñ‚žçóJII fΜÉžx«ucŒÑ…¦F{¾V’¤hSSùý~ ööR8Êõ¼®ëdmݺ•œN'Mž<ù3Q'¨Â;l6Û‰+Vdý~?õööR(¢H$BñxœôTŠ Ã Í›7“Óé¤)S¦´Ž¾ ‚ ¬¸áúëÓþžê=}:>•JQ&“¡M›6‘Óé¤iS§^|ÁH²,ko½õVü™3gÁïØ±£~Ú´‹†'¢‚-d7”––J3fÌÇóàx<Çå Üžž¸Ýîh[[Ûݺ®·~U…Œ±1ç:_(+'Mš$ æxÇã8jjj‹Å„ „o±X>ãyþ‘¡× %Pìp8ØÐé’çy€iš¨­­…¦i2cÌõðu¢(î¹ýöÛ=<Ï?ë /Ž?NCÓfÞ4M¸\.X­V¸ê<ðõ¢(î^´h‘Úðè£Ü¬Y³xI’~3o8p€Ó"pýŒ±ü@©®®€©ÃÀOEq÷’Å‹•ù÷ÜÙ¦‰ °l6ûƘXP":©(Ê[¶lɃÏf³È?~<“eùY·ÛýÕj]Ã[Âû>cì&I’þ»lÙ2åλîâî¯ôz!Š"`ZN´Ú»äÁóüã×]wÝc»víbC{Þ4M˜†-ÁÑ£Gs¥££C?|øpÆçóY–-]*Λ7™¦ £ÿ~Ó4ñ£;îÈúýþ»‰¨¹ Œ±)ªª~rúôin8ÁÇÆ0ç_72Ü?äÚÀõÝ»wC’$À±F "Às9Ž÷æÎkH’³?÷Ï›*2äþÍͰÛíGÚº¤Ûi·K˜Ç!»Ân·;fÏž=~ݺu€< aE†{ý¿[ZZÐÙÙI<Ï¿@D±6/Ùp»„Œ+GÓ¯ç]SQb›ØØØ(æàÏtδ"‘ퟹöíÛ‡×_cÆŒÙÝÛÛû¯Áí^’Aìq wŒË^zù‘2ìP±÷‡7v9°ný&ˆ¢øå=Ï@noÛ¶ /56¢¶¶¶³½½ý‡étú£K*P¤wÕej|¸Â#;𲋣ûrx©¥¯üe$I‚išè ±|ùrØívÔÖÖâš™3Q__¶ˆa  áåµk±gÏÔÕÕùÚÛÛ—ƒÁæ¡í-bE˜?¡’=¿zÉØ"Ù®B°öÁKªoÁ®÷?ÅêœÁšW6 ‘HàÇwÞ ·ª¢ÈãADÓp°­ ‡sæÌA]]— ]]]Ø·?v¾óƇÝ~ õàÁ‡£Ñè{çb¸h1.aÁ„+¸?¬^<Ö-æàÇ@V½cÐÃ>¤£~ìý´/þ‡GOPGyyy6™L6uwwï,--½Êf³]ët8®…Ã-F<‘€¢(PUªªúOž<¹³µµõ "jŽã¢JTáÞÉWð¿ûãC^U²÷¥è,…¤zAdBû‰`¤4˜I ûѰ.gQusGGÇ}DƘÀ„ &Üf·Ù®¶ˆ¢;£ëZ"•úüðáÃÍ™Læçcñ4Zªò §Õ O?ÿP¥*ZðV’« ’êE6£÷õ|ü4̤–(RDpL×;::Â,uQÀÇýŒ1‰ˆô‘ðŒH ÌÍ?8}¼å‰ç¬TD«Áª@t•CR½0õ8ôp'2ñ Ì”£_àT¯€û_Œ%}ôz'¤‡«¤ð#(só‹g\iYõûªÁ¦@HJ$Õ #©õ¥M<˜ëu#¥áDPÄ/Æ’¾€¾ÀkˆH)äùâ‚Æ@¹‡_:óJñ±gï¯rYÒF I­D&Þ =Ü #Êõº™Ôp,(ãÁÕѤ/Zàó~øè¥„.à Œ-â®(¯xza•K°ºÀË dÕ I­D:ÚTØ#Î¥™Òp¤×ЇþMú©7°vðò?jeÅüÊïN¶.òçUN¾?ç%Õ Y­D*Ô =샑 ÷'#0õ>ذ¸1’è ¤6ø€&"J8O •{øU7Nµ=²êÞ*/+°ØTHj%Dgôp'Ra_^¯›é8:zlX²&’è $78П*ü°Eüo¿7ÃöË•?«vV‚­’ê…ÅîöA׺rÕHjÈfRhë–±ôåH¼+Üà#¯^̬òµÊ‹ùgnþ–}ÑŠŸVÛYÅî¤z!X•~ø/òf2Òhí’±|­6¿ÀkD4ìtY0 ÿÜ­×:~±|~µ—XìÅT/xÉÞ·@iݹ^7S(kàÓS2š´xW ±ÀÖQf4àAƒ¸¼ˆ_6÷;Î…ËæWÛx«Ñ^Éí'HÐC§ GzòVW¢,>9eůšÂð»üí«–þ‚ ¤2pM­wÛ« ‹s`SÆ#uæÒQÞê ûOÈXù×p¬þ=ÍDdŽ&<0è,ËîÝùq¢« ²» C*t éhŒd8Wö“±òÕH¬+h°ÀÆËä¯ÇÛN¤ »«53}ûšØiÉH.m/`÷¿¦Åºü±foø'ê¿™ ˆÁ‡"ñ,>ægc]Qdb½yià >è°àÉõ‘ø­þ}9á!³¢({<.úvcÃxT¥sœÅŠ÷Û-xú -êóG[l&¢m—ûËÈEqfEEÅ»Éh·Ãã`¸ªŠ0k†Ñ´ /lŽD}=‘¿h!¢·/r~œµÕÔÔ,¨©©yÊb±”†D,D¾Îãg|Ýá-6ÑÎ˃zî8çV‚16±¾¾þÖD"Q(•JéÞ%¢÷Gñü1jŸ*¾ñ_«üÈ|DÏ€„¨IEND®B`‚dkopp-6.5/data/0000775000175000017500000000000012343020444012050 5ustar micomicodkopp-6.5/data/userguide.html0000644000175000017500000016334412343020444014743 0ustar micomico userguide-en

Dkopp user guide

introduction
concepts
first tryout  (1-page primer)
file menu
backup menu
verify menu
report menu
restore menu
DVD/BD menu
help menu
toolbar buttons
editing backup jobs
script files
large backup jobs
technical notes



Dkopp Introduction
Dkopp is a Linux utility program for copying files to recordable DVD or Blue-ray disk (BD). Dkopp is a free open source program licensed under the GNU General Public License v.3.

Three kinds of backup are available: full, incremental, and accumulate. A full backup copies all specified files and leaves no other files on the DVD/BD. An incremental backup adds only new or modified files to a prior dkopp copy, bringing it up to date. This is normally much faster than a full backup. Unmatched files on the DVD/BD are also deleted, so that the DVD/BD is left exacty matching the source files. An accumulate backup is like an incremental backup, but unmatched files are not deleted.

You select files to be copied using a GUI. You can navigate through the file system and select files or directories to include or exclude at any level in the hierarchy. These choices can be saved in a job file to automate recurring backups. If files are added or deleted within an included or excluded directory, the next dkopp run will include these changes automatically.

DVD/BDs can be verified three ways: full, incremental, and thorough. A full verify reads the entire DVD/BD and reports any files having read errors. An incremental verify reads only those files that have been newly written by a preceding incremental backup. This is usually much faster while still offering a high level of security. A thorough verify reads every file on the DVD/BD and makes a bytewise comparison with the corresponding disk files. This is normally unnecessary, but it provides an additional assurance that hardware and software are working correctly.

You can list all files in a backup job, or all files on a DVD/BD. You can search for specific files using wildcards. You can compare a DVD/BD with the corresponding backup job, listing all differences: files that have been created, deleted, or modified since the DVD/BD copy was made. This comparison can be done at three levels: a detailed list of files, a directory level summary, or a job level summary.

For disaster recovery or file transfer, dkopp has a file restore capability. You can select and restore DVD/BD files to their original directories or anywhere else.

An incremental backup updates a DVD/BD made with a prior full backup. This simplifies both backup and restore: you do not need to track full and incremental backup media, and you do not need to restore files from multiple media in correct sequence.

Searchable log files are generated with time/date, DVD/BD label, and files copied. You can search the log to find all DVD/BDs with copies of a desired file, using wildcards to simplify the search (e.g. find  */joeblow/*/planB* ).

A script file can be used to automate backups or run dkopp from a shell script. The GUI is not used in this case.

Incremental backup and verify can take less than a minute if the updated files are within 30 megabytes or so. For larger jobs, the DVD/BD speed determines the time required. With 4x DVD media, backup + verify runs about 150 megabytes per minute.

Dkopp is a graphical front end for the command-line programs growisofs and genisoimage. The added functionality is ease of use, espicially the easy way to specify the files to be copied.



Dkopp Concepts
The files in a backup job are specified with include and exclude statements. These have filespecs with optional wildcards placed almost anywhere. Examples:
   include /home/*                  # add user files
   include /root/*                  # add root files
   include /shared/*/documents/*    # add shared document files
   exclude */mp3/*                  # exclude files in mp3 directories
   exclude */.Trash/*               # exclude trash files

The first include adds all files owned by users in their home directories and sub-directories. The second include adds all files owned by root. The third include adds all files under the /shared  top directory that also have an intermediate directory named /documents. The two exclude statements exclude files within  /.Trash  and  /mp3  directories at any level.

GUI interface: The above statements are normally generated using a standard Gnome file selection dialog. The process is documented in the section on editing backup jobs.

file selection logic:
   loop:
       get next control statement. If no more, done.
       if include: add all matching files to backup file set.
       if exclude: remove all matching files from backup file set.
   loop-end

Note that excludes are effective only against prior includes. They have no effect on following includes, which are processed afterwards. See the section on editing backup jobs.

Restriction: include statements must include at least the first directory name (top-level) without wildcards (the GUI file-chooser does this automatically).

limitations:
    + max. 200,000 files in a backup job (compile time constant)
    + dkopp runs as root user (password is requested if not already root user)
    + supports DVD/BD media only (not CD media)
    + not useful for disk imaging (operating system backup)

After installing dkopp, please perform the first tryout exercise (below). This may be all you need at first (if you are like most people and RTFM is a bore). You can enhance your security and ultimately save time if you read this whole document.

License and Warranty
Dkopp is a free program licensed under the GNU General Public License, Version 3 (from the Free Software Foundation). Dkopp is not warranted for any purpose whatsoever, but if you find a bug, I will try to fix it.

Origin and Contact
Dkopp originates from the author's web site at
   
http://kornelix.com/dkopp  

Other web sites may offer it for download. Modifications could have been made.
If you have questions, suggestions or a bug to report, see:
   
http://kornelix.com/contact



Dkopp first tryout
The following short exercise will check that dkopp functions correctly on your system and help you become familiar with dkopp usage.

  1. Load a recordable DVD/BD and wait for the desktop icon to show up.

  2. Start dkopp (use the system menu or a terminal command: $ dkopp).
    Start as root user ($ sudo dkopp). If not, dkopp will request root user by itself.

  3. Select button: [ edit job ]

  4. Set the DVD/BD device ID (choose from list of available devices if more than one)

  5. Select full backup and full verify

  6. Erase the default backup job shown (select and delete, or use the  [ clear ]  button)

  7. Select the button  [ file chooser ]  at the bottom

  8. Navigate through the directories and select some files or directories to be copied
    + double-click a directory to open it and enable selection within that directory
    + select one or more files/directories, using left-mouse (or shift+left-mouse)
    + use the  [ include ]  button to include all selected items in the backup job
    + use the  [ exclude ]  button to exclude items previously included at a higher level
    + use the  [ include ]  button to include items previously excluded at a higher level
    + use the buttons at the top to go back up the directory hierarchy
    + use the  [ hidden ]  button to toggle the display of hidden files

  9. Select the  [ done ]  button when finished selecting files

  10. Inspect the generated include and exclude statements. These may be edited directly if desired (e.g. erase mistakes or redundancies, change the order, or make additions or revisions). Re-enter the file chooser dialog if wanted - new choices will be appended. Cycle between the editor and file chooser as much as needed

  11. Select button  [ OK ]  when done editing the job

  12. If errors are shown, select  [ edit job ]  and fix. Remember that exclude statements must follow relevant include statements - excludes are exceptions to prior includes, and includes may be exceptions to prior excludes.

  13. Select menu: Report > get backup files. Inspect the counts. Be sure the total byte count is within the DVD/BD capacity. Look for zero counts, indicating possible errors. Re-edit the job if needed.

  14. Select button: [ run job ]. The backup should begin.

  15. Verification should follow automatically. You will be asked to re-mount the DVD/BD.
    Check that the error count is zero when finished.

  16. Save the job file if desired: menu: File > save job

  17. Select button: [ quit ]  

  18. Next steps: play with incremental backups and reports




File Menu

open job

Open a previously saved backup job file for re-use (edit, run).
The default location for job files is /home/user/.dkopp (or /root/.dkopp).

open DVD/BD
Open the backup job file on the currently loaded DVD/BD.
This file was saved on the DVD/BD when the last backup job was run on that DVD/BD.

edit job
Opens an edit dialog for the current backup job (from the last job file opened, or from a prior edit).
If no file has been opened, internal default data will be used as a starting point.

show job
List the current backup job data and diagnose any errors.

save job
Save the current backup specifications in a job file.
Default is the same file that was last opened, but you may select any file.

run job
The current backup job is executed. Backup and verify modes are taken from the job.
Be sure to read the technical notes about DVD/BD mounting.

run DVD/BD
The backup job file stored on the DVD/BD is executed. Backup and verify modes are taken from the DVD/BD job. Whenever a backup is performed, the current job file (including any edits that were made) is copied to the DVD/BD.

Note: what is copied to the DVD/BD is the current job, not menu commands given manually. Thus, if you load a job file which specifies incremental backup, and then do a full backup using the menu command, the backup job stored on the DVD/BD will still specify incremental. To change the job written to the DVD/BD, edit the job before starting the backup.

quit
Exit program.



Backup Menu

full
The current backup file set is copied to the DVD/BD fully. All files are copied unconditionally. The DVD/BD is initialized to an empty status. For large jobs, additional DVD/BDs will be requested as needed. If growisofs aborts the job (declaring the DVD/BD to be "unknown type" or "not formatted"), the menu command  DVD/BD > format  may fix the problem. Be sure to read the technical notes about DVD/BD mounting.

Incremental
The current backup file set is copied to DVD/BD incrementally. New and modified files (since the DVD/BD was created or updated) are copied. Files that already match their corresponding disk files are not copied. Any "extra" DVD/BD files (not in the backup file set) are deleted. At the end, the DVD/BD is 100% identical to the backup file set, with the possible exception of files modified during the backup run. See the technical notes for more details about how matching files are recognized and skipped over.

accumulate
Same as incremental, but without DVD/BD file deletions.



Verify menu

full
All files on the DVD/BD are read and checked for errors. DVD/BDs need this extra level of protection, since poor media quality has been a problem. If errors are detected, clean off the fingerprints or discard the DVD/BD. If errors happen on more than 1% of your media, consider getting a new drive or changing media brands. Note that any CD or DVD or BD can be "full" verified - it does not have to be a dkopp backup disk.

incremental
New files on the DVD/BD are read and checked for errors. "New" means any files written by an immediately prior incremental or accumulate backup. Files not written are not checked.

thorough
All DVD/BD files are read and verified that there are no read errors. Those DVD/BD files that have a matching disk file (matching full path name and modification date/time) are bytewise compared to the disk file, and any files not matching are reported. There should be no differences. This verifies that all hardware and software (driver, file system, dkopp) are working correctly. DVD/BD files that are expected to be different (different mod times) are read and checked for errors, but not compared with the disk. Dkopp considers two files to have the same mod times if they differ by less than one second (the time resolution for files on DVD/BD media).

The following counts are reported at the end of the verify job:
     + total DVD/BD files and bytes
     + DVD/BD files having read errors (should be zero)
     + DVD/BD files having matching disk files (by name)
     + DVD/BD files having matching disk files (by name and mod time)
     + for the last category, the number of DVD/BD:disk compare errors (should be zero)



Report menu

get files for backup
The backup job include and exclude statements are listed, along with the file and byte counts that are added or removed by each statement. Look for zero counts, indicating a possible error. The disk directories are read and the list of files included in the backup job is saved in memory. This data is used to determine what files are different between the disk and DVD/BD and must be copied for an incremental backup. The file list is static and is not updated by disk activity. The list of "new" files that are checked with an incremental verify is also reset with this command.

diffs summary
Report the total number of files in each category:
   new             on disk, but not on the DVD/BD
   modified        on both, but not the same content
   deleted         on the DVD/BD, but not on disk
   unchanged       on both, with the same content

Differences between the disk and DVD/BD may be caused by disk updates (file additions, deletions, updates, or moves), or by changes to the job file itself.

diffs by directory
Each directory having differences between the disk and DVD/BD is reported, along with counts of new, modified, and deleted files. The total bytes for new and modified files is also given.

diffs by file
All files that are different between the disk and DVD/BD are listed in alphabetic sequence within groups for new, modified, and deleted files.

list files for backup
All files in the backup file set are listed in alphabetic sequence. Use this to check that the correct files are being backed-up.

list DVD/BD files
All files on the DVD/BD are listed in alphabetic sequence.

find files
Enter a search pattern with optional wildcards (e.g. /home/dir*name/file*name).  All matching file names on the disk (in the backup job file set) are listed. All matching file names on the DVD/BD are listed. All backup log files are also searched, and those containing the target file(s) are listed (by date / time and DVD/BD label). These files correspond to backup jobs, one-to-one. Use this method to locate all backup copies of a given file or group of files, sorted from oldest to newest. A file may be present in multiple log files for multiple incremental backups made to the same baseline full backup, but it actually exists only once on the DVD/BD, in its latest version.

view backup hist
All backup history log files are listed (up to 200). These correspond to backup jobs, one-to-one, and contain a list of files copied to the corresponding DVD/BD. The most recent 20 log files are put into a dialog for selection. Select one of these from the dropdown list, or modify the input to select an older file. The text editor gedit is invoked to display the log file. You can page up and down and search for strings using gedit.

Backup log file names are formatted as follows:  dkopp-hist-yyyymmdd-hhmm-label
Note that one DVD/BD having a full backup and one or more incremental backups will have a log file for each backup, showing those files copied for each backup. A file may be present in multiple log files for multiple incremental backups made to the same baseline full backup, but it actually exists only once on the DVD/BD, in its latest version.

save screen
The main window, where messages and reports are written, is saved in an ordinary text file.



Restore menu

setup DVD/BD restore
Specify the copy-from location (on the DVD/BD), the copy-to location (on disk), and the files to be restored. The copy-from location is the topmost DVD/BD directory of a tree of files to be restored.
  example:  /home/joeblow/documents    # note that mount point is omitted
The copy-to location is an existing disk directory where the tree of files will be copied-to.
  example 1:  /home/joeblow/documents
  example 2:  /home/joeblow/documents/restored

In example 1, the restored files will go back to the same place they were when backed-up. In example 2, they will go to a new place.

Files to be restored are specified the same way as in a backup job (see the section below on using the file selection dialog). Use the button  [ file chooser ]  to start the dialog.

If you need to restore multiple trees of files, you can do this in multiple runs, or you can simply begin the tree at a higher level and use the file selection dialog to specify multiple sub-trees.

list restore files
After performing the setup, use this function to list all matching files on the DVD/BD that will be restored, exactly where they will be restored. You should check this list carefully to be sure you are restoring the correct files to the intended locations.

restore files
When you are satisfied with the restore job specification, use this menu to perform the restore. You will see a running log of the activity. Use the kill button to stop the job if desired.



DVD/BD menu

set DVD/BD device
The DVD/BD device and mount point may be set independently of the backup job. The DVD/BD device and mount point for the current backup job is modified. No mounting is done.

set DVD/BD label
Set the DVD/BD label that will be used for a subsequent backup job. The default is to keep the same label that the DVD/BD already has. The DVD/BD mount command will show this label. If no label is assigned, "dkopp" is used.

mount DVD/BD
Mount a DVD/BD. You are asked to insert a DVD/BD and wait for the mount to complete. If the mounted DVD/BD has been used for dkopp before, the date-time of the last backup to this DVD/BD is displayed. Be sure to read the technical notes about DVD/BD mounting.

eject DVD/BD
The DVD/BD is unmounted and ejected.

reset DVD/BD
This does a hardware reset to the DVD/BD drive. This is sometimes useful if a drive gets locked-up and cannot be ejected using either the dkopp eject command or the tray button. This sometimes happens when there is a DVD/BD error or a backup is killed in mid-process. This may or may not work. If the DVD/BD drive remains hung after several minutes, the only resort is a reboot.

erase DVD/BD
Writes zeros to the entire DVD/BD surface. This takes 10+ minutes, depending on the DVD/BD drive speed and medium. This works only for rewritable media (DVD+RW or DVD-RW or BD-RE). DVD-R and BD-R media are write-once and cannot be erased. See the technical notes below for more about privacy and data protection.

format DVD/BD
This uses the dvd+rw-format utility to format a disk in a few minutes. The entire DVD/BD is not erased. See the technical notes below for more about privacy and data protection. If Backup > full refuses to start, this format command may fix the problem.



Help menu

contents
Display the help file (this file).

about
Display the dkopp program version and date.



Toolbar buttons

root
This button restarts dkopp with root privileges if your password (sudo) is correct.

edit job

Shortcut to the backup job editor (same as menu File > edit job)

run job  and  run DVD/BD
The current job, or the job on the DVD/BD, is executed.
Be sure to read the technical notes about DVD/BD mounting.


pause  and  resume
The currently running job or menu function may be paused and resumed.
Use this to inspect output on the fly.

kill job
The currently running function is killed. You may need to wait a while for the function to die and screen output to cease. If a backup job is killed, growisofs will gracefully exit in a few seconds, leaving the DVD/BD in an undetermined status.

clear
The main window, where messages and reports are written, is cleared.

quit
Exit the application. If the job file has been edited and not saved, you will be given an opportunity to save the changes.




Editing backup jobs (see screenshot below)

Select menu: File > edit job  or button: edit job

Fill-in the following items in the dialog box:

  DVD/BD device    /dev/sr0
  capacity GB      4.0
  write speed      4  (x 1.38 MB/sec)
  backup mode      check full / incremental / accumulate
  verify mode      check full / incremental / thorough
  file date from   leave default (1970.01.01) or input a later date


Select the DVD/BD device from the drop-down list of available devices. The DVD/BD capacity may be set from 1.0 to 50 GB (for double layer Blue-ray media). Full backups will be limited to this amount. The default of 4.0 GB leaves a leftover space of about 0.7 GB for incremental updates. See the technical notes for more details about this. Write speed is the speed factor to use for writing the DVD/BD medium. This is the "2x" or "4x" rating shown on the DVD/BD. Leave this blank or zero to use the default speed. You may select a lower speed than the default if needed to increase reliability (i.e. if the verify function reports errors). I have never needed this, but others have reported that some media and drive combinations are not reliable at the rated speed. "file date from" is an additional method for selecting files to copy. Files with a create or modification date older than this will be ignored.

File selection dialog
You may edit the backup file set (the include and exclude statements) directly in the text window. You may also use the [browse] button to get a standard file selection dialog, with additional buttons: hidden, include, exclude. The [hidden] button toggles the display of hidden files (file names with leading dots, like .gnome). Select one or more directories or files, using left-mouse or shift+left-mouse, then press the [include] or [exclude] button. The selected files/directories will be written into the text window as include or exclude statements. If you select a directory, the entry is modified to add a wildcard at the next level:
       selecting directory  /aaa/bbb/ccc   →   include /aaa/bbb/ccc/*

You may alternate between editing the text window and using the file-chooser dialog. When you are done, press [done] to accept. The include/exclude data will be validated to the extent possible. Go back and re-edit to fix any problems. To change the sequence, cut and paste in the text window. When you are done, use the report functions "get backup files" and "list backup files" to  verify that you have the correct files!

The include and exclude control statements allow precise control of the backup file set:
  include /aaa/bbb/*             # include file tree under /aaa/bbb/
  exclude /aaa/bbb/ccc/*         # exception: exclude /ccc/ subtree
  include /aaa/bbb/ccc/xxx.yyy   # exception: include file /ccc/xxx.yyy

The file-chooser dialog may be used to quickly converge on the desired results. The editor may also be used to make adjustments.

Because of wildcards, newly added files within the scope of existing include or exclude filespecs are automatically comprehended. In the above example, if a new file is added somewhere within the /aaa/bbb/ tree, it will be automatically included in the next backup job, unless of course it is in the excluded  /aaa/bbb/ccc/  subtree.

The  file > edit job  menu command (or toolbar button) pops up the middle box. This can be edited directly: click anywhere in the text area and start writing. The right box is the choose files dialog, which is started with the [browse] button. Choose files using the right box, and the middle box records your choices. You can navigate around the directory hierarchy and select any number of files or directories. The [hidden] button toggles the display of hidden files. Click the [include] or [exclude] button to get the selected files added to or removed from the backup list. Selecting a directory is an implied selection of all contained files, thus the selection appears as  directory/*  in the list of selected files. To make an exception, go down one level, choose files, and select the opposite [include] or [exclude] button. You can refine the file selections manually if desired. It is sometimes handy to use wildcards in the directories to make more general and compact selection criteria, e.g. 
   exclude *thunderbird*/Trash* 
will omit trashed mail even if the overlying directories change (they do) and even for multiple users (the leading wildcard includes "/home/*").


You can add comments, or disable an include / exclude line, by putting  #  in column 1.



Script Files

A script is a text file with a series of commands that can be run as a batch job. All dkopp menu commands can be scripted.


The format of the records in the script file is as follows:
   menu1 > menu2 > parameter    # comment
The menu names must match the interactive menu names exactly, including case.

To run a script file:   $ dkopp -script /pathname/scriptfile

You can also add the option  -nogui  for deferred execution. Dkopp will not create a window or ask for any inputs in this mode. You must leave a DVD/BD in the drive for dkopp to use later.

Here is a sample script file to get you familiar with the possibilities:
   File > open job > jobfile1
   Report > get backup files
   DVD/BD > mount DVD/BD
   Report > differences-detail        # report changed files
   Backup > incremental               # back them up
   Verify > full                      # verify all files
   Report > differences-counts        # should be zero
   File > save screen > dkopp.log     # save a log file
   DVD/BD > eject DVD/BD
   File > quit

The toolbar buttons may also be used, e.g.  
     button > pause    # press resume to continue
At this point you may use the menu interactively and then resume the script by pressing the resume button.

The command  exit  may be used to end the script file and return to interactive mode.
Script file EOF does the same thing.



Large Backup Jobs (more than one DVD/BD)

A full backup may be larger than one DVD/BD, and you will be asked to load additional DVD/BDs as needed. If a job is being run (rather than the menu backup > full) each DVD/BD will be verified (if specified in the job) before the next DVD/BD is requested. An attempt is made to fit all files derived from a single include  statement on the same DVD/BD, if possible. This allocation is made after excluded files have been removed from the backup file set. The job file written to each DVD/BD reflects the files actually copied to that DVD/BD, so that subsequent incremental updates may be done individually on each DVD/BD, e.g. as follows:

     File > open DVD/BD
     Backup > incremental
     Verify > incremental
Note that if an include statement is too big to fit on one DVD/BD, this strategy will not work as expected. The backup job file on the first DVD/BD will have the big include statement, but additional DVD/BDs used for this same include will not. If possible, break up the large include statement into smaller ones.



Technical Notes

Mounting DVD/BD media: Starting with v.6.4, dkopp no longer mounts DVD/BD disc itself, but outputs a popup message asking the user to do this. Insert the disc, wait for the desktop icon to show up, and then press the [OK] button in the popup dialog. The appearance of the desktop icon may or may not mean that the disc is actually mounted (this varies in the systems I test with). If dkopp responds with "waiting for mount ...", click the desktop icon to encourage the window manager to mount the disc. This change was made to relieve dkopp of the need to understand what Gnome and other window managers do about DVD/BD disc mounting.

DVD mount errors and growisofs errors: There are many of these, and they change with each new release of the kernel, Gnome, and growisofs (the command-line program used to write the DVD/BD). Often they are bogus errors that are not really errors, or they are temporary errors that will go away when the operation is tried again. Dkopp is unable to distinguish the difference. In the case of a full backup, the user is given a popup dialog with the options to abort the job, retry the full backup, or ignore the error and continue. If the backup job did not even get started, use retry. If the backup job seems to have completed, use ignore. Verify will detect later if any error actually happened.

Blank DVD/BDs: the first time a DVD/BD is used, do a full backup. A blank DVD/BD will not mount, but a full backup will still work and make the DVD/BD mountable thereafter.

Flakey DVD/BDs: drive and media combinations sometimes have compatibility problems, resulting in media errors. The newest drives (2006 and later) are much better at adjusting to media variations. I have had a few DVDs (<1%) that passed a dkopp verify and then became unreadable later, so make regular backups and avoid depending on a single DVD/BD.

Media errors: If the dkopp verify function runs into a read error, the DVD/BD drive may lock-up for a minute or more while retrying the failed read hundreds of times. Give the "kill" command and wait for the drive to give up. If the DVD/BD is dirty, clean it and try again. Otherwise throw it out.

Privacy and data protection: to protect your private data on discarded DVD/BDs, you should destroy them. A few seconds in a microwave oven will completely destroy the metallic recording layer (with spectacular visual effects). Do not inhale the fumes.

Command line arguments:
  $ dkopp -job jobfile                    # load job file
  $ dkopp  jobfile                        # load job file
  $ dkopp -run jobfile                    # load job file and run it
  $ dkopp -script scriptfile              # run script file
  $ dkopp -nogui -run jobfile             # run job in non-GUI mode

 
The -run and -script commands are intended for shell scripts. The -job command is more useful for a desktop launcher, leaving the user free to elect the backup mode or make other changes in the job before execution. If the jobfile name contains blanks, quotes are required, e.g.
   
$ dkopp -job "my dkopp job"

The -nogui option will prevent dkopp from opening a window or asking for any interactive inputs. Use this for deferred operation (batch job). You must leave a DVD/BD in the drive for dkopp/growisofs to use.

Deleted DVD/BD files: growisofs is used to perform the file copies. It can replace existing DVD/BD files with new versions, but it does not delete files. For incremental backups, dkopp replaces deleted files with null files (zero length). Full backups do not have this issue, since the DVD/BD is initialized. If you recover files from a dkopp DVD/BD using a shell copy command with wildcards, or Nautilus drag-and-drop of an entire directory, you may get unwanted null files. If this happens, it is easy to get rid of them like this:
     
$ rm -i $(find /dir1/.../dirN -empty)
(remove all empty files in a directory tree, with confirmation of each)
Note that if you use dkopp restore, these null files are invisible and are not restored.

Incremental backups: a DVD/BD file is considered identical to its corresponding disk file if their lengths and modification times are the same. Incremental backups exclude such files. If the modification times differ by less than 1 second they are considered equal (another way to look at this issue: file backup times may be wrong by up to 1 second). A thorough verify will read and compare the files unconditionally. File mod times on DVD/BD media have a 1 second resolution.

File names containing "=":  genisoimage requires that "=" in file names be replaced with "\=". The file-chooser dialog in dkopp file restore shows "\=" instead of "=", but the files will be correctly restored with "=" only.

Restoring file owner and permissions: The file system used for DVD/BD media does not support file owner and permissions, so dkopp writes a special file to the DVD/BD with this data. Restored files have original owner and permissions.

Special dkopp files on DVD/BD:
Directory
/dkopp-data is written to the DVD/BD with three files:
     datetime        backup date-time and DVD/BD usage count
     filepoop         owner and permissions for all backed-up files and directories
     jobfile            a copy of the backup job specs last used on this DVD/BD
These are ordinary text files which you can view with an editor.

Special file types: pipes, devices, and sockets are not copied. Symlinks are copied as such. Both symlinks and their targets should be included in the backup or restore file set, since it makes no sense to copy one without the other. This normally happens by default, since symlinks typically link to files in the same directory. Symlinks are used commonly in system directories and in the hidden system files within a /home/user directory. For user files, there is no need for them.

Killing growisofs (killing a backup job in progress): this will sometimes leave the DVD/BD in a condition that growisofs refuses to deal with. If you decide to abort a backup job (e.g. to revise the job specs and start over), you may get this condition. You should retry a full backup on this DVD/BD. If growisofs still refuses, format the DVD/BD (dkopp menu), then try the full backup job again.

Duplicate files: If job file "include" statements overlap, resulting in duplicate files in the backup set, this is reported and the backup is terminated.

Microsoft Windows: DVD/BDs created with dkopp use the standard ISO-9660 file system, which can be read by Windows.

DVD/BD drive and media information: here are two useful commands:
  $ udevinfo -q all -n /dev/dvd      # DVD/BD drive information
 
$ dvd+rw-mediainfo /dev/dvd        # DVD/BD media information

Incremental backups: new and updated files are written to a new "session" on the DVD/BD, along with new directory files which may reference data files in both the old and new sessions. Nothing is changed in the old sessions. Thus, incremental backups consume more space on the DVD/BD even if the corresponding disk files are not any bigger. For DVD+R, DVD-R and BD-R media (write once), only one full backup may be made, and as many incremental backups as can fit in the remaining space. For DVD+RW, DVD-RW and BD-RE media (rewritable), a new full backup will initialize the DVD/BD and recover all space. These DVD/BDs can be used until they wear out. I have exceeded 100 uses on a test DVD+RW medium and it still works fine.

Growisofs progress tracking: Growisofs (genisoimage) outputs a "% done" value every few megabytes. Dkopp uses this number to compute the current position in the list of files to be copied, and the resulting file is echoed to the main window. The update frequency is typically less than once per file, so some file names will be bypassed. Large files may stay on the screen for several update cycles. For full backups, the math is straightforward. For incremental backups, growisofs starts off with:
       % done = 100 * (initial DVD/BD bytes used) / (final DVD/BD bytes used)
Dkopp assigns this value to the first file in the backup list. The last file is assigned 100%, and the rest are interpolated using accumulated bytes.

Linux error codes: Linux error codes can be misleading. If an attempt is made to open a file that is already open and therefore locked, the error code translates to "no such file or directory". The error codes are the same for an attempt to mount an empty tray or a corrupted DVD/BD. The same is true for an attempt to mount a DVD/BD that is already mounted, or a blank DVD/BD. Dkopp outputs messages of its own that mention the multiple possibilities. Hopefully this will improve over time.

Backup history files: A history file is generated for every backup job run.
     location:      /home/username/.dkopp/   (or)   /root/.dkopp/  
     file name:    dkopp-hist-yyyymmdd-hhmm-label
The file name corresponds to the date and time of the backup and the DVD/BD label. A history file contains a list of all the files copied to that DVD/BD at that time. Thus, a DVD/BD used for a full backup and two incremental backups will have three corresponding history files, each one containing those files copied by the respective backup job. A full backup spanning multiple DVD/BDs will have multiple history files, one per DVD/BD. History files accumulate and are not automatically deleted. When 200 files are reached, the find files and view backup history reports produce warnings. Delete the oldest files or move them elsewhere. The 200 limit is a compile time constant: maxhist. This could be set much higher if desired (and if you have so many DVD/BDs before you re-use them).

DVD/BD label: The menu  DVD/BD > set DVD/BD label  is for an optional DVD/BD label input, which you can use as part of your media management system. A subsequent backup job will write this label to the DVD/BD, and the DVD/BD mount command will show the label. Recommendation: for full backups, set the label to match what is written on the DVD/BD (with a soft pen). For incremental backups, leave the label unchanged.

genisoimage errors: If a disk file is deleted after growisofs begins, the DVD/BD will be defective: directory entries for the missing files and all following files will point to garbage (which may even be readable). The error reported by genisoimage is ignored by growisofs. Dkopp scans growisofs output for the ignored errors and un-ignores them.

DVD/BD media types:
   DVD+RW      good for many (hundreds?) of full and incremental backups
   DVD-RW      good for many (hundreds?) of full and incremental backups
   DVD+R       good for one full and many incremental backups
   DVD-R       good for one full and many incremental backups
   DVD-RAM     good for many (thousands?) of full and incremental backups
               (perportedly the most reliable media)
   BD-R        good for one full and many incremental backups
   BD-RE       good for many (?) full and incremental backups



dkopp-6.5/data/images/0000755000175000017500000000000012343020444013313 5ustar micomicodkopp-6.5/data/images/dkopp-jobedit.png0000644000175000017500000073572012343020444016572 0ustar micomico‰PNG  IHDR`òòQsBITÛáOà IDATxœìÝwxÕÚð÷LÛÞ“ÝM'…PB'ô¢RÄX°£¨ˆW»bã^E¯]Q”¢(½÷ÞI ¤Þ¶·©çûc!×› ~žß3O‡wgf÷¼sÚ EQ€ ‚ ‚øßPtAAÿ¤Š ‚ ¢ ¤Š ‚ ¢ ¤Š ‚ ¢ 0—,±mÛV>a˜K—$‚ ¢ õéÛ¢(ŒñqYP#³ÿŽ;ºïÞ´´T@]É ‚ ‚G"@`Øð›I^õ—pѤª¢¢üûïÿã‘`„9AÄ߇ßç‹ð ]x EQ£‘㸆:4à÷G"atÑòHo0¨Tê†òÁ`  ]¬<¢^¯W«5n·»´´ôäÉüÛFÞA*â?¿‹&Uc~è³³jkª0Æ5µ5Š¢\ìÜG©U*«Õ*IRóÄIAW‚¢(§ô̙ݻw † &2&“©sç.11± ËȲìõxKKKöìÙ£Õj/øš:®[·Ü˜˜X–ceYöù|¥gÎìÛ·—㸠v©Uªî=zÄÄØ5Z,Ë+þ9-=½Wï¾$¯ú“»pRµcû6VÓ¦m»ššjš¦ß|ç­>½ûð<ßÈ ½ðüÄêªêf • ‚ š]8>vô¨Çã¾ëžû.V¦²¢ì‡åËzõêŸà÷û;V_Ww×½-ïvÕ;^Ïž½““B¡PÞñUÕ•wß{ÿÅÊG¡ÏgÍìÑ£G‹-xA(+= …û ¸H^õgvááçß|3÷ãO>-=SÌ0Œ¢(öØØÃoùéçFc´@4³n8µ½{õ}õµI€¬ÏN Üe'×mÚ¦iÑóŽ¡v®_½sï©«ï¼;Ë®‡#thA\B¨¬¬ô¡1ÃÅ3˜¸øÄ˜ØX·Û”œŒªªª>|D#å-V[jZZuMu‹´T¨­«íß@#åÕmNNNYiiËÌLQ’ââæ~ýU‡ŽLfK“¼G¢™\8©Š‰‰þpî|£Ò²3ãÆM…ÃF“‰¦(@0–dYÅnݺäævçT*Œ1É  ârWسËmFëT•%;·íì0r4˲¡PøŠQ]tlîœo|XŲ Åh{ 6x@Þï;÷QUNî\»hÕ6Óñmz?x÷Mœ&ø¡V«EQRÑìÅ*5EQšahZQÀ V©$Y€Ý»w¿>yrBbâù…§OŸE34…1 *§` ÇöÙg§Nš‘‘¡(ÊŠ+fÏž½xñb„BÃ0cEQ"‘ˆN§ã8®ùß:ñ?il¡„ÿ$I8Ž3[,½Áj³R ²,‹’(ò‚-6–e8P…$U!‹·Ë-aŠ«ÅÄÌXÁj5g³Æ0, ù+ËÊXÉj1Ja¿Ûã“1ÐŒÊlµªì©wÂި ‚4§6[, ¡€ÏíèŒ:>ƈ1™-5‹Ï¶ ã°Ïí ò¦§^E=õž`$ú³¢(XÃ!¯7¦Mnz¢CÁl|¼S‘åóîd_U•)Æ®«ðFbT ‡É§˜ jlj<DsDQXQ0à†ò‚ `ŒGÃÍIeeeôEQËœ+OEËÓ4&Œ?cÆŒS/Lœ8xð`Š¢Î–§ŽV¬äSùÑXR¥(JôÔ" Qzƒ^‡(‹ÅBS”(м ÐbhÆd4V0IªB‘#·¬š=oeŠo›™(Ñ Í`¬#žŸWÎ[øÓ®~·¹upÇß~¹nçQZ¥Z{͈»÷m¹ôëY?o=ÔoèÀмã!¬½îž±×uo±eÅŠ©Ÿ-îvõÕ²·¦¢Ößç¦{†éÅAX–eŒå¼m?¼ûõšÛŸ™r{ßø%ŸÎZ²ý䄿ô2³Á@#„t}J«œNÙÉ*1&Æ*†üçÕôÕ£Æõ÷–OçÍjêlŸ>ùBèr>¡hƒSÃ`sš¦Ç?pР†2o¿ýö¯ÊcŒÊgeeÍúüóW^yåî{îQ«T£Fzú™g@–å†òŠ¢` Ký54–T‰‚€Ï].Ë2ˆ¢i ñ|„çIŒ5jUŒÍ Š¢ˆ‚ Šâ œ þ”BõE6ï°%wóÔØ’-ËÖ•ž1`EDYÁâÁÕ‹KÞñääÁ½sÊ÷.X¶zç ÛÇ^{u»í‹¿Z÷ãbGÒ£I q©¾–=nuÓ€o¾š·zéŠ=Ÿ4ô™©-œÙ=oúÛÏ7®_Û©S»ÌX•‰@‡>½;l˯Ì?ZÝÊ\ö=Gôlç ”I² €ekõšüÝwþP&«lWßpó€mAòªššÚ`MyD@Q$QÄ¢H>ÅŠ6]´7ü\b$ ‚,ˈ:›ñȲ\SS‚ Dãõxã³õ(€(в,7dHÏ;Î6­[û|>›ÕÚ³gϳÿ÷ìë#I’$QTdIªþKª$I:ÛR…¢Áïó…Pt"h||¼ÅbŒI’Y¾2qÄŸy\˜B¦¤ŽZeh+’wï9 K¼,IXÁ€¤‚£yÉÙ½{ôèÀ»Ê*N2Ø“’3»¤¥&ÒcÖ®ª«pkY–¢49Ý:ÚüGc-†Óõn^F!¤ÊlŸc³jãbaïqo(‚±*z/ ¬½{¶}Ý™c»wHaAÎî7€öºÏ}¡ck|ƨGgtúˆëô”·>=tøH»v­í,’þóQÅr4ÁÂX–$ Ÿbâo!*šôÈ’$]ä#InXrcY–£w,çÿGQišŽ–—% + EQÑaQüñÇõ68X´hQlllËÌÌp(MÃdI’$IQ’Sý%\Þ˜*@ Vø/ÉŠV«‰^ã´´´‰/<½sÇ®ÏgÏ4›Í@:ˆ¿=N¯Ój<ÞªÂÊÊ²ŠšpDbÏNøÀâzôî¼o÷ù_-~]oG¼ÉyÕe§ŠJ¤•Z½Áj·E*Œ#ǧ™²ê:·Ö’¬çhEQ0ˆ§óò[«Å%e*£Å¨Óâs½í’L·íÑyù”%›­>­o§”Š¢ý;·í±dvhß2Ñç­ ´P8¦¨5ZŠ’ïÞZÀ{ö63r$è÷ù2FŠÌû¼^-¥ÐˆÌÙ&þÖ( Z•–-[6ùõ×9ð|ÕªU‘p!D! ÿ§û‹¢¨È²Çãq»\µµµÑ¢(FÓ¬hR­'ÏΠW”}ûöýó_ÿºvÈ·Þ~; Ýsï½O?ýôܯ¿ÖêtˆBQ ŸEÒRõ—p‰1U Õ1ƶ«Á`Ðhµ…B&%9åèÑcÛçóÝ2|¸Çë%ßÅÄߜƚйMöš‡?žâ2BPf54MŸû 1éýF¤rßÎYõ3ËÑ7_Ó§oÛÓ‡·,9² dIéÖïÆœ¬˜ ÛCèЊyÛêÊJÝí¦õrD”dPq`Óg;ÝW Ã»¬z!ânø¸±æ9Éú®Hz—¾Ÿ8S°zÅò UBç¶¥§öÏ™¿ÆöÖÇ$µîÖµ›U#þ°než Oí9(† Ù¸|ÙöãF­F*?þÅô¼.CîØÞðûÿȃH¨†L’(¦§¥=øÀh›-æWe”h…E½õæ›?üø#˲¢( „ÆÊÙÎz’Sý\VKpØÿ~ãß6›µá{œã8Q”¾™÷V«¾~èPšb¡À•™ þĤéÚïj[bz}Ùc,R$ èâ¾PFëF{k3–âû\{¿)¹^1†Dí­£îi“Wèç½ÅžžÝ–” ¢„zøè{ùÇS|z«D¿«c ˆë{à –°K u)-[qR rnÄ Ö0høðÖnˆËJ‹øÜ1ñ-îðA&%Kâù–mº×ˆ0¬&6)=>.Vð×\sý°Î¬9–•Aj‘ÙúÆØL£Éˆ ‚`r…sƒ) âo ùý~FÓ®]ÛnÝ»ÿêßÜõu’,G"š¦£K$ „‚Á€N«ÍHOŸüúë¿*ïu»DQ …ÃZ­6ÚX ƒ’$&&&<ýÌ3‘p(p@BBü„Ç÷{=<σA†fð9Wè}ÿ›K´TEdEÁÓ?ûX­VŸÿ¯XQQäX¦_¯>•Æï÷“³N¡Õ-Ûu΢iY–(Š–e9Ä&e$gjBÁ`@ ²;wG`HQ·îÐ…¦iEQBÚP8€1 Î’Ô­—-‡|õ¢ÀG?Y2gîܱM(à…á_æ=ѱ)í[©#‘H8Vcsº§Š¢ …hµ)-ÛÙŠa!AžZEÁÉ9]Óh: ˆ’ähÕ¹…FÓðRáp8 ýG þ4YÎÍÍ]òýâ¡C¯ÓjuçWdY>tè Z­Jˆw¹\EuèÐaýºu}ûöµZm>ŸïWå1ÆGá#‘Œ–n—²³³·nÝ¢(JbbReyYCI—Ëà€üü¼šêªÁƒ‡¸Ýn’Tý…4šT;‘w޼]…ßvè"„8Ž1SNQ’$y½Þ_ý2‡Ãg×ü<ÿ;Z8¯Á c¤ÕlV$‡ƒõ¼ëì?a`U›ÍÆ`Éår…è˜Ã‚Á@0ý«¢(n·»áçßÎæóŸ×» …B$‹"ˆó‚ Ñh†1mÚ4ƒAÿÛ ³U«AƒŸ9sFQY–)šºþúëgΜÁÐLÃLÀóµHiqÓMÃΔž‘$ !DQÔ ƒ¿úú+Qhšþmy§Óyûíwœ9sFÅh_$Sõ—páõÍþ5éå¾:yÿÞÝÑÖ)tn.èo‘ š š BÈb± †ÊÊÊó“-½Ng³Ù\n·Ÿ u"ˆ+BQ­VçŒs^¬‚ãy¾ºªJ–åèyŒ±Z­q:Ë|DA¬ªªŒNŒ–W©Ô§ƒ¢h€ ìB’¤ªÊJA8›r)вeËæûî@£Õ‘:÷Ïì¢-U›Ö¯­©©¡™ dÐAñÿ›¢(Xi,}¡(êüF)¬DǬ_~yÜø¢í¿(! 2,{9‘ ‹&U:ur»\€1™tðkäxü·ÈqÅ'ñÿ B(à”••âF“0âÏà¢I•Éle.þ m‚ ‚ šEQœJEÑ´LæýÓkl zt\-Iª‚ âBÓ4ÏóŠLš©þ¨Æþ‘<¾‘ ‚ âò4šTa2¹ ‚ â²4šTƒÏ£Èá™o=iw8ígrJêˆÑO¼ýáZ]jw8Ãv˲ø¿!†Ëúwí`w8ã;të·6ß§3[~µÇó ’‘7ÞôÝŽ“f«µ‘b—/äóädµZx"b¶XYüzê³ÃnCÙÑurê=#nùùH™è)Û>=³íA/e0/_¼÷pô¸ÅÅ'Ž}ãuÌörùNÞ4{öwŒÉrîaR% Îî7!qä¸ÉƪR©)/ñµ¿3µ,Œ9ŽûÝá] ŒÀ©h‰É)Ýz_óÕ¦“îÊc½»v][ “Iâ½ï¼øÈ¨1ÿÈmßÎîp&%§tîÑÖê#œÑJQTã¯\[²ÁîpÖØ*•ªüè·v‡ì†aÎ/®ßfw8ño~Oñ'×¼YѤSZ† cÅl0Mxñã)“ÿÕ§+SpØcÖZ(€ »rÛÖ® ›˜Ù-·“I¬C §:â_ÚQyG<|:qÔWŸÏÜó+–ÖGd%¾gŸžf ¸«ìÙ]áYìÉÝ»'ÛMfNo y*7®Ý’Ñ®MÁ±¢×ôc"õûöOjÕÎH¹V­ÜåLvú|áìܾv=Ïó ÒpG÷[=ûËúM’x÷?lºú™ï ¶®=p$ßêLÏíÕM#Õý´|ƒÅià ýð„g3[%.Ÿõ f¬?O,Û¾j»æª®éîÊ“wŽqm(lä€d7Xò}.sd;gè¿é©GúéÄ[שrkÌq½¯¾ÊžÍk·‚Š%EoKìØ¡-«„îÞ|¼¨ZgïÞ¯¯C£Ù¿·´ÒÅ©©•Kç®ßYžÙ«W¯V‰gr–3~neyg¨éѶ߂UÛŸ»%wÿ¶5' +9µ[ÿ« xßáE´Z›Þ¶g<Ê{õ½7q«ƒ;´·à›¶z"RbfÇ®Û@°â§em ± ¶¤Å›ïÙPbF›Ní[3ŠÐøô`rZŒ}imu;€Ê½ßÄwíkÚ?öÖžë¿ÿaDç}µgöî;1òÍ/Ô_UÖô¨?ò}lÎ URá}’jjjyeš’íf;E°´d6ØimÉÆÍ»Â •Þ¦S‡6 -4f `׿ ¾ˆ€©öŽ;h)™Œý$âÏìbëDB—n©úm–€°°`Æk£ÇMÿijS¿˜=ó‹ùœÉ¤Qi€…_ÎøjÁ’cǎΙ1mÉÆ<­ÅzÉ«+ü÷ {tì}ó7Ÿ¾îöÕ²gûæÍ‡îŸ?óë˜ÍšŸ¾ùÚÛ>qòx~aÖhT*)Tþæ Ï.^µÃ8=fÌC~£‡J'¿úêžÓžÚ¢õc'ŒÛS\}`Û_x—³X£+³] ¢¹®=Šw©‘ƒU»{c˜¼wßû䨉ß|ñѼ÷slÕýŒ]v ¨¢úÌ?_~içé2Þ]çñ{W-]|ÛÜÇyŒ3÷ÿüõËo¼«7.ùfŒO”/IŠg8z׺Åï}ð鑼“?}÷Å3ÖèCw~à“ÅkÊŠó>úàÝ-ǫ®Cw~lçác‹føò ­±ú™¾ùÜ˯–WÖÔyTj݉§)š»à‚¼çciút5¸Šò:Îd)=±î…—Þ8”wrÓÊo_yýsš Nyï¥?mÈ?YP\–ù°Y«--,(,.ZôåÇS?ûòðáoOš¸úp •>0î+Ÿ)«8=ýƒ©?¬Ý|øàî§NÝžW«5!F"á9{àÇwçÎþrý C¯);º¹ÄU…»¼¢eè öÞ` °ˆ^“ÚkÖ˜þó?_ˆ¨K¼;Pã©yõ±§âÉIï|—£Lÿý¥+×9¸ç“§n=V­3êÕœFßãã{{Æ‚Êò™MY¹³@c2“/,‚ ¢I\¢¥*êyÆÑ­²:^ó­*>Xt|‹7t¶Z û ç-Z¢XZe©TþºŠcGòá¶ž]xÑö†W¦)¦]¿!7wS‹ßæØé?¬…Ó°ãTuØ]²hÑš·FwÛºsßÈç¿~öövPyfÏWõÊ÷Üýâ§_MÁ—­Nr¦²,Ð4•’h0Yt ëœsÕ¤ÇÿÁÊ}Ö~»‚vQ«ƒçßq!(©e»nÅŽm{Œ;¶ºê¾“+f/SZfð¾ÚÃûSw^k°<ÿ¯g2¡jË‚Ÿ9•宇G(7>yßUæHbò7Ìß{bß–S¼3 ¢‹Å5ÒT‹¡.๫]{YIÿ±Óz;¸§æÎ:˜_×±O_Z ìݸA~¹_n«ìGÞûü–D€êÑ›6lˆm±ÏžsÇÜϧù~Ûe‚ûƒñiIɉŸ}þña‡Ûkß™S=ñ¡^;n¬50ñþñÝ3؈¯õ€QÇä¬{åÙö¸öÙ}jÃÚåa݃4V¬-ÚÝñ§Ú¥ÃÕR×ìN¿ñB;×áë?9üÄ´ŸGv3O}öÖes¾¹å£!1zëÄWžtœþ!é‰%Ý g¨ª¢‚ü’ªëzg"Ÿ·±s 0Œ±ÏÑ؈GÌÈí¡xç<~êÔŠŸ²n~9 ‚A>c þ¯Ò1B —š‚Š1¨öê·;u…êƒÇùª·ÏZ¸¼Ó€9–®.>_\y] BTÛÔŒ«ß˜ùHw‡%òøÚU+n¾!—¢(I’;€Aq.+©ú5Šiٱψ½A Í:¸J’tÑ_#YHiÑòÖWæŽìáABlÀUßx—дªóÐ[®êcª?öð´{2N¼·ðàéÓ;|ýÄ=sxY¯Ór§ÔVU*’Ä0\f¼áTüþ0E Ȳ(Š€`„1xp…ǪՆ14ÞRšØÔž­“Ö/›ž·úè«ûnyæîÇ^Ÿ2vhº$ð2R¹KWª9½ ¾¢‚\çòz¼îh>æÙ‡»?uÇ­¹ýŽé›Óxß_”]ožäþÇ÷ïúçÜ¢'®sÄXî8åÝ'‘Åj]eÛeá°_…$ID «B¼ÆÞêB²AcNË©h ¸´Æë­óüöÉn¿¥UéßÜSлtÉݦï?PnR££>yïõGøpSë|Ϭ9³7¬];aD7”8hÕÒ'Ã|¸º"âôÕèt$ñïgÕ*ÃbŠÒiL€3%¥é-;¾ùÙWí-²,ADÁºÚÆßþyÍA8¨Y¼¥¤íKÃ@gËM1.Yôñ‘µå•^öVξæƒu?l+ÉzàíK¾;@dO¹a@@ÝñÚ‰“W‚ÎÊîöƧs;ÛYT" øË–+XD1HðÉ’&Ì ,ÇaDšÖ ‚ ˆ¦qé1UŠ¢œ_ëDGY…‚¾:\S#ŠÆ8ZêÍY}sRæ~8‰­¿ÑW[›Þ¾sûT óÔµcQæ·|ûyL~ÜêE[õº>->ìHhµïЮYó6ø1½:·Yøõki„ p»N)jµù¥ï|2~è°uO{¢{Fìü/· >š_x»Z‡ƒÂþ#ëgÎý‘>:ßÞþ¦Azpׄ¯ìEÐ Üåék³uyìÖ8“;#aɬW-ÒHÁ]kMhÝ5“V°"pX‰Î†Œ6Fa ’¤´îuCÁ“_õúÇ[­Ìà­¹ÄS¥1V! Íõß»t͸g?˜v÷­^÷óä Np*éšÞº`¨~ú›o¢«¶åWÝuçÐŽÉNÏk§Í?ý㢞÷¼æYEFà½Þ¬Û¾|ÕŽ[RÕÈÀ cDÑàìxÃ?®[ûï‰Ï.žúлw¼9e^B"ãwô×i·mÝzÊ÷Ôø;f~WA¦4‡aÝ÷ßj;$wo—üÝçÿæO·ýiãÉá“§Ñá#¢,ŠÎ¬îÝSx}Ò¤û‡v+/«kÛµgfŠM<ïqu C”Å-ß-qÞm«¿/ãZÍ™äóºî¼¹[öM/t¼ó‹þ•Ÿ$ ;–-`baÇÚ¥'¥„%c:ø}ÞKXó"_ r»ªj<Š¢XúôLÿðͽ<ú†奵mºôH·P@%ªýü½wœ5½Vï99èÑG\¢H†‚Aÿ;zÒ¤I¿ýí¦ú¸* ESˆó“* U«ÒsZ$Å+‘FÅ%¤µIN°Å¨’ÛöîÜ«CV¤òôº-;<ݺ[ßx '6:N!ˆÑ²Ey'Nž.Né0è鉶HjÅÕåmÝ*·o÷.×ÜÙ¥Ulzz*ªÞ¸egŒûöŠÓêmq)Ãn»Q®.Œ­ÆÞÒqùw?©¬Î~CnïÒ>3RµoÃÆ3½s[UPo|õŽ…B¡KCLLëDç G^ȉSee·d•k7nu䬮ýSšmJë]PÐoÑÛÒÛ¶‰Ñ kLJZËLŠ…<µþò²kF¿Ô.Ž x½ï…B`n‘ÕµòzssÓÊJ¤£Hf½+W­-¯r§w”aç—/Þ4hH·£‡N õô°«²líÛʸ|é*GÛA“ß#»«8F•’ÙÆaÕ™tzâÝu °¿Þ4…Iª( FgË4zËø°ŸíríÈk²?.ý¡°¤ÂѦoÇ ý–Õ+6nÝëæÍÏOyǦˆí[èׯ٠¦–=tsõñý{Ÿ1î­;${êjí¦Vm{tF}ݠܢý;7ï>(1æœnÝ-$5ÚfF!œ SÝ·çÌ™Š¤¶>˜ó¡Ià}^ŸÙéH‰¹á±§ÒŒb(ˆÓªN:XT\êlÕë/>vbÉçñ4~`iÅÅdµíÒ… ùh¤8œ]Û÷ì<¤WÛ’;7îÚ/2¦œnÝm:dÓg·íh_ûý†î½:œ¿ß‰.!„œNgÃHd¿Ï …膕å ÃÄÆÆ¢ ¾®Öd2…B!žçcccƒÁ€¢`ƒÑ-ìõ¸mútÒ»‡7l™ Àçó]ÎQ iÚáŒSd©ªªŠeن݅~ àpÆÕTWɲl·Û€,Ëf³¹®®ÎU²eÐMãßõÂÛïNPêkùH£›(ŠrÆÅ×ÕÖ‚ V«­¶˜ºÚ‹ÅB3,ð‘pÍ™m÷Œ|é­Í;s X©­­•$Éf³q*5xÜ®p8l6›1Æ^¯—e™˜XGô˜4>Ñ¢(g\œÛå ‡Ã*•Ê[_W«×ëUj (²T__o³Ù(š€ ßçóûM&“V§WdÉëõZ¬6ÀŠ\[SƒθêªJY–£o¡áȇB—hŒ¾ý†¿ú}Þ@ €1fÆîp |¤®®îWe.ç€è+ÔÖT‹¢È±lŒÝQ]UɲìùáE"‡3®¢pÛ¸&Ý?ý»²L=¿8AÄK«Õz<îåK—>ôðXVGZÖÿÌ.‘T‰lk IDAT¢ð«¤êONÅ(3(¤H¸¹¯<x ©õöÄHÀ¹ŒU—~A$õÖ)ä—Ƀ3›…£ÎA´ò“*‚ þt:Iªþ*~×@õ?1Qa â•á \v˜Ò0*UÐ]-6Ñ ˜‘$‰dTÍxY&kSAMïÿ[R¥(Jã}aM»¯è3§› ÆøŠÿ÷„1&ý}AD3¹DRUWWëõx–½2ÑAq¾öíÛÿbQâOìIUVvk2×€ ‚ þ(µ5ÕdÕ_Ã%’*>‘$é’ xAAüÍ]úÙAAÄ%]"©Â—zìAA—žý‡Ïº"ÁAAüU]ºûï/´ò'AAÄåÒIi¦"‚ ‚¸¤KtÿaÀš"¯*¯¨øvÁ· Ëff¶2dCÓ’$ýï/ÛŒq(\ò’Z•$Kb8àYPk´Z%Åh+«T‰ÿ Cg¢Uz’°þ±Ü¥¥G×®=¶u««ªZEQIñq9}û¦`HO4é²®ÿ- PYtlí’!_͈»MëxmÀïoî¾þæ[{öì5›M›þÉÇV‹%|Ï)'‚ø»¹ôŠê¸)ÖÇØ³gïüyó&¾ð‚Ãá˜5kægŸ}öÔSO‡BÁf}Zˆ¬(ÕÕÛMúSeE)G6.ƒ@%G)¬ZëhѦõÕ÷kâsI¹i,ËÕÕ[LúbOa‘ ˨P5K+¬JëLÍɾz”&®Ý• ƒ8–åC+V¼òìs844Þœk ”[ÿᮯÞoü‹/eŒ ÿ¨ÅDÜ5¥ßΘ”dÁƒûfÍ›>é¥C¡æ¾HöîÝ;~Âc|$rºàTAAAYY¹ÕjAˆš÷Í<š¡@gQÍšaiš!×+AÄo]z úÿÞRuútášÕ«§~ø¡V«Ý´iÓ_ý×?@‘å&i» ŒqMu^¤î­’ŸË« ä, ñ4KS  ðÉ‚ykZt¹.íšÇd]‚(5cb‡1®®:©{§hEEm‘ÜĘ̂Xš‰†‘_°3oUj—ëS¯'kã›5Œ?^°¢pGÓô•JŠáð;£GO_µªhP'‹‰ñ»ë|µ4(ýLÆkl~‰ýÄc̳d©À0ȃƒÖ~ÿ©J¬ª(]ÉFƒ†h²ãF˜LæW_­Qkjk«ÂápYYÙöí;\.7XS4@gåÌ Ç2gã!⊠#›ÿBšýÙŠ¢vêÜY«Õ©S§¾ÿÞ{É)-~_}}}0ÌÈÈhòz c\_—ç®Ü)–T9ß¾(AQ±4EQ¢å+\]±&œ|ݢ³K¢Ø´4„QWwÂ]µC.©Š£øN@òkdþü0÷ôª '}3¶fíŇB!Y–/öùd9N£Ñàf®¼<¸uëÖ›GŒˆW©Õͽ»ßÂ?O›öÍš5ž:WÕU+sÙU`å¢Àï©õV×h5úEWe}yäØ>zýç_ˆ¢x…Û«"aÿ«ž»·Û‰£‡ŽäŸ¾ñΧáJ môxŸ÷ÊÄutϺ4'§ð~kA—š{w$¾GG%cæ¼³ IÆ899Él3*˜W±4MÓp®‰Š´SAüVã³ÿðºÿ~ïÆ0Lbbb~~>‚€eÚ´iãÆ{dìX QÐj5ÿûŽÎß^PjËtõ‡3 bÐÃ…"Fc -*à ^`ŠaY–a-:Z,Ù„k³ Ó´D7>ÂãÚR½ëHKƒôpáˆÁt6 > ,C1ÌÙ0„¢¸îD3…ݰ¢ÔÔTwèÐáÅ—^qóˆ}ûöšM¦ü¼¼§žzò¡‡ÒéuµµµE7_ÑMÅqW ¸j„'‚Áà„  Nr»Ýc„Psï0V$iÞûï?cp{ꥀ?mÔhÝÜ•{@ıF“aþš¸›î<â —”ŸÿÓǃ(¨8î ÄÖ°­ùqQ›Dõá£y‡ }£Æ¿ ¢ \ýÆ»] Ÿ{9&ÆÞД©( BˆãX†¡Že9!8›þ^ÁB6²ýÝ7⯣ÙSC!ıœÇãEQ&Ožœ‘‘1oÞü—_z9!1±®¾^àù&Ãf³ÊT:,9iAöñÎØ*m _V[›ALRÏJ?…ED©ªÞWTXP´å+µFÛÖ116u`#‚ƒdï´W©“ùZ°ÚÚ f’zVúB("HUu¾¢Ó'‹·}ÓLa4à8În]»fµÃîàXcEo0ŒÆýö%%&^±nûºúº›Ög´Lûè“gÏùrö—³·oßîóžm jÖ0\ÅÅ´ÏÛ-Æ”_QK°ZhջDzÇt‰ŸËCJî!ŸÀ+Àûü2Nˆ„ÇUjMó…Ô@ÅC;>Ù°¤Ç]·Ç¥»ªùà->A[³‚ÿ•iÆSËâ‚ù_rÝ0 Ír,‹TW×°,‡Å©•še9‘¯x‚ ˆ‹iö1UûöîÚ¶uSßC0Æ‘HÄçóegeµiݺ¶¶vËæÍ.—«cÇŽ’ÔÄCšxÞ+k(.ÑTy[QHpij¼ÖÔ¬v±]FËœÙÿÃëNlRSŠš‹–QñU  ¦¯-"¯¢¡ØSU!²P!±^SíµµÈʉíö L|Ë_;˜¿UÕ†P 7Gg!„Ñî° ‚!ŠFÈnwä»ò¯LR¥( MÓ6›õĉã¢(MžüÚžÝ{9ròäÉÜÜnéié,ËRÕLi„«¸ü¢F6È2løôÓNÅôü;ݦ/,˜W<~ÿ¶ï>¤GH„°ßÏHbàÔ)kç.Í̯ÔU—.žw"-áVÉn•åêwÃqåHØQñÍ´Ç‚í;Ž®YcÕi££¥üå[²³íÚ¶tÌ?z¾ðœJ«~ûíWdµ›‚Ø­Ò²¬š0`ÒõGq—1¦ê¨ßvïܶiÙ{z-$ÄÝáóz7¬_Ÿ‘‘¡7ü>ß©S§÷Ýk¯¾ªÖhý>o~O‹¢X[·Ñãu%ZÞà¬-/Óh‘6ÅŨýõ…û1£Óht,«²êiKÓ ­a)àiG#‹‚PW¿Þãó&[q¸ÆQWQ®ÑRºhû0£Ñhô¿ƒA Eš<ŒÿÀ˜¢iŠB‹…BšÇ ŒŠ¢à;Ý—¦iJ§×ÛívƒÁpìØ±Öm²s{tß¼qÓöí;öï?0lØ0½N×L+ø«ª‚>G1²²„3°í‹O‡Ä%P£€¯=·báÂñF ddŒÂSb}=4ÿ¢H°Ø_7}ä:ïøé› Êí[ǺA¶§!^n®½ïX²dÜÌ™ïX,aŒÀ(­°r#‚'Ž­öùìÇ'ët:^…)N¥e9 ÍG@Q0™ûGqA—±¤Âïm;Ù»{ÇŽŸ¦Ü?H‰OoýÅ·“×îösj}uuµV£¡iº¶®îËÙ_de·M;»*à÷ùªwÙŒLýAÁ„UáëƒÆ8¾äèI[œ†b5ÁêÓv³F§fš¦išÓ€b1–š¶¦ðû¼þê6£¦þ `Äjĺ€ÁÉ—=§AŒ:X[h7k´Ñ0†Ónú0 „hŠF2M²¬Ã0H¥R&ˆ&UÍ[c"D!Dé´:ƒÁh4ÕjM$Âcì½êê« ôþûï/þnñcãÆü>¹æÜÅÄ9côz^᱌eh úZuÔˆÛËR÷ko8:÷ A p+¦¢ehÞc ŸÉÿwçÎí($uë^|mòó ´vµ0„I«[J^O3íÝa4Έ‰-Š!Qd° ”ˆ"+IýFKqѧ'®˜»@1i­˜«ãX5èàŒ’VA\@³tÿ)вkûú+?zðzΑÜúÔ‰¼DtZ h¾]–yy'žÏiß‚“¯# R©}ej8ð¥¦Ö[æöfPb™p½ULj¾’@ÑF5­3é(ŠB)CǶÄ)J°‰ÃPk<¥*êàWšo¹Ç— ÃeÑÑ‚·d?¢hÓ/Â`éØÖ€Ø&£Atı¬(«Õír#„(š­Vc6›€eÙfÚõù8‡Òë f³Ùh4ªÕjšfEÉËËÛ¼is\|üèûG‹ßL«ÂÚ³²Ìv§»®L-aArjlÚ YøÈ(‡Ší7eÖ¸ »ç êÛ¢¾:ÀÀjX•íØ©9"9‡§ªŠ¦uíÖ0ísŸÅm4ûî¼WÕ»¿Þì(ܼ Þ…òy^”e€`0@¾×ÛÞh|Ðç=öï×ã&>Çp Íb†¥ɦ‚ .î2VTÿï¿Ôwmß°{õGcnÒXI'Žž8qìÔ†cÚñϼ‘H¸U«VQápHàùæø~Öëõ©ío-8zÜ[¾!Y4665!h³²˜Š.d€(„a ‘6Y²‡†Ã¡&Ä`0¤æŒ<}4ÏW¾1Iµz6&-!`½pk±´7çÌy„XŽQ4M>¯/:‚ !Ä0¬J¥ÂX–‹žíf­49Ž­V«×ë5-˲E…Eååå'Nœx䑱11öP(Èó|3 ðÒ8j›íDÑ©«9­Ÿ½hæ¾s΢Eà l+£ÁùêûCîºoÊoWV«¸\u,EEÅ%nYQšït`€zApP”€ ~Iò¤ätn¾œ³nÿ.„€æ£¢hAtöyAÄ…\^KÕóºuóÚ#[fŒf0X§òN•ž>½bŸê¾ÇÞºjðp>Ž„ÃÍýT5Y–3rø›ž;°ë°Ù_©g)Ú``UjEÆ#PÎN ÇP[sn}Šàq7ù ¸,ËÉ­:ò7=w`Ç!«¿ÆÀÒ´ÑÈ©ÔøBaØrnm¦0P©8• zAQ0MŸíìS…eYY–ÕjUóçT Q«e2™A\¾|¹Z¥ÎÉɹå–[+· ¢£¦›) šúàƒ“¶odÔK<ð³YµËþiÃÆ×´ Q¤¥Ÿ~šsôDIQq7†²­Û‚â ÷ß<Ï7G<"_SYøaœ= p"_£u¹êÞKA$–e%¨2¡fPÚ'+ Š>å3!Æ•­½v¨ñ•WÊ‚n@4fTÀ¨€æþÏ’ A\dEõ¿&îþÛ¶eÝÑ­3ïª3Æ8O;V[^üÙÏò³¯ÍêÝo0 G"‘¦ÝÝÅ‚Ptú˜† ÃPQÉóÛªÓFØxY8—ÆäÇS»šv·4ß”užç ŽiéH å<¿µ:u„->Û>‡1Æ€|ØhιYÝvxÐïkÞ„!Š¢¦2}õªÕ‘H¸oß¾Ñ_1‚ã8^àï¹çîfÜû9:ŽBìÛ·éÒ¥Ÿ{®ef&òû¼Íú È(EQ’ 3fÌ[Ó¦¾cÙ”ªäè‰ÇUH-" ¸+H[W¯I¤©žVë‘Ô>·7 ÑäcþÎE9uhb\¬ÛãB “ý~OYq‰#Vá#†Rñ¼ guÍ:D~è„ñ·Ü?T\49jTå¶­™4c¼þÆØ÷Þ®W±Þââ­[·±*†f1¢( !À2Yø“ âb.5ûï¿©æܳ{íôÇo³°zÛá½ûÊK+lá&½3¿snŸ`À/6Ï£`~c¬(J(ªÎÛ/‡D"2” òTÿºðI= ",ÊË$Äf_ööÞ’*Fòltµèfck’@2.=ˆ0êB'ôTa) Ð^&>¶õ5Tl;_I•Z­nò0~’¢ >|øð ¿ ü_5÷ü"n·»¹oˆ cqqñ´>Œ±Ù¾øü ðû£‹w_™»1_(ÔëõÉK–/_X]9Ôlkò|DŒq†¨Ujƒq'/G´º~óæ |D„惢ԭ;=sp˨´ÔĸøÖkÍÊÝí³9𯅅‘–™úz-#³Ë寳,ÛY­Ððð™zµj/ExøQóÇSNœúzöŒÍÛ6y©²˜4«æhšB’¯äò÷A-—HªdY¾ü/PAà[$YµFÓ¡}ûŠ‹*6æYžxeJçÜ>nW}³>ÕîW"‘H$1ĵâ)5‡ÈÈj­êv{ ÎUW_#‰£Òh9M R^©×édYV«ÕÇ5áòH€?FV©Y,@Ëì6\·;üUµµ®Y•VË©ƒgÊ+ z½Á`hò0~¥¶¦¦¡nŽVçwÅ^™šR§×uïÞý‘‡Ç2,W_W{…«‚ ?~ü¦wß98c札[{aÖaÐH’L–hºF’óý¼±mÛ”±¬Z·În·'''@óÄ™îL¥¼ra Xâu»Z$™-V6 |4Ãèü£ƒM  Ã(ŠÂ0L“Ÿ Q@Ã_=2~è®{ÍO™òñ»;÷n+®;¡MD‰V ESç.Œ0H¢$Šâ•¹G"šïf›hr—Hªþ«ûã˜Ø„…rÍÌ-™± yÖû'LéÞ{ Ïëiä!¾Mc,˲$I)® Ô×è0étžþ7†jëÕZ]\R*Æ8:ö—ah†a8ŽcY6ÚÕ4U×¹0R» Ô×è2éõîþ7…ªkÕmü ãâ¢{ùÕošuQ±11Ï??1ð»Ýî߯p°,k·Û‹‚Áø[oÕ·Ì8uäèአµÇÃȲ¨×«}fKÔ¹s-ˤ9ñqq4M7ÓŒ16Æ `Ô™žÚ­ÀžùCgÊ?ÍÐÔɼ€Ñ”¢3§„Ã|´ýò \Õ&SðÎ[x~DaIeŒ8Ú©i–B€1DWÊ€ÑÙU1È ‚ ˆßºŒî¿èRU—!99yàÐ{¾ùzæ¡Rï󯼗ө‡ßëi¦Þ“FÐÍ0Œ^oT];ŠêuOÑ‚Z«Vkô:]Ãí>¢(š:+úpchꦚÿ„qÝ}T÷Ù0TjƒþІñg ‡Ãá0ø£êdš¢l6›ÕbQ,ôî…Buµ²× ²ÌœÍ¦1´-§âhŠ’eYÅf:#BjµZ£N·;2eÉ/„Je)XæFÁ 1$)«ÕPÑ!äWàªð±Ìm÷? ±°:ƒ–¢™8÷”ñó×PŽeÉ¢êAtK*ÀåÎôÁ 4tè ÑQ;ø ÷ú5`9–¢)ŽSIZ X¬EÓѦ „(Œ•†Õ¢ž#ÒÔµÖ¹08I«‹ ! iš½âaü9}Ä{ÅX–)Šb9F­Q[mV6-•fX‰¶/J’$KRD¥—‰ÂHQ@Á˜¢tS ¡†˜h$ѽ_™‹â»E /§˜ÀG¼ÞfžTAñ×Ô”³ÿEñú|ˆþü‡|í"„X–ŽLŠ‚sÃÆ£ãÃ:»”ÀùÕU“‡zÑ0dYV”+qAÑ>âÆ;õ®Ì¹h8ïÑë󉡫¾îr:õþ«q–A+Ô¥‹ü7_ ¡úüÉÿz§^„Ã;VÏ™»ŒÑ¡u+7`.ú°9ÀÊá«f}¾ˆ3Û¨ó¿¾1>¶gÕ§3¾å,¿üý¯¡~E‘çùp8‡#‘ˆ ²,+Š¢(Š«âàËÏ¿AYíTCûøk¿øÜLŒƒ¦.ã°ü¾0DQâ½ >}wÄMÃóðϼ¼jofà“IÏ®<å7 PW²óéÇŸ_¶lþÈ[n3vìϼôÓÎ<¬6üïQýýàã{V}úÙ‚èu…åðš¥ßþ°zC×>ýØ Ø¤šýÑÇû «µZ-ÆX WLzêE«g9$\ •O~ñµZP«8®Éã;ypýGÓ¾b-1ÑÙ ¿Òä»k\C+]‘¬ünÞw?n¢~åëdN­ŒŠ ââ.QOãs·Ò—¹‰þ3ßÿ}HeJHJn•™)ùOÿ˜`ÐÓ4½ïމufgeaE>ÿe1àÒ‚ýëÖmcY!tù»;‹¶]NI•ÆÐµKW¤È¿*ñÌ]0›¦QÔ»wŒI>÷L´F7Emß²-¦M¯—_{mhÏÔ?ñÐ7OÄ©ÎL{o¶Z£E løö“]'ëÜEùú¤œ^{cøU­§¼ðè¬e;´–˜ß}p®ü†±röá}d ¸ôôµë¶ž½®•œ’’’œ$‡K¾Zø¨4ëV­>íâÕZ-P4—Ûµ›Š¡•_¾ˆ,ÔÎ[´À«21,ÛäV^½z=ÍÒ—yf¯ôQE()%¥EJ²à;öèøGA¯£hú<¡d#Ûßt#þ:.«ñÿrSa×ʯsÚ¶ŽKH¼ñ®q§Â:Žž¸÷z»3î–GÞDú½‰Ý³e圯歞;·Þ[w]ŸA³~Ø£u „îZûùßPjjÊó÷ÛqYmÞ^¸Me±ëµÌÎmK{¶Ï¶ÅÚ˜bb!ü›]ÿv Ôl´;ãFÞs÷˜'ÉlÕ¡ÞäT·¾d{ayÞ÷vgܨÑ÷feµ~{E~¬Ã}MwÕÑ_™¨°ô÷Ó'ÙqI-RkNDg×jTáˆÿú¾]m1±/ÌÙÒP¾‘M½“Ÿ}ÐîŒ6üæÇþùÜÈ»Ÿ58œ•?OHN78t·^70·{Ï¡C_sÝmGzÁtg·XÛõÚ±M›Û÷ʽ¾™1{ä ¯Ôîúv_h¥êÕÛžûéB­{ìÚ&ûæQc'¸äËÏü2bYö’D½¯=ó€ÝwÛ·õé7pu¡¨ÕIOݫݗݶÃô÷sVûΕSûônp8«ŽÌ‰Ol¡sX&?=ÚîŒËjÝîÍùGlÛêoÞ‹OHj‘zßÓï¸h‹J£¹ÌÓ!N÷ìÒ¹G¯ÞwÞ7曟W ÜÏîŒë3pøž:ñ%{w³;ãº÷¿nm^Xv¯³;ãÔ§§xUZz–_oœñÎ3vg\çÎí[ddM™3#=-5!)õ°dv—¸q`»3®C×>ÊiƒÅr™Á€V£²Zµ5y×÷ïþò—öîÚòãší³ÑaN vX­>z‘K|õ“/>W˨òw.ïÐ6ÛîŒpýÝÅT¬^¯1èž¿©»Åb½õ‘Iõ´õòÆo71Rÿè×Ùq™Ù9 NxS“œ1¶xX2}’Ý—š‘9îõÙ‚.¶ìäæŒ´4g|Bnÿ›ª´N×ÞpMÿ:¼{ô¦Z•Ñhü}{ÿõ¥"…}ô²Ý×9·{çý>ÿé˜ìY—¬s8ëO-³;ãÔÎäõ?-[öÓêï¦Mwûê†\uÓ’Z[Ìï~ûd#Ù~ÇFü…üž¥@õ±Io͸ûÙNåêdç¿øä+Wá²%[ 1Æ«§Œ|e’ j–Òécïzúùœ”އ¶¬~zÔp] VŽ¡´ZSݱy¯ñ3ÆØ{hîâw_8Xîש¨¬¶ýö:}ËÆ_“ZIÓÜåõ¶°€OW¯]>ÿõÌÄt ¡Q\l¬Þ«SS) ­¯X]|êgOÞ©DÇ6Ñ6ê¬Ú¯Nÿñ Æ‘pqÍŽï®Øe0ëhŠþróEÙ7ë¹Û¨ÕêÆ÷ŽØ ꑾ½}ûö—‡g³¬Ðp؉6Þb¼íµù{öz`PêÂÏ¿Uku€1@(à“êëüññVÑë“M¹7w[ðõÆÊ+8[öýv¨®÷„ƒ>ÀU狱[ Qijñ_"$›A}ë#onX¿ù§þûÁ‘A}âko¿¿{ûæé“\1÷““µ²Ý¢qÆeÓ’:É‘²3µáÙ+×××¾÷øÀS;<üâGŸ­Ú}hûr©dïkwkŒ¦Ë9@!œd3ßõɆµ?ÿ¿âËøN#NWT>88ùç^ ùòüSñéûFtOŒüI 9 €Š¤„,N£±héA#;SV½èÙns¾Xç †¿úÚ×ßþtÍÒ9\\îÉÊê÷ž¸êåQw§ºœã…éÈÞ•Ï{á¦W—~öü¨Û­Rëýß1Ƴ֨ˆþoæ~3òÙ/1ÆOàþq×£Æx' Ž˜¾“„T±èëïVj/ûhüÖÉ-3~Ú]Š1v»jFuMª­÷€«hùƒ/²¦°fÛO3O®[¼åXiëŽý6¯þiïÎMÃ[ O^`qڬƻÞY¾aÍÏ#2µ^¯÷wp>wÉŽænØËã3Å;²¬*¤µêÔ()± f!91‡ÐpŒFgûÖkmZtÛ»~ÙèërC®zÒHqA—¨~¡¶GÞ[Ë0”¿®jåªúo×kPíÁW[öŠÉ–Ÿ$Gd„!*(¸\º ­«^QÀ! (:RU˜Õ{ Ô‰L‹X«+±ªõ™íd€pÄ‘a/CW†á/ë±68)6= *¿P’¥hÌ…BMØã¦ IDATÍ$%d3Uµ’Eg ÷P9šbDWEJëÞñšú¶ !¿h6-1Ç ¬gz¥¶8RÙ&6 5~|ÔZs˘$ ²ÆCS4V(šÀ£½u¢Y½˜æÔ@ÉgˆÆ€@‘…Â’jƒ=AÅiú^Õ哳>媮ý×—¢(IÑ;E‹K«5æV8 \FS0V© i1ñ€_69꽯Œ}"§ÿÕºà„TŠL3 CQÌÙP)Ö NÿÀm‹¾{oÕÞvWºµµ×bµ×æ\-ûGŽz¸K¯¶‘@àò[¡Ífg§$} ü`MM Öˆk~\nHëÿÊ-m©ì¤GÎN-C³ÑcHÓ PiÍ9mÛ)길œÜ\P›8¾Ü/@¥Â¶Úôó×fê´ñ¼„±r9Ç0V„Ó¥§[ ¾ÿáíî3çúvpôøž;€¡i,„9–IlÙ²»åÀæ“!`¬&gŸl‚•Y 1eÁ@¿/±ðœÉÏúO¨«©F²,àPE¾Ñì8³{ýÑŠÒ OOì˜îذäß/¾·òæ»þ½»Ž‹"ýþ™™í`weIéFQB±[±ëìöÌ;»»ãŒ;óÎ>=»»ELÄ@E:·{'~¬òÅø÷õgÎûµ¯;Ù}æ™gbg?óÌ3ŸéH0­œ`ÛˆdÕªˆIŠR*•Ÿêr€YYâêfƒN!BÍ Cÿ³QEE Úr¹ºX€”—QŸ=_+öƒ£ÓÂ}CÞÆÿæñ[äàêåäèÞxÈП{wïà*D½Té§Õ™ÓŸåe¡,ìE"”‰¥¹OP/+t Ufœ~ð,5UM0<œÍÍ[gi!;å‚sÁüa)›)‚"ÌL®ˆE™ò”„¦¬äIÖ3’Å1ãf3€'¥*'Ü»ûòìK©¥ª’¼´§…U|B€°<ÉI¹T ¥OS2ÊuµÁô¾$[Öd“É€ð…¶&M‘ =ù'PÀH Œ=A’8A Ö­+y1möƒ+Ëv^ˆí؃ à^zú¯3éC{›u¥õçeÜX¶ídD‹.6 °|@Š ÀdT]¸p¦”€Ô[‰”ÐCfyœ­À–̙ٹq¤±¼ÁPžÐά)T— \¾ÐÖÎeøýÚ¾Z“µbw"Oæd½Å AP‘s»‹ºú!H§_ÖÞp(Ô€)a¦~qÁ1îÉK·0›Ír±Á8Uœ½€!ôû}jBG?éàékÄaR±ŸNŽÎÖ2®Î^`wO’$ (ºeA>µÜ«×ï7tY·x7–p’8üÖ=&$þçÙçò½ŒFãû.u£6"±ØF@øUoXÍQéå»óŽºz­(;{;>‡I’`c#‹/NðÔF€-’ÀÁmúLé¿üÐÜî5ÕJ¥Ô;¨IL`X§q!Z½QÈÇVNèÅǰ&Fuœ±cñÐÆzµŠÀñ¹úÎæðõÙ7ã<çï|¼êô_žžq=c¤Þ~ãWìu¯YA• µ}LžÞÛn”W«ÙVk*èÕ¾¹ÈÖiÌÒÓ‹ŽÜ¶ãר¿aÌÈZ¶§NË™j‡Ç}s7xûæ@2©—¢<Ù/ãÆª¯®b0.î{o—Šoù¹»{UÍ0:7‚Å­6£OD„T¶xÏMŸjÍXH,’¾ØÞÞxB±ÌÑ¥qÿÉÎê³ fgï´ñbŽXü¡¸|‘³WÕþ£æ·°ÏnÞc’GPXlCQLoJ*•òÙ òÅ$ÂàrûZ’¸Vˆ2 qÛWõR•Võ ÌÞ2ÜÎ5Â6~à€Ö5t*¥5¸ù/çˆ ÔôD¿ È“ÏÍ®n®R©”o[ÿúž)5„Ï4î>ª„m»)¸° ,¢ÖS”çáå@ÙÛÛóØ ’$>|ÙßûºÔZ5¹C¿:>qmûDˆx\–¸î/í#œ]WK k‡ØˆÄb!Æ éíè/²Ýq1oï€ÐcªèýúŒ/Ú7yëCÍfNŸ:cÖœ²Ò’$ßìíGD ðB럤Å\.—KeŽ´*%ÏC¬òÒ;©ƒõÍÒâ"UQÚ¼9 ¥áƒæLhǬTaYI±H$b²9–•è¸ &“ik/•—•â8îà ôÅMÂb2M|¡Myi †ab[»òÒÇÍš‚Ík'ïÙ9Iâååe¥†‰$¶5—¾· ‚88:Y—ŽÁÀ$vÒŠäe¥¶öR·”——Ke2¤´¸ˆ$IE+¯+P”—™Ífƒa]WŠò2‹Åb­¶‚R^n2™>d…P¸jËÊ¥ˆwÇqƒ›€ÙhP«Õö²Šz­Æh4ÚÚÿ§©j¥ÜFüŸ/-.‰D,×ú§Q¯S«?4Ù#†aö2‹É(—Ëy<žP$®h—V­¼d6”JeåeT)ä"±¬´¸ÈV"ÁXì’¢B©TŠ` •Bþ±Û¥‚­­-“ÍQ–—‰íì+ÞT”—Iìì•òr±­a1—••&õÕÓÛüqãØÝ³v&cÅ®h}$@Å>lm§ñƒ®J¿]å­Oâ‹ÅÂæòÊKKD"ƒÅ¶¾¯U«á m*¦*+)¶w‘¸¥¬¬ìöU!bgg‡1YúcG1C~Ù+®ò€ë>\ÑŠ,).¦ÇTÑhŸ‹ÍV©”‡0h0—ǧ¿€_³÷U$I~ªEîïõ˜0ÃÕê´š/r§¨Qþè¯MÇÚŒ](6ªŠïðnUÊ”vßÀó «áeT)é¯ß‡Àõ%§âUíÔ¡a ¼¸èG6Dáڌخ‘Á^¶ú/ôŤÑhÿ T}CÞÿì?ŠúÐgÿ½—Ì;væ¼¶ZµJ«Ñ}’ ÿ¾CÕ™Kõ:•\õ]ÝEQ¥Rñ].àÿ¦À±÷ðI(Š–~ø€­ïÊFÕkF„F£þážF£Ñ>÷UH¥;æþw&“©¨ ÿ“Tõ¯áËoÃÿ+ àSÝrÿã ¢¤¤äK·â‹!)J¡P|éVÐh4Ú7~ò F£Ñh4Ú'ðþ»ÿèË·4F£ÑhïõþäŸtPE£Ñh4Ú—B§‡û†¼'¨"HÇñ*F£Ñh´OŠŽª¾ï ªÔj•F£a0ÞסE£Ñh4íÃà8ŽãA‚(+-S©”o<°a0¶¶¶‰¤jµP:®úV¼'Z  þ<í Ñh4íǡը1 ËÌÌÌ~öÜÕµ Æ`XŸKZ“n“¤ÉdÒéôÕBCI’¤3«+ÞTY,f&“E«¢Ñh4íSÑëtz½A½{öôëםЇgåO ÜB„^§»~íÚömÛÇŽŸPVZú¥šJû(ôu=F£Ñ¾Š¢0 «âáI„Éd" ‚$ ³Ùl±X¬Ï2©zíúuúêß7ƒN£Ñh4Ú—Áb±Àb±X£&¥R¥Õj ‚ E‘$‰1°/ÛBÚG¡ƒ*F£Ñ¾ ªÒ`©ÂÂÂÁC†Ì7Ãá0™L I’Lõm¡ƒ*F£Ñ¾0‚ ºu˜xáÂÅI“'KdŸêDU4F£}a]»uc³ÙjµúQÚÃ'NNœ0ÁÙÕíK7ŠöÑ>qPeÖ–Þº–Tj ¾ÅÛ¨)8~ôD©ùO£Ñh´o×äI“€ ˆÌÌÌFjÕª/Ý(ÚG{OPõ8éÄÌ)óL¢ÊOÿmÎì”9‚TÄä™mK‡ºtÃ^ùâÍâG§ê5hp8—¨¨A¯Ê™3aòǪJfÕ…“§óæ×Þ aܻ֜nýÞ“ðÎbVFå“)cÆÞ*Äß]çSöôlËÖ-®+^̹ͦ:nüä\ò]u’ù‰NÝ.|mÖ¡KdaLŠÔyË–^.c~Há7¹5}ž•ÓÄ àÕÇH+ž_²xþ“wÞAš‹'.\t$YýÚû¸1·S—®;Kþ]“h4ö#0èõ8Ž[ÿã¸RIGTߤ÷U¡qµ«º;º¥S«ÊªÖhZÓkŠ ˆ³›×Ú‹Ï¢:Žž3º³½Ä™œÛ½³ñˆoš$8°n&Š¢M{ÌŠô‰ÙÛ—ŒEÄÆF©)ÖµiµÚ­“¯®±“:^—«׬Þbè²Ã‡sy\oOwŸÐGòjבeÓä>Û±y—îWÊv-¢¨‡OÈ®¹޶0§][‰Ä~ÝÕüÊR¤ùÒþ?ø\Nh|ßHo_[GÇs»–#ÂápzŒYrçÚG—ˆ"…baÿˆê ºg)5íã#Purõ\}&Ñh´ER€ã¸ÿ‚À‰ÿ Þ[í«òžäŸLiHã@ç‹k6V }ìßÄ ]ûã«y`ÅÜ)=» ɽ­ÑH·öê4è4T( n2¶Ô  Í˜±ø÷ø³æ¶DtZÄ`2‚Ããœh¢Î<7xÒ¯î—PT#1/lùÕò>ÁpïàøE[O®>‘”»mj—êÒÎäk»¥V'.h;yÒÖ©Û8X—e|Ô±÷Ô:êî×ç¯+Ùÿèa.ex¨ÔéÖÿÜJ* I¹º%ïÊ‚ >JhøžöCžØíÌ“‡¿F ê¶èÊßã*f€ ̾ó6vî×HäßÛÀmÆŽncW,L,p»:w@“Z×ÏM€:íG7sDF6®>X_V1¡®,{á¢Åþõ:¯ŸX'¶Ñˆæ †wpÍ}ûððÌv=&7l›+WåzØúöþ§|F#P5kÞqÔÂåÇ×,œÞ§Ûðü”O´?Ÿœœçz£ŸI£Ñ¾b¾ÀÙÅå›xHNn΄qc^ŽpAAÔú‚ Àáp¾tiá=¿Ž*nïµuåú\¯ª1µu…çÆÍ™éU½–ÈX*´ JÏ$Âcb(Šhµ\”óÓ°ñ1UU\lqyFÒ_Ç–½SæçäààcÁÌ„›­“¦ÝH€ëGwK삦Lëûf3jtáÚ'Ò1%íÊöMG·¹êë,óCŒÆ öóƒÝ,ÅÀ¢Å[75˜Í8—/Ä0”Ïf¹B6 ˆoíÉd%Ôn(“W®Ÿ¢AE10Ê€ò¬L›;²¶“ŽŠtäŸ(Òê]¼Úw“±Ï.Øq£ò„¸ÉH¡mÿñ‘qÁ¶œ±Êì;»Î­X½?%*"ÐEæ ƒ‰°X¿ÚÙ×M^0¯ÊÑ[B!²¡¾©¹%%Åwnß6ôZ­–Á¤ƒ*öõ „ùyN¾~þb‰äk­©Þ¾C‹õÂßÛšzîì™W³YѾjïùu¤(fXÚ¥öœJq™8:°3B¡Ó ÚœûWï<t o²Ó#æWŠ'ðÌ{ÆÿšµCÙœ«Æ.‹åd¹Töf3øóX‚ÏÔò°’;·Û®¾ø[l–ıGR¥Ž¿ÓùƒR<&ägæ…yY΀e=Þ8qÚ_Γ£œôjÅ£+‹5‹içöm;Ùhcb®k׿o,)e0˜­_?÷@†Ì;xSrí<[êïéh[ÿtçæ3¬ ·D®Ñ•§âòùþ^’«§ö¦ù¥—hµÆÒ§I7´]unU£"‰}SŠ"H¨ç÷¯™GgÞ¾XªÔœºrþÙüA#7<þ¶Úçf2wþ½£Eó^ÞÞã_Y£Ñh´ÿ^«¡(ê¿À›Ã3ô÷îÝ=zäÐO={[S“æF~8½ÁP^^n2›áÕ±¼ÿ) ÏÈcÚWåý]üà80Û{ùû€ŸWÿÿƒ \=èçä‚¡à3¾mÄÜŸNìqçèŠý3:x36¢øÞ«:wŠª4Ã0×øËO½ÅÕÛ¯ükÑÅ…Š~]‚’üÝ]YÂÀ™;Wïõjæ³¾ÕÐÅ›g×pyUb µã†t’{³¾ÿ’ ½úŠ0°±‹X2v´…´uVÜ›KJ’$‚ ( P¥Óá¥×âÛÖò _pô¶7o+Ü<¸"-éÁìëy•'áØºÔ§ÑOÓÛÝòó q©Þ¨s•Ô„5nø{¸b šÙ+nÀؘäcÝï˜Ð~×ÍÆÎnÞR¡C½o¨›júÔ) /5õ …Üb±|È$(Š~ÅÇ1ö-¢JKËg<ºsçŽP xk‰ZQQž^Þb±¸jµj{ʤ sæ-øÌ­¤ýÈ’$ß|wæô©3fÍ)+-‰ÅL&ëó7«"`ÿ„C¹×NnñOaØ…¿f¿9“:ïà#§m¹Vh{èú ¿ÏÕ¼¯ùü©Bnnîƒ{)-ZµÉÏËÁ0låêU666(ú®[t:í̳‹‹ ?[#i4ÚwO!W¤¦Þ mš4kñßÊü³sÇ d±Y*•êþÝ”f-Zq¸¼¯í`«×ét:-‚ ÿìü»yóæï詺qíZŸŸ?{vèàƒsyü¯mYh•}Ðà˜/¸ ?Ù¬ISí ¨ÆŸÀÞ¨–0*Ò¤°da–¯ñû˜ùþ;÷ºµÌ·ÀQ‹Åœ0~ÒôTÁË3Ek`Y±&Ü«x,X4"‰aåü(E~frêÇ€˜p?»{7nd(ë´h!Bßr_E‘ŹYwïeÅÄy9Ip³ùÃgc1*θâ\½j°‚›þõÎCâšó'.IÜýj„WÃ^­‡¢È’¼§)wŸÅÄz9Ù~Tóh_ƒñèÑ£©ÓgÁ9ö"Ò¹Kש“'ÅÇ×7šŒ"‘È? pú´© /ýì¥ý ~”ÇÆ©Z@UxÛ·‘/«6k醊?éh 2™ƒƒÁ CQÔÚ©‰1:½®[·î:½Ah#ÄP( IÂbÁ½¼<Ö®YÃas(’¤(Š^“ß>ª$ëþî]ku¯[ÃññƒäËwr«·éhÈL:|äŒOÃöMjøôºšÂsžÜÛ±å`oÿÈOg‹é#b#³¾hó_5ê=*$" Õšÿõä¤EuþÔIïXVhíh†îÕz(<7óþö-ûzû†û{º|Tóh_±žË‘$¹nÝ:F3nÜ8ëqiÚ´i¡¡¡;v$’ÇãYË¡×ë¾p³i?’%¨z÷Ñ“>¶¾Wå‰Éd²Ùl•FÇ`²­ÝW$I’Bá$O ‰D€IQo½²LûÖP6ËÉA*°‘ ¿íOýÚ÷á[@“^˜“vÿ«V&›£×i_U‡ÍvvtâðøÔK8A\mĶ€ $Iþë¯$ÊvX¼zÅâãzŽã¯ÔCQ6ËEæÄá à#›Gû •†[Ü»wïùóçŽ2YÏ^½&N˜póæM{;; ‚Ê:8Þ¾´Ïïãžý÷ðø$E}ü%ôI.ÖïóÓŒ½‚Þß‚¢èè‘Qõ 4uýËlùìöÉ–M]-DÞúR[œÒ­ãOwó ÿû˜*Ò¢Õ¾ÁÕ'^ÎÑ2:¡æ¿n½7÷&Ešö®šÖ®ÇPÓ»J’o­UÍßÎÖ¶]ÿÄ÷›Ï“|Ùó„ b#²qrqvtrtrrtpÚÚJD"‘H,–HÄ ƒ"©Šò´¯ŸVQ¸wÓÊAýú 4dÞŠÍÙJc ÷/êÞ±Ãð_&Ÿ¿ŸÃäòp“rÃòEíÚtJ~–¹}Û>¶ãý½ëûš”cá E‘I@f^=<¼_ÏþGIJ§Ø|2{üσ2xààI³–e”L637ýæœIcºwýi؈1ÿ$¦1ù6(ŠR¤¥8ëÖØÁýÆL]VPøÇ™;ôÖÛ»æ§&íÓ}êšcRGÛ3û6'´h³bÝú±#‡ÿ<|ì©ÛOQ¿âGÔ¬ËëкíÜeô,îóÔ¤q#†ôìÑûçc6¼lañ1  ³’ŽŒìß»ÿÀá‡QìÿLKûÊU>º®]»¶F;wŽ~’™Ù³W¯Q£GWœÈ!BôÚðΞª7≠7>aÖ…SO®;}ÊâÝ ‡uw  cêÅÄ„‘:ņµ?ܳ.·KlÙ'[Mmê\é^V2÷áõ}ǯ0(£G@U±hòS×o;Ì·s‰iÜÒžœ¼‹tkSKS”ºûdfßÞ-.Ÿ½V¯yG_W]râù¤” ©[ƒ„ÎxÎÞ½GFŒŒo‚|ºJϘý{ª á/sÆ:0ôj¦ÄlTPå9i»ïåÊBê ;E†gÞÙúUCQ„Íbùy¸2\½œÙƒü|¹ÒÀ©³ŽîÜrþØñªÁ~U„˜Ål¦H’òvsspt1ʳþþ{Ï!`Òø.…Éç?|ÜÝ7BÀ—k}‡ ¿yêÀÕ³§}y½ŽÞW¿!8Ž#2oÞ¼¸ØØ5j@¯^½àE7 E½è§üNÏri_­wUo;Ìh”P;6pêžã ŽÔ±Ç—“ò›Oï*¿2%W‘ u#éQuÚ¾CÐtHÅT&ÕÓSæØE4ôb*žæ”SÕ¯#§‡&tÆÊÒW¬X?¹ç‚©ë»µ9ûȶmgð~½„âÏõ£Û%TCÚÕã‹~ÛŸÐU"¶‘—ßµö¹™àrhû:ÌvA}þÁ ó®h#÷n˜òfkI‹úØÁ µ» aÛ³~Ëîk]ûu>ò ?žIœþ{C⽜Ú1ag÷®•kڜܴMÝ0ŒÌÚ¸n}¿EžÜ»¡²ëW(Çþ:µñÈ™2‰@Põþík®ËÁl2¡(:uÊGGGë;›7mêݧÅlÆ0 AÀzÈ‚ –鸊öùüË1U&‚@)Љ:‡.>–; âŸ=¦{PñÊ~¬Ê»÷\ÃùsʯˆüþÃ'¿%ïÙyâéêN”>{Rn'®:±eÐ߇³å÷o »štëx<Éú˜f¸}ã¶_tÛa}»€.7iø¾ý˜Ô_]âð4»´¸ÜÀ9w}–ì­í,{zïAžvȘ8x”z7¾ç¢îÔ'7?}vëäÉ—3,³æyvž/ê=ºuðÎSWÝ«©ìdÁõ½™äÙ£}š-äP¸EÄ"žå«ZupçX,®.nÝõi O¹¼Ï6$¨Ebµ[ä IDATTŒæÚ8 ÿnGUþÉ¡€²˜Í&“‰Ãá°Y,A)Š3gL|pÿá‰ÇFƒÑ¿RßÄÆ†‡2™s[vpPÊ•ðÍO£^S®V(”‚ _ätFH‚0$E™ zµZm±FT/Q@–—–*äÌÒr9ƒÅæ°X™7oÛw±N£„àñóGOØ<.‡ÃVkä=®Pâ8AR$‚ ŽŽ2 n9¿o_Àðîƒ"(P“ÙlÐL&œ…¢/Ïôˆ¢‚"XPZVÎ`s9l–uÔß‹å6—Ãb± jy‰V]R*7[p‰ÄPdyY™\Î)-“cL6‡Í~kkÚWA^ŒWÁq|þüù)wïvîÔ©_ÿþ3gÌ8xð Aýú÷7  ÖËßë` Ú×ì_U÷îdÉüÂQÿ`¿?V.qi=ÞÀ`|q‹ò¹KÏBZ-©\žÅd؈¤jªLEÁb¢µ›ŽÞøÛRFµL¨6ohßð¸æ¾,0ÈUzƒÞz x yQÌb1…T›²iƒk¥ÊMƒ0YàLß$a¸p欽OíP1¡³ò•8&‚@|‚£;-žUŒ… h>7rP5ÿ¿@iæéý ¯¯ô„awàfÆã‡;Fyן¹}y÷&›Ïb1€2éíDb%Œ°›»ø™W›(øNYÇ4Tü›ÅbI¥\‘ÈEAÙ,–³³Kyy9‚Bròív mÕj T}#Àð¸ûON<¼ŸlðpVç3ì.Oòï®™7O€+ŒOŒa/ǰ“ɶ>>—–Öïê*˜ŒrëþF½üŸkP„¿Ï…{ç§Ž»‰š´<[Ÿ&-"uwöS@Þ=¾ûÙE(ÎÉ ˆkïæh›UôM߄Р·Xlmm›4nܯ‹É8}ÆŒ)“'³X/S*"`½ÝáEçú—j.í‡ôÑAUÞãó[þÙvåbNßY[ÀÍÉéaêÃØy5À W^>¼ÁÞxþ±Ñ{UTêÞ¸‡Åú^^³n»­:5«HãýSý=ÃÆ-^dÏ42]üÔÌ»jÌÃ[kªõ/(Ë{¸gÇÞÇ™éÇ÷í±ÔkÒ¢uÓ‹³ÿ\ü8I¡µ¢[ÇxÏ91¾VUŽhغ‹¯Œ¹iþˆëº°Í+~}­©ZyÞ•¤”„…³Pfͨ:ܶß6òÈíì†Ã¢êKŸ-ž>êQlM£ÖÕ¸K φ¢Ùkw§ÿ}Î ¶._ÝxÆë·7•­]¶AæëÕ«m+–/$eM,ްÅqµ£nÞ²\wû±Òynk |÷RG(„É`9ÜÍÍ­"Òâr¸\.÷ࡃ‹¹u«V2™J¥úþÖÃ÷JâÔ«¯øæt•ÁèèäÊÁ-;&p.\7`6ž!eer”m2^~,[œG˜ÄΞ ›6y’§ Ø Ã(ÊLQP`/s‹­[ß?²jnZ*V§Z­!fäV­ßÙ$Ê.Ó°¤¾MëK¡#Šcušµ;ºfŠ%ðªVùŜܷSY (·•ùŽŸO (N2¸U¢æLá¿t—×È<ªT0FtâHf‰ÆÝ7ØYÆ€æm»báo¶öÉí㥄W£*€°ª7è4Xx3«D÷Ûæí6á˜;ü4 ³½Ÿ]ââáïáÁ€ß×.º®pwP<=½ûJiÒïN@Q†q¼Ü¤%ZSXÇ¡ñ5¥d›Î=I±5K(#°~û‰|ÙµÔœÙÛ¶»¾ÙˆïI’Ö³y>wôØaWTXdýÈrá8n2šjGG;:8ªTªgˆöù™,ÏÞ£e» ÃH’Ðë zšÄÄ-ÚwAÇ- Óh4j4š¨¸š<ž\.7 ¿°ØÐÚ\Š"•J•ÉdA½BjÔ6^>ŠjµZÊ€¢ÜZ ZÄ2™&³™Á`$©R)•^»FEN§3±v=»™Ì&M¹Ò+¼nPm¶N§7˜Lµ¶`0‹Ã8Ž+äÅ”õrÀ¥]÷H­Z­Ñjõz£õ·ÖbPe¤Þ·0˜|-!T&,"®Qm6‹$)½^¯’«=‚ýCcLFc—oEóèèÿ[ADddÍ#‡DGG—Vî_,*È£ÑpíÚµzõêiÔŠÎêBûÞý˜šR‘XÄd²þ3Xác.QW>T}žkÛ¯)C1Îsd½üè#Ú`*×Sv<Îû'|óÓïï½jÅòþf=yb%J¥T*ßú˜‘HÄãòL&QÑþ¿Pæ×Ο8›Úbôø(¾F­®ü¡<ûÎæ]G„na{pfë  ´hß ‰Dr'9ùvòm@ðæ`8’¤j×®œ——g=F †ÄÄ+#Fýúµí ôcj¾W×Sõ¯·åÙ ÐJÕGµaÛñ>xÂdÿ®ÈÇÈ`0¤o-CQ”^¯ÿAVí aF5j×¼S¹R©V©_»EYæ¹`Yƒ¥P*tÝ—j"íÿ EQr¹¼VTt\|ü[3½!77÷;ÌkCûF|ÏÕ¿­ðköàÞ=yy¹5y:ö…!b ¦Þüž"€<ËëŠô—ø;õ<;ûÝP «¸|€ãø°Óhÿÿ¾\PE×ÏþóDáÒ³s<¨Ë gæ‡DErÞÓèʲ֮úãvFAµ:Í{u[:iz¡Ö"¶slÒy@B@x÷ä”ùäæ»¯É7mXðZÉÒ¬³çÿAðe݆N¨`÷žz~<ŽNNôöm)**¤+Ð>§/T!h¶³;Pù“7¬;43*Òñfù²éÓ3>«×¯}œž©ÈK?q:iãõ;Ì” V. ©ñ§ÞÑñ[ôèÒúö\ˆ,¨ü>i.;i7®gÇjùÜ)^ÛÖ8¢¥ü>ÔŠ®Màz¤Fû¶°ÙìÌ'™_º´ÈÇUä…=¿u2—k#iñóâßFDÇÖˆËUèdU‚ÿ:sæñöɇÎKhÛJ®B7Ÿ;˜¹cÅ€ñ &»^‡a«ÿªÍ¸Ðªãìbel›¾¿ý>#Vî4ëpXˆã³ë›¢šŒ`±¸éMËÚŽ]³~S"÷Ÿ?g>8:3aìÙ¬GWàe¿‘üÙíË—Úa}­¢ìŠ­È^(šI‹ÐFÌãPúÅÇØÖìÞ¯]mxµ·É ÊÝ´åDŸ5kŸ½¶Tê‚ûi…æßûv俤8ñÏÞ¹§uÐWå+z£ÁHU4íÛb0¾th?– ªSþð_—î*•׳ÞûEèvíú‡âó.7gõÁ™µ<†ŒYñÇâ÷¶ ùeÔ†¿<ÖÀBh§™°'å§ç+~k8mÿÜA@ô˜¹&¬¶Ïß)ðŒê«Ê©¹péÁñs¦@Ùãf/n¤3ÿµþìô‹W*7À¨Vqm8?¹ºÿü“:á‹Y>þ§Î ­2¬Ý/2 p‰+‘¾ÖrŠ´Ü8s„°õnÔ¸Úü…I¯}J5l¡ÝãC 6ΫYUf6ÑÑF£Ñh´óqAi*e0Da/ï¦O½úÏèiÿôøy€ÀDÈâ™ìÅ,à:Øhž?Ú»må•”¢Z~B>b1è-‚ƒ¬"aÉÊ)¦ÀßúΓô¬‚âÂb€«_ §èÀª¹ÙhÕå2€JNB‘ !*ªó¶n\¥6õôö­³s>]ŸŠ‰lbÛküBkáÊýL£úä‘}Iù6š‚KÏsl½œÝ3Σ¢/ŠÍ`ÑurË.åc‡ü*þËÕI£Ñh4íGõ–lCïÀâ»DxŠvÞÖ@QAYIö]ûàN=;v2QÂl"-šä”z€ gR=Ã#³Ÿ—u1¯[·^ÅÏó1ž¬^5»]€J¡ÔéÍöʬ ‰›M€ð¤µ‚Ý×.[å9hÆk ¸ùˆ©Ýι„wСE /0¤ëµ:6Oû7­:y5 ^MŽÅâŠf.Y÷÷æÕÝÛÔ–9x6‹ó€çwNÌž¿A“_žyßÕ<ͳ…r‹_àÇ­F£Ñh´ŒŽþóÛöAÑÞÞ¾‹ܬßsš¤ôP³–mão??’Bòî_h}Q½aJ×–Ñ~ 7騡SÕFím¸MÆ/ÝûÓÉÙyÈÔßJõEš5š¸dßé}‹»Xjð ðv05òZx2€]+~@³7ò!qªÌY3Wqz±Ý•bçî½Ú°©çA¡õûÅXÖÀ‹IèK®œ9zçqÉkÓ!(“ãàëææV-À¯šõê`Ù“ë»wï`¹¯ú}ì¶¡c;MøÛO ïþ£Ñh4ö1>eFõ£[eBG nò?¶©$7uão+-^M¦ÿÜþ‡ÌìoŠ>°:¢ª°jÅòƒÓÕi4Ú7çiVfÒÕ«#óµÒéŒêß«O˜QˆŒmêavø€’ï›užá­†´ÿÈ|zw¤½ƒÅlÁ ( èÜ6ßA ‹Å¢ó¨Ñh´/î“å©B†Ì«š þçÀÅÙ»VÿaµàSTE£}8ƒÁpûÖ­k×’Ìf3>þ3CQÔÛËÛh2}ÜT"´‡„V¯®ÑhþŸÚF£Ñhè“UÖè‡ÄÅ¥j™³  (ʨÓ"l‡‰}Tlô/)ܨSh ;)ý¯“S$¡’—Ê•ZŒÍw«âŒ½eF¤²´„%–ñYè;Ú@àæ²Â‰t¬â!d¿ºh©,/‘«´(ƒ%±—‰ª¬¸T‰±Ø"[©XÀ©¼tÊ¢5Áqw•iÊ K&G›¢üR‹Å`ó^4"ËŠ ¹¶Îö»šôÚ‚êÔ*˜b‘è¨ôƒY,–‡©©·oßš:u—ÇÇqè|Ff³eÇöm#Gÿò±ª”Ê=»wÉeNNÎ*•ŠNŸM£Ñ¾ OœQ]_zâ¸Í¿m_# HýŽ%sÅ Ã:Twý´sy•‘xpâêKwîóãý×2y×~=1£@åé_sÝÞ?Þ(¡-Ü«ãÀ±û®ÄÚ¿+-»+ö k4¨o‚>÷þÚuÛJtddÃ]ZDg\Ù¿ü¯£ö6 GÿBÀÍ ÇYΡ5\+Ï‚"L‰gŽØù6}pj ¼mðõàì6¶‡X{0©ž.Y´}ÐÜiúûWNÜÎëÙ·+·RUn{Ï_úë\‡ùúÚ䨛ºtØTSZ“úƒäu¸›VräîwùîÃÿ(´€W¥)bœó¢Ï=U ˆfVñ \úgÒ/}£÷Mjý÷Î3m¦6 ®íÿ3¸å¤†2(L¿ºbíN‚mÓ¸CÛa9÷Î.X¹ÃÆÉ³Y—¾uC\s3’֮ݡnT›~]âÛ´ipfêæÔb²º ¥óÂ8“Éh¨€îáûìÌfó‹ÞîÅær_$Φ·Fû¢>"¥Š"I'§•Q}qIÁßÛöhâIGœ¿Ù¸ë€„VMD6’˜ó'¨€%ªÓ,<$¼i‡fòÌ…S&-òŒmýSBì©þ¼žm†J7â™t¹³çþ?`l÷Î 6\äyzÒ¡g£Ú÷–”^¿î\ÒùãK`Шњ´CKþ<¶có.–{X¿žínŸØºûÄ©)3í5$ÄÃ6/§˜¦òµËæí?ÿµÆ“¸©´äyvVZߞ݇N\þ¶å3ß{¡ÆÏ#€É±qaå 0iÿ‘#z¶3÷µ‚ 0¬LQFàø›+‰²”Œú)ÁÕ«a½a¿Ç¹z-ß:ÆÅ­nÛ_ë÷âõj †19U<ƒomY/‹‰°ò깿¶Š‹«½ø¼~áÔF`R=Y´îÒ˜%½`ý’•¤GÄð¡CBüªt™‡Îl5bb³š.W­.±PsÆŒ¯ºÃ†v–H€!u³£ ÷3³?|ãÒ ì¿ÊtDõQ$IúªFû|DP…`6?÷Š8³ióœQ½—­ÛBJÃÛøÚ l¤z q±óõqeZ­f˜»³õbâêáï"ó”ð–ì¤SW¯îߺqÙ[Š•80X•kf²m¼lmÛž«Ã„ àñ„Í[ôŽt±Ô3ôت¹÷î=zôð’ùóä¨ñ²¬K7ïÞ±xù¹e&]îm­(¼G|dÛf |lÌÀ¶Ûtôêì¡Íßl¿Y«à¸ÿµu‡òL¿…§áÕô $®Í)Ð{ú lûÞÓW”$nM,’î^÷µz(Š"MfâmçÓE!Lûß¶L¿º3eçÌ‹é÷g®½p*?÷ìÖY—¶/É2À«3E=ü|œ½=ªx£”aŠGLýëòå« zù».ïÚ`7¸€)¦ºû¯¦f³DöŠG§Ïݸ¾ëeëvÕêQ“9ëÖ#ÅŒá|¼|âB­¤æ8Û`9¥å¾qi@ÿ,£(Šú_bªÜô£öRë+aÀ,¾½”ñ΋¿ªüÓöR¦½”É|½¯úsI—fuí¥F{)—ƒí\4Ê^ê°5Ÿigÿú3µ¬ ŠÌ~Ý»n?yOl/ýÿ )BÝ¿Elÿ•'í쥂”f´—:(ì¥碖[– 5“c/7¤­½Ôá÷½I|{)оò#BâªÉCºØKšÅb‘àÌÖ…öR‡©§ÿ·Å´èK¦ý2`Þï‡$oTE£}‹>jàêõð×õÛ7a#O;7XH„B_¤/¢(J§ÑY^äEŠ0Ì&]YxKY\^xD£mû6U¦*ú0–dåá«%W:tlulüŸSC%EEwq€ô´\¿Z~Á¶sþnÁ0>‘™¶`ÃZoë”ÇwíßÅeZÖú^^Ö#TäêlÿÊ£f0+ $Ò €ø(§ó× ^_6ŒíãhS˜ àEd^?‚q\í°»r¨nûîÕBé…Ùòa!@˜Õz£Ð†+qq¶!î]¾dâ»`.¥//,¦¼=þsp¤„Ñc;Õx…±x6ö®PéÉ—ï¦ç°vuøÊ„ÿnðÙ[¡ ›ßt h;^;«†â»×n®;h½¨ cÛþS žmtÃ&UlȉWKZ&‡íÖph-»¬ëû—í½º!‹lÍ-³¾xò°A÷_Š‹mP+2ÂdÒß¹ujÖâ%ó·dLúk~thµ#¿õŸ8mÆ”©3³x=ZԘڳ3gÌ_™I…t‰v˜0wÉæ½ÇK ,Š…SFo>xíµÆ# vDt½Ò»»?~Õ‘òQóz¿^å·jU5y×Qgß5ö·9ïŒlì´rê´×nÔ֖宜=óXÒ=«f|€<½¾¯G¾i(Þ´jn¿~ý†ŒšÁpë3pxã vß¡£FO]S%¬i„ÇëÇ}‹Ù„ZE$aQÛ¹tÊìÙW\°`múåÝva #1’ÐÜ´zÒ„¹ tlÕá'/ï–Ý£¸ÝŽ˜6mêü?öàà1a`ƒ9=Ìš=gÇÙ`T™a~¾q?-‹.}ÌŒY7sJžH^9VŠC͹ƒúÿ|>9íÙ³ì\…žÁb3P,%å̬yËÒK-i7÷Í_´†Ïã)²Žöï×UÀgMûë¢õ»ÊË 7­]yärO$±nb&“ßwt»ËË=}–Å•:¶ !uÆsû×Ϙ·<ãÙÓµ 'o8žÁe«'xøÂµ´Ô‡ÓŸ2„yNâè13ÒŸçݼth¤e|ðöèPHª· ¶cm¾­bâ¹ÛÏ=íÐÝoPïá²sn\Ø?iú\1{Éܳ–®-QªÎœ>¼rõF‹ÅÀ°Û'\¿xhÒ˜qÏ má£Ù3g¥•O @0¯{¯æ©;6–ä«ô–¶õ[2Lº´KÛFŸ™šõtÛê9 wÜ ‰åóænÝ{,3óÉ­äG “>cØð‰³sï$šG¹x±cVh„vЦûPÓzÉ q­âÓµ÷ðAc\L)–I jÀøe5Ÿ‘|[›ß|ð$ߘ; %rôrT:%e²E=ztSWæäîÈL9kŠiÖ§_ªû`/1€¸Éòž™ ¶ÀÞ7È…)%%ËNÒ–ëè-¦`ÚÜE¤´ÚMÇœBêÍŸç‘ö¼ÔÉ'ÜMòJ¬j´í©ï¼B -휼—ÿ¹ßÏ &a¦[ükýà¨iËN»'”eë A1­÷îmËž]»ö"Jt<iõÐ@`øü¹É7ÒØ6öAA¾Ö¡u¦Â4cµÜ‘˜ÀïÔÙSvì —Æz—ôêã&˜1êRÝÎ3¬sD1NíøzNþJ¾XäÇè=cAµë·)&OìÈh2lžwÝ{ bïÉ÷l½ƒ¬Ïâù"§z†:Še¿/œ)0æ_‘x6lÔ+¹^ÐÉKwR£Ú„ß¿q˧z\›fµW;y­\:G `.º&ón¸uȸáabyy¹A]‡Šj¦H¾°QïuËç  P•"“BÙ6L€Òè @qL QôefÀtØÈ¦AmFH;…„w_%¿ÌÝŠS¢I£…5&l}¨/YÞ‡À-<&ÅDÅ ´ |®PK -‰®àf̬õ­Ùwå¼Ùäz@u&ó'¼ÊI£}6Ÿð15ÿÞkÕV¯×§z½O?»¨ u ­çú¾ÂŸ¤Uﮤòõ Šq‚"cÿÝ´ŸE^§É-×È<[·ÈÙáøÝ®™Ï#µ&u7òE¿‘A¯Í.ÕI\<¬å¥ß í¼PкKãi»÷k ?y”V¼.-q?‹ÉoØ­Ç©jÿÙ[NÛ=.iÚy°ƒ@Ïaó´ÏïK<Â{7  9Rrÿ’Þ¨_2¼ŸN^eç`K’ÚMEâŠÿÜí¼{1”ÝfÙº6ˆ€ndèÊKî\*(-Z4yŠV^ÛP·nÃãK÷§d&]|D^l!;ñûƨ N\Ý+6ÌÔ?ŠˆmÞ"Æ[êàæÝ&Lj5›Íû²±ïWÅÚÉADÅþãø¸qãÔjõÿ±wÖñQmöÜï’\Üà.‚ ww+w-ÅŠ-Nñ¢ÅÝI$A"DIBüÜWæýã ¡ô%”(ûýÜ»³³ÏÌìÞÎþvä™N:½Žƒà8^nðÜßëA¥^d'í9q‰Ÿs.É0réÕá—š‚¬KgÓwý?¬›£¼a7?æ©™÷„Ørì|‚*Ûr½jÿ²uFûÆÝóÇ,ìZ£¾åÀÎC¯½8r“ø0MÆ’]kV¸¨š»÷<|ôz€òM)¢PP·µñ•&y{à[•j/¡¬Aýºówü°KØùìÖ˦Ÿuô ý»7m?èM¥ÝŠIh0FV»ÓpóŠ +öùŒÅJn¿¡]£á>Ú!±cHObÏ¹Ë ŸöÌî¡Ø°ø§ýŽî½Z/îÑ£B@à8zÇ.BHܾ±Ó[Nç$­u7•ÉE!Qµi ÷Zû3f–°´ê¸vâÊmª»»D-8.—¸Ûsîú¹¡/û÷«;Õ·õ‰)þ®ÛümGBíP^!ÕkPÝ-Nó5ò%vZ.)ðÙ%H)''_N)€" ÷Ïî½—e˜å ÀÙÔsÜñõB»4qÍ+Ý›8ƘK.$*÷÷¡(|ü¼Ž_»ÐêqÌs½I'â;T«\µÞ„=C›Ø€ÙhÑhôCG5¯Õ»kÿ!ƒz7¬ëNQ%svçàê›ÏíÅZ­ÖÁÃÛÓ½êî_~±öºê FÎðÙÖ¾ŠÏÂÚry"633Óèå=z”ÄoÐÅÁ?(´Z›åKÿÛÕC笛6/xÈjÀ“e͆!Ülàáfƒîœ\שC³â’‹mÔiÀÝ'Hqÿ¾º?¿vÉ¥ZÎû2ñí(f³©ÜÛtñâÅ|>ïèÑcgÏžFýî¹öñÃŒ¤ö•¦˜xñP0s×µ.!Nx§Ã¿ß{*Þr`—ÔG ;utÍÄù[Ø87v¯*¶¯:uÊ41ËûÐÆY[®f{öí1~HÞµ׫´òCd# Y ’8¶­ïó¤Ý°ù½#tZ EQSÔ½o?N/q+‡ø«9ÃG»Èê Z¸råJøˆŸ†wq2ê5k–·lûÙ"w§™K×שdË”7>±káO{Îf³ùzò00}êgœÉ·ô{&›ÚrŸoÛ¹{Q€‡Él ­f`K’ØŒï¿÷A`¶X,ff“Pvµ-2¥ÑƒÞ>Roh¢PÕzÕ4ÊÜ.½ûZªx8ûÌk>våbåÓ¾`4hg̽qÏï©9âïü\Ó—éåãë¬;|#ð¬Áç²Mú¼ÒÐ|ATLTẤžÝç9wäÿOYB–s»·¦q<ÇõéÓ[´9«—,½ô qÐâCýýÑgÑæn^»fß‘ Çõñeꋟϛ½ìQböüÓךH¾,=ñ£6j:U«Å²¤2Û­·^5P«`äø¶ šÍ¾é!@ƒã ½ºKÕê|Ò,–Ú¬½Ö•JƒToÞÏî·…­Û?S¯¶ °5 Ãè -ÖSjã®´B$AIq¸\P«”$IÙÕƒÐ\ V«8âjAQ$Åær1Ze­FEÎ5?o0Q|ÃXz£¡z›Ú4cp ÀôjǦ BSo4J]š¥f÷Ô›Œá¿Ög²1(ŒpK·«"',#L`ò Üh6}àb½–ÇÏJ» ^ÈG#G”=Ü{ÆO½g” `µ÷£nÜð(.A"óöeôŸ²ªÿ”?üžûãëØ2¿¹ë÷Í-s>ý1ðˆ¢ÊI$‹ÙŒ(JÀçë´š÷¿h?^Uá8^R\T6D£Ñ”‹£Óéu:}é®5¾J¥.»k…Éwÿõô #b”“¡²ÑÊî–K]§ÓétoiG…BQñbU„PÙ$JJÞrqWR\lÝÐju ÕÁ› l½7þr@dëˆS”V§Coç*•ʲ–ßWLƒÁ`0¼Õ¥VÿI$ Í×EÅÚÌ1 +VåìѵQÃF+= ˆÂ>Ú¶nÕªMëŽ×ò€Òå|?²O£ÆMzôê>ÙâáâÀå2£¯îԡ˃ô´öÍZÞÊ3és÷îÞõJÜ=[¹|ÐøIýûôùaÇ™r©¤=¹:bâ’ØØè9“FŽ~µcÕ‚9?l€É}ZÎ?ü €/°™¯Ýî±8R>û/gÜÓüÛPˆe4q‹…$ ½N«T”ÒÁ ×iTJÇ)Ä2è_Ç1è´JEIéð[ Njõ„(½Î ×i̓ZgÒ™„I˜£†"Iœ@zA§Q› zœdê´³^¯TM…(’²è-FƒJ©Ç Š"ÍZÜdø`VœQ‹Wv u†“;eãÔï9uæØ7{ä’VT¥X¯IQBå~f‹E­Ñàñî!ëïË~ƒ€©5Íú¿Ðÿ°€ÍÓë´Z¥‚þÓÐX©ð˜*w{·½‡ŽJ—êw];³ÇÁ/,0S7ö­Ù´÷‚_˜^E¹$&žŠ$Ï\RÜ=uò¼ÎÄÝq‹‹›XÊç¸~>¶v¶aUjoßý‹-ױǚWC;¹”QE5"îÞ´õsÌ’Yã`Ïm–›‡/Tö²7ºú€… ¬ƒ&B‚üTþi> ¡rß V(Š*í€C½¯3!TnÆ8BÈl.ßÎTvrõ(B¨l I’Øß÷nþtûCâÓüM¬ËÎTü¼/EU½ûïý¯‚2›¿‰’ÒÐ|8Ý)Ù‰Hƒ™‡ašµ|·ØÍ7ÀÓÃ1‚"œ½‚^Ûe’na ä-jßN€7¯:ôú ÄåJd‰Š ÈòvŠÊKÏ.ÊÏ+ àM}iÇj¥Ì‡é—R™ÒÐÐü=^wÿ}ÄïËi©¢¡¡ù–©°¨Êx•r¦â<³¨!0dÙVj[¯r`ÎËBؾþþqŠ(³I—™UÈ㉚õ¸|Çä•Ý=3°*¹ÈÒ2 ò_夦å±ù¼bEÆÁ Hyò‚gçêüWý$,_£.°u#6“-’€“³4?>`Ɣ˅yOó0Œž…KóѰhgƒ_EUtAeÇ9lz% Íç§bÝ ¶$*¼ÕÙ™}s•œŸîp½ï¡±Ýz^ª_µZ£z5ÀN:upØÈöu8Ç™¿œ ­¡"m·.é{bÇï³–ŽhÛ{`lHP“n}ä|LÀ—Ĭv$S·üðw]õ𥎡¡U­nEÛvЏ³`õÀ~·šõìíç ÐnÖœuaCz?n¾tÝR/&wÄ̉á]Ûæõ÷ã´Á‚OpMh¾ELfµjÕ¯\¾Ô&¢UšÓzÿ2ÖëmU~ŽãÉÉɡժ½à Íç¦b¢Š)ÞvâH™áÜõ;ËF¨>àvø€×;‘>Úv¤Õåù¹‹o&“ÏIL°qí¶ÒËU£2¿&cü^‡³«.ûù­T€]ùÂû¥' |"î>ŠøS;44‡ÍiÕºÕ_h4›EÒõþ]E±˜Ì[7¯«”ª i#Š¢(ŠŒìÒU¥T|ÂEñhhhh>‚ÏãQç}öþ…Í ¦…Í߇Ádð8¼¡Ã†¥¥¦âN·yüË00Fó-”J¥³³Ë‡Ÿ…a‡Ãqrr2tAÐw††æóò™<ª3m¼«Ú­‡h¾$Ìf3‡Ã f`=ÿáßI’bq…B¤(Ê`0àø§ÑÁ$AjµZ³Ù €q8‘XÈápàÛ¨©EŒF½N(Äb³ù|žP$„o£ì44ŸŠVTáú‚؄ BÀ:Ý][pïîc‘“w•*Ö_=«aLyö4¯DçÒÀËžolŠ0¦%'½|™ï߸§˜A˜ÕÏž•hLU›4·gÓ?ÍÇóÚe¿Åò̇ÿñ÷BH§Ñ={ú4)9Ñd6‘$!KÜÜÝ«…V·wp UÎуAUðä¹™!mÖª)õ¥GeNLH’{û‹™úÄäÂZ k#³ñ˯šLFSfzÆÓgO•Ê“Å,à lmm«W«áëç (çs Qxr\t® …Ö©ãd+"q¼´€Š‚—¹…¦ê¾w.ݯ׶%Ëbú(44_+ÿì£"iÏÞ¥»Ïo0||lRŽÕ…Ñ_ԃʬ˜©S¦üzødªâ­RmB̵þ}zl|bÂXrûêÙÞݺœ*þ‡J@CCóM`2šbccÒ3Ó† :kμ¹óMœ<…Çç_½vÅ‚[¬íU€ðÛ§ö÷ýô³×³ù¼²c¹(KÁ¤qã/>ÍÍM¾ŸuçÂñ_]Æ0ìÀO3Ž'à]MÞ1¢ ‡ÍžÌgÌ´ÅSÛÕbÐ5444 ERy¯^ä›ðÂ}ûömݺõ®]»Úwè÷˜ý¶¨*ÊÎXºæŠ+õoõòáñÅË~Èí•éç ›#ss ôò’ÚÊÅ"¿Oè—®§@«Ñ¦§§†‡‡×©×`þüùaaa³gÏ®\eÄÈÑ÷ïß7 å´ÑŽåK³ŠTþM:Ë™9{fÚÉÍÊäéSææèÀÕÉÎË+˜±Ým]h=Eó R1QåâíråÈf5@Üå EL±2íâî {/^ÓB4rÈÀ$Ñ7/ˆ‚6mÞ2~D¿àÚ3Ƶ½q0–dÀ¨q‹‡6 m=¤kX°¯¯ëÓ+G¢2ÞÉ(18Jߪy¼k¯Y8>¬e‡Ác‡6ô`$ÆÅ¦g@Ú“èäb¹Ê?Ö™BH_¢ý¯Ù4444VH’LMM­S·.tíÚÕb±Ü½{wРA—.]jÒ4,=="ȲÍQrç1½:¶éÒoP7ÿqwž¦qHmÚÝ;×9`ûÕ—W)”f‹¹JHèÂ… cccµZí’%K† Æ=z(‰Êö`Þ·~`Hï!ÍåXÎùK§˜`)މŽ10…lög§KCóeP1QÅqh2µSÀÊ_¯>LÕ÷>"óöþ÷®Î=zÓÙäš5€_ÈÀAC€ÃÌÆN*XGžblDÈaqlï=‹ZÎ4ïQjq»Þ#„o§‚±„6v2›-cpØl¡H6"Oø¦!šö¨Nó@QAä'ü‘$Bè«{Ë~k @jµÊÞÁ¢¨«W¯ŠÅb¹\.‹8ÅÅÅ€AYQÅ`±ùb _ ²` Hì1“!³óþk$3nÁq ìܹ!$‹9ΩS§ÀÆÆ&/ïƒù–TâÉl9,Ž Øe'ub00ÌV&gÓ.Xi¾y*úUÁlѵyçSÛô(6¿òë6tÕÖ5ƒ_´äPÓkT`]ÜÍdA¥ã7i4ëu«’Wë^ic:Ür>uL@¹4BF“·XŒB ¸,&› 3GéÌáŸÏ6*Ô< ø\¦Q à†}ùBi¾XpOKMKM{A$ãSô(c&‰ƒ|ý|õú?Y ‘æ Ãb±D«Ñ0 ‡³cÇ[[[™LÖ´iSˆÅ &ó-?/eÁ ˜L\ž7©LyéY80þéªÿ\ת‡š4i²gÏgggW¿~}Ðëu¶6våâ›-¸…ÀÍK¤Y `T””¨Àb}}Š’†æ“Ra?Urßú íb“Ø×êôpØ772ê¼€ÃjÐkÎØŽBõ©ƒ{úõÅ‹¢ùkv´èV·’ø­O8~à°¶~O(W_ìµå÷¤É¨Zé亽Óórõà-ä@˨VËÆuZlÄH¾@ø¥•…¦,1¨°Ü[!—Ÿp €±8Bë&¯t&± /³:©mËuz-ùSE¥Šêõx&—ÿF²YC|¡è]&G(äÀ{LÑÐ|ˆBYYYŸ\Q@ÕÐj6nøäfi>%p¹Ü¾}ûíÚ½óÙÓgíÚ±9œÔ/.^8ß§OwO¯â¢Â·›jˆÍe"Üb±ÆåJLDQ@ 3—ËÅ, ÅárI£ÁôåWJ &£VíÚI,œ?·s—(''§¼¼¼K/Èåòƒ†(%åâ#Œ‹ñØ”ACQ”–ÁqqS…Ѩ7b—Í$Ì&ŠÏg~vEECóïSaQõI*ž,èZLÂ_üëejÊ…|ùuÍ—†—Ë„àS÷ 㸅ˡð~Ñ „&‹9aâ¤Çcoܸn6™<<=,XÈ`±ßQT€*u‹2ßš€l2™¬ÃW31c`m"ÚÕ¨YóÖÍ›wïÞ¶—ÛwïÞÃÝÓK©(!I²\ñ­^ì­ÛïúË%ôzý¿–yš/‡Ï3ý•–A4EhölÞåR«EëzîÎŵéÚ®¬ë²¦’}?¯Û{ú¦ÌÎÞ»r½ñ3§Igzžéä鉫ûø¥C»Ö7*JÞ÷õ\n4ˆº uöäY úMïݦü?-Bôì¿/„Ùl6›‹ª††V¯Y˨×i †ÿþ:Í!‚ JŠ‹D"QT·îÖ@·XÕ$=w•†æÃ©peAáúƒg¹9; ™¿Ã0‹&}êøñ×âr1 ;±s¡½ÍðµÇ>ø9D öŽ™ºÀP>>•ÿàÚíø0;ñ×v8Êíºš¯øTÏ?2o›=œÁ` ›ö†aFF5·³µmß{B>`¿o›È`0œeRéø-1L=(ª¥£½ÃÌM—Þ)8J¿ÐÉÞ®N£–Ÿ«0 [8¼ ƒÁ˜²jÿ»—HW”6ypg{¹Ý9;0 {|ñ@e7[[Ç¢o¾Rc09 5 ôó.̸7gÎ<ÖÛ¬KA¤~Í̉›N?>v3zÇÖu½1)Ù&u‡k³áÄå-+}ß§ù#üKÝømÝó×rJè€o µJU\Thý™L¦ÿ¼¢*‹Ñh,-»J¥úÆkš bõ¢Ìqw¯¿zéá§; €¼ì’®}×q€Îƒç=Þ·„¯›‚-uò“DZ±±©/ Ð;j£$7íÞÝ{™™:’ÅÈxó$1ÕHaš¢ôC»¶mÚ¾ëiv±–D˜Rbc¢“Òr¨rvžõâÙþ;{-<{dó;ƒ¦àqlÜ‹äÄ„„D3‚Wé‰Ñ÷ï?IJÇrÒžDGÇ&&'%ÄÇå*L†™uЬ—ùïêŒÁ³„éq IDATì6xrÜÝ£¨¸€ȼµæ|œ^¡TNmÈ;cSûákBùùù#š5ŒRëàO?Jƒ;=Û5ycŒÊ,¿cV'uì9í`±"æàÜ ³Æd#˜¶`Õµ¿PïŽQ ðóÇvçÅ% ÑÃmó.äÔhÝûqüÅPW/5|}¦Â^åö~•*ï/Ò¼Šõöðppt ©Õ4“cÏ‚5C+uìØ¾eD·˜|Äã¨× ‘Û;V©qèažÄÖ&ñþ©UƒäöõšDdp$Û6¬?uíÖÞÕÓ²žµl5w|·Z­ÚÉí³bTI·E"QIê]ç~rÏ×Fàå4øû9Úéô&‘HªQ)s³sý½Cy@’2Èå$œÚz,kÿ¾%˜™v…ðͽásgä3€•ásç…†æë£bÝ®8¸ïפ´\ýžÕ”±ÿ Ö¿í\·ñäã-¿Ç†ðˆ¬¼b ³  çmßýÛU™\Žƒ`ÌâMõݰR?RfMÚèýÕB9¨ ]X›WýøÊŒ©K4m¦þÔQöèÒX‹ØõÀ®]Ç~Å_ºf·ÄÑÙhÀ-ÚѦ·ÔEh.?òàiÒïÝîJõŠlðìÖöºígÏ]²T[bò]×Éýð]}uŪ^óV'nõ[t±Dfì+g¹Gí^>àùå_Füx-æþ¹w ʶñ |™y( àbfGŸzÀ`[’nßÏ/mˆ6Ø/gòWyή•¾5~píƒîBíV¥V4¹O¥NAÎ/ã÷œ÷rsÊËw7ÊxÞy­#Ò\\ðÒÁ¥Ú™kúôª²ðäUhÝŸ+ᓤåk¬ØÒïl¿—¬¶Þ##Nè ™—N11GWM›¾üø¥Å«z»Mܵ(2èÀÊÉ{Ö®k¼mÑá}»‹Œø£³»¶lØ<´õºc‡´úÓ–Eý ¨è…D$”Øz|·}YtöÖK—ö¼¸íÓît®.çæãî—ÔpèÕJ“ºØÁµ’ÈzÓ«ã‡ö<|žW«ó` /úñ­¦NÐd'Õè0^ða½xFEê?ìžþÛ)Õ‘98éó_/šÿk©bñ\Vlß;ªWëq?ü0¨ucÒª5Ã{va¡7>áõˤÈüõÐQ¶}¥6íÚrÌwï%•µS”t).—qáÔ±e㺠,jhצyZ5¼$¦£§ï¸Uï±mÑänÇ,ó}m͞ݿhv"ùº+bÊÚaräç-øad‹Žã×l àq¹m#F,˜9cõªù¦¸ãLǰãGï›VoÝâUíZ®ÞóKãF=öX¯KŒ€ Zasæ-z·˜!„(“Ù€À¹nŸ*¢Œ#F¾ûã»"T¿½ÙtäT H(sI‰ž)bS–·œ`„‘ÉSª\µ‘âp˜`6[ÞMÊ ¤*Ñ >Y(  Fòëœ;£ÉMõk1”гNsõø¦ ³W¦å)íä<]±€ËçËüø$›ÍebxNò騒 T‘fŠ…!¯* ¬$nu~–ŸW\¤È1tvKFÔX4xD†…µêÈ R&·gàú—Åf6‹)ä1®Ÿ>¼ñL´«‹s÷¨Qgºý >éØìͧ҅"ñÿýÏz|åbô“C &®ØúÛùS‡3€¿\ÿ›†††††>Âù§I™“_¬ä˜Áž fEB¥„w'èF{'÷þïÔÀµKçlÖÛêͤ±ñ¬& XÙIÇ&ÿxèlì¥Tiáâ'/ó KJ,@hR™Md—Ý"«FF´ãpÞZ£!@fª•ÒL`0ÙöN¯›ÌÚ¡C00D,Òˆ‹¥Õë0J”J.¾{Žî¥vÞ'`ljÅšì›R¯º€"+..Û4§M³/àzÔݵÆÎm¼¼ÊžÇÛbC@HD°kܰIÄâ÷¦€1˜‰LĨԷkËËk~‘ºy¼{Q¿ª4h’¹hEL3¨¥ ë¹oØØÁ}»N;° Y˜N[pàð™ †>xoëד[ƒìjì×çhæ%‹º„ O—+—~Õö˜C™`` `09@áfŬÕ'fzG{ÏÙßÕu0 !™w¡¶S‡¼wngçãtîìŠ)dR©Š ¸y±9%Z‘“ç‡ä?¨qϧOz+”…‡„º"ÛNÞôjf444ÿ‚ßï¥>-¾B’d™F‰òqF{UýZø[c0MzÅÓ'‰Å ÅËŒ¥ XŽ®ŽÊÏ’_)‘oÝ`Ÿû·.«tOã¥æ¾50Å-$̉Ȋ+Q_»÷0§@ÃÇôŽ> í®Üzb0‘ “‰•y™ñ/KÌ|§ú5«%FŸ{©Ð½HŒ’úÿ‡nÝð¯ÑL÷âÌí´¬ŸwĶ?ͬՓIQ¢(ë?ôÙ…-mÚ÷‡?i„@Å/Sž$¦+”EOÕ€Þˆ)Èyºnïýß €›ç¯Êƒë‡ €[·zHì¥_cžE¿YÒ{DDý±Sq†‰|ê7÷ãÏßwáÔÉKÏ*A6 xù<1õeQÑ«d €"+nü—rlaÍÚ²b߈Ý|4cèü¦Ô“¸§:“!9ŸÒYàëâ`:ìÈÒ>æPílš~øwóKnþP£V]³›WÕ*Áf±TÎM>ngë’%l°dN?¯ºÃZ8åU =“XÒ¼ ?ÓÃxGÆd6nÙSeï_©’¿½Ldã\«¡?åkïz1±Hì^Jdª z:X'´#–ýÂ_ ¨Bð0ŒËµ9¦ö߸°§Vo¾xåˆ3“iãRû»}ISê0Õ*åÿïäÚp„R'7ÿÊ•ƒ+ùZÃè)«444Ÿ©“ψI££úŽŠì=²}Ïá­£7ëØ¿I»~ Ûô®ÛªWýÖ½IÖŸy‚¤ùR©ð78›'nÖRâÂÅ«§?o>(utK¸qLodŽŠ ­Ô6ªuÊæesgýéçþÓæm_¾tâ¨auGM¯UÖfSoßš!ßMŸàÑ}`7§ÿ–L>»z£V}ý;@Vmã3v®_8kèÊ̓§6¯œ1zˆ½[Ј9óßÉ#´^¸ØÙźãà^¥]„ÀºmÐvɤ‚eS'Õl5t^ªñ×Bí=ü¥áb±sÇÈö “ŠƒküY)ɘÓ{\ˆ ¬\eÓŠ­+¿xdË­ mïIú`&™‹OφáÖ¨ ¢úæ+¶,Ÿóã„Ýwk ¹­Ô?Pòù¿,5dî ÐéK¦°~ßýóõø,/ßõ«÷l˜ß_(Tòõ‘ÙÚ@`ÓNƒó,í¶üt €Ô{gZ½»yd»£ ¦±FÌn]MZÑ;õ1ˆðQ«Ð¨UÖ]³Éíü½‡¥G‹rbãÚpä¤/L½Ê,ÚtêZiZÅຮÞ~õ~“Ñ:w±5|ãéû,šÌ• $ëXÇ-V¹C’”ZÅš°bï„{­vHÜ#4¢Ô²Z¥´:Ñùk¬1 ï9©lÍ„pœ )c2,6Ë:ûïùIÄÀL“ÅbÁ7SöÏB¨¸r_åR’ÂqÒ‚$$I$’ˆاX‡”æ_ã#–©ùø\jço6º¼/?¯Ý¯¿³]!ƒÊÞ_§R¡<|HZŸÊT…øäËÔ`”.%!Õ&¸¶»Œm2?¢\¸6ë衳U#F5 ±W¾ßïT…0 +W,_³nÃ'wþi±X¦|?iÝúô25_8A¨•êøøÇyyy$EÚ˪††¸»¹ÃÿYŸô¿EQ!--59%É 7H¥RoŸP&‹ _sÙ¿ðej`ã—ì:nJvv²Š*‚´à¤UT‘ñ¹q17w.•™~ò8½LÍW@…«xô7ø$FÊýŸÊ…ÿév… VôÄ>ZQ*~s¿P0–¤n󿫍€oã;vúÜO¨¨¬0Þ¿hîß$I“ùÿãÑ|VH‚,x•ÿëý}úõŸ_N #B»yÅ ;O åöÿ²PF”éÁ+žgrÂrßl¤9oDßWS d2YÅl"¤R¨.]¼øøñ£ˆvíFŒÕ<<<++sÿþ½‡Íf—‹_y½EÓvL¹½µ)ëßDU”rð×SL‰¬üeGÄ…£[¦L[Æ“Û3éÇæóAÁ¥ù·¡(J¡Pü VïÏŸ*?V fppð½»w6nb}W}!k1›oߺY¿~ƒ¿oŠæŸ!¤R*Ï?׳WÏÀ ÊÖÀjÕkV«^sÇ/Û^¤¤iµÚÒø“×)ª;!rÃuEñdr©Ao¶uñp±uêÜì—ƒ…/²ñòu/ÌÊ4ZŒÅ’ÉL%y ž+”yùúp159ƒÍeªµF[Gga(,VÊ=<ÝŒª‚ôŒl cËì]í cIÒó,©­Ä`0Û{xK1åŽMÄ•[yx{Ê8–ô¤ Âxby@%/ÊlQ—”˜XB&«¼ úkÌ&óÇÅRñБ¯ûÊEb‰¯ß­›×>0dØEIqÙÇÁÎ1`Å+¸)-918Np„277g&"Šr3óŠTLŽÀÕÇK¨ÒÓó">Kê mfN!ƒ-pòôrp r³•*IÀ9:Hò_fSIPådTç¾|©Ö›¹B™—¯7S“ÒÙ|®ÙŒó¥¶®Îö‰÷Ÿ:}Cƒž=^”•¦Ô›9|±»§§ˆËÀ *gàûÿ@CóÑÐ¢Š†€Ía·kß~×®]z½žËã‘$ùID•R©ÌHO›2m†Z­¢ûþ¾XH‚LII©\9¸TQ•Ò¦mÄ•K FiC&"õ[×ÿDº6^65¨Y«¶Ã§LM¿{•òn}eïâ ÖÎ^¾ÃÎÅÝ»R­é?NÔ»‡ µoׯÁÃùú‘Ã&ŒE¨ËøU£#Øu5jÑF•ýñ$•¼“â™Rßí'öü¾îÇ#W EBžßü•?Ù–\ k9jÄ÷ß'ߺ$¬Ùÿ×yáÉ/^X -D6#úט?s†a:=¹â\tc±PIJØ^h²¤¸D«S>¢\xã&aÙÙىϞúøúêtºÒð‚¬û‘có‰œÆaa5ÂZ{K/ ,K¶ì¯ÊÏìÖ% ËÝ/]ωÙ5rÍ1ã}ëÕß»é¥ÖåÒþ·­Ó§œ²ã·›­Z‡ÅDÇ5í©|‘ðàaâöÇÙNÏÎM]°-’$D_5ª ³nãÆ½ú‰MŹ:ÁO¿lI¸óÀ`ÔnX¹q@¿VG×®Œ~‘CXÄèy3µæq9>—Dóy¡ky&“)‘HGŽÅd1 z½ÅbÆqËßüîææ:iòdÜb¶˜ÍŸ»ˆ4ï…BT~A~@P u·l·»ƒƒcaQBT¹¾6;[{'‡-Ú®^þã¥ó?<»p ôç/]ë;÷ÈÝÛ·÷ïXãL¨ýÝÝF®;¹fåòÜ[‡òü#FÕ žÙ·!±«âî5qÿ¹G çä KÓ‘kbãŸUûuçÆ}gštÔ·{;mvrlb–­\æãVeÊ×Îοwp­Ä!dXŸžÓ~ع~É8[¿WT‡íÛ83òÖî¹Í“È?n §Á '0¼3bÁËË;%%…Ëã—ýÆàpØþ!l`× ³íäîƒÇêûÈEÇ¿ŒÙ§çW‰üèâù“‚ì fë¶C¶oXÛÏ%þÐÅ›&NíÖ¶ÆÕJ€íëîÜÒºÓ§/lU-»Hvñú½ƒãmÞ¸óÊÙÖÃÀ£jûKÎìßÏÚUõð™¼ÏÁßöûJàibÞw ¿kÕ¤ï/˧5òíÐ6¼}ÛVuüÄÇþF0X,ÝëGóù¡[ªhh^c2™x<^‹áַ˧Q:î[[Bî+CˆúÓ{„an±`ã}“N$bG@ñ+µÇ7‘¸€Ï—;¹€RQ$ÁáI\Ä ‹æ¥ÙL6ë:¡U‹ºk×”Ú{‹â貳ݼü]…\‹ö•­ŒgVy6èÕ»—Ì¢hÕ¡¿ØÁI•øH,–#€Â½ˆ'Ò”¨4úW™$T>²sÅ…§¢Ukg»ª¯oÍ-ùØÒ¿wrBˆ ßÛŸÆãË9®ÍÂ1 R“ïÚùuÙ™ԎHyòÌ-¤{·NUüºýk*&ßÍÉR[OÏP@\Qd ùÍ»oÕ¢VãÚ5¥öÞÆ¢G\žÄCq6C€aE%¥ºÐykg¿É«ÏÆÜS\&U z–Í-ªhhÞŒ 2™LV¯WŸ¯ËÇØ· “ÁpvrÉÉÉñõ«TîP^Þ+WW&뽓H'H’ÂøLYµ ïsVÕöCiŠì8 7›8b׆õª}x^Ù¯Ó¤ñ<¹²?E"Êl’¢Ì8Ž›ME$åY½E•‡OÜz:±MЋÄt`bÄI`€TÄÍz™œ’YÃÄ]‚š¸ÊY“OdE:}tñEB‘@ÀGˆÂÞþœ°X,ÅÅE¾¾~ùçŽH(„p³ñ( ! cTkÛ;oö”3 áÎæ–­3›Í" P3¢—eá¤[éÚP>çi\Bݰ !‹ÙH˜,$n1€'y6®õ«×Zvü¼ªO†IŸš˜ä‡(ŠÂ-@Q!„a O ¸:!OËÒØ{Õ ôpÚú$µ¸€¨Ø 2š ú뙆æ-°OÍç.Íÿ‡Ád>ú¬¸¨°l¸ÉdŠ~p?((ˆ1ÊŠ*ŒÁ©V³NµÊ¾Øõé1X§]û²ŽžTÏQ7¦_u[!{ß–­Zz;ˆ ŠÛwÔŒÈPîÐÈÖ§ÎK3 „R—n‘Q2Œ# sµãRÀjÐ$,8°þæ«R,îбóÆý§l©LîÕ¹;` ]zvïDuP<½´`yÿÖ}û =›):†T¯]­ŠàT§IýªàSµ~óú5õü¾cöÈÖ§ÎO3 DRçn£¤CФiS'‰S>m*Mí×Ï4`V”o¯.=u>£&`î>ÁÕ!¿f4ÿ*ì§Šæ[ã“û©¢¡ùÒÀ0 <{öìá£Ø-ƒ‚™LfnnÎÕ+—¹\nÏ=µ:Åb)ßÖNµJ"•©UJ Ã$R™F­b`˜HòÚU¯A¯E$«Õj6›-–üáÂW­RJe6½Ç ©Ì·˜õz½ÌÆ–$pÇyü׌Í&#Žã"±D¥T0 kZ€Íá€Ålâpy¥6•Š[;ÜbÖh4-~a~áÍ›×]ÝÜš6k&‰ Å£‡±ññq´³“—UG,‘j5j±Dj2ŒF£­ R©TÙØÚYãP$A’$›ÃU)EY/—•R!³±À”Š©TÊ`²%Å666ƒiµYî*éuZÇ­G§ÓIe6Vû æ=-ZšÏç³ØœrnVh?U4ÿ2t÷ Í·BðZµjyy{]<þÔÉf³ÙÁÁ¡nݺµëÖ7ôf³¹œŸáR§ïn˜Ëøû(íM¶X,åü€ü…½^_6¦Ù\$IZ#¼¥™Ê8z(k¡¢¸º¹vëÑãÞÝ»›6nP«ÕR©ÔÏÏÌè1<°¸¨°\ƒkiYþ¢åx§ìÅÖ ¥RiÝ(u³òáW©eU/ Íç‚U4444&³ÉÎÎnHψ"Õ*%Žãÿùn\œÀÙlv›ˆˆˆöJ zÝ»ŠŠ††æ/ E ÍkWF£ñÝ™ ÿyUaíN²X,–w¼òþçËNCói¡E ÍàœxÏD§÷0˜L‡ƒ}êEihhhh¾.hQECó£Ñø06öÞ½{8na| Ó  Õ«×hÚ4 0Œ *&ÈhhhhhþKTXT‘fÕ–Ÿ~0Ë›MÖÞ?yÕ¢Ë[»lþ´¥ÛÆœHÜÐ)ð=ÑPÌñM+N=ß±k£øÿ̓¥ð’Îa-;¯ú5aËòzC§ôhü©ÿIJ”åÂÁ5QCç±9ÜV½§Ù4Ëzº^™Ñ³S¯á;nvôç¾eQÇ~™7Ë}õâá’…—tmުÚKƒkÙ¥Ç]¼;gÇO-pôüޯǟ?{=kÖ,ÿ¡íUÔjõéÓ§nݾվ}‡’’’8›44444_.UTbô=—JuC·­âå…³wœ<”*,¢…#SŸÄ>~šÊÚÔi1uÉÖrÎ> ©¿s-:¨QÒ>ŠîRÇœýøÊýD"?SääÃx™}ïQ WêX·Ysg!ó!‚¯íÒ‰kª4mÊOvÑ-qò¯âSòòÙÅë±|‡Ð: ¥æŒ ÷’D|>‡ÇÆn­UMŒ½‘’Í“º4iÕ¼yÇã4ˆ¬°øÞÄÙõ…(îÒn¶GËŽþÜKçOªÔf;“«×¸±.%š) ÞÕš¼"?õö­3ÆõmT»’}Ìõó©9 ™k@Ûfµ!*éún?tp-»³¶œøýTb.ÿç}Þ{tr¦}%à8~áÂ…•«Va &°û'ò…Âa#F.Z¸ }‡Žt Í·L…EUaÆó-{N5×{Lí_;'ùz·~¶ÿöÛÕm›^ÚUâS²pþB[¿>î.ò‹‹ˆ“–¯€J¤9úä)K/ݯcI_½âÇÁsîœ6Õ¡n Èz¡°u#@ùÝø™¡á­õ®_ˆ/Ù4«g¹—aÌ[¹ê·Qûî ܤ޾{ßì¾ÿäà²Ý·Íš]$õvb™/?Hlë’:{Ë㆕$:–­R-ôß9iáü•›„©n]¼›~Ù,šdPV¥²Cöš ¿º«J82sîŠj>’G¹œÐ ?³Ä«RìÛµÅ%‘¿~nDVÌœ”A:7¨UÕ .ñÀžM›± aûŽÏöì-‘Ÿè[•O˜òW¯>>b÷mð­šì”ªbÕjÚLJ{ùýª0›MVEUQa„aIÖ ZTÑÐÐÐ|³TLTa»Å€É:Üð’É@1hHçÎMõ·g^ºÞ ç;„¬ùqÑ›è8Àë cø»{ „lÆvwu1gßN7yX4'÷Êö™¿—dÝÙöÛå+ÕÛE’uÌ»0«çÛÉ¢wN¼bus ŠXlA丅Ý* çŒêûàê™Ç/±Ë7VƒåyïþKK„‚Ž3w v¸ùX˜töÂíß7œ¿S¹E+ ×<¼þF¶ ,Š•³æÛG|)xvùW­¼Q+¼2˜EZÖ_رqtw÷§ùž‘ífŽŒ?ò”¸>õè…¸¯N[½Úÿy£Ã‹f ~Uµ(jáʾGæ¤Ý=•Ë èé`çìÛox?ÜOZÇ]bDôÔ™¯ €ÉdBðñÓè›MCCCóMS±Þ)„iR+5¯?Ç1ÌêÓVi41l‡-Û¾‰ ˆÁ`PiÉdIER8N°¤Ø!4FœÃihÓóì‰cWn>{q]¹D cþŽÝ×ú,Z N0Y|1›¤`²0;w`.Tð¸&ƒI’&•Öb6aâq]Æî™ýÝøÕÛO\;8‘º­Ëæ'S•ÖÎè]ÖšÍWÆlYAQY¨4aƺ®jA±Q0„Ûˆåä›r±0ŠÉ€Q,–ví½Ösî2knå.î—°‰}«£Tý ¤9DŸ¾Çî&é•)×ïaÙÈÿ…ņsmóñ ±•Û3™ï]ʾŒ&¢^Ü?ì`+“Êlçm?ûW›A½+¨(\óó¢Y?í<ηµ·€ˆPÍÞMnïжçJnÏáp¬á¤9¯WÛ¦r{G'—©O‚Ô^ŸØ²A ¹½ƒ»‡÷Ž{Åvr{kf£Èí–ì‹¶‘Û«óåöá}Æ›øJ¹½ƒÜÞÁÞÁ±q»J=ŸÏ{tfK¿q3á©ü_cvÛ†MY$B¡ðOùo€ãxaAaVFVVfVA^Édúv–¢HR­R¿Ìz™™‘ù*ç•F­zåJš R±–*Ò¬¸üû¹„Ät²äABV%9g …,žw­ÔÖ³÷ŸòwäË=kÖð· ªæ¿çôžø ažžv ª8Ÿ=xX--ÊÈQyÔ‹ 9¼ädô£”k÷ò´~þMF‹— Þu¾²+(Šp×Þj–I“JºwYÉwëZÃÞø’!p P Š¢d¾yÜ\²ë˜³â‰Ü·z•J%·J´Hˆ(DBA-œ=±ö׋"9Q¨eù8¯®Øùû{N_¿u×- ºáÑ9äY¯£Ã›dEQˆ¢(@ˆÂUO=ˆKËÒ ²›UuØ6pñ’­‘üM˜C£VLÙsè¼äúÎçCZ§J¹\Ìvì\ÇÞjÇêš¾íõ§ _IÆŒcÚ‹ÔĤD³ÅD8‡ÃðÁÁ•ƒ‚ƒƒrKTíË—Jß`RŸált›^]™ogï“€Hc\l¼Ü;È×Ý·|šbÒÐüÓT°¥ ×¥$¥To\ÕWŸêìWwìøQÔ8r@ûÀpŸ;}¸ée“§/N¶P¹Çèž5ìoܺoâH†Ì/×ç8N“,ô–»­^9"ùÆJõÚLœ8 Àaß¶9y.ÆÆ¥Øzú”M‘0*¯Ýˆ mÖÅú½Ì`KzôïêiLQ×ÞBýÜ'ý4‹™o‘úO›;.´IÇnMªT©Õ,¬A^ƒû:Õ^³td潋Ò||ݼ|G ’qïÌÍ·r‹‹¯ÝN ï1ÈšŠƒoÍÛ:{76 S•Úõ뇺=#÷ªÓ¼wÜÃg°Ç¬Y[M¬ºwû’ÚÛø¶™5>*ùÞÝðqkxáÃg«4Ž´®Ve}ì­ÊïïÞ™¯™ìG‡Æ›¦/Hè3xغk)Åé·çÌùä«{tø~bAq΋Ýx’ FŸ/JNJW?ŸüÝìGI/Ü<;{ör‹PÄf³àU‰¡ã¦gææ>MÊ2ì¢Ìk#'Œ}Y˜pÿì”ï3E¢“¿îܱïhbJÒÞmëž‹c1ЦMžýàirìÝË‹­Ì3³Ÿß?þÝ´ùñ)/ΜºÃ²­ôù×äÄsl¼Ùqg~X¾&&µè£¾Ó©ŒÉ& öd(_=Ÿ3}Â΃¿-^üÃõ¤¾u±6Œãàã«ÈÏÃñrkk°´êÌ ý¢Âû/jÜmt‘O@ê^¢N=zGN?7u_Xß^ƒUÕÓKÊ0'Ä'^=y%røt\K‘Ô+Uáà±Sú÷Œ4Ëü¢Â+k^%¿ÒãA•k§]Y?fÊüÌÜÜ'I¹‹ûäîé¥ËÖ={‘zõôžE«pÅè§% ·î?–žžñ"»ÀÂx6i—÷ðLÀ‡·o}Õ Æèè…EùÃGŒœ8éûÉßO=v¼Ü^~óæ “Éø¹.¡Oé?|€QÀ³>eÂ3Fm–JX¬Oà‡$È´ÔÔø„¸ Lønòä)ÓÇMø.¬yó‡bRR’¥Ri¹øŠÜ‡“¿›Å’HH]b¿¡ý,Þd!T1W¢¤¹pÙÂE—ã²ÄbI DCóÙ¨ØÉ{ŽŸ5¿lHÓ–r·j v~õ&ϨWæ¸CïÑ_oŠ+ÿ®Ì[Í¡ÚÔ)ÕJ÷8ugÌ«[º[*J˜aŸ‘“„.ÞÖ]Œ)®Ö¸! [€ÛŒù _ŸfW£…ÈlÀˤþfÎkôÆjÇ©¡K“5ÉÎÕúÍs®ÒÈ eC[€ÿÀ·‹î0xüÔÒÊaQ•âï:x¬ÀÙhÊP¯CWbÞØkOKÆu«Ÿð<«€Úv¼§˜ç)wž¶lvUÐÆž¸ÌáÉ:ôëzê±ÝÔŽOºp÷þ„f­Í†W w/åcàÇáX,ÓLTH‹îáuCd2%èSsþ¸áLhîf[ÿRêÄ“§OåQÎU8zeþ“øg•Ÿ¼~{T½0¬H™ü(6SO¤Ç^k:`íÏ :¥ŸÛ|Vò‡€0å¿J-äºVuÉ\½xñŠmkÝ*:çQJeSà,Wjqdýø:]X°gäÎzÖú÷ÖëõËã_Ÿ‡I1Ú·‹pòèzJ|~  ³ µdßñýõ÷÷]¹p "ÇY[ª@âØØƒwåìÖ¸'ÌÙêØyHù‚°n}+)ïüùÀíûY.¢WÆö«êL]Qb·^‡AMkx3ªS‡¶ßϨѬ9 3D_>W<1àîóÂwîWå(•J=Ø»ŠÙO”!â˜L¦ –ÿ+ƒ"©W¹¹ ¥bôØqeÃÃ[¶¾Ã»ùøñ£f-ÂÍfs—+–›§Ì_±ÅL1Ú ž·pZo\Y”pyϱKäN='ÎÛ¯í±•“ç¬=dïì=nõöêØÃµ»ŸnÞ±<'zÏÐégÎ_Ý9oôø¬W¯ž¦dTiÔ>¬šýÚU›¼ëtÞ·wÁšéó‚šw‹l^uÿÏK³-3Æ;Éœ˜mš·r;ÅàDŽX4sBäÅ_Ä%Ç é;<²C÷0?óá3F*¼ûÈéSFKHmEWÁÓh4­Z· ®\èëçßÀ ýûö±9¼ŒÍ]«Öݺ{ÐÐ)CzWñ»/Ÿ2ü÷7ºMZ6sxç┋gϾpÿy@­V»Nì>ÿnÌL=Iq¤Žm£"ß²òÞ“´°¨±ËVL…Ìë}-ßwã,–=oѶ®£§Uw¥fŒsùazãÖU%6‡Ìöv±pt}Uß»7Y¶a;S÷Ÿÿ+Ò|íTxLÕ¿Ãùcñí\¼yŸ:uppõ`þm³l;>öÚÎ'º)_?âª5œÐϧbÃúN<½rèøÃÚƒzQÃç‰EzE®Éb„*­Z[b0ª>5z¯Y<ÿçí Ó½,¸Á`Úƒs“n¦YSÝÏãàÍ™­Óü½³Ž«"ûø™yo^óèî”EPÄî»»;×X»»»»EAl»E±Hé~]÷÷ÇC,nÈîºêîû~>|>ó†;çž¹sçΙ;çœ+-(5åóÔ4æâRí§UÇW,_qéÖýݡË?|pªÖhó’·Èüð:–P«Õ\c @B#œùÒ'ÐÔÊÍ©FŸÆm»5%´¥rT}½Ã04  Ñê J%&Vj¨ü%å1$I"„à7ë>`âÄt~ò“ä”´çWpûƃ:´;¥×¹­c3>MZ `7Ž :¹y§U¿Õ ×j5 æ‹›GÔlÙ?ÊØ·fSQYW\Í[Õ›Ly‘p$aûdKÛ—RÌÙÚ¤×Ôc+-Ü}äRÒƒºô‘©5¦R§-))bš"I—ÿ…~MÓtJÊûÐÐPýÏŠE`­à´´4š¢á‡±‚#ZGGŸ¿qí컳K7_ÉÄdO"ºNØû²èÅóÇ:4È{¸{àôõ—róŸ>¾ÕÌϱ8ûÍû”lRç¼zùœÜ7/K­ê½L~(y|òôé³ÔTAêÅgŸª$’25Æá°KòÒ² Ô<‘ ¨×¼cLLlܹýÏO.Ú—˜×cxog §ý·wo`:dèì‹wÝ»zBþòòÞÃñB±qUûª¤´LKjõUÅñMllâåíýøÑC#‘¨âø6||Ÿ€êGv®÷qüPœå=hËãóËÎm^þ¾HuåôÖwÓ·99]]sÛ¶cjmþâÙQãá‡önºwl‹‘o‡ŒÌðòȤû,,ÙI¯ž8­H}Ÿ¢`¸¯ì|˜Ž’ÒS#D¹×o\䂹FY¢ujøàñ}gô~ÇŽsB‘‘ÁÁËÀwÎ÷žQý+Y*—Xƒ!õ«PŒQËV?q;¸µÙ¡¥³ãsTyžœfªÂÌŒPdN*²N_}Ô°AOë“æî:d yùT§ÞgæÃ«wÞG´lSXŒ(ŠŒy“öhËá‹ü7ÇMª·ìêå-óq:¶i¦HÕUURdãâÞ¤{u£«“6nä"È+дèÐ2¼i«›Û·œ $wÅ#aàê/tªÝ¡¶ãÊ}g<´ïÄvÕ¼œ«<ŽcËÙɃH¼Ÿ¡vêí ó\y÷áÔ¬ ùhÑ–¥%ÅÑïÜŒ¹|?;77îZRó@ײ̇OÜì6¤ëµ³ñ|#£œ70S—àšž,ºŽdÇ¡-Ç¢óoîjÜq– €ôcÞv„€ozÿê A@ÐãݱúÞX¦”œ8uÅYòêÀõôf3—=¹»Âµñ\6ÀÓ»WžäÐíºtLÉbµhuzîŠ>ÈŒ)ÓavQ­ÔvˆY>nçFA_\«v0UôZ‰›ÔãFöïŸ@€ä …©™iåý ‚²²2 ÿ…UÍèTñ§v¯Û}ÎÔʦ¬¨Ì#Qržxuln ZZÌÁžÝõk»¨5iÊ #±ÀÆ1±‰=æ`ï^`^›–ae®¸ÀjfsñuV …9—'À0ð8|Tþݸphëú}çM­,K %…Ñá%ï½IK9¸z~¬€ÅXÖ¨Y¬zjER$ù«ÿâóY2õ©F>A±š¦qZ§u²téïÍbÛØ•©”ÅÙéµ»lô5%dž–x8WïÙŒ.M‘+ÕÍÚ €ý›ÎÜuMÃõ³4µÅ0Ä"#‚ÃÍ{ûÌ¿Ó-Å\`ŒLìÛôì Àñóq{!)F†¬%¾{¾VÔ­•]‹94mÜØ oKçÝ"7ùÞ‘3qä˜è¢w®½—–éLôËEUŽIAô»§Wž8vÊ”iÇâŸ|:œÒÉOîÜø²@÷™@ôâîùèØ_\yv׺{™ŠªËÐcŸ¸•Œa˜N™í>õoˆ6ÂZõ›²víg6ô7jÁêÃÖn>sÚÏb=ûô tµ²p 7 å½ ç¥Âº7â¶–Ü;UŠ üòÄ››ºãʉ³7‚£ukî§Vh­\ÌÉŒ Û½—v@ÿ±Sz6p‹;}üÅ› Ž™-Ë806f»îù¥Ø‹—‹µ,¾Hذ×ð®!÷._ñm;|Éœ©ÔÚÁ˜å¸•?cïïg*D“ÎÒ§Ò¨ÚPŽa.Õ|,Åðìa ¦SN^z´‰§ãÂùùGo^&ûÕã"¯c—ÖO®\S2l¡€gnnÁ!iîÕ+×T<·e{öùˆÁ9°óª½ÅžR›†95Ÿ¢Hý\—¥ƒÏøqã€s@-¡Bê\£vîùÈhÑÄ‘éqÇî½êçÊÑ3ác·ˆÈßxÜþ›À0\l$ÖÇ»UB.“‰ÅbÿEFb,ïjÂÃñ›nݺu«®£‘¤«;]š–@‘:™\áêí-}/ÐiUR…’'0¢TeJ€œÔ -Íà ÂeÅ4©”©tZ•dj’+2á±@®”S™ö>“fsõ1:ºðòígs<½u+1ÈNÀ$ 1”ÀÈܺfõ€9‡bNŸ‰>|hg+?[µ¤¬ªf—Ëåp~å 7MS*•ÊÌܼÒ~†šÖ(hš¡F •+i†f±9ÎN6ÙI7(€ëï8û¶0”’RÄâ˜ñß%=`nÞ|j_£žˆ+Âim €ª¬,¿°Ø„³«SþËk4ÀÍkOÔ¤g(ÊŠF©TiX,CÞ??_k¦ gîž²-›b^å·õ®|O~„ÉûàÌ…ô[ÿ¡¨"ÿÁÌ¥ÛΙphæ‰ö5ýœ­þèˆ_€£QÉœ½Ü]D fŽ0©ó ¥`V˜vkåÆƒÑCÆÀgOM‘/f̾P¾"ÿÁÌ%[£ÿ㢿D+M]ºj{§ g‹?<Û¾sß›^ZÒƒàn£êز~Üw2„S`Ç¡!\¹LjÒ{|[RVŠqìúŽé/“Jh¶ìÞC§Õh4D—±sºhÔ*œ]{ÃŽzúÃ¥’2ý"zv>Ͷï‹ÒïTÈe igï3¬ßp )²´LJæ=GÏè ©“–– L|6ìÜ«?D.“ª~›¾cÚ|T¬¬´äKb”BÇeÆÂ¥÷T±@híÞ±uÓ\Y¾{›^îaPVZbäÝd¦?·´¤!„aDç)Ë:,¯R*SÀ„ŸÂUJÅøùkÊ»C—•J0Œ×°û˜†ÝtZL¡Ð+cfW³çàp©¤¬„¢0 s hPǗˤ#®ñQ¬´8sþŠ.8¨T*ïú=5ø©…b…E‹¬ßSZRÌ8O]¼ºügÁ۵˼)áµêí„U‚ÅÂ]\]’“_ùTžÎLIyïèäÄf±*ö¾Ø²i¨ß¾¹=oì±I—PõbÜ1rá ›NÎN®mMœÔgB¯ÐäÚÕ¼-­íûÏ_×­N 38Ù6ÊŽW ²¬ €é¿³Ay×*ß`ñ,Û4ñí=}Ò½h·¢uPhù(ÇgY5 ©¾~B‡3v¦é2º_,ã¨:N-CÇO™¶à§ÎƒÃ« ŒŒL,ݦ.Zîc©”ýŠuø; ¼Ü[;ûŠû5jMFFz»ví4jUÅ×<‘©« Z4‰úyz¤±èÿƒ$Žsšuè»Èœ@ÿm<6oÝõ‡Œ6…¢)@ˆmlß=ªåÄ“b¶![‡ê‹O ¦l\'ÿ®Á¡ÁÞVeZ6O ªÝ¢?lS«VŒ“ejßЧV2d3ðƒ€ýê3fÞœYsç/,.*261&ΧQ1Ú½K'OZ~ÀÁÕ{ê–cá§Ã&8tùôÑŸú½0mµmz÷µ³F,ÝvÒÚÞmÁ±»í«á—æŒ þyMkwR•ÕjÐŒ£çj2)ã^>`Îúô£3ûÏÞÑ $ȪÙÈsúmž>`þ¶h;—ó]ˆ¬n•cH·{zÏ'îs6 õÿ©”2鋤×Ý&íX2úæE­‡,p÷ò¿|§¿ôT“!ë\ŒÍMëºW¯Z0ºû†#—íÝ|7ÅݬgQ.ìçöu‰)W憋£Y=ºcY“í êg6˜Ÿ_ê_¯®\¬=x:ëàÔqë[pvö Úèæ™ýF/BÐeÆ®Cƒû´m“ðÂ7´ÝÅ‹Eúfù¹ÏC§i›G=¹¸§ûðÙZÄî3eíÂÑÎl˜Ú}ÒÚ€À‡€«×¯89­û¬­ç‚|ì"ú_:n[±=Q»oÃL`d‡WÏ1oÏê냂M+·À·cú5C‡ר5¿å ôÁaÓ"#…VE~…'½Z¥Z¹rÅÚuàOÙ²óæÎž;oAE£ Ç1ŸG"\«Õ¢oÏb±ŒŒŒäryU¯†1FBS µRñôï †aIEŸ9íâêÒ¼eký)Ó4›“}ºk—®æ² – Žcb‡fp’¡H¤E@*d"![­Ð1,8£ X*¹±8‹Í¡Õ|®Ð ¾€ÃbqUj9cëp©–ñ Œ&L´*‰˜ T\ !R‘$£c@(ÑE‰o®¤µ¤ÓÑ0P¨e@§ ¹ 06GÄZ­QScó J£dªÚß$½xñîýÛ~r¹<`F§Ó:yB(DFu*.*¬hT±Y˜±_®a±Ø”€'’«5H#‰ÌÕ C0ZWjhž‘˜ -+ ŒÕ‹TÉ…\ ÃT$‰€¬¬ÄĈ«P!‚Ëærù’¡Õe´B&£Ñí½Sw÷ˆ_בÑj´¡SKùl8&µŒ"«–[!-5åÎíÛã&NþÞº±J©T*°±‹ö3%++! h†¤hIÓ Ð4M3HÄç>{xkÏü~™igÏœ2l8_ üÞÎÅ@Eª6SUðêØè{Ò5JUâ®.Sê}ëøèÎO»4¨ïÔaßôîw£·^{R”_X*5 ‰R£wsÅ6âðXBW…\®$-~ÎöD’$ßD¯žz>-?õô¢WSJË .­Žê70òÁéJõ§ÞÜ›u/É ŒÔªëŒØ|º¥Ë𮑧n×X²èxF#V¾hÝu® ±u¯å—G¹Ý¿'ñJŽ¿}nyôíÂô’’´3sûwòäêòÁ…ívçÃEPðúâá[¥7;<¸ÚnȦ‘¦m§ä®Pçåý§ƒ¦­õ vÜu7  6¥Éê?|Þá‚â0@pñÀL•¨¶DšpmiNöįP’–°=öíçA ÍX´êÀÊ«™íÝñ>m›®7.;²÷®V§U½Œî4ã”$%z[LŠJ­º³óçÅOÔ “eœŽÞyüxýä2u]yà¢sÖå›–QÎÄ;Sõ• B¥’}­Œ5& å2™ÑgäˆZý+!÷ ƒj-†aˆa¾Õu¤iZ"‘ü©CY*’¦¿-øM`ìîÝ{=~dóÆ uCCqÏÍÍyòøIÝmìì+YBCl ” 9MÓEI(ŠËåâ€R¥#IÅ&4 5MÓ4³Xµ@§Õji‚a MÓj`)¡)Jl %†Ñ"|‹äZ­–b±‚úT\‰±ÙlŠTI44‡C°X,ÄhIŽ©zÃp, 0AÌê•+Ä›”––>~ôÐÑÑ12ªSiIq%oš¥†fã@j)%%'µZ–J£¢(JË0‡Åbi• ­V‹®Ôi(ЦiZÎ0g±T ¹>”Rª ‚ HЦ:®$ûõø¡#ß«¼k·;¸£F­"I’aÔ4ͨ†QŠ¢þ;ÒÀJÕŒ*YÖ+c+×+§O+ ÔS¦Í€À ÿ×ÓÖG®¹¥‰w`}IúE°ÃÅqÀ0Œ ØTÙçÚ½€+4¶·†Ò÷÷øf.±§O+ ¹s,«T)¢•1O7¹@:5O`QÛÛ@â`k¬ÎÿàìÓT j•‰NMñ2- cpÒÉr“@h}ü„Zb9oÞ0xvíô²M± /\`0”ìðö£­çì-IqEü< mlj«“J Ҳ qÜhM®‘Ð&X¯ ŠÒ<ŸNæT²,.‰®7t ,Î3µvr2Æ´Áޝ_:E€šæÕðtPd½r é¦ffÖ–bx÷øž–kQÏߪ7­ÜôMZ‘·ÛÿCªtuþõ0 £Ñj¿’p‹R·ntô™>ýúW)}«J¥ºy㺻»ûç‡ü¸93BÔGwøÿ! Ã06t؈—IÏß½}§Õiíííg̘³‰J|lŸJMDÓ´>jUI’ÝÑ´¿Lsð)ëÁ§2Ÿ6*M+ê“|^N§û$D­þ«³ÈŽ5nÚÔ¿fÍîgffš™™uïÑÃÎÞ±´¤˜a˜ÏO_£ÑVR[ûñÞÔþò&ÕjË•d¦RB„J'eåìs÷Eù©”ó?Õ üÐTͨrðªélÜ«cGÐh€TïÜwzÂÔ©Ïw/Ï©5×ËÝåøµ[YŽE…RK+±Èˆ+Ë-ðÀX1/–’J•$;»ØÚ³[rÒ2³ ŠH—€z=;v…¢r¥ež'¼–N˜Ú>†ûª•  1EÛ¹sU_˜eæh)ÜÆÆ”Ô¨hCÓ ÆÙ?ÔÛSÚ»kɘœ× k6¼âX0 èýÝÄjó:o4MS ›¦H„˜J«¿q…vî–¼ØLˆt†‚¼'·Â3÷ZÇœO®Þb@á­W¥£··®‘™‹zñöC€ûÕ›ü£z¦®?ñ 4;ã}F®]õîÚÌF'½I+)s€Û‰7]ëöÖ»$è‡uo7K0˜SßÁ‰ˆˆ8³tñBšaà‹¯A°íìì:ué¢R) îÇ!¤ÓéJŠ‹¼¼¼}ýjêw*ä2µºôXFéÛ¢·KŠ‹ù|~ë6íô;uZÞšüÇ‚fôíÿÏÔeÀÀW¢jF•У˲‘ïl¬­ÄÆfM/êŸõ^å~pÚôW'í?çÂÞyíž'5tu1±°›uðv¤ÖªéM4‹ Y}èÀ´‘M7 ªìËwò²ôm½¸ßSÿÀ`O!7t¨Ø¦Ã„ö ö¶¶B#ã6ã·®ÙðSˆÑ&^¾nã_ß_ ßñx<6ކñx<‘}ýíó2Ü-,\=kLÛSKºýü{5‹Mp‚ÇçU Ù>a¬ƒ½@(î;ÿ¿ôÈþSgï¿ ­Õý´ûºëÛ'þíúØÎbó¸\'ø<ŒMŒäÍ î ¯Ý¹Ç\Î)JÛ±vÂÎm?5jáùÃj?eëÊ!SÜÎuµµµ ªßùÂrÿGÖYT °ÅîSÇDuÜz±–2ç@§FÁ^eÏjYXøU·«×ÉÌ¹Õ´È VV6uýÜÛw è«·R–_®÷ÿ“5<’¿Ž`½úô¥¨ª»á޳Øj•R]áÅÚÀ‹L&ût'bö¯·¨*¢ÑhÔjõ§Ÿ?x ²߀ª9ªÿc÷اuŠü£ûöz6›æÅû[koYµ©Q¿iÞ\ô÷`¶¬wi4>¼†àWÿOekxFïîÄßÍåŒ %9J®àÖá=yÞ훋¾ç8ux8|ßæÔ÷ã¨þÏ€ãxUûùŸðb1`ÀÀ?€ÁQÝÀ?LÕfªþùkÉ5²í7zúß^;†˜4í¯‹ÅpQŸQ3~[õèÆ…c·“=}Cz9´óf†,°^›A <<¦üU üíü¸ŽP 0`àÛbÈ58² IDAT¨þå`ÚeXh—ÿï‰;'ê+¨aÀ€VGR$ ?ò÷> €Åfs¹\CÜ´oÈW4ªhФ(šÅá,ü·º8bhŠAA`_`aP¤c@Ó‹ÅÂSæoÁ04©#ÃpÁ!Xð±FŠÔál ¯|24Å LÆü%ò)R‡±9lC ¥#—KÀßn6!†¢hAàÆÐ8û×ÚV¿jÎÂ!†¦À6û ”A:-Éáq¿äZ0`àßR©¼{çNBÂ-N÷'¾}?…Âààà-Z²ØìÿÂ2¾O¾–QE*ó7­\8qÞæ>‡ŸíïQó7J1wŽ­™âùÉ3Å(PU׿Á¸£—o,ü©á¸ùýùUéuѺ¸Ã«zZ̈|ê¶;{v‡¤IMvžÿ®­ó/W•BLôÞw3W.ò%QêìЀcN%÷óã&Ÿˆ~òNâ©/Ôí‹AÏn›º`÷ªè+feé·n{‘ë0ubO1ÇaÑO®Æ<ÎÖÐ êÕs¦fhì6­šþ‡Ò5e÷æaJ„~Ý;Ì€ÿ:t:Ý“ÇSRR6nÞú­uù«Ðuòäñ‹ñ{ôìUR\ü­Õ1ð¥ªFz}ïbô¥ûB §æºÙ¢Ìó—ßvëÛ1ùêéTvµŽ¾o¿šð”klժװ s752Å÷›ˆ€¡¤§íÜÍ•^ºv§fDkÕ›+cîÑE~¡|€—‰11× -]Úöìífüùäóý‚=ßY˜æ¼HØú:Á®FXû†9ɉ{Ž^4²vnÔ¶£•êÅÎèûfbOÀÃÄ:5¼{ñ؇oD–n]öiÛc´´ï èWßoõmÅìz"æá…ýÞÍÆµu†ÝÛ×—¨ì=œ ¬}—®e.|"»w×WŸ—öääÉ85Æ÷oÜ©U°ã…#»ž¾Ëµö¬=¤g@ˆ~·Ç&°G??~öËûñWß›8ŠRM µÈÒÁFC ;wmsçÄÞg…f¶n­;vv6Å.Úù,]êhgbZ³u»Z6±Ç>ŸcîèÛ·_U˜vòäÙBéZ#´KÛp`hmâ•‹zÏó3†£‡O>s™%¾ŠhäX±¥RžÆ¯]·éƒš‹óù Z4÷rµw7wßÁ=$Ëv`–×cÎYû…ø:™\Ž9Í1µIy÷ZRª´r²Õè‘m\ü]jœŽ+MÉmÖ©››‘díò=~e¥Å<O£þ÷¯ªkÀÀ Š¢—¯Xñ­ù`±Ùݺ÷\¼hCÓ8Žœ# |ª-¬*~6~Òb§ZaFd梥[ybáÍØ#ÛvíZµõ˜ÀÁ³4íþŠ•›yvÕ¼«¹)d:x—[‚]Éæ ÓÕ8-Í9zè`rê³ñ“Y…hÕŠ¢R•–Ì™2c{ú\YòÏó¶Âga†¤2kÙºØ1{P*eIJŽÔÞ‚s`Êg¥%s¦/ m<ùš¼u«×?¼~âôµ§’>?yàxrúýÕŽxÖà Ì\z 1hJ_HtÜza" é«·^³aTÙóãû_`¤ÏÖm9•žò6ñIš£‹ó‡·÷Ÿº ˆQÌ›4é½BP«VMJ‡e<;¿fó!· Z÷Žo\{³(UöŠ ±£v/¾@ ÄòV-]ùÉ7œºL?Ü{äâÓ‡ž¦ä›™Y×ð©.}ãÁs…™·ç/ßáäwzçÆ—’²“®?—àS'¤øeÌ’½‰I‰—®?zí[«Ž•™‰~lP–$Ÿ½ü¶× z$/®ìëâZÍXlTé‰Åb/WGg7OWj"!Ÿ"•9EV¶g6/:ŸYvùìÙ§iE€iNÜ}éòÅ“ñ¨’G»Æ=òäÉ»šRåªx¬²·ËVï”ËÓf-^@¸ûššŠ _ øW‚¤Ãpø˜ð‡ôiBÔ˜þ Tm¦*çéÙËwo;ÅǪóÞ§«=yB—%ÓzÕn=|Âá'-ݹöݰp­;v@·ÅIø˜BÇ0g{ŸE ¶µ¥úÃm‰ pT§¶¹fEq% ;/&\wŠ«)Ïz•®ûü†@¯®žPZ‡5çÐ%®qÇ¡ÛzpߺõüV\–ÊbçÈ~ }•0`Y±·áÐùÝÍn½{¿‰»¿-æêe37gYvr‰Ø(uÁÏ£§Õ¶¾ ð$f/î£Ò5‰Ó“ÿLFvjdýª´Ô¨An­‚¢ß°@'wõAú‹è‰úïb'×/ö Þ³]»–ü[-V­±èõõSrËVBs7¿î]n¹"¯Vë¾#¢TÙEÍÝI¹T¦ÍMÚpও‘~®…^Ï]NéÙºcòÙílânü®Ø+w8Æ‚²i¦&2[o“œÌŒ<Þµ‰@ß8¸Á¶Åt'¼u—^µòÜ“ ýÂ=L*5–µGxÇ–)çÒˆ¦ÁÕ@G±µíÕªaË’°m§º;9‰ŒMcÙY™™XY¸µžž¹GЦ’K%޶^S¢aÍ9#×K™:‘Íú¶ôÛóä¶ö«å17`à{1 ƒÇ1„€Å*ŸüÀ¿À¡“aœÅúÜ=!„a8†ýŽÛ"bc±þï÷ùUa±X4M³ñr»Ša ÃqŽ!Ä0 ƒÐÇd]ˆ¦Ëç~0gá8CÓ BúíÏô,? Çq cša†ã8ŽÿZ›0 ƒ0 g±ðu‹ÍþsÎ`Øíooà§j3U"·qäœÖ:{çÂ:ê΃箶¶RÞ€±‘Ã0½ NG›Í¢ÔZ ç°0 Iª”Z¡P(Y@‘DNS:#‘ rèþmëÖ>}éÞùÊËÔh¤é[ö%ŒZ½ŠÂX¥¦€áñ"¡±@‘[È&ØB—Ô©äJF­B´ÎÌÒbðìKÛ6¬;ræê¥}ãÕ²œ…S&Q5º,ª’äåÛgíŸ4ÍP4Y*Ó"šR«”út¥Òò¥dY,–µ©yžþ¼´¤HÈ£H-|È“ÛxÀÖý ×®Òk‹*-•3Œ>¥FѺ2™–¦HF•ûpÛ”÷®Ý¼uyëT;.Íãrš€ü2BÈÑ¥Ú¬m÷¶oÞtâüí?·v©ÛýÊÕóÆ×MÌŸ€€"§H ¡ÏÔ†þ“h•ùµýª9¹zìÝ·$¼Yo3s‹‰ƒ{O_ºKlnñûA‹Þž±²¶¡Í-*­I©³'WÀ~¾Rä§"q»[YÛ¶ì4œcn¡·«àKˆ´ûŒ!¢šo=5h³[†Ö´°°®Û8úYÁ›ë‚pp°'¢Þ°­Å-ì¬,¶î¯®¤!Ò]=´ÜÊ¢šw¹[/Àöi ‚¨ß²ïçäJs^uijnnV»A‡€ô‡ñÕ]삸óçÎÀ0§nà[Sµ™*»:]¼ÙS§-[ï(DÈØ§­¿îplÒ±ÇO÷O½ô(oZdû¸ë ç-YãleìÞ)´šq@XÍm{ÖD÷ jêzdÝÊtk2·Lk_»sĹùœ,¸™X‚:×i¹¬ïœÕ K¶³ Õ#¼Btòë,{Ÿ&N DÓ œAÈØµ^dÐÝ©s–Ú®AM}]ß_-3"„„¼ ¸|fñ‚uI&H)r wÕ]ÞvâÊäå·ïÚW«Y'ìùuËZ­ê~Ò¯{‹!Z[x9îlÌ•'4ª¸û¾ÍCjŽé²pḭ̀W¡}­Žmº‰[·l£üíå¬ {ûç=;VÕ»³€ÖÉïÞ¼vçÚÕüœœów“DEj6×!%b0·õ¨(:{õʃ‡KðpˆÞì•“VnS?»÷–Õשn£¶ÛÆL_™SŸÐÉ=C;WãgÇ^¹kmc?²ÿP#€¸}{ë ZjüQÕJ×EQœ>wò”Z½æ mŒal;G»âË6í:Ò¢S$ŸÏcÔ4è_9â`?ÛÇw¡T§´|…·@€túYs0 ŠK27l?Â}s½vd[G«3˜Sþ« ÝÍÓû2 e;n¿6z©ßðvw'{pöÄ_?s)1‰glÓ0ª{=/¥R¿BsøæÓLG;ƳZ #ùÁ©3qjÄ®ܬs‡ð”ûwœ9jݰicÿ@_âÔ¡ù2­«Oݶí[ˆ@KQTчäÏ·4iá’•ÙÏo;vÙÂÎÊ. 0çþÅÛI¦ö^ûôvè®ÅÅfdå•Ée"k¯Z¾¶7bãqsïq“‡1e…:ŠþÂ`êÏÁp¢ï„…ÝÇôï×o÷üdªÄX"yYxc×°ÍÏìXˆÐX3z„ÿ²‘·Ž-u­ÕõÒŸ7 m5uÒú¾ÿÒHR×n9w𽤕EY÷#Û_¾»Ãà^s—]ÿ¼ÒW÷/¼R郳³zL\hJ«wYO‡¶ÅúëWЀoAÕ9ž›ö­‹ŽID\‘SÍ+aöÌ;-&ÍŸ|å9 <§9s¦^JxŠ8bgWcpk3t‘XÂbqy‚ÎS& ãŸZZ›/o7ØÍÔ|æòIÑ—^Ô8¾‡kmbûîÑñGèZ»nÅ IeÉÄÇþùÀ☎š2Eäi ,jØø©BOk“Ù?»(´ŽoÊS§Q8zpܽhSTÛ×ÖÌaÝÚYç®=ŽCõúÁÖ*عݗÆq­Î„ %± ¯›t*‰³÷m8ÚEàl¥™=ÕÜÓRã¥Õ¥%=‹ì5†#´Àp è>k…mÜ•b¸ÔðâÙð—/b'–Ãq±Ø§N‡-]hD{×ï2Ièà"È›`maÇWâfæÍ‚^ÜM-î7av_›`lí²É ÉÙ”›³ÆÃÌEKæà·Ÿ§—À gA±ÆÉÁ8‘KÛÑYsãS&ŸZ¿1VŠL¬ÇŽ/ð ܽ^‹I|«·¹ .ß,ªgi ]Gü\jçì*ìÖƒ%­Ú¹ÇÔØ¨”¹ˆ|&YZØ‹T$ó9á%cde7eƒ|ÙÊ…@3†—?ÿ=0ÌÜÔÈ\$V”™÷–mÚ>}ÄK À¼øÞøi+‡Í˜®É|ôÓè)÷Nâj5MÓÒ섾£gM[½¹ôÙ©¢ÒBÃ0 +œODo]Äõ:acaadbíà,âjvoÚ‘«äÖ òº{XŽŒÇ÷k,+)ærGK3)Cääe_9²býÑ-GOé ïý4eéà33®ŸšðZriïäv<ü@M:lË’ùñ> ;·¨µ}Ùró݃ÒcŽ=ÌŸ1sŸ”ÿ©œ,\l*y“¯TÊÀÈH(š@rjjʳWúIñ[‹ñqB8”ñQVó–njÒÀñü­ÇÐ×ï“uI&Klw¶ ‹K«UÛ+7£lÍJòrÔjåçUç¼Áy¢õ[vú¸g\xÐS$)68™øA©rFuLàÙ­ÇÇÕ}õ ø.Mëp,ÜÛF¹W8¸nó6«²iÕ¦Õÿÿ#pŠŒtªPÐ-²«[ÅŠôÙ¤y«€øæ «{ëÅÙ{ú€Id·ò=à»úòÀÞÌÀ²¨ÕµÚG©uÚT¨ÐoñjŒ]> Ï2qö2ðu5€uã–¿‹ÿö»‹oêÖD¿ÂXbïÚúfž{†5hÚìÓ¦N^Îåê™Ö1¯ Õ¨z€ ƒQe࿆qBZ4ów‰éÔ6ÂÎëíšÿ_é=íú®W)o¯ÇÇ+ SÓ^–ðØlš¦SoŸv 8cX?y}$~ €t²Ë±Ñ9RM^V¦Ã3Y¯¾¡î¶n]:6µÊº5àÈqZ`›Ÿ’‘šaUG8Ž›Û»„–´k`{à¯kŸ©}Ú6‰_·Å²Îð©Ãëšò}‡K÷Îrsrñî€F§.¢@¨TùûOÜ_ߺ\¦DRÆ0 M“2¥Z^VB`,6Çi€2rŠLíX8›ËcÑ€Äy"¾4a¸€gã.°a´Ê¿)›€hТƒAþÍ?_DP’úøyf阩ÀÒÚîeYÍ^¼ïn†{½É ‘—äkÜ\í…æ¶"œ,Öhžsd{q=W³Jrš,.(àˆ-Lø¶NÔs‰ƒ™èâ‹—ºÁ0|øÁù}£êÛwî¯tƒý]bÿŠ„Ð'ïÎß—ce ø~ðh=J8£Çð鸳SÑ–?ÏFê´!ï†=M6,X±cŸôÉe‰R)nH€÷½û1ûàѪ3IóÄnÛç/ ŠjVsþà®M"BÔJ]h“öáµ=4 ù/ªAˆ¦)جì^2u!–÷àR‡i»D@ëHŠ"I@R4t@R †¡·‰çö\O™8i,W'§¨_Ÿú]˜··ãv>š‘›º'îY¯¦~Wvmx™Wú:5jþ^xzÿÇÔ­¦@PÝÆg—î[¸2?9™3w¹+¼º²{иg¯æ5º7­¾qÆ„{N<ËMBí åvÌÆÝÑïR”»ã_ökáKKsæNÐmæ°È:Õk·]>kÙ²ôÅÄó¡ *Ú³uçÛœìÓ[ Z´ p€a4ðCÁš3gÎç{oÞ¸Þ°Qc•JÉãñ>E èaHeÜ鯲¡º§ÝïĤPšÒ3·Îþy¦F³;Ño”Bé®ì?70¸&ñ‡á-H»}ጠ¯‡g+l-Ä2"鎬šýžïWÃNTIBþ›¸Å®5m\ûË$“ç7/¾£v t1«T^Y”4lÀˆÄ¤nõÍØ›û'wL õû ±ÌƒÓ[N¼Ò†ú8W* ˹7uξV­"HeÎöu«–-YeѲ—›àS”ùôúþs·|kr1 €¾½÷Äã‚°šž:Uþ…K÷,ì]„Ü¿aщ÷ïÕ ¦(Ê0´øOAÜÚ!N®B»F­6n¦Öæ–^¾þffÎ]šå|‘GÍú5Ü­µ*°Emêy~È.õèÚs¬›‹¹‡³Ó!¶xؘA¾>V",¼–GA~™•‹Çním¹¨T¡¶vpó 2æM‘†;9:Wóó7²ÍLLkø×1³2æl[…zåæ6î:a@C¼ÔÞÞÉËÇ׈[YZT¯Y×ÒŒmmnæ^3Ì–«ÂmÝœ€Ò~áÝJ‘äÝ»w›5o³XH]˜©ÀŒ»öêAÇÃÃ%ɖЂ¨ÁSW€@À­Ý°ƒ• ÄÖÎÁÕ¥R²Û¤%ÞBc!áæß¼š‡Q-$Ø£øÖÕúîÆf½¥Dö‘"YßÛ݆Ë!œlm¼ÃÌ„,®Ø6<ØSZªj?bN-BQ’__Ú¦{/+×ÜÖÃÆ¤Êë·nÞl¡Õhô-PVVš••U74¬ªr¾6$I’¤‹KxRO&“ƒà šÑGL!„€C°ós3#H%eoß¼©L¿:jà» ÊŸÿpáë¤xü~1‚kÔ*²súÍ+÷3JúYÿV1iÎÛûO²†~A½²œ„µ{/$Ì^±piØ¢ŠZÿEþƒÅ[NÇMY Ÿ½™Z¹´láõÇUFYôbþ†ã{ÞÌþLzue_îµwÁOú´ vN^ Óß–Ä伺·™ðÙ?„Æ6íÛµŽÀ¢×€þï®^¸[M*x=ÉòR»cwÐÀE•|ÒR²eÙ¢Ek¶aæ^cgÛ 7>÷ø¼Þ³Ö4ì±úx­:ÉC®iú$Ý6péž±Ímë× O+V×p01îØ°c“¶®ž·L%ÉhÓ°UÔ¤Ù·Ì’-(Ìz 8ÏØÒ­†I1€ÖÏ9`ɾ¸ºµjÚ6ªwéztéÛ)°o]«>}&¬™xàv[o#øG%š¢$R©J©„,³Î_†Mff¦<ÿSFæJhµÚ²²2Vûû©‰¾[p…ÆÆÆ¿•²!$•Hä 9 ovÕØl¶±±±ÈȈa˜ïç!JÓtIq‘~[¿¡ŸN¹\ry¥ò!‰TZqZ­V«Õ÷|’ðIf% 2Y¹X•J¥R©ôÛeee¿*D¡P|Ô§|C©ü•»ßLïéQ¥–ÿ¼ð/Fìox1€ïÁsÅÀ•ªUÆ[t ¶Ñáå¯I ˆ„üV:Üw$mÿ¤»£ÝÒY2qqI P0JmyRuŽ×ôð62ò•\O7]îõ#·ŠHR—·qʹìÜ×÷]xr;+¯øÂÒ‘sæjr°R½ùÉW/½A Í„%ˆ¢ÛÌ:|1Üzb¿®nÆo9öXCQ,êuT—¹nõÍÇì9Ê)ñžÄûõÅëq§–Ý~­¼šýáÔìéSçt][švÿr’zÛj¦ðyûNÝNô ñÓËÇzü4ûFÐâáif­¿Dæ1wkº¼›@AÒžû©H©T–\_Ó|òô¨¸­Òœ¤ËO‹ÇíëPIOÇtô¬•ƒ#„c_÷^?Ì|GÌI°¹I,îçì½s÷~Þ³£½ö]y%ÄLªu.x¹ìФ.ç¸Ö=Zy_»õPñòuó–.¶vAãûöß×½i³]zɩպr'‰¢·÷Ľ¦i:çÚŽqG2Ðݸû"´Õ(Ì|-AfkŸµeÒï|p sâþ33U¡Ì’_½‰„,Ö×Z¨ûï!D’¤@À c±ØŸ?Ñ5ÍË—/‹ ùBŽýFMÓ4EyW¯îâêú+ßp*--MLH‹Ølâ›hˆ¢(Çðˆ† ‚ ¿'»ê߆‰Å¥¥¥VÖ6ßZ•¿¥BÁåp”7:ÿJªœRÑ–fæ"[ÎY€„Ô"Ò‘:gÏÀr¹l@è‰Rð –æZû´ÄâYš ä^ØxEÈ2ÞªMƒ·oS¹RZ~ú`L£1‹é´¾©·½€ÄÔ„§+É·vc 2™¹±P§¡0‚)’Ó¤†Ät UI±…GXqêkdßpËúFpål¼OÛ¾6r•cá¥J¾Ð´(§Ã 7%·|¥BR¢%¹õ>Ƭ¨‹Òm½[Ç”K+TðàÆ-Ë¡Âò6©Ø>Lrzaiî;€òù¶7iyˆ±ÀÀHd!È,.çyæÞ­ÀÅÁœ‡³]<,–¬ßÂÖ” ¬k›Ôì ò” æÿÜPþ¤¬À¾FK 1®µ¹ˆÈz÷4#_3 ¹X9×ìÔ§fq™ÜÂÔ¨‚V_R§{™ô²EËvöÿ@u#gOŸJzñ¢NHˆFóËÕxÊÏËS*={õÂ~3ñWyÿöÍÇmml¸|~¥ôEM?~ô8¤nˆ¯ÿ·ROϵ+—ïݽۤiSöKý üuØlvX½z'Ž5f¬Þùq_¡PÄ_Œóðôì×2`à+ñ×(š’bp‚çæl™›v5•w6äå–ÚÚ™‰Ù•GŽ IDATy’ÌŒÍ5àù¥¹º$==¿û¨nì’ð.-#¯€q«ÓÌßVÇ×|}ss•*)N{ô S3sq|\òS«V‘mìèÍWž{DƒUZ–†fÙÛ™?Ш>CÓ äÔÀâÊëºÙ9Jmé›;ɹgwЦÑ4ÃÐdÅ(<=,¾u›Û­2¶q.,(±p«)I[+H¸ñÚ¹v#¸ý(©v×åŸÊç¼¼¾ãä“Ùó&³ô9ßš¡)š¦1wô®¥:r Ù')dˆÀÚÁñÙÓ{×›¾bKÔÆ§ÀüBnjÊÏJ+jæV.Þ¸ä*¤¦gæäIX·ãOZÖêm¯¿&a¦·¨þÉ‘…a©T¢·¨~ëSÚ÷†a†…„†ž>†ãMW0‘¤R©£££Þ¢úA½ÇqÜÓË;11A¡ D¢JFb˜ŒŒô½zÁ·»jú«Þ båŠeÍ[´0L3ü“¦Q«'O¯%uØÜú|>?((¨S§nJ…ü‡ ü+©šQ¥*zѨÃ&xâ§žtÚ2¯—W:ˆ-lù6„càˆÎÛx» M­ç»Ó å¸±«›n|Ñcݱ£+æ÷éP¯VpM¯ˆ–vÕœ“æçìãìÑx”À´é¶iIŽv¶|‘Iç{÷ùT#C)ãÏ^ökSM¯)Ævtv6pGŽNNÖ.!‡w´··q«´üHlͼ݉)„ØÔÒ’gʸ8ûœ¹ØÑÞŽ/2¾êl á#ÂÜ» ‹#°³³322ssesxF–bÝâÙŽŒ!D]M›»dé¼Ó1-ëE¸Œ–´2góŒ~Û¦>s·°¨×ªÏ‘RœúPL̬o åV ’弊‰¹0gÞd½ŠFfvÎZ+ÐJSFÿàåk¾±S÷a…Ëöª[+à‹Í1‹jÝ;ú«QÃÑØËÇßÖѤ{·N~múxnoé Ƕ̟¿f7ÏȾ}·ƒ&.ëbÙwþtW?ÿ¬Žƒ6o˜a]¡¯o@°Ÿ›{Ä`E›÷?<þbûG}¾Ù»¦~@þsN²ß„ò†Â0š¢0 ƒÏV·Ç0ýßtR• iÇqýL¿òÄÔ¯îûMO°¼Óê_6~àgú †a:Ù®CT»QßZ—¿¹\ªÕh ÉÀ·ûÕ÷ïysfÍ¿°¸¨ÐØÄ„ 8ŸÒÿXOýT#©*¹{Ρn??Ç¿ºÔÍs;q—Îáþ&QΣø¹¼–í#?éY1ÝÔ?3KÞÝ—`1nP=ø:æÔ†uk†®Qk~ÿ¹«Q«O:5vü„ÎþÈÏÏ?}zðà!J•ª¢ò¡¤/„"Qº?ÜIUbïî]5´±µ«ä7M’äþ}{š6㛟 N§[³rÅOSR*UÔ·Væ¿Æ2µü‡èg=+îIKM¹sûö¸‰“¿·T)•J¥6vÑþÁc¦dee#Í­#išš¦i‰øÜgoí™ß/3#íì™3C† ç „ßÛ¹¨H•}ª¾’¿GhѪËÀ¿^;†aíÿu9ÜbH%9ß6ìŤZ‡qÕ¾MÕ¿Ê߬"/E¿Ê‚QÃ{0L…õ7}ïÚ™Çï©q£{ÿb?€´ uÊåoŠ8ãF…Ÿ¹L-ŸÓwÙÌÙ Žq³úõŒˆ¿¯ðW˜ücÈÒU³çÆÝM²´¶6±rê:dló »i¶˜³«±3—¢¨—wO8óÄÕAt2ú’™¥¥±¹C§A£Û„x"†a†ÖäNŸ°|ü–Nl¨xF”*{Îφ/]é*Ä>?Óß7ö¿yçAýоŸÛ" øs|ïM¨߃œŠ¢þ¢œ¿‹ïMŸ¿ wvu¨Yy?¢Ó_?¼•ð*O ¢÷Ïo?|7fùÆ€ê>a¡uJ.œ¿YåHóO üýDËãã¯ö¿sÇ–¶µ-GtlóNj̤íØ ƜؾöM1žùüyõ6¶ïØÞ)ÂeR¯öGçá8Ža‹ÍoÚ´©ø³"FW|üäÉöùt4<Ó 0ð/ êFbJ‹òŠË¿ÿrƒª¸ ûõˤù;Å4ò²œ¼BôEïI¨ 3µL¥-ÊË“«uáÕ g§ÿšV¤ª4=£à‹%£²ÜŒ|©æó ©|Ÿü*-3[CU…J&‘Ðð'•'µÊÜìl a¦VJÊd¿Rû7Goáe¼¼P͉ ·{r6/òöpssoÔ0¼a‹NÏJØl69¥_[‚ ÌÌ-Ç­;Çf³—þÔ ˆÖmš;Ø;žx˰Ùl„Cë®ÅžØ¼e/LìÑŒ  K›‡Øˆ_ŠÙáïçÍåñ÷?‘³ÙlЪò ›råNâÔþm=«7X°%ÀÜ΂ËhJÞ¶®¨—pä¹R_^¯-öG“$!šRÅîYÂãq9Në^òÙlZšHDëÈöþ¾¾}¦¬ÅØìì䄆µ} ‚°wt?÷žf³rì,-BBCkø7lÓ+dëO ,­|"Z›šYF œ>7ÊþÜõ3VÏ˸²3©ß¯?ñdÆþÅFl¢Fýæfæm{MXܳîê%+@ï£Î4bh&‚w÷θØYQ#¨a2Å -Œx“Z„òx¼F†e3åÕ}‰Á]ñ­CUšÒ§m‚ œ]=×G¿g³±íakï¼öÌC6›=¼]pÿ…§Ùlöós³DbóRYjX€7‡ <«]É„ò†eÈ[g·‹EB.—Ú¼—ŒÍ¦dï[†Õ$ÂûæÁ›ùÛ!ÄÖí6`ÀÀO•*šTÄŸ9pþÊ‹?(¦•\>³·NÚÓcßüv)æiìž¡ã§É$‚VžÒ²ió[¹¥3ÆŒ=û0µJ:WD§Èìܺͅ,€Ï\ÄòßÅÏ]´í 媜A£¿”}.'ýÞ¾Z!a[DW®EZ˜ºböœÜ?i¡ç7N<2›‚ç÷ï=²iÑêiÒ/3IÿiÎÞèVBèõî.|ý·¯fo=x÷ý„Ä»£Zyn^²,ïõÁµ‡ã_)Ñs«Ž.àbkÚsäòKñ×^ž¹bØ`Àqò8bc‹’ôƒ[Ï>@IRíš14> ª×éEÒ]É©±Üäl6›'´½»oÚ¸y‰—㲟ΦÊr@ÿíëÜÞYJÓL….~÷ˆñ­‚ | ¦/kÚ>wЩk?0°pá˜Ì+‡å[7ÅMŠysdÅ+Ÿ692Õ‚a¡³ *oXŒU§Q»çw_%=¨F¿˜¶ÿ ü ¡LR4(#åUÿ&ô/½9¿¿.lÀ€U¦ª)¨³Û×=H•uùçÍÙâì“•ONX4Ít×£‹¹)0³ë3yQϳnæ÷óÈN±‡ÖŸ¹üÈľƘ9Óœ8€ýüâ!#Ÿ¨þ58—Mþ®¸Z ÏÿØ;ëð(Žÿv÷\"—\Ü] w§¸µ¸Š»)PŠ»K‹”¢…Ч¸{â÷KÎeeæ÷Ç…4h --ýþîõäÉs·7;3;{;÷ÞÙ™÷§4WÕé‹)Î×¶î?_»ù8kyÆÒ´y³—²‚Ø–=†}Öäô®•{OÞõލ3bòX/`Œ’.1»Ôžôû@ÌÏþiãšu! zwîÒÅÉrŸ›µd›ƒgP×A£b]ŠgLÿæè¯—´nÁ->iß±†ÿÏß.:~%Þ;ªá¤iÃ3¯½—õëttó"cH7cªÑ.¸]£*[–Î ï2®A°bÍOŸlöùÂ`ü–—²gÛ~¡_ó<3/à¯}þFpy°€0¨K¼ktyX$6ìG2¹s¸‡DB1̹‰÷í”Á)WϨ²Ù­;È)öñp€\=)€eYŽa¬žœÚ¬¤&ãÀÌð½ t˜'…„(ÀhvvuI¨ `±˜Sžfgåd”hç`„!ı´*;U¨¹uîWƒÚç§Ã§9–¶pbË—þ½ÄqÀ0ŒQ]âäæãi(¦jðå€pO…·;fò#\d)¹ÅBu>)‹¹væ„™ŒÜ±{< ùr;×p'\B’ʳâa̱ X̺‡IyÎ!-ÈšÑ~×|wåþé^[04Ãr, ‹>.9×Ñ7hÚ°,IŒÙD`Î+¤øÅFãÝ×ÀÁέ~ XüÝ©z °,ÃYc‰½UUYÏšõµÂ¯ÆÌ1}®œ°YUÖ¸ûøÞ pçî#Ûôí',Éà›-àá_7ÈéÀ®}ß?)u˜ÛÜ}ñæ4$ð½xê8v­¿v]+@[,ŒE»góâ_.fwø´­½ ¬È®Ÿ|3>uÅøaj­¹Ã ©½ZDš*YR1 Cü7íUmذa£‚wíÅÈF-ZH¡ôÖíxÐ%ÿrâd>Ã]5×z¬yzwí·?¶¼2öCëž.Úp~Êæq£¡´Là3´Ç3»ÖÝÈÍž:uIáÓÚV÷ذlñÃÛçŸí=¤šR3ùðâ•{OÎmÝsyÜÂMüt'®Ö˜»ñû‹CW/]Ò½ ¡v?þx¹n]ÿc? ª×æ“:Îvï㲉c kÎ[4¯Fx°¾äΘ™ë>Ÿ¿ÜÍ8cÉ.àÌE{ö_î6cþK­CP‚šõ jë^·N”—À9|äWuMîPËgã²ÅjQȨ¡}W‹í2xTãêþ9‰'¯;0tÞbqÖå™ÎÄÄ]>ùÓÖï–l8x¿u³ªÞìwkV?°ôØ­¢ÚÁ +~xôâ³=¢ô©)¹{w‰V}u{ÐÅe9Û÷?¶äF|º_`xÅéÀo ²&³þÌDv=׬^Ö¦ºÃîõktêä“dö¤ñíëG‹ò0ž[µ(÷YÃÆzôšé¨ôìÕ­[¿~ýý”™¦†¦Mº§é©õû.=l¸º@šõ¼nÅüêÞÄÏ;wc„¸ŠBY–%m£U6lØøó®aj(exµè ÷bŠ¡Pܬa—ê ©¬ŽÛÌGOrŒtóŽõ"ž'§+íH¸9¹…£œކŒë„w«^ÍëÝ?YòôÒ¶K·/m˜?ۨʑ*^ –ŒÙ«?ÿ ¯Õ»60eb‰S‡ƒ½ÉhŸï“®Ÿ5ŠÃ7© fþîc7JTüØ®ãØ_rÐ…‰ OÜ?¿ýìÕß¿™m.͵® Îb½ëvtc¶.(²J‹v5¯$ø4¯Wåæ­;P³j€ä§\` YûÎ&\Ì;å ઀g¿mQ†unàÑh@èG`N¿Ô›çŠyî³j+áÅeSÁW¸D¹Úa?—§)‡¶¨´t÷±ëY*azµv{ÂÜH{€¸‡ç}j¬çï]chõØ1{aÔŽI=b«uòÇ986è5äúozL7bÌÀȼÕ’Ø‘;œdC¿œQ–w+Y['ôß -ò0Æ@»>ÓnÞ &ˆjõ;ܾº€5¤HdŽº{?8:ŒkÞcâ÷S»S€®Šw%‰TZ»íÈ3–*œœE@(±óò"#„1Â)têrhÑm/‚ ‹Y{ú¡'Àc©“3eø4&<¿Ø|1ï™@k±`ŒD™»»; À9zùØ oO/Ÿ¬Óû›vwF$)‘H†Ì?¼z| É8僵&b§ðå_ÿ¤W÷¯ûŸ·÷ë1º¼S"¾ðôô6c_™½ÿq3GàúN¹óh2EQvvö£—ÿ:o°¯—·˜Äˆg¥‹ƒLdm ÊÕI0µIà4’ô‰^{ái—(Ñ ª]¯a´iãÕ|­‰V: fuª>‡$}üÃþò¨WM¥ÉdD‡9€ äç#'ï3ŒGŒnÓ{ò¯Ûú[4q±QUU?)xqÀW[Çt¬JÓTaOðVÉ‚"ÊⲬ}O¸Ÿ”Y­V³e‡Ny(5#šž ¯êÒ¨LJ›Ô«½nÛ™ž½[Àð 3'ŸLQ#œ•n“Öœ˜ÚC©3›bûÞ=ºø²Ï‰òU"eîÎæ¼kÕtÏ*(«Ó´ËÒ½‹®ÒEŒÐ_Yý—™[tåö£Í›¿svóyÿ\lüÏÁ%2Çmz#îã2ï%H’¢( {¿¥_+÷Ä# ‚ Ë_1ˆ¤;~€¥vN‡z¥Í†ÿÕUIÿ-¢cª}·ìK³… _x¶öajŒ“?7{¢Ô3_$S:&C²@`4X$RJ  ,:I‰ø„Ñ´Ù¬Ñ]ÜDDdç FOïŽý×mÙ8úµ…Uiû}2lÝ!@ˆÃeÖ©òr…‹”%¼¬\ (G©Å¨Óó,f“#Æ3 ¸ïø¾›¾ik͇3åÿròA‡A €¦–eµzÇ2½")ÐêÍ€ðøâj~·Ë “#˜Œf™½‚3¤À“¤gß8sþVõ–=eÏkhPå$¤«jÕ® sZƒ™6ë@ ’‰yD€$+aRfHg0›Íeî …Â¢Í€Çñ9nÁÕàìäþ]:ßr±áMJ³î]L(9°ï–3c[ù©³nl9žyõI…`GÚu*—w:Œògm@Xæ»|Ïéå{†V«u@J&l?qÌݺŢ6k÷‹ûεîiÐë>öh5ê˜æãv·¤´Z ⸒¬¸³W‡·žõ>_„?_dM¯ÕjÚô›Õ¦ß¬Š²Õê2«ýAOú×jC zç&³O6£tZͪÝ;¬ELZµoÒª}ÖôuY…WêÛ–\` €Æ4K¸Vëø(¥Ü#Ñh4fW…÷ý'­[t:-%pÿfËáo¶”ï«×i¿ýiÆH§“Nž¿²¢’„À}í¡ k+•¢×iY–åKBvœ¸xÊE»N,ÚU)^Ç2 kÑ^9sŽïèå@*«ì;sÃz<‹™&|vŸ:Y‘Þl6™_t¥z cx>ÉÉ#²ÙG©étÄÔMû§>Ë0´Fc¨ÒnDjÁ뉂×ýxfÝðBÃTÕVCn´R‘Åâ˜ø4·âmÅùª¨À{O ¼tóáæÝÇštºëìEãËlü¿† € àcކ@R€ÑkÎ[7`‚Ä@€°-Xôc0™ß6]‚ @,„ìlU«¾Óº¶k>¼WËÊvßMTѺŒùs–Þ}pRê¥r¢CU{’,÷ä$HKt/ɉ±ÃÇx¸*[ôžÔ$TX«E½Uóg­6ôú´ÏgŸµß4m܉ÊB¹Åvë9{êü%–äkZÇ&Ñ##¨¾ƒG¥9 טÎS4®T&ûèêE¡Otc÷ò£Œ1F€I ó©7¸ÕÝáý‡¸‰PTóÑηNåè±`À€!¸ñ`Ç#³‡Ž9'ã¡Æƒ>‹(Sƒ¬qOxþL§<;Ö¢:uúÈ‘]GÒéï^lߪþš…ƒûôér36Ê5¤Þøþ}êx]·Hàé"`Ô)©9˜'ñ X Ÿf–2!˜ÒRrevR¹“»”†'Or\䔋‡Ú¤¸ Ì9ú†¸‰žKZŸ¿ò›9.Æ î€-&µ‘rt6kJ({%¸¤¸¡ƒ‹¿·€º˜qPR3’òL4O.Nø$ øb'Ÿ ³ßMÉä7Ÿ1ª £7XT&(Õ‰rΠ³ %ZVbïnF”§‡ƒ ðYR±Ù{{;òU%<Éqô ñpßúå™Ô€µK†UTôy™*¡ŸŸ³u aÌ.¤|\EÖ&3'ǧì”þ>î€@_”¥“ù„xÉ0&¤:ùD¸9ð %™„£¯„@ª´<ÒMŽeö CQò³äùs6~up_è¿¡¨þ¼£úOŒŸ4¹¸¸èÕÉ1I* “ÉôÚ_ß· ‰¤2¹Éd|×ÿ<Å%%W/]0pàkÕù|~ÝúõËÊÊ^ýÁ')ÊÑQQVVŠþAp’$ ¢¬TõçÃ:x°eË.®norT/**¤þU9Â0Ì®?L˜0á]Õo9R­Q—2clV¥ë ’(Þ¢ÏxB‰ƒ]àË­Ó=mذa£Þ•“û4¡M—Þ2fY楥jÝ•‹g}Bkúùz Hd´p§þxvûŒ÷©"ø¡1Õ+o‘[ŸÊ<]¬ïù!•ãÀ]B\Ë_‹ƒB‚*}$ˆ­ôÖ.,ºJÅ›Šú åîÓ—mù=!;ZÇk‘½¨J;:(ù ‘rå]^Ÿ>ã—ÿž_&å(ä@Iå©ÜGéöÂÁºú‡¹V¼á9EVq²¾l?dIû!÷ô“Wª¶ÔÇõùQ„(4ªJ¥´$È<Â*âI\#«”"uö}žÄ)È«ÒR—Ð(—]ÂGŽ5„˯=ŒPqQÑ{äj4/ŽšüíЋP(x­Z¥HÒh4ÒËkm”BÅE…´n¯‚Þ½%Y–¥(êM×x<KÓ$ñúû« ³Ù,‰Ð;Þ4ì;tROS4&‚ Íš‚¹SǼ}—óÎÝÎÈtðŠF÷ˆ¯7Á1¦¬Ô¤‚"u`®RÌÒ¼Ïû‚XKQÎÓìì<ŽàÉ]œuE¥î‘nN¸›×…NÞAQÑÙÔùÿ& ëÌðéË)ƨU—rËårëm³Á` x ´†÷¡(jàÀþ>Þ¾«ž?ѵñ*ÌͫѲM¯ѯ¾Í«ÖÂ]*@¯ ?õw”V’—áÛpþHÄÝSEWwúñÀù]GŸØ1ÁX¦¹æ¸gíÆBà°í»ô¿IQb©ÜA ï¥æe‘$©ôðS*s³³J‹òø"±»W€ƒ½\(SűˆãXÆbtTzWäð®>Uÿ4W/ÿ7þZüó?<ÿ‰ßr>Ÿsçî§OÓÑf:%1–HÄ‘‘¯ÕLnnn¥*Õ×EBzëÔ«!T¥J´L&g^ʦ(ªzõ§NŸ,**ü÷΀ ×hÄrÜ; ˜‘IFØ*ª!©Lvûø)ŸÅ#³,Ç2ŒP$ `hÆ*Âl¢ê?ÆMI~A!@ –‹¥2‹âú Þ IDATÙ B“•§±st÷ò– yˆ±hT…EÅ*Ž $öÎnîî" £:;3ËÄbžPâäîåâ Õ—äå™XLñ…Jwg2k²Ÿ=32ˆ/’)=½2bÀ˜.)Rw©#3ådOY½[K)ƒî»[hBéQ  ÉË£x|ÎbÊÌL×Ì<±Lé-zu~N¶fIžÀNáêææB!æ?чے š†´´´¡ý»Öô!gm8––’2krçIek¶hت3„utqcLñ~—Rï,ª8Z»ÇfÚ±î îàÍ¿÷Œ±hǦÕë¶´íÂÄ^oH†“.Úu5û«¯&HþP:`ó¼QCÂÏ)<ýsxûÞÍcüÞRú[󱬛:RÒù›! ¼^Ê!ëáþÙØ:÷OåŒé]ó&W9©}䟬‰.ïVϾSøÎ~‹~ÜA½OåóSînÚv ÷ôeáŽðèæ‘u@·ÖÑVWÇý¢%IÒÝãq£F%%%B¡Éû«!‰¼¼½¡ÜÔêå6ä UªÆ¸ºi5ë"磚 çìää Ppè5z…$I©LúÉ'Ÿ¾ÝõÃA„P(tww'’cßçç=Ń€H$ò ôöõåñ(‚ 8–¥Y–¶Ð¡@ÀçÓcŒ0¶=þû¯`(ËÕs‚þƒ:º‹ ,¿pç®Ó\•ŒªÒ®u#¡¦dÿ±;ZF“'rtõY9†ô'ɧo%ð¥eºn½;9€¾D}áÜ͇ù<¥}—^‚À¬3ÞºýäQzœ–¡>ëÞ>Ì 4ª'¯e[¼vkÎMJj1¢G5¤pž¥0&Â5³Ø¼zã–†í˜Mz ‚FèÕnäE•A£‹­ÓLêQXÚXX JD JW%ÀzM™Fk x|…Òeè䊒¬‹%:Ì©ŠËäÎN|̪5:©½#2©‹Kµ…ÙO³ õcÒ–”‘<‘B©ñˆW¿ G]yöpSÈÄ5IuÕF³ÙláöRÚ¨-,)£ø"{G Ku<Š"IxbW'{]Y±Fo"ùbg¥€ŒqqêÙ­ÇÞ^î¥×–ª5FXÈ1œ½BI±ž<`ô˜fÖâ0G±˜Ëíö2}Y‘Zgæ %ÎJg> ãÒŒ«kö_?3g¼(h³¾D¥Š/ðeöެ¾¬Lg¤x{G…gíGöŒ<²ÐÀˆ)),¤ÁH”JmÔ˜hBáh§Ur…žP*ñ ÚR (d±×/*¡Ãaö˜á*Š-,ä—è» ý¬õ®gñÀÎJ7w’x¿…\ÿ8Æ Ã0,ûÚŒ1IRÞ>¾Eð‘^>0Žã˜çÆô¯&ಳ³W89ÿkáŽ0 ŒY–¥æýæu!„¬:<O"KÄBŠ¢hš1˜Í Ë`„å2…T*ÓhtÖôÿî2Ö¢)(a6Ln×n%b™½‡ŒÏ°,ôêY?>[à<¨–r毷B¢êÎø4ØPª½¡iZ/ÔWN.Úyiâ²!á÷¦{…ŽÜzÑüïk´hâÇNÞàìœåüÒ¼âa‹û×8ÿÛߨjSGw7yciÝÖvÕ’îˆn0hü´IžûêØí®.SG޾•˜ëé8táîöáŠ$ø|>°ÆŒ ;¬üí~m.~Ðð¯F-ÙpzñØs]åÂZH€}>}’¯ãó…-F.™Õ³ve×@LÙÊo6Üyl<°vîö9R¯êÛ÷­ßûõ¸u‡®z¸{6ølxcá¥ãwDûòä,ßÿìéuôJWI’ßóë#›ûcV»}Õ÷]—q™u5Ï*ÒÛ¹úx8Ê>ùbI'·Œ!SfpnŸ^?¾€»zpÝ€©ëCÂb>°prç^í[e‰Ø~ÈÒƒúaÎppûMǯvz±u8Zóʯ—~Ì?0  Pµà—«úëVï;%–Ø5ì>âëQÝ€ Äür mIúˆ~CŠhcá”í¿¸&ïY¼ýÆ¢53§÷ØhüÒ’ó?¸Ôï?¾Wã…“ú«½zl˜Ó6äí?ðÛØã7`ôø!ƒúÇôëòñ(*Œ±U 0/?cúøyÓM$Æ!ôZ#†ÿ¯=Ær|–e>Ô¬”wà½oå-4c¶0I"„ H’ (ŠeYƒ^o¡iE ù|ŸïçëÃçó9Ž£Æba¬Žö6>r Ey.uÚÀšU[ÎÄÉ•îvžÁõBXöÕ’3jþÜeSê*,e…>5‚`æ‚õ‰E¦üÅózGûÕn†Ân_<;nÁžêƒÆ¯ï]µ[ë¨OŸBx€€O=|Ÿ]¤UiT àYÒ¹ë#í=ƒb]ºÕöù6%-*,Xæêþè×Óñ ¸>ÍønçONµˆú£k² Sá!åà+“*ûäå{¡|eˆ‹W¿Æ¡;³Ò ~8‚üôÌ«yj±gà83ËØÆ©þð( `X®T]f0k7lÅèuF¨Õ yjr‚ÂÙ#L$`2[xñvô]Õ…ãVî «¶8•@ “J;´ìµjÑò§‘“gì?Qµ «'½îÝ? å¾8óÜÄ–$ɪÁr{±È( ôgò.]HÄI ³Îlžz´8çá–}§o¾vCuyû’ÍßÍêYûÅbqöÃSw í–Å@ dÇß ©¥˜:¸×¹sÇ^ÌLKK6鳞ßDÕ¶¹ýþ0Ï«w4aI'Ï;¼òj|Ñw÷æÛ¸yí·#›/.J¾v+‹ÚÞÑ›-|Юs·Þ}ë™ùèø®®Ó¾¾ÚkØ•“\ÛáI€èü)3×oHÏlËȰù™Ú1!á7íµµMæÎ|fkiæÃë)†)³¿XO0%ž¹™v(!=ZdìÓ¶‹ÔAÒäÓî"¯È’ÌøÃWÎgèæK’£…2{—ÉÇÑÄ̓[¾ÿéÚá™#{¤å~Þ¥gç¯öOìúˆˆÿöî=ú$Y»|K?ˆ;·ƒvmÖP €´âr—l_ó0ÑASêâ¥øxœyþ÷†¸ÿ÷ŽèUþëLj­ý¬ãTp²XÌ&£‘¢H©D‚SÕºuK¹ÜîYƳœœlßã?öî²ñ1@<Ì1”½Â=ÐÍÑÅÍÄ”¯)1°¢€*±À±Ir,° ¥§§LH 8‹Å¢§„G@ŒÜÙÊJ‹.œºN° #ý|&¶¨EªJÿšfprõèÔ¡ôñK uØÞÎ!Ä2´ƒ“’ ‰ÜýCDΞÖ*U|qæÌvöÎ;´â(âìå¸Ò<]fjöÒŸ´1^}>uÆhÞŠƒe8ØA.Áÿ ñŠ÷¦âñ_II±¿§ëäîUoÄç?såÓNmæŽí4wý±¼\‹·O€5%BVËâFøßÃQ½4§ ¹c HR,¶€2³s‡8WŸr—‚Œ_¸|¾€ 0ÆØ R5Sr¡¹(-ªáêáA¥Ò/Nô …A¬vÿ®“ŸLZ˜¦ùB;'à2™”ÏéJ¼«*U;ÈD Ía‚Qi‹ÑŒik(h28&$À_>®ÕðP8yø\µÎ´& " °Ì JJò‹@^F!F,`V‡Aù¼mhu¾Â¯ 1…Í Ü¸pÙ½JƒHay›üÞ>´A¦pãÓ„ÖßÃQvgØ‚%=§ÍН§ÓˆƒÊ΄ W÷N_rjÍÎÍÌ“ãÉ,.r*!1uBÓPˆiX«l÷º_÷ÛÇ~€™‚EË~váiײs'cX¸„/×Á†ÿo`Œ0Â@–‹+–e0Ÿ¢(’$ ‚ptp Rº8Ÿ8ö+)VŠÜÂ9d›ãòß@dç”ÿè>׸^.-”s‘\é&Í…â#„¬OkB˜ÿBÖâNÑN )w“\»“Ú¾zL­º#9J¸x#­F“O\¥ÜÃøT·`ÄsRã4µj ©”¸xÀ!,2P}3ïêoŒPyÐLôßB““•¨'Azâ“RR¨½ŸS;Ä•ˆOH“ùx:Ë-vB\B3Ö51ÿlËÙx¬ƒNã2•jÐg­òµ°tí6—àZßî;õõøžm›Õ]²éG¿À0ë蔵Ûy)‡wUˆ)).Ñéf½’´¡³"y¢PŸâ §¯gê#,*5ø:¹y8=zí‚8žÔG)NJÊtF©éù}'}á°ucÀ½Ç‰y…âÐ&Ý%_ÏÖ A÷Ÿäz8zV.47þJ‚Š·¸S$XÃ…`L›Í€–ãý«¹á_gèÜŸ<ÁB‡@É-³IB`\¯ƒèø)N*ñ÷qy”Z w.¼“^2xx;àX#Œ8„8cŒ1ª2=M€y n¼dóoë†7MOÍòh`Κ›Ì­££Ût@Wï&4½­¢UžÝ>:ÿÛ‹[¶¯vð t£KW »—Z´ŒÐ³}ݘÇ'nä>Ëäñ@(‘ð³ãK¡º‚Ñå9·Œñóþ55ñæ@›¸÷·ô›÷OŽëúiƒ =œj|ZEØ{ð² 4 ~Ý¡siضëÛ§ ÍBjrZ‘òyÇ! IêИX’äcăž@ ЙX¹è»?[zµéÓ«µ……§éÏ.©õ|_AóFþ!D£7}¿çDRåæÈ²´­£þO`øà8ä¾tíV½º¤f‹>`‹Ù8xÂB¹£KͦLF=@’$‡¸W®ßMTYÔÉÃ!¥RAÚžñ%EK&ÕiÚÔîѵ,ÁàøåÐã¶È]'oú) Ñ¨ {ûLî1øÊ7›ÖÎ?´óˆqIUÂ[öäí_cÏÒŽZ·¯åߤS?ž ÚÞ•½»×®É“:¶»:6úwQÅÑÚÓ¿^ŠlÒÙÃzÀ”¨z­Zž )P\µê5}ý£7íœÚ Y3¯°ês6mÉ;ü8ÓÉÝ)˜5{9שÞeÁ¨ÒOëÔHûÎÙJ?»)÷Œ­î )¢£«¸ºø6¨ç.µs ÎÛ¸rî©Ó¿99ú˜ºtâ—S‡mþed¯.vÏiÖgò¼÷.Ké]«Z­Oúïüº]Ü…ïUfçvÑ"¨Ð7ŒV§3‘ òÿfæþ£¿:I ª7iä]³ù¢!¹Ý{ôŠôêðùP;à+úöï8b\Çg£Íê9¿ÞÍ1ý©Y£it•Û×δôuDXðž¹IËWü²tR—&ÍBk]Ü€5d®ßzqÌ–SPIBÙ.Q6¬ ò* I^bRâŒé3+œñ1`Œ0Ã0§NŸ*Vs²Àúf$´)ªÿÆGº0sä¨y&‹ElïR­IûÇ—®¶]¿fûž$½aî –4hÙ[t¹iOº÷;Ì"ìàZµ¥\"tÐÏœ²H§× $ö~QõbëûÄ]½ÐáÛ‡,Æ|¡Ô?ºžO´/kP-˜³\£ÑP|‘ÜÙ#ªv3!¦9Ž \=—›G:ùT­ÝÒT”Ü°ÛÆ°zíª7­6¢ïl‰«ÕŸ8U߭ݶ ¸€â‹d ÷ðšÌ9i]üÀ°ˆä |"jD† ‹Á¶Ôô?ÆN®Þ÷ïÝ¢DòÐØÆAXÌŒA5šwÃkËŠK ²•^,M#„ òåXõï¦æ[TQ"gÖ<¸u]Y¥­Ÿâ¯æwíáõI”¯à/æ“rûH©¤q(ø}Шœ“;õ~jŽ…1ëí‡èþÞ>`WNíݰzk›•D€I•zñfv›vÍàßÐR2L ÿ/+ã0A‡ŠâÏn]6ù¿á¨Î;ÔlÒö¯—NDtý=©Õé¥|*eHÚ)Ý\ Xhçܵjôû?žÃt@`Ä´Å›«Fˆ‚Û´ þë5·aãdzšç lú&? b?˜õ¶ Fˆf˜ßÃѰ,c±”G±4uÛ9ÚLÓæÊ{"`Yƒ®òÎb¦-/¤y)“áŒmù+Ž«¨ƒQ¯©”âå0á ý¿°^øÿ'ˆcUÅEÛ|V’ŸEòø¯& ÒQé–Ÿý”¦’â½j¦osTÿ;³"ÒÁ'¬®OØ_,— …žÁU=ÿB6lü€ €cš¦†!I› 6þ* Ëäääðø"xC’“Å!L$BåþAVC+ï,ª0b2Ó’X¡[°Ÿ ¼Åà‡3§'%$§dú×oé*}S2mafZ‘)¦J8ïÕóª<8Öœ—açࡽ¥ô·‚Rï^Ÿ!®’—v7–>»ŸdlX?êÏåŒ2Ý48GEyÙW¤çCrB\zznx‹ÎAöbõ7.߯ڬ‘ô«Š–˜Ä³wó÷r©lÙeÒªRRŸùG×´Biq––¶óótÀÏW“Û°ñÿ G2§›œ=€¡m×€ 6þ8ÀoŸ%@QÑá º—ß»ŸwUœ%ñÑ-‹}-«¨zˆ5g¤<˜1qNÍ¥§·u~Sª¤Ë¿,8œ´wï·²?*×Tö¤Oß!koÜÛ7o~ãqóz7xÙyáObѤ 0ôËóI!/YŒêŠâOœ.²Šª?„ÖeŒübd÷nWNczâ½ fw:ÐlE#GÖ”Ù»WïS…9áïXIÄh·¬^¡¬óÙ”Á*mÆ/\¶åüºÃî=œ”ý(9ËnÀànUC¼(Ûz]ÿ/AMúYdÔ›z6lØø°äåd­Xq£âí»Š*fËÜ©'ЇÍú ²“/üuí&5Ÿf›–ü°Þ ,‡¿_·q×1;¥ï¸;[výbcêÝCr)p–ÂÙ“—ôž»(å®Û²·Íç“Ô—6_²7*ÐÍ!ª‹àÄ÷‹–o;®ô‹ž²b]MWþ+»´gSÔgß4q±?%]Ý·úçåyµÚ6¤Ý“ßö ýrµ[`Ô/¿ PŸ6ÿGw¥ÒÎAJ¹ÔÚ0wèž5_mûù¢Ò¿êü-냅€1wëðN·úŸwr‡cûތ˯Z¿f᳂A3W¹åœž¿a[p‹YÖòtEñC?Ÿ ¢©†ÝFÎÚéÇU_®Û÷[põæsV. ÆÜã³yA­E¡Ò(_¢ìÐk” îò%H’ óö\?qL^fqѳz6¼öˆKw)ý"†Ï^R[™ß¥ãèzÍë¤gçoYï/`íZ·þûÃ…ǘEk"ƒ½ÏžÝ×ç옎˦Ž5;ñk³aë¼HÐ(3§ïpˆî*r÷úHÕmØøW())€’⢊õ"Çq÷—ÖÔI’<þÄóûWž?eµ/bYަ-¡·ˆ Ÿ/‰ž‡I´aÃÆßC~AAå·ïê¨Î>m&Z·*)>¥mõÚfmnFnáŽùË®Ïþ|Ñ÷׿Ö1oýñüª_.EÚCV ÀË+Õ•OžG†øGµ¤ˆ¤uiÉI¹Ï®L™µý×”ø{k¦|—£-»;kÅá3ñ· O®úù¸k'6¾4†d*}²hËõí7ÌÆ2až‡VVéÛs̹Fþ‹¦¬š{únPê/ÍéS_lÓõÓì£oúÝ”+·Ú},ñЕ«é¿>à«ó?ΧµévÝœpâ7P'hŒ0fDëÅ}·¦÷ÞG–|=p©,wàÂc0¦`ÝÔCÜ?]ñãyE&Mî…©KöÞ)ȾµbÒØ)+ޝŸÄrwþtsà×{^jŒ1€%¯L_áÖl4ªý?›³Â+¥ß”eþN&Ìþáç¸Ûìµ]#fÎÞ¾¶GVnÑ÷ó–ÞŸ?|É÷ç—´~ûï‹\ªæ 9IúRQ@ý­ GõiÕäPï]}¡,ëÚ©›Å—6û”þzü꘹SSR‰÷ãÂGÛt• Vhš~øàA\Üc„0E½O¬±XVµZ5ŸÏ¾un»Åb¹{÷NRbâ»Gˆ„   j±±b‰Äd2effÞ¹}+';[ |ˆ Ý$AØ;8ÄÆÆÖoÐ@«}ÓŒl6lü ¼ëê?‚ØË%|ŽG€@(ªY­¹@D°ýi9þtxÖ‘ö>.<Œ_A¡°sàó¢I;¹Ü˜}OÕà €ª}¦H•y}_BZ¼ɓMš’:õ»¼RªåôÎï:M Z+–º¶îÔ„L¸Ÿ}ÖƒëϺÍDŽ"¬.³Ökî#»¤3„šž&ÝIT•ª¦N›ñ³:ôó[·n6kÖ½9ô2ÇqOŒFã{wæÔÉk×®¶lÙ²¸¨èîÛaaa:t@øõA¯ÿ2„Áh¼tù²ÒÅ%<ëµ%ê “~½ž3dÓ*°v::u)¸šqpósW³Ä9,"•J;‹Agâѳ #Î7¢j§>×.ïm͇ÕgŸ¾’Ünø `†e9£ÉÌ2f³ÙBR$˜-ŒuÆ7Oh_?Ôû×§Ð?tZƒƒ‹·©ø4Ü»÷Ô=¬\¼ú f‹¡«-5ù)×ä¶i׌À˜HF•@NMÚ…Љ(418ªFí:õ]Ö._hÝKõì´Î yÞz2wWCÙõBW€²â‚¤ô:5Ì´_e7ŽÝ3ÝØfc‚ ÄNÑÖ©Z6Eeơǟ8 žGø~L‚ ¢s—OW®XÖ´iS’$ß$ªB÷ïߟ:mú;gÍ¿Õ'­¿Ý´Ñ`0ÛÛÙùùúèôz‚üP“#)’lղő#G#"£Beݶaãƒðn¢Ê¬N>drbzšÀ)]£>¢½E–_œÉs kév¼O›v.®î}¦¯ÿÄžjØ¡ùüác&¤v5}êÀ.Õ¦ú´v¸›ž²s‰é6¸Áƒžƒ†Š5ÏLþ”Áƒšx~ÞªM;¹XÕfôÜ/šþ^$fï]¼$ ¬Vß*žªa«¹ŸÔ«ÞÔÞz6jêîìØ|ÀÌ*Â_gè°}ù®A¾:>µu»½ a߯ûDëI‡Æ±ÎPîoûüc,ܽsóþ¾OC³×D êßcãúIMû68îãÖàÓoFh}¦ù'mDÒÉ›¶Ç%g¨§-ªVQÅŒ;ÇFŽÛ–Þ.‘ ~‹ÏZÏÚyZÂÃÛIEÔî/‡ìÑ×è4¦AÓ™7&6iÞÒÙÑ>¸AŸé}\Éç­%óoëw¸›vJg—^“¿q°—ÓÏ•+Fú_¶ìo:yÍï­bR6l¼Pá–þÞ׈U‘EÓZÄZÜ; ¸òüI’å8„`̲ „5Rᇻ´ Mÿq:6lüÞÑQ,¬™å‰eÀTºÙ¡ž;•#“TåÁeö ÚeWiÇb’W1,m-Ô¢Í[µðß–Óz·ð·&z¡Øç;õFuˆ9Hðòñ¥ÊnÖC>,@áß͘Ç$U^#̱ŀÛ'^/®½tVŠJ`BüòѽI±r &ù/޾¿Ðzˆ È>ź¢Ç#G-_üÓ.ÏÀQ½<‚éümØø'¹tñ¢³RÙ¸ISëDu–aöíÝ;~Òd«( „ž>M¿{ÿŽÅb)žÁ˜âñœN®nnþ~ö¯•DA,^¸`„ñ Ë1 óÚÒ†Ù¹cÇÔͬٙ IDATé3Þ#ØA›6¬ïÒ¹SrJJfFFýúõYŽ%Iêƒ^àG™:}fQam¤Ê†¿‹ŒŒg{÷ì]±jõû8ª!â=/^”•b¿*U‰‡§ç[†äÔ¤>½ú½´aé‚[·oÈeò°Ðp''g„^˜Ïôg¯µ÷½Êóÿ=ÂÆ#ø@sªžc›JeÃÆæÃ†©ùëÄK9|„ë¯æö1HŸGýþí*ذQŽV«7›_9RŽ+Ÿfž‘õ” qˆC˜/PS¥*ŸÏ¿}ûfttŒ——÷KºêcŒÿ÷¿Ü{ذñ?ÏÇ¦ÆÆÇFqQ¡­ß·ñ‘ Õh”.¯q!®ô-/?wîœÑhäóùÖ1FÎJç   èˆ˜Ö­Û;~”Ï绺ºý•¨á?ý|@­.£x¼7 ‘$)Šúôîû²z#0`Œ?ô8’mœÊ†Í»¯þ³¨·¬]J;57¸5¼yT†Öç¯YÞ?6l|ÞòmÄÏã7Y_4jÔH àC¹zÁ!’ T¥¥&³I Ö¬Y;--ÕÅÙ‰¿ï|BBüܯ¿6[hŒÃ0•l 0ËrZÖI¡X¿qÄ‹“Yqù´®>TfSU6l|`ÞUT᜴´˜º­}£€QWôàn¢ƒÒQoÂUjĈ ./#íif>%”†V­9uÁ·©½f `dI|œì%Sú³,W¿¶8ýar¶úé3š’òT¹© )Ù|©cX•GÑ«K`púµÊ7לü*?'3^Bó¤ÎaþnÚ¢Ìq©™£h¸”)¼—˜# x| Dα~Ù©qÏrKRETlŒŒŒqÖÝŸOÆï·ðK‰¿[ 2Ú;;Z ¿°*"SAëµpYOky¬Eûðîæ9{†¸ç¦Ÿ/Uxûº–d¦ö‘Óf6¶~nÖ«âÅ[0Oéè*¸q垃‹ÃñÂc"iU^‘š ö-L¬ùxˆÍ‰IiI!L…ÅÖRqô…“ä-bœ`ÎØa©ªBÑ‘¡èa]ÙºJ6^ÏóëZ† ’ ¶^°˜C\Y©zÙ µfø ~…g~3ç|ïþ£ƒû¯´~¾ëeÅçóI’ØbÕLVc„Bð4MA‚Ww´Ê8„‰ÿìê?Ö¢¹{û‘Ü=´j´c2½ôiaV|\JH$’/ôñu‘Ò4mSU6l|hÞUT±·O\¶çÔgã·|Ù¿fFÜÉÍ.Ù´þž{¬?Ö×»xÖ—“t¤ƒ¯_k§˜æÁ‚Ìb5ÎΜ3¨oÿ¥çn×b“fL3tÞҳǩ¤žöæM@G¦1_Œ”x2Z•²þ å£Û¾ä¨ÎYŠW.ýqÄž+€YÝùÃû -¹Zñš½›vÌþòz®ÙU.vŽlÒRñhà¢35B\¡£QØ?gÚÄ)RO?ciQX—_õª˜Òíõ]{,é#‡ 6r”†•ÖŒò m~oΕ ЫO·eKð|µjßÈ6pdÃÆKT,è«x‘˜”DÓ>Ÿ_9Ç %óó’~º¦w­ç»}ó¶àþ+ŸÏ"‡â€ P©T#àBaŒÆ!Š¢ô:ÝKÕ{¾?¼TÛ?Ä\–4lì„]güزniƳ—q_9±mÞw—+ÞŽÞroN¸Sn^žMRÙ°ñ¡y×05‚î“K²g ©¤sëþS‡úTötÆñó š° =°uÅó俇w 2Â/H* _o:çÊ£"Ç;7~È>÷Ýôã¥Ù·¿Ûú·ÍûG•Ý?¶ÿ—c0ºí‹Åâg·Ž¥ÒÞ›p1I ;M\Ù§ªÝŒ¡}®ž=r)NwáÚ `{ô™_R]Þ{éñ!nWîiÂ’Nž¹xlíù;É ú-º}ðäÁã_õª‘w1¾L¾»‘#Sð¬IËv=z×8'aëÆö_-¸ê>høF¶“ È’÷Íò½«Ÿe6€Ü;kUØïÂΆÛêO[5¥ÓŽ’´»÷³ÙYKb_n’®1ukÏ(骃ϜŽ÷iÏïUS¶QÀ™²Vn9¾?;Ûƒ9?éPˆŸ~=~4´æW¶¬Ó†>¾~ü\í3¼Âüü­Y¿j ö—NU'îßÊí ØHƒ|ꢙÒ=ì ([_iÃÆ 0ÂкMk±Xl¼/ê–kOꙆÕΧŽeõêØé/–‡   \SYEF"‘˜ I„Þ¨™¬rª0åÊô›JÔ&‘X,Wvªç÷ÓÉË£Ö|¯;¿cëÏ76ìýþÔÚÉ·T3&tŽôñPx„²ú̹_ŽOÈ6ÖsºŸÝcÌÌ^ŸÔîùÅ쎃Ä$›ÓÿÓ!²ÝzÄj ’lU6l|pÞyõ²”–¨±»Õ;€äóEPf2˜‚qtñ®”ø…›§çvO€&hwM0³ Sœ6¿q÷e_|Öui7•záVS¶{×¹NÓ–¢_ê,ÀeB! fƒ½[°¥j™D€9ÌaºLG[L&̘HN߼ߊÁݺ¢O;N'yܱC¿Õî6X 5Y൑â ÊŠKù$y¹*Œ8ÎÈçI½žW€Ó«ìÜ¢ÀBb’#àÊù«>±MƒÈò6©\[’± @Âã‹ ;¿!'Î Ú++Ž‹C$Å-Í,F<Š*n:×N˜ k<¡ cæÜå'æ=v9MYÇ>éÜw;/gþtñó§™¹Jw·»û7Kk j`-¯’–MNÙxWH°˜Ì&F yIi™9E<Å"Aht””â4EYãÓxb™§ˆ‡„1[Ì$@jR²£‡·BÊËz–Nˆ•>žN™‰Ÿæ• Ø4‹óxÀZ )ñq%Z£XîQ%ZBrå>±¬îÜÙ±ûÙñAƒŒˆŸÆ¸Õóšä<}œ˜VV­vÕ’´¸BF^5Ô=%þq¾J'ÛEǑ͒bÀú{÷kÖ«f)Ë}˜W«~ £*/91ÙÀ’Eªb’”‹D¢WÑd2Z/ ³ÅlUcÆŒ‘IdD…°@¦Ò{ÛGT¯÷ÀœéwñÔ3i`SÇá»@§Ó>φø“ÆN&“‘ †fàÞý;Í›µtvvÎËÏã'àó g™Lvöì__ŠÇ³ÙÄq\År?„Íг… ÿ½³Ž‹býø™M¶èîn¤E1»Q¯bww·b7WÅînÄ”PDPB¤YXvÙžxÿX@®^ï½Þø½ûýðщgÎ9ÏÌÎÌ™ó<ÏymëáõfOËж8›Ñ…Ϙ¹+ñØÖöŽ,3M½‹gŽŠ1¢kÿ™Ú”@¦1å2™ž:“Ou¿¸g×RnÒÖ³É\¡T“*®-ËY¾ý2„/u*ÎÊ‹E L/§DÉϦmN*®¼tî2™£K•]¸;g”çˆ_`éѵ/ê*º«çÜy8•¥=zY8x„ÍôËß³{ÿáiËæÍY=}qøùW¶.1ѳ:¼#lζíζ£û 0¹¹`Ñö%ˆŠZ§‘K~£QÊ{›â8ˆ$ +¸o_C Ë{„ô52· 9´bêä–.‹6¯0©Œ+ª´´V¥°dúlšq§Ýkxk¶,%©¨õš²*3ùŽ®µ¿ €¥aÐ5ÐÜÜ`Ø` mÕ®þò¸ëg“2þV§"Ïÿ62xÅÑ·«nݸի÷ØN>¾ÌY¾m½£ÿÀa®iw•Óm:4û4ZZÚþ)@L¶– Spþʃa+ûhÁ¡mGó?‚›ƒêéã×&M¥‘ä8ôtåÊ0 ãû/Šx•€Xí‡(ÇÈ@¶1·ä¨k’H˜‰¡MßìcÊi\Ý=!æ>ïE¤ÛàH1 W/®l¤ yûüæãë× “É(*//( 19–¶:R‰¸yB'PÕÒ50ÔclÃÚgØ…í=õ¹üðÉ5c6Lïé9pÝ©m£ ŽûA,•]…jcÝ™ @§S-Ì<1?Oy9wì¬~V‘ zn¹Sö»Á¤–Ö4œ ]¨˜L&NT UQw”_ý1ívÀ(ÃûçË‚çí)©©Í£Û„b¬_eUõÓø' &ó]^¾€Ï'pÃ12…Âb±Pe1™¹¹9¹¹oæ5ki5‹@@ñ«çÏß|€è]¯ËT6ìÞÛg€Çgåj¦î¾}v_R3rqqPk̵ԗãÍݵ‚Ky±Oã`Δ º¥­j¤t­”(ù¹üÜŒê?™Æ¶tôüq킘طÿq9 oãû™œ–2]ó¡C-3øü±"»à+·}KI„¬éê®ù§Å”(iKƒA%R‚A¥â¢:]ÛîP_Ç¢é%­ðYÀ1Œ€ÆZMÇ ¡28,&&³Ì“×Mç+Ø¥n(‹1 \ž_ø­fiÁ ¯)é9•¼xÙ-±HÈo•ЄЀË$š¦ TÞÜÌMjšGÇ dL6“Éb€‘•  ïˆßó>þÛÜÝ= †"DBÕý7Ü–²8òþc‚ǹ\&•J1 ÿöñwŸ©€¢÷EsæÌãól›N§Ñ©4‚ “É…R©ŒÉd $rkóZà8$pî<05~(™¡* åÀd±U8zsSŸ-¥ªëЉºÏŸ’XºHMªêœƒqLøõ5§Î]”3LkßWÍZ¶mÚj*Á•ÈI3o˜¹ÎŒ­RU[[ÓâT)]*%J~6ÿöŒê•ñº#$ ¡~£G€0¹?-©ô¥”ü p‚Àä2Ãq!™t¬ÓxvWÔ³öê8™£e©A%!T­u™‰ñ1ú<Γ„Ì êÖ=GJ-zònPÁGÅeElN@'Ÿ÷OfšRÊDò*?'‰rÞf›÷ÚM6§)GqyjT´±F=¯ÑÜΞTŸ5så‘Û/²Î­^¾WcÕÀ!ýgoYí±–!âÚ[R)KωIpï¾)ª¾_Ë«aƒZ;[«wO¸ô>|ò10=~7ÿg‹A¸bôßÁƒy¼zzs¬>YkÁe ˆDB„DÂqùÜÝùV'Çq‰Äd±=}}hn7D®ªªF"“¤RŠ+¬úSE8®0hƒD"Ñi4‚ Äba#Agq˜r O CL6 E<ÅabX½D‚ª0ÕhäF¡@D£«±¨r‘PŒa@çèÒh¨¨®ú79â•^•%?™6;U&Íz‘(gXx»ZÀ×ß÷¸¼1=)>)%Û}ØÔÎæj_+Vóþuú~÷nþÔ?wЧ7.iµ¾ËÒvt³1øîÈ š}qêåc¡ö™„†ò—÷y#†vý6ÉØ«G7y&t[Êÿ¹{„˲ÓEÓV_”'¸å¥B„af¨ûÅ®o/|ÿNªÕÛ¿*á½Ê-µup⨾K”’ÿeÈTÍù§ê„·_'™®…C­|zj›`po_Èè¥qZF}&Ìu¶¶ž5y`þâñK¶œºX¢3dÚ7s`hݶiå2W»…+Ñú™/’ _0­®7tÆF:.kxåyyuƒ",e2©âH¢pF9b[‚©ªVo²^t´¯§‹÷Šáá^Î]7on‘pßêEêºÖëŽèØ%°QÝ'ÛØ0z÷Úå^­AŒ3Ž{àäÁ=]ÃæÙ–1êëëµuõ>«ã—Qä… ’O»5«Ö€X,”ËQÍ‚Nàñ]9Í ‚ ˆ©S§”~,c±XàŽ ˆb°!‚ T Ç s PôRÿÍñ SEQAs:« ­w Š…–!œ‰¤e&ÄÖË­PzUJ”ü\Ú©"0‘ ^ æZF%ž\;óƲwgsµ¯Á‹3cDtîæÿy…/ÖdÌ^´þÌ›ágéÆxÌêÖÅ-+¯fÍÑ“E§ÖDDg,Øy>¾{äÛeL¯¾Ýß6Œºõá栄 wiY¤ÅÞq¹>)4lV~YÝÈ…û6Ì ˜9jZïe;º¨W/\±kĬI»–„ï{-ˆ¹¸÷Zú¶Û5ŠÒxtÃÕC}«·Ô?x¿½”k%¿Çqnm ˆ›§7ùM ¤U,DQ¬±±±±±±µÅö?ÞÂR7îâÌ«¯ki~jÑû%õõõ_Êi1ï³£0 k‰Á”—Whik)PѾ†ãxuUUmMí¾ˆ=¨\ÞÒ[‘ü³ù¾ (Šºšš‘¡‘"#W[ûª+Ê­v¡è§ÞîþN!߀2N¥DÉϦÍÕW½îztÃû¢ÐYŸ ´µÍ=“¶ƪ]w¶tÁ/ßÏ~û¡„ ÀãH _Ô””6òP:ƒ*–‰Õ÷gl¼ÆçóÓ/„¯yTXSzÙžè‚òŠŠ§ûû ‘|í³à ¯4iËÙ×Osz4HÅ|ë¡áç.:Ô÷FšÙÆ…Ó¹u&UÉ!ã…uÓ œ9ÕúeZƒMnLÒç•••DoÙgrúãêWû/¾Ú°¸YzfŽ7–MœµqE­þuT®C_¶2R¥äïEQnmí?¥]1AKKK ¦Ÿ’üŽwA¦PÔÕÔ0 ûÔ ©-ÓÔ|ß-Ö¬¡iŽÀoL‹õý(½*%J~2mý„¬ŽËk ëS€B¥Y[¶›,+•„R—ö= yêªA|J²ŒÂa°Èd !$† ]V[`Þahëê›ðª²c$89bÿ~a]Õ”™K?WŠ‹ïž½ä3~= €‰,Ý.Þ¶|KSM~qž‰cO RŒtÔ$BTÍÎ#0è52^S˜T'’îÚ³GʗΘµ¢îXva ÐØ(æh¨ÉIT]CG´Q̤‘ ¢¢N‘ —TVÔÃ?ºÂAÉ+[Ÿ±`ÝÎ@‘/ãcÙVžþ:ŠsÒêaŠ€–¦¡‡­˜›iñ+kNïà=È´y?.çsëÈl )Tk‹v™I’•£PZUS+*YÝG:SÕÖÆúô°IÏ)‚nF¹ {¯ç”„Û€ª×h3êÚ<‘év+”då—òæmòûNóVtÊÍ/q° l¾jJJÉÿQÌ‹ øÙS©T“oÉÒD„\.oÉO‚üù¼š-’Ûz—Ñ|,BüÞü9-‚¾¡FJ”(ùÚÞQ½õ'r¹ Æk“ú IDAT$2”LcjÔ”¥VÔqšZ,‹ÖXU `©l:R'@E"~ee½ŽE/ ÿ!•–×re¦N^îè‚Y³à÷‚ß eÙ1/k¦íŠá1!ðCdrTMßœ.}œ ZR.ÃHzºjr‰¥c(*'ÜÌÉ»CGÊ¢¹3Å@Î/ŠI- ™;ä(Šc¸Å0TŠaX«ìäU4;Û\ÍÏtgðêùFVåWp€ôÔBC§öŸôʳÇüæq~Àýð2:¡ptØ2‰$󞥼ìeëõþ}•{O;ÀÓ1ûtêp‚h5ù—L.© %Sh@&!±h Ž“VP˜ ®žÉ/>ÚÍt­y}3…o²aÇÎóÏŒìv‡–SO_kòõÔÒAíMÒâî°­‚ìÉMZq°1¥;¥äÿÂápÊËË™L¦L&ÇQýÓã¾D ¨«©áø… UŽjmM5™Ln«‘HÄd2„D§Óé4:ŠaŠ.í?ïŽ%‘HÊdwJ”ü\Úöá"æfvêqþðÎ9Óמcëé’ÉMn‚P8ÖÝ:Ú =œœºö|Và u í—¸uÌ ±3r1“Ya¾‹{ù- ?(çèi»ô_ÔWïK·ýîËÔŒ5LCGx‰ílí<=½ˆýJBž§i×ÞKà“ËÕôèãÖÙ~=L͆ÎÞÝmâJWKu™PÐòT²ê2­ƒæ+{{{¯m×_×–äK©Ú8Ðâm4ÿ#k,Û³cMï1;ÓžŸ°pûÛ:½ýgÃüâááé½òÀuuû±c:sl¬¬Ö^*X1Wð1-¿DÐ#¸¥o,Q’yÑÂ8P) ÄG.÷ó lŽôaaõ‚–þbîëàÀîk÷œ?¾wá´5§éšÚT ­Y^þné—÷t ê— 1š=iHÔîíÚ¹]Æê„ÏY~lÆ¡óc&…fߎ8uîÈê_Ÿœ¾vjrˆí¹k?y—/'Y7öÓ9k¦MW‰’ÿ:$ÉÝÃ#:*J*‘Ze„j+ Ï<<=þØ©"‘È^ÞÞ7®]Cår‰ôíò…©)Éæ* †‰Lª®ª¢Ri8Ž?áOaí«WY}úöEå²ßMB¡D‰’¿äw»d®[³jíúµ5ÕjêêTê§nãÛ\-¥‚Š£ûlB–¹«ÿ Ìóû—cVãF÷²ùA9ÑÇWäâ! 'ú·ØÙrZpIéÊYëûo:â£ûƒJÚ@eÆÑ%GE§̆ŸšÚ¿w÷ä©S%b‰¢¯É™S'ÕÕÕ¾y%J~"ËÊBzõöðò®­©V܆T2%ûÍëääd©D‚ ¤ïø"¢ªªæííeooÈR©ôn+*…’™‘žžž!•¶A›Ívwssvq¦Òè†cóøýûB2‰Œü„éùÑP×è¤o`\]U©©D‰’¿„ŠÎŸ;¿s÷Ńâ'&ÿlÕi Í¶,«¨Î\¾å*9+ü/‘Ó{ü¦Þ_Ø©!³:ª"r€ÏsDü©Òï¶Jßcâ©¿oÏÏ@SS3lÜoTP‰’‚G ZLD#p777OOOùÚ¬€Žãr9JÈþУBÃqïö>|ýZfóûFù(Šá!•Ê‚ Ó郆 ûN[¿G••%?›Ÿ;MÍ¿æ?“ðWù ­ÿñ)’GÕ 1âk*þTé¢ÁˤTDþ·§ãWò?‰LiÝ™IѸ&‘ÊH$Ò†Õ ‚Àñ?IªP'•É6¹T_È—H$"‘ègßþ$IéQ)Qò³Q¾ÿJþ^Ñ£h2þRY%ÿfPTN£Ñ>ÛHö7†Q¿cæ/QösR¢äƒ6ßɨ”÷ëæÅ»ŽÜûã3YcÅö•SH$Ò´ë¹_/F¤\Ý?pÔ”ÆoøÎÃd5=½\N$¿5jô…„œïø4l’#çŽèêµóiÅ— ’"»OøFÉ8ʛտóòkÙZþÙµÍ$iØôõµÄwÚ .<º~U¦à[$`ÙÏž¿…µ2 “ÕôîàzôA ?ýRØÌm‚Hñï·G‰%J”(QòmTᯟÅi›{x†@CmQÔͧ††Ü`Òš#‘Ñ£öœqþÎ;ܽ¼dùÖ¬¬çç®Æ3Ôש÷×®Û ² ;W¾#Ç±Ò žXX4aÒ¼øáÅ#ÛÏÅ.¯¿|îAße;¿8?„\ÄçÕqŸÝ;»qÓ”I“‹$áVä'½.kä¿?i¼  äEô¶ˆc õ¹£'/•Îþzìi!ÊÍß´!üuQUc#ÿcMcrôå®7JÑúzNBD‚}5*“òÅr‰°bù‚UÅ|qInÊîýGy¸tù‚¥¹µb¿±'¼Çp&‹ÝÈHHгW±gïíÉG€¡‰©«­±šº~;ïöleÛ¯%J”(Qò×ÑÖij¨†Ïà êJÉ$`2˜ƒzÒûxElb0޲õÝ7¯iI‰.oI(EBHVÆfL•&¢HJJQË+Kæ•Ū®ˆæ?;r+6ƹ{φºªÌ×/FÿV-‘—p£šá2P ¯¥PÙf¬ìÀ¬š6*íÉì Ú£ ëA–ó˘͵ªŒÞ‹ÕŽ)°{÷~âÝwã­ý:Öóªs“^ÂìàÒŒÇPýmn4i9¿½ÀÐK6¼[¹°çš­I£ Us$Ï/L\ºëø½ÓÅ%nPš¸Su_·p®4ó÷œÃác»T½MÉåR6ö±ûüühözØwð˜ØÿÁ‘„GFFAƒ¤·ã-&wqaƒ¸ÆÆØ– Êbè[±X¬AAcÆ„wæÇ-ŒK¬ªBU·ß¶~¹BÔ“D¾XŠ8øôéèHè5tjòÙë]Çõ ¨H?rèÊyNžüªòwEÕ 8Y“F LÃƘq8Ýf)»õž:z@ ¼ ñvÍ.ÔÀÐÂ)lò/²Ç_+í/§°W¢D‰%J”|7mý‡Kë¹<ÁPxK !O,!‘è**$:“£(‰ã‰DH$\Ž …(8ŠÉ¤r:akYO(¥p"ï5îÈÆeW©\X~ää“1á§—ËÉID™ÂÑ0ÄäU\:J¥PPTÜÐ(“ˆÄ*e±è¡ó/o\Þ­IοxùIq³É)Nu|‚yõ< €Šš‚À€„à:ªšxKE9­µ!ƒ¡÷ïÆ9tîgÒ|NP©ˆ×(ÕÖÖQ}Ñì°±Nó¯ù0{Ñêz¡@­ÀEq2(€L†bŽ $„D€z‘˜L¦«Ð©*LU€€ãxױ˕Ük3òftqž#HxBQ­€ €I}:ÞµfEË):þ$3íÑÅ!>ÝמÚ<¬}5—Ç«'ad¹¨òÄ™§#VîW”T×ÑП; @EÙÙ\‰%J”(ùëh[ó&áÞ¹~#ýu^ÎË„´ü:*³É·Âq‚D¡›yû’øy»_Šºs'³@sò´/¼y,9ë]I»“«QÔé3b>TðM|{¨U\ŽK¼“XÅ—[ûOW«¼}èÚƒ[7.¾™ú[xnRŒPÕ¼¿ š§rGQàŽêV­ðu‡Îœ8ÿ@϶½³­žL$ ‚ p ûÀ1DΉc7Þ¼váòÃìúâì…¸¦È”Œà¨ð]qaôã—Õ•yiïkˆéæY=7L^qÿñ£¨˜4C¯ÁºhÎÁkQÎfôŸµˆªŒÜâÀ!›-$ÞÅŸïÛ7Ÿß³)¥œÔÍRþ4)³ùä¶RTô¼¬ÕEÇ=zþJ %Q¨œÀg!Ñ Úù2dÅ;\ˆºõâ]uQNúõ{ϼ; éˆ0= º™W¢íÖ¿‹‰tÃñkÑwnž¹#FEi¤j¤©¥ºúÚuÅÙWïÆ•Š 4+‘G×ëãÝ”´Å‹RŒoÓÕÿ )¿äöí§ÚO_&(»röôGq‚?*‰g?»Ÿ]Þø £ˆªÂìgi¯¥Òºûwb¿c¼&kH¸óè‘£·îÆV7b͈²ÜôøŒw_ ” +܋þUQõþͳ´,@—Ô6H¿{P…%J”(ùoA^³fÍ—[ãž> è( UTTZ§6Á¥uÏãSMlÛ™™è6JU:x;˜9˜˜ëi¨²ôü,´4}\Œs³^óŸSG_Mè8:ªÔW}¨Ùº»vô±{Ÿ¯¦oÚsP¨£¹yˆ¿YlLš“»W@¿P3-Íþ]í’âŸó„¸Mû@ z‹F¹˜{õÜyï¡þ.¦€ ˆž±¥ƒ‡NÑ×ѵpjìž›À6vž0c´¹ž¦šC; ]cg;k{;×n^FÉÏ’D„c‡Î —êUÜú·*…¢kdfemknáád§¯­­]_QX+ ûwöç5ͬ¬l}z0êòÞUh[¹Ø™Ûõõ·LLxá8|J/§„k^WjOÒš|D‘rt]<Üìä¨ulçZœÿV t {K&Úêlc#U2§³—iú‹w®>¾þÁ­õ5Œ íL-ôÕ8,}_ m®¦yÙ¯y¨“g¶¸,%ùEE­xȼ Î,7_Ç‚´—[ÛÆÚ!xP——r2C;O'sýœäØÜ•æúNèá ÚÆ4¢°°T×ÎáÅKLÛ@wKøÞ¸TjJ²§—Š¢ŠÃóóÞ9¹¸P(Ti}Þ¥›>|8**¿röÂåk·½ÂÆ~ÕµožV ß'ÐîOòæcÒú#;·¤U‘:µ#è?eö¼m­Ž¬áÃÙSWê%Òô„˜ì¼›3 âª«W¿§˜z|žp_.øp3*ͯsûoÉðƒÉxÇwoMùˆujw"boLJú»œb’®¹‰&CéWý òÉd²¥¥•H$Tža%J”üýðx¼×¯_+VÿíÓÔ&å×s™:FŸg(o;µÅdM3ƧƒÕ¢+U:ÀÓÔü<¾;ß:¿¦TEÇ„Ö,ä;$|6MÍݨÛÃF /{•°b}8_µkô…u„¬záôÅBEÀç»ö[<¡¤.wЀ15b¤ëˆ9[çD¤WÍ[3ð×cä¤Ý9x"qÿ𸫣ž›FX‡žZÚ@8èíÖ>™aôvÔ˜Ù…Uü ‘óVÏ ¥|asEnüÆíǧì=åIæÖCú ÉÊáFFݰE˖Λû )Ç£GØÁ]3VM ‹M+öêâ[ž_2qÉ®Zù£Æ/âJ“–-¢U{jÁ©ôçöi¼O¹¹éà½õ'#S÷ÌXu<±«£ˆÇu°bEƒ“§”âŽoî©àãî ëÎßKvõ ^¾m;=çÊÐÉkÕ ,§¬Ú5¬“ T¼MظõÈ„]§=4àî©ísVí š±}Ó’jÊ–ÖŸÃý{wi4Z·î=ÓÔd¤¿(þðA…¡òOÛõo$(8„NW‘ËåŠÕK.pTÙJOT‰’Úµsµ±µ‰D"‘(555?/!‘H­fŽB„Íæ¸»¹9:9¡(ªpŸ~hšš¿ÿÅ€PTÔtŒ~\;‚ lm³—Lm«Ïäü=§å;´ ¢ªcò݇U,,}‚Ï–v|äwX{~amJØâä~Ý/M˜à<ãÔ¶P‡Fõ+Ä©âX™°æcI%HkÞìî\=cî?æÂÒ°.–ö•Öíö³òü`ï3§DÞl7c€‚ ŸÎ3&}«fÖÁUJù {ɾ£¹ÛgmÜm‘[åG‰aÖ›+;'[Îð³´ÛÐ?xãŽÄ›—,^¸â>YòËÆÃݳf®¸bë>ÔO_Â/¿ú(Ñ+`ˆ ÅÛûõ5¨«, ™¸^–3`ÙÄÅÓ"U&œ=vlMpèQ)ˆ {¶¦Ö¨e½~ •%™¡36ïˆÓEœ:hÚ {çËí4 )>†eäí¡Þ»Ô¬²e»ö¢Õ Ò*†y´®ˆ’Ÿ„H( ;ŽFW:UŸóà^´X$f0˜-[ÈÒè1ãÿA“”(ùWQò¡èÝÛ\G'EXLæÒå+Zí'p “Ëdeee÷ïßG1ÔÛ»½@ øRο}Tý¿p^šÿЫñ'™ª*–a4jS2k}]óÎ^f -ÕV§7ÖeIΆ:› €á8@ÓT³T E•£ :…*¡H¤b{Ÿ~h_ü>³ºª8óÊÉÂÄ›L]/7ÛÏô ëK J)n^©TòýS$*Qò?„P$$“) ‘H²_¿ß²¤ †a8ŽK¥EhŠÍfûwô;uútGÿÎð—8U8*Nx-g;öèä_U`Ò†˜èëÑ÷“ºÎÛ<ÐQû+ňى19Ü¡ýéúÖ!d—î1ê9ª!ã¹Qû.nz ý‘ß=¾Ÿâ;&ÈQû3 µïãNß©Z0'ôÛ$£qç#«L:«W>9']ßĺÿ¸Ù~Öêp,7ß¶§ãìU¬¦2 Õe¥Õ|{Gê·Õ¥¾<ÿ.Ã&˜°¡èmòG¡A'O3þ‰¶¼¹—à! À’a8Ncé8Ü|-›æBã742Ù,2…!°B¥‘PTÌ¥ªëéè¾®xà‘˜Sf3ÀØÁÞcȸð´´>-o“oðU\õšV¥2)ð… ¡«§_–àû(¶ÀÁ¿ŸL"‘Jd¨\&“É4µõLLìF‡¯n×ÜòûàdøýLÞÉËÇ2îPô;¹³š*"“J@&—J¥Š—ŠbÍMä GsÝäçÏÆv²@åR9Âb‚䥬+*B™®!óCÒá:ªsƒ&³±°´¥;õ÷‚a8Žc8†ÒWhA€|±Ç0PΣD 7Ý#‚€D"!ð&ˆfär¹®ž¾ªªÚ×ä´ùvBDGGG[[û‹‘)tg77rÕÛ¹UP‹êüŒ»âЯ—hAP™¼~çIc£û·nåU‹Údsk„Õ™+¶1vüûL¶™¹Õ7Êsß,ß©ç`™”ðDÅÀ}XŸvK~ðú'#ðÆ3§OåzÃâ™ 'OœùæÊàÏnß‹I×dCÄÆõgoÝ:qäðåG©ØßØ×­ÉYóûŒ^—›ûtú²MÒk4Õ5ZÙ$šöªˆÅ'Æúö Yy'#šFf–†ô]:ì¾ÿÆÎ» 6!!¿ŠeèسgÆ­Í&/ê.“é¾gb·ÀnÝFŒ›óª UÕp”»ÏÕ¡{v´2¤¹«‰jÝiˆ¬ðVÇŽ~×ßéìß=©¶¢š EU}—¹cýt èÞ£GØô¯žNX¸5='{x¿–ízþ\ìÀõsm«xßHsâû….|‘;qú‚«ñÕ#—ÎFò¢;u ˜8gU•ºÿ†éý½†NÛä1p¶›&lÙp*tßÞOÆ5󯄒ßEqÞq\ù×òñ»ÏBÉÿ¯_gÑé*ÿ´DVÖ+º ÿó‚ÿ 23Ò9ªªDK˜à·}Íx<±XÜ2hO.—ãÄW_ömTIWr2öõªÈ§îð6í’Cûá£G‡ff¾?™’ê©Ò°uÙü]Çoé[­¿˜ÔßÎ#ØÓú>CPч^ÝÃÖ\àŽåÍX¼yÂú_óÏ.²é”{w½ÓéÛæÚvꮾ‰íêSQÃ\uà·ß÷!9·}wµ·ÍTüüÆé˲s;œu|ׂÇG× ˜±ÅÒÖifx¤»àF׉{¬ÌLT5Õ):ÞOoF¬ž<øÈõ§ú¦ö{î< 4D\z;2Òkò^'àõìØ)9냻G~eíì]×\êîŽZ´Ñ¾ûª!ý½ $ënçàq8•4nå¡u“–‡…üz5ÎÁ­Óî+w:Q \{á¬UŸ9:ì4–F§0O_­ažì‰ÙÔŸÚ³ÿ¤j¾$häÜ[æe ï5%ÜÌÒnòª=³‡ú[ëér8±eñÓ Õ¥# {†Î¡ª°3rsÃ6ûÅ´¦ÿ€‘)oŠ<º»y=rṴ̂3Qñí\U¼\ÇK®_Šê»1Ž0vâ ßÝŒ:MîÞ£ýßß‚K¦©ö5»Ï¨Ù-[‚|=ÀjK„"üÓ%5=ýÓ4íéŽOßд¶ÿòÝVÂLN]ºÒj5ðÎÃÀÖºZ~e/ïp™žCõ›6j[=| cv(Ž?zõ“ØÙ ×@·Ž[vúÀ£'ŸÄ~¬mhY.O?*ÔîÚ½)½yäšËÐ n<½ 2[Û³÷øÅO+g§l: 5o.•œC c³’Ç k¨çòÅd …ªÂÒÐà4ÔÔ©éêP‘ÿ§WçkuÆ â‰s+ùyˆD"@üŸ¾¬"¡^û?kÇ·Ñ(¶ôfi Aå‰ÏŸikk{·÷!“É(ŽÿqÚ©B•§£o™.©«:…ðëuúôÅ+sý¶ï¿Yü"&)·±ª¦öUfJ €hIw, !tT5¨l&Ѝ«qÅ·œI—Ëå‘KFHÊ>Tä_Ùq:.‡[w1|Ⱥ)Ó¾Ô[›÷ôTlÕž0€:T&é8;²°è»üÙõg1+wÝæJ¥9©§îÞò¡¢~ü¾øK³'Îߨ^Gýö­-wS¹ok¹'–. ›¼’´s±×. "jŠ}:÷LO;M¦´{‘r:û~´×àio_ìW—V±2±º´¤øèºIu9g.Æ E¢[+çMž ‚ª7WbßÏZ>@ŒbX#¿,­ÕË2® ?´(7ýì™ë\1A&#…9é/óÈ* e ë¿…Bþte ‚ p!¯êIôõsgÏ\¹z3·T@¥bq׊Q …ò3Í (nÆf+Ðâw™§O\ª *•_]rëü©´ü2*ý+?3ûð.=-³€L‘F_½ƒ2X5¥÷o]½uçIii΋¬:ƒñ5Õâú÷QÑϨL|s,–J¥¿÷éQYQñ23ÃÏÏ×ÀÀ #ýŽa¤?k.o›SE&á–VÔ" HdªžŽ)ˆ#d¸D&±mç¯(©¢!k},B%!€B!“Q^©‰ç 2T uTy…il=»ç÷îeÔh„oÙñ¹RLxëÜÍ.36!“ЙZî6:<=UIu©‰C „b}m5©£±UÄ"4\ʯ̡h˜=¹s'Wd±9|<¹}ϱW˜1@£HBS¡s2uM£º .ƒF€ò B\VÛ(£õ²h2@T™oî6ÔŒUå ñ4NË¡C{U™ åZú‘§ÊaRÔš÷9—®=êØwÌ€àN+6,,8>Þ‚£7Ÿp¤RQÕÇ:cëvŠS““WT^Q&*Ñ·ô€öû1*Û IDATMj ŠÙš†¦:j``¢)áò`ßö³¿D^ kO_ºÄËÊqаàž>ßÚë/D¡¬M7Ø÷ñ¥¿Ýø“týUr”üƒ ˆ·ƒÓá´Ú¼¼ü9}l/;ÛÅ»‹E¯VÌŸ5wÎ܃çQÙâ㛄E³¦Í¿øÒÜÁÎK‰ž>iââekî½(¤2XÞ<[2{ú¼yó÷œŠa±9?þóø§øZ8J±‹À$Qç"¦L ;‘ô‘Ng^;±û׳t AEÂeåß¿á/9)éâÅ‹,»e ¿¡áäÉ“åee •O%[]Y \.¹wñ×ËÖgf¿MKMJÏ©¡Rñ¸‡1\9‰J¡üDƒ›íPü˜ôþ•Èi3'\~YE£Ò’œ9iƾ¯¨äßÿ™á˜ìÎ…ˆû/ÐiÔ;7¢äÄß<½aËÎ×…5•_g¼)'#¤¯©ææß1¼7€ôí?à¯PUUе«£ƒ££žž^CCö;­ÖüXEPT2#QfÆFUÅ9%P]ÅC‡Cç—UB¦©1H5}F…÷²Õ”ñò^•Šnü†c¨Œ ðÏú$‘Ut{8ëŸzZÜÚz-3~q¢ ùyž‰›?ħfyö¨´yQrònÁ°%óõÙn½F=zHÿ m Fµؾmkp;½˜¨ku@3Ó7Z{õ2çãÃçžJEåRU__¿,?nÝÉu î'i¨x–œ /ÒÞ›¹yË?Þ}Y§=ÁZN…e¬ËjZý¡‹¨DÉÿŠŽ¥Ž—æÎ{WT©gbN%‘ Øj]{’~mÏ‘ä:^6yêbšß˜°Q¶–ÆüÏÇÏØ<~z7oã½kÖä×Kbo+kŽ›<ÑÝÉžPÌÓõßà÷c»Š¡¯gàmcrëèáFm#+|ÊÐ>®®nýC'eTHU´{—„të>°_P@Иñ=ü}‚C‹q6ÚX¹vî8·ví‚ûýR€s Æ?]ÝÿM223'Nœt7ú‹ÍQl™7oþÌ™³p'‘)-Å_½-«˜\ô&+£ã°5ö퉈ˆâÐX_%ä ¤˜üÌŽ9>í}‚ƒº;9{²9üY†÷÷ðð˜0oc‰C£Ñ~ÄÚÖ7#Žã€®Ž®§…QRJ† àí»,cm;غh¼­m¯!ãsEt&S%?í^H€ç®Á™rk—Î2OPQ•·j[dÔ½ ßr–P5€ì˜Óö¶6zì»öœÌæ4ÖÏ7ØÕÕcÛùdŽ3¡-7,ò•¦7wOmm]~O(™[XjjjÊd²ß-ÙBÛœ*QMf{ß­/Û5{Ì¢HucS-- 1XªlŽŠy—Aþº–žÞ~I jHÈÄ1¯öMê>pÔÜxÕÜ^‹ƒ½çm:¬nëªç¼kŠM;7¯Ã·Ÿq¬\9z}WŒ0464°µuØxáÙoîy\wÿ‰‘{€3£É`Žª*B„ÄápÔŒ;žØ2¨‡ŽÎà9û†/Ûçmo@#ä4:ƒ¡¢¢ª¦jç;5Ôƒoldhkëp86¯,/›`›t±¡B¢°ØlMEMMD¦Ñîæu »Ü‘ûêVèä•ojµ·Þ>qkvg++›ånp¬F¬ekª£³ýA݈)•¹‹*Ð>]M€—TÎâlçÔgÉÕ‚~fœÙS›ÞÕÜÂ"dȤB¡£ !>¼iܼõµ5Þ®¿cqÝÛ!=|I$’‡o÷{Ùµ !½°mѸ{¿È+‰ëÙ}èòûǶŒ›»ÚnX雄Qƒ¦VA‰On?üAB9ÅMÛQÜ2iýºá4µ-kô{nܾTØPcPW^±gû’ek_f%gp¥•/ë@g÷òéþþý]m‹.¾|÷æìÁ½N^çÖrù4¦µ™^NZlZAƒ³‡\.ÿ÷ßn_ã«}ªš[gÈ~}G±‹Vžyª­­F@Z»-"9)nrWÃð-;02½®üíG®ìTlâÔöÒ°ÐY7_æŒr¤®Ø}6õñ™ÜÕÔ¢­S]†øv#‘)$éo¿ýcîœ9[·léݧïÅ çQ2eê¥Ë—óÞ½µ³w6 ZŠ5]ñfÈTfÿ>=Ïïœ`ëìµrûñz„Bà⤴Ô`„-Þ•–;ÆßTÛ)¤ÀÆÛÌ|‡g¾yã­U±|n8•FWd×û>>û8N$÷.Ô¬„„ìôׯª†ÖW¾IØy-åuyÅÒ AÁ‰Ø¾ó€ß¨å™Ïk^¥7`ÿþó‡Úz¶k§‡˜ºdÿö­#t3RÒ QZ‘Ë¢ +£N¯¿óëæ§9Õi½,%gæ·WTÖWüFû·Yû%$I(*¾Hd2†aФ-@Ûz9³t=ÒsòZo‰<å îaÛ# ÿ”uý§¬û´[?àEvnÓ²ÿ¸ììqŸv Y”5dQËZ÷Éá'‡·¬¶TR.jQ™ý†ŽU¬’è+öìV,/ÚrÀ8´ª&´ù¸¹»;@Ç0(ÌÝ1`nS{bÔé;=‡ÏVTXÝÂgá"8}`Õ_X¶¦uË£UJVnËÊà%/9¨X~šÙuärÅxJ*ÇxeÄÕŸ£›wºŸÒ²ê¼íØœmŸön½q€ãù06 €e±ýäííÍ{wŸjªðe˜ÊêcCÜ4@Rý’fÑqŠü{âR2AJbR _¢kêàîjþ&%]×®žŠüå«‹víµh¢Ø‡1BŒ¢oéäíÔ³Š[¸júB1@†néÛà]FBnQ5G׬ƒ¿]*HO~^ÝHh©Ò4í:Z±…ÏńЖ&Ëо½‰… ðøû—tœøè!{Ö,Myûº^ª¯i¨:¦¯Ÿr¼{[‹År‰”BU;˜\9ÕGDÜôš†Za]íåCnÃwlŸÛíÜü€+%tS…By]ÖTAƒ€¯¢Â´v ‰¾~½E-lSÖ ª—› j  ÃZ2’ÿ·øÚ·8Žã‚Žã8!B9{ŽÌàè 0 ŸM£3$hñÔ £k¤ƒTpº z}O6èèìR¢ ÐÑSÿbBÁ»ºûññ%Ó~JEÃÇL7Gþþjþo#‘H/Y‚¢èˆ‘¿(¶ðêëÔÔ5ü†/ßôXsï¡ø Y&²ìå“kK—-÷ˆŒ|[ð@GMƒD"&¿utÇédIrÜÞ‡ÄÌ—éµ/Ë 2PÉ´.CdÅuüt¸Â½a˜-è„Oš:­ƒÏ¤Næ•ùõ%™O܇GhôT·üR©¬’ÌP €ƒî”TØh²5 !ãd„ P(•¯b"Áæ©aÂz®¹m; ]ô·¯\á|ýéWî¿=£:£?bòÂ׎ Hß°E?.†þ5rþœÜ½w?ŠvÓÜ<*ºnËVºý\m— îݺx>:ÉÒÎæã›ý§-­½òìFòÔn¬]'_ì:ëûðØÖõçÒ‚ƒº72Üœ,„Þ‹%R„ð.ýþ¢µ7’îÆŸÝr¼¡Ûöœuúuë’ü®f‹úè¯çš•GÝ»l·¿9õ7‰È ìÍ‹$àwqRUò?V¾x'êéùK=ɧFêæ/^²Î½s—·©C—¯‹=^ÊC+ù¤žþö2Ê=4ª¦æêªß¾÷döºmÎz¸¸°ŒkïàÊ Òâô}½4aëҙǒE&:jÖ6úï¹q®¬O޹±ýÜ«ů(¯b®®Þ}ÎÙÛWÛ€§cfrcËšŒ:’™†JTLú¦­‹5r3’1ýWM/gÜØ)=ûÔäŸÎàmf\¸pñ&ÿ^½¢Nê²óùpÒƒ-nùø>¹ønÅTW ?œ»’¼1åWèÚ- ¤$‡¤eëÛÙïÇggú AóÞ>zÜšj´±¡P¼\ÎÅ]ܧ\ŠÊ±žaB11êwcÉøPG+3+÷Ž¡!cC.Ìfg¨AP æ.ž’zyçã—U††½û+’nüÝéKþ*øýæ?©TJ¡P—¡&—É0F÷eW-Ý5wÓ•²”3ÙÕôò÷Iù·6ß“/c˜ ¡N PQ+B¥PÓ ¢ÐYŽí<†Lw|ó@ÅrA•Jÿ%«ÿ1Äbñ’%‹9{îÜ¥‹ÔÔ5jkª?ó @*•*R8VZ”CQ3uë:øÔ¯5.A›>ÊQ‰$—Š“î¸ðlÓõd €PŠ»99Ûþ1·Ÿ5HDÒÆú:좳Ÿ™¸LŽb2)î9r°ÚGýNªŽ˜*W37r°¬º%†>)1IºÆ®º45¶ í}Þk©…YÂól™IGäK$R™ DŽÊ5 ÍÍLœÏœ:­pbøùšÚµïqðxŸ%UtL’Éþ4°¤¤uf»òò²ø¸§õõõMq,‚Àpœ p‚ €‚ ZωüÊŒêÿ¤¨¯Yξÿ6umEÊ+½yóZ™P×K[»0;#§]º|göð¹áÈ¥§w AÜËÙÝ…ü›Ë£è§LdlÃÂÜ ´8jT¬¬äa‹OOîmºâe ¯(ùÊõõT 7MõÜìÜœw•þæ&­õÊ%üø'Oíº­bÔIdN¶í—Ϙ¦ÏùðiÇöež½o[>ðÍ™Ic·]4.ÔÎÕlÕ†W»vÑwq¶Œ¯mÛAC‘õæMÞ‡g-µÂWq§o§Ì:EÕ?O{å9r7˜›L¸ú.cÐØnS® ÌÙ8ÿYîa@}ûq‡QËWŽþ‡˜©¤Ä2 |ü¨Å/óå]m¨ñ±16Ý–³rÒnPt;FìØÆ¾³Óʾ3:Ú¹ivgëòç™2^faÁÔ­7z2«ŸÝ§32nEÒC=¤m³Ù+æ$å™XpÿÑnÿ!¡¨Í_½Ó³áóù8Ž“TŒNœ<®£BvŸ3g}÷ñËq„A()f­Ï\¾(”ãl6ƒÆ`7†}çüÙ\%a;zâeµN¨0EU T…áaFhj°¤EUßbõßÁ˜Bxx窸ã·~› •ÔÄoY¤­«;öìóoSáÎmï=pÌWŽÏù’¡´¤ƒ§ÛÙ% <™ùþè:¼JVÖÞgÿcÑ—Jrúžý½|äÕƒ»ù¯O-ùÃ’€\P¼rÑ‚›9ïàÖÅFF†½fÇ ü#Û—hëê?öàË»L–ŸûøTBì’ µôï÷eféY¶swL9µíþÓçׯ%?Ê{»kñJ—%®Íš9’&'-š°2;÷IÊíÇ 4¹ÚšLÒ­“©Å|ÔÐÄš†×Üxú<)ó ÃÐÎËÓëmÖÉœçYg²Þê¹øwp6:¿õñ³¼«É7ÞVÈ?­¶üä®m­Æíã~HÀ0¨0œDe´ðñŸ}2éÎí-‡_^¾HT× >®„ C+w[ú®m‡Ÿ?zåjZaÑËÞA!Z&µ7ßV¨ììáíGoa¨·‹NàÖ휺úª{/ßW6Pú÷òË8±'í~ÎÍŒ,Ë©½-sëÁóg.¤µ¬ÜíIÇ6®j5n¿6Øx‡’«3Nß¾»ópNÏ‹œ]Û¼ËØ›r7ípúk®——“åí ;3²’®>.ap ?)ŽÛvHku-›yMþ¶å/| 8A,Z´ ¹ê”)S++Ê•J¥B¡àñxµ<^]]P($B¡PÔÔÔTUUñ ÃäryUUuee%ÇCQT,WUUUVU ‚ïôxýßÄWתÔJN€ %¤R.“ÈPP"ª¯‹¤|9JF@,QˆêëäR‰LIH%|¥\.E© O!—‹U•¨V ó¥(B"#@`²¥BñOW÷ÿ9”J¥@ PïœøÍ{– œ P‚†Ph$ UHx5Õr¹J$—*Ä"1N“JD8Ž# RID|‘L¬ÄÉ$B¨T2ªTþ!?ƒhc|¥L*‘HT*\¬Ä”b¾D,©+I *òª«PJ•JŒ@DB¡ˆ_+—©„2‘R,– ¸\"Dr)JVHxr™¬¦^B($*‰R*–Ê”B9F"!R©œ__§Jþ°£:A 9¡PÈ•JªT(Q%Š¢*T…¢*T¥R¡èo,üþèJvëlBƒŠÛ¥SðÊóvï8eÛÂ®Š‡ ™2BЇiW’oe3´ŒzŸpþ—$0mц ›~îül ëÞÃFØq¿\}Á_‰7o;|€‡Í ]­âœÔͯ[¸uìÓ¥UYÞ­ÝG9FÖ]{GJžì:“¥ËÑdh0HZŽc¢ƒ2¯K¹ûBÓÐnàøÆT üÅÍ“T«.“|¹'ã7çó­œl5‚ÀÈaÚuO.ß,0Z½–2Áû½ÛãøÍ¥]H¿®>Y—%e¾0´õè?l€!/¸—XÏr]j ÍÖ¯Ï^¼\V%±t¶ÔÉ¢‡rO¶í;£ahÑ¿•­Þ¹}[_ó$ª4öæ{ýÌy®©“««5tšUn ¿æšÎèÙÛhñºÚ„òaÊ•kwÓ9zí»Eúµ4x•“yüøED¿uNQ×n-°ÿ4(Ú]‡NQ;x!á°¾¹kW73ãú†à³F5”¿—x‹íµeå™§[vêCGôm‡ ïàtZ¶£S_ï£ÝLI 1ñô°4kÑáÍáÌÔG‹6l²t¤»´˜T·oçÉcGÍ|¼»2š—ÙP’u&½*mkcƒkêØNœ4l}ºpq§Ž.¾pábÐÔ=ýÚ™?¡¶5´4>Ä“£­Ñ«ÿжíìmÝpøH¾[€·&-¼ßÀ:Œ·yÍS®ÖÚeyhZ¼.Ò®k_[ãt[#3#ƒ1ã†×>z”WÝýÉÍ뤎Q~/ÆYGΜ0sòvõk¿`û’•‹wé˜ÏX=C[–·?±äÆS{µxtƒ¶{7NØvâ´sðøáaf:Cz”ݺzËÝÑÊÄ’åÝaH~ñÖ›©9Ûz±U‚—_Ž\¼¿ye¿|ÈáûAàxuuÍ¿Ôùéï‡z°5­y!•6%—r.uÓºEÓ(Õ‡ÛŠ_'Xÿo¡¹® R©>[ÓR÷×— Šr¹~*>£yVÓž¥RÙ¬­»Ï Ÿ7»âÎéùË\1“UîÙŸ24ö:ˆ__J¼jig¾}g΄áÎM[6ÎO?ýØží»bB6È÷,žyuš>"D äHøÏÇM_5g÷þ‚K‡—ídïš)x'NÜì5oýg#¬xq9ùfw£m»rõiéÜ©’Û‚¢Måï¶ÇnÞ°s•‡§'ý}áÎ-ûaƒ}š²(ÝcF4£¤õ=­_³ë¦a Áµ<Ê´\ºF@Ûð¡mÃs=}ü g0@ÛvF0tÊÜ&žãf/ùÈ_X°çT¢ÚçAÈVmûZ€•÷ €«¼›×½e`ä†ÀÈW W­SÿCù¬ýg¯±›UÙ¼uĆÖjÑ€W\ðøQ.Jf#6¡cÌ¡âé½ÂÂw ÓÄwp/+­Ö V˜š~RÙ_ø3À¿ñõù_ߌSõw‹ò 1LLÍêx¼ßð¤þ{`jfVW÷Ï‹ñ033¯®ª¢ü iL©BŠƒ_hçç«È$`0˜Ý:Gwqv°ìb¹àÁ“÷L•‘]‡ÑÑÝ?+›Lù$174a0ÉT‚b¨¯'{Ÿ%Öj=,¸c%½øm2¯8sÿ¬tË3'Äå¯kÀ÷‹bñ§×O¢–U=Áí3rRgÚןe^«B Å‹Ôáëx ôÃöÒÎx&rz•tõá}Ié©:ÖfâŠW"=x{÷j½†S´-ÈËE-¼ZGômýªºh@Ÿ€å" >Ý}2ß&}¿ÿüýIJ3êÓ•Kn¯b›ì 2yã1êL(}–Y¢àLêhþ™ 4Í­mÇa1ÖËjúvsß¾}¡Ähûð¾€¾N¾¦ð=ÑÆÇ¿²ô©W`ôªAíÀ#8Ä7ó å÷¿¨ AÕ›»O«\B1̰Ã=‹lŸ×µìä¤óC=øñ7OÇ4m{gíåþ¬ÒŽç#Û? *×Ö™û‘ÏgÜô,œæ¬Ž%©\ÏÔ IDATèluW›´h»t]„Ö¸gjaüçø5N?Æb±~5ægP*t:ýËôØsþ ÿ0 ©TB"‘þÙže0˜RÉ?/Æw‚Á`VUUýî4߃©€+DB±”h<€Q·_*'SY\-ÇU :€B¡¢Ó …ŒJå‰FF(¨”J‰X®Áá°˜ZPU'@• -.'bä}Û†}µP9ÿ]Üñûã·\S‰ J àbœÀY,M &[åÕd2EƒÅ@å‘D)—I Õ321ÿÊŽ…j>¸¼:áìð1Ë@®PªpL ”‹" €z¾„ p ‘)öF†%*°¥€R‰Ò pxWZËÕ·€¤+·½ºDéh¹ˆWR!tt²%!$• ã äIÄb6‹Éb±”ò*‰Ìd!ïr.®ÛsíÀ$µH H"“}¨(B&“±Qp …¬’É þ]æ¬5G/æÜ3éì±S2˜Ø˜Ø|ì—êÇ#þ¢:þ,¶¿Ã‡D:í£òL¦#;ˆþ Ý÷·aä×ý…Û·nJ$:ý£IEQ~C}]Jù¸ã~¤b1“Éüó|~L©B%å{·ÇݹûѧZZjùšj~Ø6 $2ÍÄ£µ6$-\²ÆÒë4 ÇËÏk÷ž '™ƒ;µí`{týª·&D•5ñŽìl¹lã£u÷2ëi>­Æ›Ï]U¯GUÒÍ:LŠñkV&ö<+bêhи—À1$‚ 8Öí#}Ìœ³Ä¯·õ iañ2µR„kAA8tšvqÕµ9Z ×u íé,jÀYý¬@m%Ôæ^œÀ”‚»ï\?vó}‘üÂí¼­7-è79fD~'–ãðÞÑ.œ1ó×næç œ»ä%/‹kFÏü !Quzà¼ã9¹YT ¥)’-p-¼ûšUÍ™»Ô¯·òvÕƒàÀ©¬¶#2¯\¥ëÛº0wlÞýðI®ˆ}”QÁ­M­\×/nÜIî9t¸=›æÞÎcçîí'4¶õ±èØáD\•÷4·¨¦ß‡~ÿW¼Œeõo¶oOàX¹ô­ûM™ñÌó‡–½ý~7À)^œ›•S‰‡8ž8œ:rü ß£ÿ*ï‰^•™Ù¹…÷‹²ÒVobÅÞÜK}&ÖêÛµÍg åü¢ãgÕŸö]%OïæT ½C;JáÍ”[†¾-í TÈßö8ùt¥~›î>6 qeFvy×N6ÊOJ!T¯rxøêRA%¯Þ»íÌèÙ÷¤HW<¼¡d™øû¶ü±q•,;+]Åvèàc_Uô þàeÓV£#º0¾v¯BPœò¨¡GAŸ™øÄ"‘†¦¦šóÅK—s?644ìÝ+ÂÔÄd÷Þ½••Ufffƒd0èÿŠÇçgA"–|–"—Ë_æ¿ø—Æ2ý…ß‚ l6û_±Dô¿ì;Kþü˜RE¦Ð[´ôôêЉLÓ6ÇȦÃúÍà1aÙ–,ŸñðAÕ´µá€U·QØÙõdmƒ1s÷Ös]}íÀ¨‰\®óú9×3òÍ'ͧ[x ;Ä^O4¶¹{ûæ%*%u·²rÜ;ŽRa‘i:S,`Yë ›8kÓÊ@sþB+7Yzæ¾m<©ŠÒR3kª£+Æ…öžzÚ†[·.»~'h6^.·Î,7réjÚf.£ÇXèèilXÅ6´RMœ`/Wu 6È £0™ š~¯ÙkôSÊ…˜‘“ ºo¾z'ß(<ÊË…“rt¢ß¦1j$A ­wíô§hÛ´š0ÚÃTÙ°‚ëh¦Ô³£éiºI7è:¦mÚzQÖmØé›I®±zúøûu뇓¨ mx…¬œkú²\¤Á°ì:|£†[5®¡gâ¼pÁ”ÛÏÞq}\z޲1ø檿 T:Ó˜K;{úhëAѺß4X«ïgV+U¿•¼îäÑ£r»v¥±±±j¥ê‡€Éë«yï6>Ïn§nÚ.Þ²x,•TÆí7èñ•@$§PèßiiWÉëN'Ytî€ …L"S¿bsù“ó –¬Ü6ìXŸÊœã'Oå¼'U>sêaðÉŒ@`ÂM‹ô?•Ú™ ¸¢|ɪ¥CfOüýE \–q=QnØN­Tý. !¨L`kpX*~Râåˆ.ŒOÉLš‘œôüÍã´§8&.s ·á|ÿ /^ÌÑÒ,+-3·°051?vlÇÀN…aaa¦ÆFyñÿÛÐÐÐhÛÞ/%寵ë×ht™DKK«Q#Gÿ² þÛA&“mÔøQT”—å¿ø`ß‹TªúºG4O±²cÉÐÕYÍNÛ²s°e³|M¿+:$½Ž?æÐƒ»6Û¶Æ6îþÑE©Ic ³ug­ÙñQNÓÈVmô"X9€F×îü“éæt0Ô-C®upwku¦ýØ…}Æ©aè›è€ƒÐM,Ø&.ŸTÖ½C÷¦ ûÐp{õß.fu¤Ñœ’nàÜÞzæÆŽV@ÓÓ ëÑDéܽùA=zR$0,[´¶lÑØ¢Ñ²]ÇÆ7×ÌßßìËöù‡€óÞÞ:jÚã×ï;÷¼9vüÂÁc:M]j*œ0mõøØÍ7n^Ýør"dÄÂ}ËÇ9àÕ«BuDõÜÌS«7_?sá`Òæ g%~{F»vŽªám]ÚZ™Éʳ»EŒ(ªâ÷6?vùØ/W;*^ç–4¨¦èÍß­ã½ïú8¯âô£\7ìÝЃ¯fåùvzêèÒ‰}Â’³^·íÒ±âMÉä­—B ^vÛ Ç£&¯Z;-zÒœyК+—+Ð(z|‡O5˜Ù6nAŸ;Ò¼¼½d‚:Ϩ՛‡ØõéÕ·Xn1dXo5.˜6ãpbfëN=ׯ¤>Úïßo¦¡¹ÍÔu‡F‡º@ÕÛgïxòIËûÀ¨^¡™Eòç³\æ î¸7ñ™w[Ÿ†òòà©û9VE ˜$ÃðQ+v,rïÂŽˆQK̬ìÜ=¦®Ù£¨MÏɆæ¶ãVìžÜ«Í¾%#fÆž¶qðØp:©‹-ËI>¯éÞ£ ”Ö\ŒéÈñðiõŸiTü’{ã&Ï?u-#§gXç°Èy;ZéLÞ÷VúÓåG®Æx+Û¶•ý4ëÎÑe±÷IazÏflKónãÓPQ4a›Ÿ³ù’k.^cëÛ÷ÈÞÅ3úG¾HQ¥ÌÙÀæÅ#×M¸îÈG·âcüíìwâná⽩m4 œ‡ôë^uîz:¯)Ê™]³ü6Û?ÓåJÁû5kb[õžQQYÙ‚œ»qoîžã 7޼rãfÿQ›Û˜QCý‡ž)-//Û¿|,PW]«T6ZÈ  ˜¸H¬‚³'Ž„MÚ]X𪮸\Â+š·`SŸ¹»KK‹´j2vœÌ…Oc€á*éÍ”kF®ÁŽtò…F'Î^½:¯ÛŠuß>K³èÖÀç1/è3zS§ž)iû F9%ož1~ööÙ‡ÓŠ î s/'¤—€°æmü¹Ô€ˆ!Ú˜¼>éZ†Gçh=‰˜¿ÿRh`ÈÙÔSo.b8^Í:ãlfªPí[½²Z»C}}ýµsÍE÷LŠ}Àççç]HܾàA%@zJ²žc&@Ü•ŒéCõµ`j {ülKG¿{OïìÛàÜ.âYÞË’’ü†ŒkÏ^Ûq(-¹ª.çÑÑŠçy‚ª{¡ƒVܳ¯l¹qp[nYÉé nŠD¹9™]l9 á½MHÌ>{6#ºú:†Êî§]aÚµ‘¾½Ÿš•ËÖ1öííëd"ãïÝy¯D ­\û èk@‚ *ó3³*™ÝÜ5÷î9!ÁÈ&ö­†#Hê‹Ï<_.ÂZtïÚÆŠó2NœKeÚ†Eõ·Ó¡ jŠ“.].©Y»Dwï?{.ú«SÉêNÆ­ ŒÏ©úöQø yÃ΃Šï‰¨E(v,š’œW|t×®»¯Ê‘?FœP]3ëtNÕ—*ògÌÝþݜы[íO/@DÖðfïá¤ÂÖo^6oûÖ-}{„téØ58¸WŸþg'sŸÚ¿ûJzN=À/¿w* A¦)ïŸCSÛÞÆ)°ß¸)“&9ÔÉž]ß{WhµeIôê}×± v3MȬ€†zªÂ)T* ¸T@¥ÑI„xù¦š¬¡c¨oX_þÒrK4Œmì-º5uÊÔQâm¿ø6ÎË8­Ô÷oóÁôªDQ)P…n`dZU˜ ‰W_µ ê‰Êå†a*Çq}S3ÛñS§M›>rp/†òä¦Ï„F–MâaúE¦Mˆ3Ôª¦R©¾ˆ>Ì–6ú÷RS@©¡d-&&Ì¿²Š/R™Ó_ß<*×éÐþkoA¨0L…©0 SHž=Ûovü²%Kir1CÛ˜‰òß)ø¼ÒÇ/‹ <,õM‡6cæìáÑž‘SöíXÊmx™pådÝ{èä×— M¥Ýá ¯xqkoü9õ°AH:D”1“(Q” €LeÑÉ*1@qq Š‚ *• S©0 C0¥ çñcxø°ÀÒÝT¨\*Ç0\^ùøîkÅ®-›ŽéM“ñ¿|CRiTôC>¦¾Ý¸y«Ö,u÷ìîÄÆC´þj $ …JÙ·w»R.•J%-Žž¾®¾®6‹Í"‘Huõu––æááa·o«éIdš·WËû6Í\¶))ó9…JQñº•›e,gÃ+'¦?­&QÙÚ:ŽŽ¶¥ÙI[ÎÞ 'Ÿ;rôbš•k«÷O.­ÝrØÒÝ×ÁÖRE ¸Røºð®™UÁ½³;݇ÿù%«œœç/ž½|YòúÕ{*•akkïèèdmmciie``X^^Õ|KŽ6ìÛÏgµtµÉN9wìê›ýÛTÉÊÌ^ `3ÿ-!‹~ᯆÊuU7“ÓŸ½\%­ª¬Ð60³4foY8-¥ˆ™¨rgì>^][oaëÈ@«×-YüZ d'T¹¶tÊK9>gÝ)Bžtôȃ’zDÉSÝÅÕ&õÈúU àøöØÄûoœíŒv¬K/H§Œ›&âZSê_,Zº®žÚ{æê]gïV÷ÏïØò~öúã+UB"5—ˆH€ã $B¨Ï„ÂÐê3`ïQÖ“Òðn ÃÓ¨¨ÿN`ÂÊÂÜeê©Çq5Û¯n â—ÞÚ}âæÝ•Û–®¿Ïjx#Ç›ä!EÔqPI$Ç ‚!!$‚ D•÷×ïOJ™¿0 C„ @}¯®‘}dŸnTj^ê[›ñiOR»jÏÅc¯VæÜ<ââ•b‰ž¨ü™ßðymŒ·O’Ó¾¿puí˜Q#÷ŒŽÒòâ±ùèÇj‚ºÐE BR¬ \ùöÅs.ÛQM‰)êömÞ^­¨Ë}MÁ¤e£i?Üo?TMóÑ#û÷1hå¨Rs[·™ g]ÜŸ6=ît[šQ1S®´öX}a—›“ï~Œ=m÷ÊqÚ¶ÎV¬HG“á[/­ŠêÆ%â;u vÐpü[ô°ÜÛwrÈI‹ké8zbdP¿Uõ¶-Úo8pÔSïc¡8Z½1öâØÌ§M)êˆ ™B³nƒÄö65Y×ʯïÙ­1»×/2²1TpŒZ.šÐ)°ePêâºdVø„e{¶ž£Ó»ö}OÄ/Ó«L;]ÌÁ§\ "[0bΛ·Žíúœ<¾ùÌòqãW´´¶™¹¶=vµë¸=fÆl-.IËåþ#“z…]ø’£m¸ðèÍ­t L¿n{Ȫ ÆPæáÔöyA¥­kK*† [v,ò râ|#¯±Y—VO®Çö›¡«o3sÏŠQ1½Sr L,–»ÖÏCÀeçöÆ·³Á oç÷,ëÚþCñ“ËëÚh`ʆ› ˆ{H °5Í 5µ€ uiE‡ˆÅýúõ.-íº73¬ûë*>W×|Ûµ»žŠœýËä6Ní®¥œÒáPNl[¿zUÇ9ùö…ñÓÇM‰éõªHklj-û©4Ÿ'È4ó¶çnd6¥ô줎‚awü>{]ðñ’ÖˆEûG,j¼Ú|êz3fFÇ/'7»ôN½û¨yYMà"/UßgH7Zc¢žu·k·»€çà §`ÏÙ”¦‡cÊìØ`õúvpÿI^Ï:áÇ“ ªŸ2ê0áÃ{ÒñI€sé§ ¿°gsyÖÆYÛtá7¬¬¢1¸Zõ³SîýCéâØ•U6¿1B»À¹´œò1OÒµ  wÅÛ‡ÇÎd™“ë3 ºv@Sæ•»Ošþg^½ì>×âÓ–i‚Fm«Õ˜‹ TŸá± ÃcÕ¹©ÓÀÜh¼?ÀôÝ—¦¼µKk €Ëç _䄦¼%»¶€Öò퇖oڬЌGO¡94¬Wî=·²ñ¢[~QISÎßf£Ñilv~þ MŽ–‘‘¯Ž_]]Òí]Q ÎÔÓ3¨­átòh •Ët¬ÝGLq1©³µÅÀÂÙ'Ûtˆ8’°£é6çüô³O©9·Òå÷·†î‰LC0hT¥\=¢T©DÈ4*‰¾LŤþ£_?ß“ùúõSEµ:…BQVÉøâ0¹¼~XK#-6ÝÀÊsÝô¾©k/Ü«ˆDe)Zw (+¾·cçþ^³Lj½hh—9s¹‡×Ï :ö,%ñ4ðiTÿ] €‰VãP»3ªî^Ù¿€ˆßã·ÏŸi9z§šC%ÇwÅ‘uƒZ4:C×–<ž»9m–$(yöbéàÒ¸ƒ˜(ÍO·éÁê'q zBbrõØ\®öÛ†:~Å{[Œ¥G…RÚ·öÚsãÀ×Á¤ÁW‚ÿYü˜RU™wvó±kwËjewöÍÚµoAÚá‹ß íêÝgæ¾Ù}2Ïî¨VèÕÔÖ†.SªÔ߸dÉÇÙÃe3¥ g{UEÆ…‡b¹\Vrm×ÌËå/Ž$\ÏÍ((à]Û4mõÚi!G>+·âyÊí"j†? ŽÀ‰žKNŽo¯?shTò­k.½+•öªwߥŽíu&Ëožù|óÚ¹õÞÊRž½.»´bÉâU“bë ïßzÆmsÂjrûD ¼8¤Ã¸99É'£æ,ºå¹zL¡?7lä Ъ‰“WÇ—‰úpªžÅç•ÓB!?cKçy ú%ï”åÝzΟr8^=iÛ¦ÓÊý£tßÜÏ·òuÑEÞÝ?]¢²¶ B,Uªoq®&ËÏ/òÔ©3 Ø±}‡ŒJOÝ¿fkÜÅ#ý¹Ž.­lÈ @•hÈø wX2(ôÂÃ*ôÍè¥s-²õòß°ìtÿÑp}êwãßì)bÞj`\«åþ¬Ò =‡îöüÈöóq‹Ú¿ó#Ÿá†=¹•t01C×ÌnÈì9ê㢿u»_ä|¿o|isünþ_A__ßÛÇgþ¼E\m-©Djbf¾aÃw·~ý¢â÷ÅÙÚÙÕ××1˜ì)Æ«é Ly÷úÑÄÌbssãŠ7ÙQ“7¶0näzrèÄ9­œDŒrpq7áÞKJ»~#ÖÔ–bðëweYü†]E29dÈ@•ªNÇpÁK¡PP($u„í“™A…B|Z &S÷Bõî]³bÎÔ©Ån=ÇÞI‡J®@EZ:úö®ä{øV…¤b™¨€#ÿÒ¨þ³ ‚ÀÔ†¬)˜üáµãKÖ'¬KÉ4Àå—SJ-Ö“^;¶ítÚÛ=W/¨¿Kxe/Ö-_g<~¬'{ñ¬ØÚ¾¥:\MqÞí%‹7D,;Õ @.QPIˆHB€"Ö3~ IDATKØZÚºL•ä¨$"t¨à9] ß}tÇÇ„o-¿"ëŸÃ)UòÚ"+çŽt Oª×j÷® ohPTPáß D‰™­›š’L‚ø$¬\ã7A`¢*}çP‘hz\¦¸ü…y‹nˆ †jpèÀ¬Ï %TÂsÇ’'¯B© 1¸öÆl€--º²¡ÚÀÚÀë:–R®²ªV RÈä ÉxÆ.UõUŽÝãww€—n´ì>Ä@$• ªêÄ &§¶¬šJ¨(¨Àqà(_‰1Zˆ£#ç•Úw‡†‰eð0=Cߥ ÀÅÛ€°ã¡&z4À”µÛbO Ø™þÕ$Sh–6¶›—™ÇÆÏ?5cÅB¶6Ǯ㈘²cËf?yV¾éb‚&G_߈ \m:ZíýD:šÆZÿÉ÷å_Tå|'‚P¼Âz… üVÿÁ~ÿH$(Šnß±ùÝ»¢¬Ì»Z\Ow72‚x¸»÷êÕƒF#߸žZS[w?ûQo@HÔ¾ 2Ž!;»O îä Cæ/µ½–V!RšÛÚ²4¯›eö^Jš¸t£„ã ½ Ç8V`äÚcójÓôœ·T¶ƒÚw˜l -|#æ8èÿ“ ñ}P(äTê7k¿ô6Á âýk8ÒK«ª¡[µ°®9wY ]ŸÝ{Ä6r2£êj0ȯ^<îæÙîÎí'FNé@BWüÏû–ýÂß }xýÄÒØã‹Ogz1ê 2ù-\”×lÙtävBú5µYF)«]5o.űçÒI½@!¬,ª©·íÒ jÞ=^±l{äÜÉáÎÀàšÙhSÒã®/ËÓvŒèÞÑŸ$U¨” èÐcàâå‹5yùW_K4Ù,…\J• i óúßÕ¨þã-^Æÿø‚¢¢¢.A]LLLÌÌÍêjk¯$]€Ã‡ŠÄåj[XXè8¡6UB"ëYµêÕ`TtµFìÝÂûõìâ爫âÒ¾s·.ì¼=L ¬ŒuÔ_ш®]«È¨èžÝ5´Œ¬MõÀÔup´ûÝþyü¦®ó•©×óµ´´rÜtñåÄ¥«ìÛ ë`Têhb2}[ÆòSÇHýû'ï˜êhcWDm=kfˆRÎÐÉ¢¹Íö„t–ŽÎO Zý ÿ:k ;¸yÍÛ·lTˆÑ›!mÙ¶õÚÍ´‰Ý}\ÜÛÏ(-Î~ÚrÈ$à—ÜŸ¶tsfVzh«VmC¼ÇÞœÛrìÊÙcÛœ:E-¢s4ù"™}kH9·ùÀ¹ä=k¦ÚØÚŽ[ufsæ KÛ4ÖÒ²%ÍcÀàN&V[öè¬Íí9)nÆÁx|ØD_G[[GÕjêâÞ?ÿÜÕßt³ˆëÆ8988oºp÷탋4û~à Ü7½ÕÜ1ËÌZ÷ž1°u[/Ÿv¹ ž1wrN§Q/0ÓØµÃVôö›»ù¸{p„¹SÇ3k[ºyž»ûÖ5 S+ðèò.–¦ÆŽÎûÒß4/W‰¯^¸îàn§^F¨¶vö:t Qml팭[?>­…ÑàÅG¦o‹ëÐÊÅ”ËÐÖ366гs°kÙjèÂS SG— Ïk_?~H7põ³ 04,,-µ´ôœìé,®‰.¾jÑ´°a;µ"‡Ï~Q£½äìç±QVVÖK&³-{YÚÍNOïÐs³‡¼¾{²¬ŽÙNšÙzÔPɪ÷íMê¿bKó*èY:Ø5®1²µ ìÔÿÛ÷šf‡¦u éQÉÒ÷ôrW<òt²q°÷4ë¿q¢#›ÁáêqÀÚÆÞÓè™ñ?õÒå½¹ìéø»ûÞ¥èjqlÜrkÓ¢rD°çÜ3ù¿ËÀd‡WOï;znMõ]C=Ó?¼ ”À•Iû×ôŠ!ø‚ƒ”÷bÔá™ùµßÇ¿—Ú³Hù9±Œ_UÿUhÚÝ}UJDmeùÅ}3È'®åñ4';ÿéý˜ó¬çÊ‘Ñ-€kéÿª¤U*žäääÞKöµ º%âÝ«g¯_½J?µâmÎ}#SO*À€™G ‚xóüIQaáî¡À´éœþàIIIñš©}ÔEûÆ,­oàçæÜ 3ð RQ[ßÐP¿fJ÷oIûgðûÿô}•V~<dmÓkȹý^ÝǾì>ö#5ÍõJæƒÆÿ¶¡Ù9¡³|ç=ÜteÓuÂûò¾±M3Ž*ÍÜ|[Gª/I4ƒQóçªÿ¾ üJ+·kƒö -À½%y€WŸ™ïûÌTgÞMÎê3¤±M3÷aÃÝ`Ý"€qSÜ`ÁŠÍÍ*JKÌÈnºðŒZTÕèh-“É£&.ýê •‰[uéÔöWrÏðež*eáµÜ½ñ?°\ø\3BóÜ×EMáÛ±ßÌØÏÚä„°¼àÖÂIsV‰¯žK”“( ¹ÜØÁ»SkgTZ}üP¥Úzúu÷ïZ/,[>e6A PSöò^viDïàwÙÉÏQÛPO3ÇOó$C][WéÉGj%¨½§×î¤/j-¨|ý¤°nÒâC†œÇfzƉ·ÓË^Wö£‡ ®_ºðò=ÏØÁ'²G›”s oJE¦¶¢ZAëà¾vlÞ©S—EÅÕ7°£·# áWÝýªßæœKLPYØZ½Í~PfÜÎLƒ@fg“õux¯4]ýBº€Gàh¼ÈuáÑ;ê÷’°öýõ¤¤)¢£«Å±W»]c¯%8M±¢]>ròìå Å"ã‹·rÃ=þ.Ó? ÚÚÚþ÷<¢Óè]»…Òé gGG0pÐíÌ{tÝ·mƒáéñy ´ÿ ttt>|bbjزԪúá«ãÕ87ea8ÞÐÐМAÍS‚ß"ø_šÄ~áoñ…Eè3LÛ¸é{ÈÀ¾U°ýÿÚψ?KQ·ÏR¾$ø¹ø1¥êï$¨,Ý ˆ¾tAÚ… þó|À£óÐoñaéÚEÆØ}–Ûœì[ÿÿu RȨ°hÅÚó#úßU}캵¶á#z:ÐìÙiÙjGöÖe§ŸªF —+€ø]P$Q{Õ½•µmKRDïàÜäƒñÂ.FâÌ•Œ>1agóÊ{S”çöí|T$ôq·K:¾‡ÐZÒ’óÉ>PBõìá]„cÙÞŠ$©TòÊjÅŠGI U-†›mÛs,rÔ°Ë{VU«f]¹(ÆÁ“W‡Ç´=°ï #§¡TNwµÐ:shCo}[+jɡå<ÒÌö¦Ù»Æ, rá(ØFE·#C} :zð‚Kçˆö–”‚œ›ë6ìöŽ4árêxÕ7ÏîÌ,’uh±sÕBÖ¾o¨/–õ¨bÇ:ÏÏÚ‡„€¶¦‡£É«*­ªr44&!Šš={ÎwŠéaGÃ’ÎÉ,B̹uñr­–ÇïnÛ^òjeм*ªT*O&p9sB=*««¯?Æ÷î;Òqsï!‰¦—SOâÕÛb™T½ïO¹p0áʳ~ƒ{Ï+ï®  ½ÛkÉÃXÐÖÑ÷pµV”››[|-4çÿ_hjhìÙ¹ã³D‚ Ö¯Yýeâß%Ôÿ(ÂÃ{æç¿À1üƒ Ðä Àõ庹¹K¥7Ì~ÙhŸ¥ü.Á/ü7ñÃà‡FË÷ ¶ßŸ?¿"ªÿLVÿ™¹aZÇ éwY‚8Z9DM %¸“•]TX¸ÿDæÆgyM!¿Ðf爱˜ 3SÐç²5î›·BÇn 6*¹p°¡ð^nrâk>[.ª-zWVQ#øäÐ]¥Lp;=Ý¥ÇV €@¶tl3<´[ù½Iw–û”z†ÎÑ¿['ZVï­Lènëj¶|õ‹)B£Ãgå+ iÑPÆ,(á×ð•ÔÔ‹§­º-Ö S)AcWÅ<­¡·¼RÐÿÃZ2÷8ÇÈ)`æøa ¬—W:jÝÉæP“—‘vã…w´kÆÙF§›À§½Oc 3¤\¨ ëìDfØ ~‰Y`lhÚÇ?¢c»6yN›ï\O®’q–o_jOs^µïÒâÆ‹‚+ïâOåixšØt63§ö Ê®'a©Çį_¿ì:2v@O«êăr!@zœxäØËóêäÑJŸóÀ´½ÞÍâòÕ'ñ?óxþ ô ¬B¬~›F*•Êåò¿Eœ_ø…=þ*¥ŠÀÑêŠÒÊ ž“‡™ý[Ó™LP[%@--MÉ¿?åáeo_1Œ­ÑúZ¦®¡¶þà,IT½=[ã/¤RˆkÞU(Í¿3Q[òV®in¡Ë”âòZ™…©¾ÚÁ 0¥¬¬´LÇÜŽCEßäç d*M]c;3Ê79‚ú„°´ø¦ßœJÁ¯l@,Œ¹ÿ#ïAE$*¢ @T@PÈs]}µ@¥ÂÈd2‚!pH$ p‰ä™ÊÑÔ( pp ˜=…S3SZóÊ–½LÏÊ“_Ûa êè©ÔKdd2MƒÍP©P¨åK9Ú¦8¡D ¢ç±zͬ&MTžq(±àáFwÀpPª$" *ä€)1ÄR‰:n›ÅT‡ê…\Îf³E|!˜³PLÅÖà¯Ï¾õöp£œÞP[Eеt¹ŸW/ŠëkL¤õ™\ÚX„ *% 8N§39\}‰PQ„©·gÍìý÷8˜§£ü‘Pw°9@Þ ºí@R‡´áhj`( *Ç1€dÕÎ[K²²€Ìæj·ãÈ€/ ©ÿÿAõå·/H>sX›¦Òf!æÚ$ Û­£çÑý¨ÿ¹ùêêK$‰Dò»Ä¿ðs¿v,Ä/|8öM‡×Â_¥TaJágÇO[Ñ=îîÁ(·oPáO¯]y>ÿä©xÍßc¨¾íÚceʽK3gLY>Ðßå ¦¿‹êÙ{ôùüZðYˆQ^ѭؽïâvÍý>¨¤lTtTàæë£]Dé©ç^)^<}ˆ®« !â]^ê˜Éë7§ÜA¯Ç[ºßÆÁÑСÒ%t¿e†!T×Nï½t§îD¶¯db²§Ù9å÷%KÖ,cdb¬Aþ'ß‘&/È{zíRjMMѵœ|“ÁÄ1L­YPØ–+§‡Œ0©<Âaõêá§¥«§Ã!ŸÞ¸éïjíÄÀ/¼|õéÃ"fOû6­k¸DnqãiYôB?_Òë3çtlç)–(ýC¢\LéM}Dà’„ÝñóN4‰ã`8N¢ÐÝÚt®>²}çÁšœKUS×läÝŠÓ3ãª#ÔëY¸û»iÌž½¬S+‡1‡÷ÈØµ«Ë¢sT @}F@£C1•^;x*ÿÍ«ÔÄS”à° ®©÷w®Þg©¯éܪ}X—ÓûW—¹•‰ ¦w7MÚ:©íØmZäA¢Ø…Énƒ—Mêõ‰ ¼ðIâÙ …ïÞ$½àëÍÖ`óësO7´2ao¼5½êeiËÂÇB9›ç±¢«IÒí„6À©ø¸»9/(•×Ò :y˜˜›â‚”»Ž†Gõ ìØ1îâ¡‹,Ï«OJ»shéG }#|Ô}Ô̙࿨=Ä»œ´µKç¶ðmï?z•ÅJ_fEy¾ñ«2vph÷ì]u\k÷?³³Ý»ì²»ÄÒÒHˆŠ(a‹ (b+vww'v+ŠÝÝŠŠ ˜HwoçÌüþ•ë½÷½¡ï½÷ý]¿þØaž9ç<ç™yæÌóœèißsù/Vnø÷ðÒÒÒgÏž½ÿðEÑš[E(4il0þÍšù‰D"‘L&Óo7ýOPkÔߥ’Ò5ªð‡ç÷Æï¿(°ví;nª#ñz{£)sÆ%'nxEñÙôÚñOÝ`YØ ˜±*rÐTyUÎ.pcùº¥Û:Ž›l‡ï?|64r°òq⢭gí-éb÷ €;'¶ì8r“oã>tÆlÑÏ?"ñǶÕk;¹ƒƒä>ŸõâRÂÃà ϰÈáQáÏÌ\¾[$wë9lœ­2iæ¦ ¡€Åc¢¾óÇö<»wõÑK„¶^—̱£A`©ð}#cê‘Ö,˜ò¶Ì½wE^y÷‘3ÅEI›œõžXÃOSñ~ÖÔ•&j`»>£¢[žß½,áÜ#¹WðØ™“lé@øë;§L²&cƒ$Å™©oÞ~v'=%&¦«wÍÕØ­ÓGÇ,­Ï2M[?çÞ»ŽB‚ÐÅÏõà}¡ÌÁ{и‰–´´çâ·2¡ìˆ¡Óê9Û(=ž7F¥N°vÙdMá›ø [ßTú…GŽ؉ šμ+äèhö·ƒ ŠbyàºuQ¦Š2²µß¸ÓØ22àÒÁÃLj$Àï8y q8³RgçlKýð1c.%½6`À’L›÷,G3tÞJÜÉSÌõŒé‚æWé78$ô Y{ Ì<ó¦ ÊÚÎÝÆšu ‚ò7/¥hìªQçK¼–®X ^Í£'…x€€1gbÿ»/²†.;ÐØC”Cï—fOkŵ$ åäì"a³e–kìœ}麴}W‹/,v‚@|Û N p¡¸hI‚Ævvc5c5fü8%MEâ;oÖØó7Ÿ’¨,‰ƒ­½‘d‹óù ãÔõãlñœ g?¬¼Ñò³d ½wß~:«°9–N‡·”œ/vž>3 #‘Ì»ctl5!€Þç]œíš÷%I¢2¬Wmíã /]¾¤Ø½™£ÆÁ2˜‡‚EÝz&Q¹­øõwS³Í$ºwxŸÔ›ÙåÚ ûÒ­•‡v?è»°îHýk_%Ÿß¦[d÷AÓàÄÉãé8/MåA«x^Ÿ÷þ݉ÜSLy×¹·˜'!“É·ïÜ6™4r;³Ùl2š®]»Ü¾C‡ªÊ*³ùëÔ6?ð?E)”ûZì™ü}Ö˜þUQò˜›wß¹§KÞ»hö’3 3Õ›.˜ÿîŇù‡Æä=¿²iç‰˶ûZQªLFjNY5c@˜•ׯ\ks¦m\}ýÖûí3Žå•i´ïf-;¼ëÆÕÒ+ë'™{5qÉW‘FÕÇ%›o-KY  ×i* ¼³ºNœ²Ò'Èqåä=Vïs(JŠ_±¤W ^†Ú…J«2ŒÜÜä‡OZð÷}¸êðþìSKÇŒ^}fÛD³6óÞ¤¸„ |“þ.;¸±ë®£ÏéráØ¥%Ó{õ*á ˆè\§ŽO3ðíÜù=úÿDš}ýûú!5<ë·üì»6eÑJ‹ ?xî“óÕŠÊnýF—”½N{óüÅóE‹é¥…O.¦£{oYÔ±†¦-<{íe‡ËÀh4›Íf•Z‹™ŒZ%¡ ÖjÜÌÉTV “ÍÝRèn ZŽ+›TàÕ«\‰c}¸~ûI@«ÆÅÝ[ÕÖ£¨Î{¼÷rî¡'M€kåîÀÅÏ¿RÆzsÕJuñóÃ'“K?¼}¤z°{@B•ư©oÜT B"éõZ¨ÔèY"‰·iËésZ;×QɽkHíÏáüO‹ý§ Á?Mž¿l:eX¯vóç/1šôÆß/àĉ£FÎ/ʧRÈ4§Æ•Ï#TT€~I@¡PŒ&-˜è©ØHظâÄÍg,›Ì•O]¼Ò““×·g$ðd––61óVûšRÆŽ™l¾Q¯ î=r¯z1Ý»°¼\$ie“×l‡g©†íX³³c '©ØÊ °pêH«V“'F7Þ¹hä#¬åáõ#€LFÙ,~­ d”Åâ(†ôï‡1%ÎïV IDAT…Vòz½†Í÷BLÿ¦TàQØvA›WO~ÿàÌбKlÝ곑¡C¯oWWaaiyY9 %€ZmBI4J±Z«%/¿ÄÃÃï³/f(Ü/–Èê»:i4K§c'¶¤ Ãâñ`ÖæÎŸ¹eõñDjUÉ7nP–¿?Ûgx⛢÷Êìl Ã><»2sá±ÜŠ¢¾m†Oܼº¤¤n{“&{ÞÌmëN$BAÊšÅG¬^cM*¿pü¸C³öÍü´*å·õìÿ?TÙ³ÆÏS“¨$´Í蓺¸ìßxàÄåGTåæ­»3v&/ظ(7oÎø‘ 8ÏNÌÙ°«LAzTôQ¬Õêõz³^qjÏÊÑ v¬»Ÿ=¶‰¸²²äÒ®•WŒ^KzH'Œ_Áæ ªª4w\Œòe=¿dëI™5C`?yÉJ¾~þ¬ñ÷ÞWŠ<¶ƒ—Oˆº}jëâueŽN[º½•+W­T~ÇùóU"ß­œföŒ$æÒ¥ÞQõU——<}öðâ SWS×NŒº~wúˆþƒeˈ3Ã]é[7]5{ú²ÊžÑƒúFµõŒŸ÷Ä‘Se¦Jü"£|_Ž5ßœùX%n)õŒóeÄÄ Æ£¡6 {Nï_gãˆ0?¿{›.÷ ‘Ô†ÖAKÞ$®ý³!Q}¤,Ô·MoÁýKy*‚ à2H|vV¿!XdÄ«M\¤{¥8ad€8À§h=³¾üܹc§öŸxŸnŠOlÖµC‹ ˇõˆép×ÛÍÆ3lÒÀ˜fö7ú ‚*ÃÅïÞfUŒ›_»IõÓ¯ýÕc§½#ÇJa»¬^8 f|Ô]'{ ;ÿñ±ÁM½^-ÛŸÿð‚ŠA¡Øõéè5³gŒÜJâÛn€Ÿ…€ ”5RÑ(#fõ:ciI¥ñ£GÎjhOýš×ß uѳEëo-_>þ£`š’秬»Œœ1^ö«-±Ó[—‘ƒ†vôµüÏÔð×w/ÜÉÆbºø­]vdî’©¿Õþ×@d¥&]MÍÃþ)£*ÿ M;E»Ûð~ñâ©go<é4`˜Œþ“ƘQyéx™kOv?´£ïOIa©ÏÌgwk‹?%¼ùf¦<ëVý[xêÙ§n|ŒîÖ¢¶Wÿ¤;äï5Qso¯_4:zØLw7ä‡÷§N™f4›ÇèêæÚ¥Cs.N!ÕɈ®žÙ3 JÞ?¸;lËsTÿrÕ¶ÃÝGÏð“‘œ}––Á¥¨f<¸°­¦ý¦«_ê­^ñêÌŽ-ë&÷Jðuuk¶è@¤3º{å´ÇI©ãûŽ ‰µ|Ò`€’…êj&ÀàÞ·ÝHRDÙ¿•»5uä/È];t¥f`˜½6¢=dÝ_·ÿäîóWHyO6l¿ša˜üâáõÖC·,Ñ4ãôÄ»³¿]]bŸšäŸþÞL]£QÔ„n˜ÍØÍ›T*ÕçÆ ñsq>x«‹Tä<Ø{ÔžËAs:ºìÙºöEF‘Ì¥ÁÈI£ºªÂwOö$WaT¿fm#·®;6qécÞËÃÇn„u‰"W½Øµç‚@Ä*¬$ú ‰º²g—‚dÑôX'.vñؾ;OÞ1Ö}ÆLtãéŽì=XªÔâNáZ÷èÛ3+éê¹k§œâ·6öqUd<¹~ÿ¥@b×®G¿FRFMàÆ—nž¹xÛŒÒ[õê¤+M[¶p»³KA^©k“6[6Vg;°ïm¾²ž ͧA·Ïn^ݳ ˆ:s`‘•U¨¬¨¨È}uààÉ2Ù­AóîÝۦݼtþÚ)—ø­SÞõ¤käM›‚}}\<=yBaYî“¥Köº×wÍÏ+kÕoTS‹ÏïîO^¹‘5½KÄ×™» \wvÏ6S«8wk‹?!¼®òÍÒøÄ™×Æ}|~kwÂáLµ øýÓÆ±“‚¬ÐyD[ ´ÆdÌgK¦=cé‘ÃGüŸQÑ1ÖdM‹v½Ølúóƒ™b¯Ïž 8nÐ6é2¾}«æ ?¯~,ÊP*[ÂÂ’”JM5€QQYêèo<²éÀòù¢q.@™B7›t5D :5Be¸Ù·:ñø¡Ž³ïD ]ÔÝJïÙ8*¦Mxi©G—>S4HÖhx®Ž`a/'cyß®®†~t:ÈÏËÒé´&“±¦4 …B±¶–åå•ÕmxInVƒ¯CE±­ÝVßHz!JON+<Æõ-óæ®oœÒtåŠx§6}G´n¨2à˜*ãÒ¥K“¶®">VLZ4·òùÉùãg?³äñýÛˆ[ëƒÚîŽ_{áš{¿ÐFžŽ>]ûõ#½<¶óò³Ésç°‚ÎáÖ¤S@lÜûöwÐíݶMèâ.Ö8~dMÜ‹¦ªû‹· l˜óà\òËâiËæÝÞ8Aa–~6ª|ü7ÜܪóÇð–íûôél,z¹sG¢}“ˆh?ÛÃûœ½$ìäáx»[¿~ìÜÛWÏ=jß;Ö®Ú´dµ3Cnc“sùÆ^³—ø>ݹtáÚæ':p˜n×86"`ΘÑNíl›:d_xT°{ÿ†Êâj 6åߙ頺ð½°^8˜…öT}u€µV«×ë®ïY¡6ôCA©T|rÃ"8† &Sª‹ÊuúÚµd̤:–Ø(fZÎɃFÀ«Î«åH=¬ù 3ªÕ eÞ±™­&n7Wf`4ž‹­p­ôEYAUQìÊÐ’xd,[É­ÄrÞk üÁã×Êr5Т߱zÒsx&€&wr±³³µ P…ìÚ”BL[1aÙØ;ÊmdŸ¶Æ(<™Ì‚@µ¶µæó”ÚS4{9›'üÔ’!wt¶³³þÔO…εš±jçÀµÿD¨d6 @P&¯Fz¹“³T\³„Îâ!@#­æ3”içèl'·Рï„53†·®å†2©4…E„B§ð„R+[G)³¨q•žÌÎÑÁÞ’UC‡cïì,`‘ ÓЕ–~å;\ ®•ߎÇ›Ô.ªÕFé3D6..ÎR€!ÛXÑY¼O½F-míííå|P¨5ªaÊj¼˜éB™ƒ££µ%ïw$ñú‹¿½}2´i«Á£Ç®Ï õ¯ß¼Uë–aÍO^eFüç¤"¡ÃÀ™›0ªEã&¶„ðèÚ®fMÚ"rd~t§ [ª2ïº9Ø:׫Ÿ^‰äÒüÔ‹þõìììz˜[Š ?«¾G|L}P¬%÷ŽišôÙ¹¯;wn-É.åcÚâWͽ,„ÂÆáÝ>”å·ñ¦ÓhM›ùxû¬>ñâíýcîNöŽCg¬S"‚ %Yi©¯ózŽðh×pAÜê9ûù×oØn‚ ý;µmß=:K  ,ý0mX”¥XÔ Q³sou¯ot°µ²µµ6s“AQWå_ºt/ræ×qdÊÒ›v8vîæžK7K'4Y-zY… C:"ȦŦmÙ·}ÖÈþÃ'>W#ÉÇ×KDŽN“V&ªô¹Áõ}/d*“OoëÙ?#û·«×s–°|b¸¹‚ vãÐ!‡¶CÃùàäéënI¹˜¸Í¾×Ô +þq÷Éß”J§qÄõ¥ÚÁ®…3&Ž\¹z•J9jDT×îRÏðþ†€¦©¬6Šl]?_‚ ¨ÈÆÙ‰'½vÇûãÎ勚8ñÏ'¥°¹ì‚œœòjƒ_›ÈÒ¤µò•YYó«õ­Ú·/u1SAÀü*5 €ÀqÂd0†ãBà²(æWPi(J&€çꀎš¹müú¾ði¤¤öþ2FõŽki¹9o¶ìºÓ|ø\(Ò’¢bŠ Í}fÙ¨§Ø˜}ö£–Á ½ýÆàØ(óÎb­îPÂe#ÿíêÒht%%Å¥¥¥ee¥Z­ §R)d2J&£$I­VÿZòƒÁ¨ÒÈzÁÇô]'7o8dh»ö—”~ÔÓ­‡Å q­çâ"—ÑÀ,âKH$IÀP©4&‹ÓªMŸvMºvôJ;¶ v,ÉÍ›|úîäͫ–ï?óÈÖÆNd+‡ˆÎÝ\Ý\=œ¤U……kãb#ppr–Pô{wïÏ,UÒØ4ÀÀMïžß]0wւū߿ä™l¦¯‹—0ow.bªPiŠ2_ûu™Ò<Ào@ÿ–„ºäóÓâû<åaON­iÞUOÒ§¾L9wôÀŠUñ™…Õ¡-“±\k•µ„J¡Û»ºÈDl:¥ÒèT½iP§P_§À uAFyU‰§¶‹Â@±Û\±u÷‘2L£ü;«R)N ÕÉ(ZsK™õ…ñ;n÷Z3L†ÊcGŸušØ‰0&ÓgÓ“xqyÓý\VÛ&ò73ŠssŒoRÞY»ºKØ ÕjªJ>.Ÿ>MåÞ{ak^YIŠ¢F½Ã1ƒÑL&Sh@o0!@ÙÜ~qø¥×|ëåÐìlœ¾{IÔ?EFü%øE¾ßÂý;vá+‘~MKßW]t˜þ«@€äÖ-ùåAO ‘ÀÙÊzØîk7îÜã)Òîç–Mˆ›±$µ*77{Ï’Qd€ÊÊj³¹ö a3¨ö¶n`ŧ3-$—/œ‹[vîㇷtZ]ôvÅÚ]ÝÆ­xžrÏÊœqèÄë¯øbFMÒí[ò€ÎÖj•ÆÅÞãÌ™«)ïØv íñ%÷°á•UUó[‘‡ÛØgààg/N‹D!¯ÒN§$.›½"alü±gwÏ@Á³‹w‹àñ­3 »õ@&ÌC·ÜÛ¿~òœU[@ÂÕ;côÅu&¸wñdŽŠ_V^ñìñ½0‹Âµ[ޝ»’™ŸŸOùpv÷õx}︒éßBðÓû'ñ}—ûrΔ1‰NôLܱÖ)hPeUÕòöŒáÓž³ãâ’I³O?Ù»eµúnqüÉS9•ÙY™Ý̰LNÙ1¹mС;ïæïÚçâÐäÂÎÑ›<ª'l[Ê¢×—fÇ  o^¾8û­ØwÙ*ëê³âãçï/áH½Û}•iðU.W½Or´µôö¬—÷áQþ­Œü|¹°˜àt\üé’ÜÙ«Iãš TAüʱU™Ú9›7Vߨֽ{ÔÖÄs2 D­n¶"®×¬¹+³tT·æc¦öm0=¶S܈ o”4Ëæ­ZÚ ™Hý€F>nré¼Qmçöíÿ8CÛPͲ§Ÿ—]—عëˆÊ–ÖŸvg댸¸IAqÛ'5§¨òŸm\6À€aÏË-fÌì ¿z~m¸=vû9 °ãà0Wbᔩ$÷vÃú÷þvuiµ2EÑšÜF¤¯L¨_͆H`åùoOßýÜ­‹Î¨(È(×êÒ_¾—Ƚ…!D¼ËÈW©ÔJ•’ QI„ImƒF­T©€„‚˜M&%J¥é”=…B•;8u°ìHâÁSg.íÞ<›(Ê7a„A£R)U:ƒ‘B¦šÌ8†•ÃiÇá£{6;?gÕ†D†X‚ ˆ¶2ûܵä«NÝIºÛÜMhÖÑ(Tj„J¶´©Ëó«õúŒuM›’¢ÜÌ2Sxç>÷/Ö½+ÄÙõ}ÏÝyþäÉSgÏŒná£(­0™ô*­Î€™Mj¥F¯7|zà3fÖP¥ÔÒ(4“ͤ’ ós+++Þ¼Ë%H(MÜäjÒ…¶žìÕ3Çž¸žÊâ I¡ÿÏ!sðԤ䧧²¤f³)ûñ9Ý9F *•JU”’­³ìÀuífÀ)Àsuƒ¢<ýds©— ¥ŽÍ«ÊÊ}%E"'¥É©XÔÀ±~eÕûÞue @Í -aÀ\¯àhòÊ++j8êtg¯Ú— QÓÙ1‹>žÒª€õæM§¢>‚Ÿ EÎÇüÂâÌZnT‹üÀÝ_V¾#>–Vf¾ÇzŠ’ÔAB€ GquI5x7´£)?TqêóDÎ†Š’]Úô /’ïS%õÛ¸„¹„eªì­8ð¯¹ý~D¾sž])Õdrr’¯ÓúØz(^¤«ªÊdÆb1ùÍ{µw¿Uð%E*Ù9 µó§”.Íz×lÏ-ݼ§.Mypô©óÑŸu‰;ÙåKñøÎýjK”µî^ó#lðì°ší °Ûgw¯Z{qÀ¶ë?•á»´8p¢ÅçcŽMÀº‰?i"®©3 E‡`)¦Ò~Í$õÚÔ©³Ï§‰Ðd6€Þh"QhR»‚›÷š;ùÊ¿ã8£6׌ñ0ÌD¸…Dβ°°iÒ¡£=fÒšáíÕ-ZQXHMbÌlÆ0¼&#üW,ì¬N?H)€AÖ@T¨‹üâõ«`{ÛÇ)ù½=é yˆ{×OËT˜Iwéð.ܦYçæ_;QYÛÈo$Ý=zâeýÎS€NF”J5°„26è½.ut¨ÌÈ,ö”=H˜)l2 SËøcFGm]¹²Ó°™Œ’Ç—_û¶õ`ßøØ3lí“ÚùaQýø¶q›“VÅŠ‰îî “î9ä|ru¼|£l0`8výÜö¯PAø5 ß¼¿‹Üýé¿ÿþQ#pü«_N„Él¬»v…RÅË6l,Tê¡£«;¨Êhn!K¹å•«ø'jU©¹cßÁžÁy*¶ÑHÜS§û}(9уÅlxµš5‹e*/±ôì¶iMeI‰kHÔüP1¤/^¶àCn ®Ä3ã#&M&ñD:)"r™,Ò©‘¥Ö¼)Ö‰EÜú^žÎʬííõÕAPX’žýâ ˉ;p(FákM¦½vÓJË –|î’ùt!…%l4{–}‰ÒÀv¥ÐmU¥¥5VWÃVSlÍJeð\}šÊŠÃ»Åº4hQ®6ÒX|K™Ãê-ëß—%66ÍfÌÊ(áˆ$­›6Á¨,a±ru0VZÊ7ع;Ä\VÒ¾§£o˜B«¼x¤X ‘Jäüþýú•n!u°´êTUÿ„qÿ‹AåØÎ\¾òmF!ÝBîæÄQ©T‰1fÞ:z=o­V‹™ªo$å´Y\“KñjØtº‹¿ØÊ¶ZKE•Zc60Œ™¿€`™R>Ñ陎d½FÓ´mLÚ‹AŠê´¦ØUgÄ"zoñhV¢6Û»û ˜ PÀðyËš¾ÎF¹RW{Vkçâ5vÜ83…ëâç/¨ú®¡ð'Œ*uUiqY5JcˆeVtBWR®±²‘ªË 5$ŽÌ‚£¨()¯T dšÄF>búj¹®üJemv“₾̊¦²òJ®…W—å—TæfWHzUE~Q9JeJ¬­™ÒÏ;YðâÄŧ¥‹v¹NØôQPZQY%0â¨Ô‚«WUæ–¢4¦ÈRJÅ”¹%ÕT2JBQ ³ä2‹ªÒ‚Šj JcZÙÚÐH@Dñ›Kû¯¾KY/¯,-¨Pè,†Ùh¶Z“J‚á8yj»v¸YŸŸ›g"H,¾X*âU—æ—Ui¨L®ÌZVC§üãM§Rî,ö~tíÈîÇrÔüùЬv#g7´üIõƒº*¿¨ŒD¦ñE–B.SY^T\¡¢3h$:×) Gò)ÁÜ]—ÔB³A§Ö’‡Yã•^Q’¹uíJ‹Þ3}bZc€ Fu~A(Wh)°áoŽqCUâ®å ×îF¨¢N=ö>ÊÛÛ‡EE¥J$Rƒ3lgBËÆa£U­úOÛ4wßÞ-¬‘}yŸG—Dwu—žjÖ*È•[Ï3°i+Ï#ýFµ=ºDîékçìÙtt¯ˆè¹é™ Z®Ü½Ë«Sƒ*cÁª‹Òj÷ɆÜX< +‰TÞ°¹ý¾^V2Y«èiû¦uÞ»} ‹Á±¶’‘) Ïðñ“BÚ5k4:¯Ä?´ûŠ53æ-<=ãî# ŒÍY—Ø©l+k«ìI-; ÈŒ§ýÛ…EÍY4bpŸ’Å¡ŽR»…‡oÏ[<:¢ãuãªÆ¯9ß½ ë°wDÂÏfýýëçû£ ¥Ð---™T€æ>x0ÕJ&m9ù@ŒD îںK&l[;:¨C‹e8yÚæË!Üôˆ½i—oœEÚð)ÛÏ“Ÿ>(°>¾v¼îõÙÞ‹x,íöA³y¥+üMÙÂþ—À²™{C›rìÚ¡åTŠ‚bN>á F­ À_«:„DæIœ?Ç>üsFJ¥—8:ÚÖuôùì¼`6å•B¡õ߯AL¡Žãze©ÑhD„+µ÷µ¥à8^U”‡ã¸† 8¸yÕTJÑétZ2××WR›mH§ WȢ괔&µá‘5 ™%–R(j•†Æ¶ôñ³£Ñ¨Ó¹"†a&ÆàŠ1è ¡¿”j4Q™U±NUn6› ‚ÀA|G Ô˜ƒ&ÑD±´åk4* 1„–t£Q¯Ãq®ÄVhMÆqÓ¨55]£q­¼¤Î$‰ ]U¾Ñh4HVŽ®¶(Šã¸^¯71†…ÜWJ1 F Ö÷•F‚ 0 Ã0–È’®Õ¨”+µbjTÕ©WN­ÙyØ”ðèÉzùhŠŠ¼ýH$ Ãtêi¶z 2]ähK„J©4›Í‰*v¬gÖkôJ¬9v¨®®F’ÐÞÃÆƒ AhµÚUM ¶“ IDAT#/˜L&Ö·ì¥ÑêŒF#Wâ.s¬É`F£Q£QI$™“»œB1 êjŽãZ”íÓ hÔj“ÉÄH}mœH$’Á`¨ªÒ~÷‡ùE§¹ùsgÏ[°¨¼¬”ÇçS(ÔÏ\Mšü±ãÞ–iiª_·1c[Ñ£cÍ߾鸢©¼vSF;ï7ôUn•Ü)vÎö7êÉé}î6¿¶½³IÑ( óê[Ïš_Å Ÿ=|iü…%£ïe)myd’_ŸCˇïÒ&Oƒ“€;g^ߦðÓy7VNîÝÍqîQ>01¦ûGɬ®@î;o:8!vïõ—2KKÿˆØpæýnSy»Ø’X<#jw隉½bŠõ@`DÇɛǴ­‡™ª—€wÙ4»µ1¸Q›âJ=U s ÂúÌënW7u††ß1ùÂrìæÃçïvóôö‹ž7¶m×&M«H ‰Ö{Á¶¸pgܬÚ>g|–Ӏ僂5ÙǶ­\¼ûÞλ/BePWr×n›9*þü3ÿöýF÷mãÓ&ü½ eáÊj‹ Ô [‹ž]\xkÜê•6ÅRÏštèvÖë’LK\±sí‚%+÷Ë· ‰<àå¾%›Ž\e²9Rׯó–ϵ¥üUSsüúµCãâô:}ÍÆóÅsg{õîC¡ý,Úí¿ƒÏ},{snûã¬É=àÓ ÍŸ#h,~°|÷»Ù3ú£`šÂ'Û½š0qàg!ëŠô-þ¤ßÞõLÛùSÜÆ¿—/]¤R©-Z¶*/+EäaòýÐðlç7õÿoÓÞå ç=½½„B‹Ï ¥Ž9Ýçù‹ÔsçÏ™Mf­» øEu2™¬{÷îlçGÝåß A¸\®H$‚ òóóFão^õ}¢¨H$úëùþi RcÑþ/þšÆø÷N9››“]]UÜ´Y~aéÜ-gû QPX@`Æp“ 3š1 '0 Çp‚ä?|{÷‚þåe•x0qõÚu5òü±•ª¢—'·»xâÞmʉ'N.ÛµaÖ‡½"[\³0¶é­#ëµdù½{g?5ÇÌxmŸI$’³›K§kiÎŽö¦Â;w3È)O“s¯nz®"?uÛ¡+ÉGoÞ©¸¿ýî½5FU¹)—žW×ø@9h§iÛ ¦޾yíìÙû…¯^>ó›Èè…Þ yC¶=fuï‰ÂííÅë7N¯IzY¸)qoÑí{¶îÓvIÉÛ{O ({#¬Ì%©í:v‹Ž 3óŹý]§Ï¿ëÔgصSºˆáï7N›·uÇœ6@aÊ–\µ8íÕuUr|ÈÜ¥qá»*³S~ÔN å劀–mÇ9õ«gÌÌQ9ÚÕ- Mò2ó"Æ­X6¸- âmâÓ,ìíûäü+ñcÎè3ÙÍ•ºøÅÙ¬ï·oðÄÕ‚JvÄæ¾Ö /ºß3þ@‡3ƒåÈžƒ—SÓ¶~¿+àÿ¿üïX#±ôè4Ëà›Wh¨Ò Ù3‚¾…B XV&Ö¥óÁÿ†~<Ãyþ׈ÿ?Ãýxzx5 nöêÈšÍæªªªÕïA …B¡PüÝ‚ü/¡fÍï¿Ñø¯Ç3ªLŠBŸà¾í‚TÈÅíØ™F+ÈÏg»ù€Þ`°´ù’œ ~²ÈYûíCA„¶RàF屩úòlßcZTÚŠ:°ûŠ)nV>p¹Ý¤¥@Œ×F@¢ŠÅ$ãêj¡M}À*<ÝlÄ 0U(­ž0i0]•_›ÑaA v’åpñÔuÿnxJ­G ´JC¥2+ŠË©(@aN)›Ìj¦ÛgßE±@Þ0: $߸kåâN°s­Pßɘ”š^™ÊIdÞ’Ý»Ÿ¾:¸W±û‰q‘}#0À Õ|~ùš&B[X¤TU×bм²J<·¬eêÊb+ˆˆölÓÆÐ‚ºçþ-ø§…,ü-ïì†Â|_¨ÕêEý~à¾#þ˜QåÔ°O7çµ¼,Ø/ße¡2ýªMÇ®Ý8ºnÖ-§ø&^Ùwëm©‡À¤ÒÑí-¬å¢â' ½³™Ìv’²^¾xǃ>õ2\´;>àaJZA Û­yoæ¼iÙ&°ç1^¼ÉiP×®"ò^ÜySM[ÙÁ >e(0t€“Ì.pl`‹^>þºÄêý SìâH}¨ÓàLÇ1 ×f])çO—âq69íM§¤ôYfeÜèv€a88†ã¸'‚ÀŒ…ÅUz½ ºíÈö‹×_;¢eÖÇ¢zÞá¦üYϕƧ'SºÄ˜’ž¥·¿¿V>‚@¤ŽEE<¿°iÍÙâ„í‹0gäjÛu‹’s}wóè.1lÕ‘G•šÔ;ÏJ*€ÁecMƇjK>Ôùù%Z¦¤Ô(Q)(],`>{÷‚d»ú–'že–µö|ûö‰ìlÁùþ[K¸I[Tª¶±©õ`øÕff]A~1JgYÊ$”_o©(+¶˜ÏüíÊêzµBe >£¤¸ÚÊú7¸ÿuj¥Ö(Y|•ŒÀM• _È ¢¿‡²Ù ­Viùb é÷IBà*Eee¥’ei-æÐÿ”ð„º²Ô@ቸ 3T+BÁ_õ/¸võ2ú³ÔÿfÈí왬¯¿ÄP2* ƒJ¥BHP(‘HôÝ7V~à/Š¢,ëï–â U•e¥ßNçUˆEØ¥Óºµ!³á±3…ÆL߈‰-4h1·xÜ®ƒáóÍQ>yb,…c9fí~g€à¸qÇÍì72mÖÚå³ë=n^¡·k‡Ãlå¾ +»öìéïiß&²ŠzZ׿o›02“ßvøŠºy\0£òêå»^a]k ¿ ŒÆÁÁrP¬aP°ƒƒÇ†=SÛtˆ²ªç?}ÝZçÂóéù;a°–›xÔë°tle\«02S=}þä!OX_@ãˆüüd2yó09‡/öõ(ß·qÙõ줎g¯=~l¿õÇ^ŒîÒacHϱ³µK\Óo`Dó€V½wNhùâú.…IÚÖ ¿¼EÐ#J©Ùž3Ý=½k磊n¾MÆ/[Î'Ãþ5±=:µó­'²pt ÷FMR–ìÕsñžPJê´¹Û-,í6ÌÖ}rLKyŸ‰ÃnŒ]="ýî¤åóVî[9ºß„–+«ê‡t›>ÛåÏø÷ƒ2ÿþÐQ.^ÜÿŸ›©‹S";U#¬„Ô'þ¿¼mœÛÞbÈá¥Ý]ÿsNpܬ޿jö}¥dÍôæ üz–æþ9á \~Ëwïÿjî1Tg­^¶³ëðé ]„¿ œÀ’Ïï]“´éÌ›ÿÔî 0£òòñ­ýG-l½üôéqBx³¾ttt7Ûa›vsKºrxçé÷SÆö—8ºJ?Ò©´jÝöïá‡÷ïÞ~ý/`ëÖ-w’nYZŠ 0 7›°³gÎ+•ÊòöÊü&~̆}­ýáè?–{Ä•[Ÿß G7@\:¬ŸàyúB*¨ãÚ½‡j[' þrʽéSŸË>€e`Ô•›Ÿ«»Ô¹'¤aó¶"V5GEØeÐÀšßbF@ý+·îÔ6vê0Ä  Ð œÂb¯„ÅÖœ|óØ¡ÿЖ5¿™×N]`쀽ëÀQÓêt”´z÷‘ÏŽ-†Ün1¤æ7G(‹›¿AÝÛOÜÛ¾æ€=`ÆÊ3¾œµ ŠIºcN?2h_0Zõ™ÐªÏºüÓW¬e£Ý‡Ž~:°Žßw¤îÉ¿õ±ÁË>¤&¸Þ³bÏÆZY§Ó:ø¶ìÑ&ШÊ[¶pE…‘âÚyP×ÐG/“æžQ³œÿáñ…kïâFƦ]ÛÿÐèÕ?TºnÕúb-‰-H\Á\¶jñšÂjƒ_X×Þ›ý<•|eÞ›´<åðÅñ"Öc‹³ýÀ®Œ·e£gO³Â+¶oyú6ßÁ¯Õ¨!íã—§~¨töv«*(kÝwœ·píÚU&rPÛ¨î-UEþÓWÑ£7˜3®Ç­;k-Ñhd²¥ÿ„þ­ŽîI q­mí„€Uw/Ÿ:sí!G$1ÅÞüný†zé< k˜´Šû÷îG ]ü•E…™´'v®»›^êhg)èÓˆ¿|áÊ bíÖdR\dÔÙ^$ý&¦€0<¸túÄ•»TŽ(´SL«Âu‹·GÏšNÊxzîîÛæMlŽ])ž6®—¦4}ݶk“g£||t©’é¶·»_iVê‹WŸ'gÜ öt…ƒÁ°~Ãz‚ (2°ÙìN;I¥²¿[®¿¿2[˜1lÒ”\£Á@¡Ðâã÷®ß°Î`0Ôä(6‡Ýºu[÷×Iûß?L«¿¸LÍ_ƒÏÉtžo“V6üoåî ÛºÙ¿½Žþn¿ºÛò{(ÂVÌù*ý“þ}€ˆ­í‚ê‹ìÛ €)ŽJÌ$[wõ¹txÇ=ì™?ùµÑfÌø1növf€ª¬lµ¦6ëTi^Úé“ ëñųw_Ü¿r,-O×;¶Ãã¹Ú½ë7›Îú÷|y=áDr1ü4¤ó‹ÇÉdS ÔÕ ¥¦ÚÆ?œUœºîXrÖ‹«'¯<ë=jć‹[í¹’‘ñA öî½TßOrpËÆM÷ñ<›ÅF¶N:µûÆkd¦?,ª¦µ ¼¾ž˜š£UN¹Æxíø)# Íšx¥?MÎ,4Àëû—6l;Ò¬ÛÀ¨î@Y¸}C|EÚ§WDâêY· *òR’ŸWDµw†ŸÞEï./X›9rHÚÍsGo¿¡°­Ã[FÄöëUz/aÃ#d•cµË@leÙ«w« ÷Ó»Ö¦)XîÂòá§ž=²ÔÄsr•ܸ8àõ­³wÓs¨€U<™ÔfØ$¨®ª”ZÛ6nÞÑŠn*QÀ×êú·¢¢¢âÌ™“ÎõdB _@{ÿ>íÉÓ'ðN_ÿ@üš®(raAÑݤû>~ôèq«–Á*u©É¤Ä Fh̸úõ›))Ï ƯQøøºøÃejþbüžyᯤS—ÔŸ'Á”H¸_vÿSì+Y‚ Nr—¨~=Ü7’pðü¬w‡.¥/]7ÕÉÎ>ØÏž PS£¦æmO§Q%–Ö à0LV~AnXïyþ^Á½‚+ßÝ¿sïÆ™ã‡W­Ù˜]ª'¡´¯¸êUeW._oÔ{Œ˜·{“ÇíÝ2^¤•ä½öm;¶‰§ÏÜa'·íó ëÙÆÞ9¬sÛ ²ôÛ7Ü;w$aÍÆ]F¥À©üú-¡Q´A÷aa!A›¶ufÓTŽMšº9ÚÖÔ¦zžîЮ[x///w¡â]nuÏ¸É µ«Ï¹óœÚ±É«ÿ:ÁÏ”ûüª]³±¡^#» <0çïÞºnÊäI©/ß|Ò!¸9ûõù3¦-[»%-³@IÐ#F­&ÒŽŸx†ÏŒë$÷Ù±õ×m:}ÿEù°Yó +%)KEéÜÎêù·9mL›v"»uôæüßQß ‘H--,X:]5€ÞÞÞzÅŠåaá¡¡aÍjþBB›.]¶ø‡Z£Ñ¤RiuZCuµòùóôŒŒÜŒ¹99¹9Ef3A§ÉŸ‚éóÛ4i ‰4hÙ¶ËÞ«ÏIl™ü““&«W§n÷²<ÞwR6ëòW-ZG„ÿ!\ñ÷ãìÞù#'¬æ …$‰À4[—L°°°pus÷ò®?wýq _Öº˜%¬Ûý?$RᇫM†Ô½M‡ÇeF.—ûí2ÿ0-ÈˉD"­¹üª¦¦k~Ú}¥Ri¿ñËkJ²µSw=ð(ûÌæ¹$ÉÍ»©æSùW7\KXF"‘ö¥U"€ÝÚ3·×äµÅoÎy‡Ó0´K‚ ’õì\ÏzÖÖ6£æo×#‚ G7Lf1õ&>ÊCäÝ£óÁ~R©´Kì$Å/T˜ýVüáí?7}|ûÊL³rw–Á¯Oâ¸Y÷>íyÚëÌzͻԗ±­YuáÇ·ÅÚÀòo¿°Ô¤|¯0G ÚïsèTÕÕjƒÄJZg# {| qlâ!cýœŽ^Yžþ6ÃÁÛ?÷ÑQãÖ¶Œ?ë­U¼L}.÷ ‘°HÊŠœü*†‡³åçr ÿ?Þ| ¥æóZ $ƒLAà•ή/—½ª>ô •J"¡€&PE€²r5Êd‚,E€0¯BÅer<}Ã&L^à]ǧ¶®®²^\~[ÄY@…šLè8•*-™Êðyê7%ö®Ôºž¿ÙlVªu8fV)Õ\GæÒrÖ’)¢Ot*>œ¿ôDýx—%àõj֠ô€›qÐhô}Ív‰HÀÓg) 4µÆÈe3Š ËÀŠ[Z¡¶²²Ã˽—ò’W»×ÈIàXþÇ78ËÊÞÚB$šÔeQPnféngû ¼6·Ç½e=w—ð(Ô¤Õ€"ïɲ­g¶ß½ëÄ”Áý Ðhîhïi/xX%Ð"ªÇˆ MûàÅÀo\Or lÿe·±‰lgós]ý›b6›KJÊÊÊ*¨T “É4¸—N«GHµõ‘H$ôÂù¿IçAà†ÌùŽnΤÿ/ û&‹Ççb C† pp°Ãp“ÉdÆq\§ÕíØ™ø9µ á(•¯zJmNRÃæ ‹sãÃä7/_É/SqÄv¡Í›’õˆˆË§°¹êÊ‚^8{Ö{óücXû–æê”Ô÷Î>~$mÁ£‡é 6¥ZC ö}û0Y‹p‡†XЉ7)?ä•RÂÀÐ0K†)õÑ•ÞDA¢ñýú—¾I^¶nQƒÞ½]ù,>ªº{ï±™D³qöôöpF1#Žã¸QùäÉÏF¸¨1íU:Wêd-ÄΟI²¶—UU©|{Ø+вž=IQ›ÉUZ£Pêò9 [È.ßyqÊ ÚüÏŠŠìy³æZ ØÇ7©ï?2ÙtñoÆG5/=È,ªbñÄž~æìGËÖ- ìÝ»ÞOEòñp¦Q@,°¦Å’' 0ÙŹo3  <éO^«[¶¬Ÿ|=‰ “õS «ïçC# éï¼Ï¯¶´’h4”vÃ5¥9/SRŠ«4ÿÇÞWÇe‘}ÿŸ™§›îé;°±] »»» Ûµ»»ElQD±P@P:$å!žÎ™¹¿?D\w×u×ïﳺ¾_¼x=3sî¹çÞ¹sç̽'Dæõ5àP•1· ÌŒä2¥‰­«¯WœÔ}/é™-\bãîÛ±O HxËú-Ço¹7¤åúiá+7EEN묨ÌÊÉ#—6tf{ÏDcûµn4¥¦mÅ/¢fEÄŠå‘;rÊ•'~Ý×eíU ¡zé ú®Xþ>bÙÆ§ ÏOìÝý¤°u÷î"òÝŒsW¯Ý¦¶ ïrnëœóÈ9>=Z›žŒÜÚrÖÆy­|[Äìݧï€[Ã^fò¸YK–E=ËÀDÖAA!ò¼¸ý‡÷nŽJæšÚ€o“ Â«»¶p"ç]ÞÇIt#«švaHwzÇòóMŸ9÷ÌÍ—8C1nÊ„÷vëü%+" Ê+¯ÞsøÊ+P¤/Y±>=· ;·@¢¢tr™™‘Y^vŽXœ¿oÛö¸ÉéDâñ¹ ŒÁ``@>¾ubß¡K\§äíµyóWð¸°pάÃ×¼/ÈØ»sÇËR¦O˜4'%·ðÎ…}Ç b3éq7/=q1'ÿݵs‡O\}ÎbU :äFrnîÛç[·î*clç{y§à ÓØ´²BŒH´²RIoР!Ôo(ËK€Â×w…}¸8_¨•TjuÕ¯nŠP]Bä“ ‡mšéh0þX ñ"úÈä%Ûmììg[Û+ó´*­½kv­àÕËZaýŸ;}_‚¥¥E¿ð¾âò ľB!ÉÏÏð6ä»hâ¿~ýÎÔˆ¸{ùÀæíÜê¹{÷#H>y0ÚÉ«IÄžž (mqvz®XêéÂÆjbÓc˜±ˆoiep¹L[G?hž.uúÌYÝÙ—–LÍ|“áXrÃÈ»ÿ†‹$I‡ï¿Ðê@=êb†XàîC×)+S_%cƒ»†xøž1®.g ^˜_Xæcã¨Vý6Iê¿!¤W5^¥ÑéA€ŽÄ0DÀ­óqÍ_7\×ht5Þ¹ çbRÈ +zͺvÞ$ ;ñ™{]«jNÊó;#/e‰nlu!Êâq‹Ò н’Æ3ÐcLéèЫOÿñ3×·¿¾ÕÊl¬¼¾yK¿2¤Ƙ±†³u}ÖÛL1Õ)K+$êY‘[¯±÷ÁŠFÚCç~øÔ…Å彤JnXNJ••–¡3·œÀ IDAT ±izå»üÜâœ3}”•údǼ_ $åñ 7^‰M{YzsËÐÁãžÞÚ÷Wv¥8yõþ§'_ïRS~ûNtTé ;ÐÉRÖ_°=ù­ëÛèÑ‹׎ž¹òx컬¼kçÏY}hÕ•ýŒã÷ß-^6˰ݭ®LßvìÙì;1ðéT¨(Mœ½lÏŠKO|ÄWûÏVŽ^ŸD£‰ìCì«i­ÝükybY9×µªEiWÇ·fo«ö=²ñûåIBøG&–~"Gظ7°©6!02k3`–$œ½œ«‹š?|ú±>cw;!,àߨÙ'­åY†XÖTë\Ï¿šß3ê^ô§ÝÂ0u 0üÖ*ÅG {#…À–¶¶¶[÷z¶ÙªÉÙ¶u} g€eâícRÃÇÙŰýɰµ·&ä)u}ÛöŸ»¯£ @høÂÐðo`MòÃC(sŠ‹%t:-##ÏÙÅ­¿Ák×®òðDÙÚ9“d­MDUg©p“õ[·•$é´÷ARJRÂÉ%áaý’bOnÝükãî2­_»%»¬VŽÞ,òÆ¡M+˜Û V©¸\Æ­w ?òЩ+ï¿È«ÓrvŠÂ®…w#€«)–ùä–cÐ+ËF®²—Ã0½^OÐ¥{tå¼iH·ŸM‡ª²lžW¨“Æòhèí¨QmÕáE‹&.>Z—OïºÖ"hݶΌc7Y& ê·á„´tË>|OØ®[Ó¦¶Ð¨‘æ‡ 08ðȺ͡‹Ï³æ/u†“[kõËWuø¿ècƬ®ýŸ¥©ù¸ŸøOùÐjóù +ÏbÿõÇ5‡»ºÀ;»°¤†Éç•þÄï‚ ¥R©V«ä2¥›»gp`¨TV2múô={v;;Ûh5Úßè B‘i‹Ð:@æãÛ^=VbÁÝÚhæ_’üÐŽ] _Ÿ;nÆÔN÷Ü_—§’"µ/o]e^ÆÝHQZlŸ*»•vtïöSV=Ÿˆî8ý ”€ 朊z°xÄÈ)ñ‹û‡¸ˆŽGÇšÆç–ÐØ\ ‚ÐIh€ HÀ°ú·ŒøõÀe%M¡Ðu s’ÐB…0ÃéÆÖ"ÚÕ{ÏZy7v‰X³»k·÷%²€Fí-I2–^öü—ÏW˜(’Þdûö`FR$@QÀ‘o]ç§Ñ‡M©ç¯<4ûÒŒ¢ô¯ß>ça¦—VZÚ»ûÕeW/Ñ!d–¤( ©J|çúÏÀ=rD"–©ˆ~õÞ‹†V>F¬ÙÓ%Äõ}©, q.ýã›Ãh¶vβè{Ñ'_{üPŒBˆ$ô! !Ì-´¯zéœ#ÑN%woU*4à4 ñ;qz£9½7(*fî!^ „ùaßÿ{Ùø3@¯UÄ߸š”‘-Å&Ôu rwíÔÂoÇñmFª–_¼ë¹©ôñ¯æ¡Ãqª åELÜÝJyåí¤âVumzÌ=¦Ÿ{ ÖŒÓ|×nOPž-,jÝ­<½¾oèô5ËŽÆd¾JD,«úõüz¶ô8¸e5æó¶@5jF+`R­Ü7Ìßqʶü‘w·ñ%¹©¿Â誃»^,z°á›·ô«U]’Ðë ²z&A Õi@¡Ö1X[k‹ª²ürH”Fçpª `8ÇĤJJ£RˆÅ37¦Z ¹Å•U{ßí'®ˆøuûî•Ó¸Oöþíõ'Å#&õ¯9ƒpšÁ@š#4cò|q^‘žÂÝýˆŠt åu®±­3©ÕVÇWQUæß‰ÏêÔop ŸŠ¼¤KWc0 šZòpôŽqYq^á{œNç°„‘Oò­Å76œzhSÇ[#N€„9Öõ1B“›"xñ8Ý! 5¦•½NI€— ¹vÞ!@¦_y.[ÔÏ> }àÔù‘4ªÿþEΛÏ·²vú “ŸæSA㸞 ­¬¬4Ú ‚ *+%ÖÖÖJ¥êÓäÁ0FöÎNâÔÛþè…‰­—ØB=-5âã_ÛxÔŠÔË ¥~ˆQ½™ðû—Ièk¿¼q†hÈðþ´‚×qž ›ì:å P'¤ÛèÞ_=ˆñé:{qo{y•¦ïÀ~•nâš a~ÖÅìÈM³ ÞÑð¦Í_æ(Ä,üûöíJ94=¢°ójÒ¯k[¡]›È…òߌ’€›Ø°Ù&zôðq¶ÐêÈà&­[5ó'ÅÊ%#SoE—j,WìXïLݺyç½LË6Ú0†ù”Y£˜ï³ %0jæ’`73Œi?qÒxÀ8Mž2èÒ«o€#7ñùË^“–èÒHg¸3¤YK_'“'wo%$¼Ê-®¤ ¦L™bLR~ Z´j¨§½{ƒ~á])EeîÛ¤ë7î2íBFŽî „IÄ¢©·®•j¬"v®w" oݺó^®c DB³:ãÆÁ âÀg¸ú6ïÕ18õñc¯°3Æõ#ú à$bêIFh»Ž!¾Î7hÛòá)÷nê™\'ŸP =$ÌïÉÝè”´\–©“k=qòD õèÝËÝZ¨ÕüƒäHêÍË$ï¦-…äÓço I¿1#:ø>‹O´ø`¨-?ßnD8 D•ç½MI¯š8kbÚ½›åÊêñ‰ih×®æJ(}ó\¥bx9™Ïœ>S“ùàâ¹³™Eh?nú€†vO^f^±/PøÆƒøÅ‰¤U“M‹ú©‘¥%?–XyùAˆó7}¿n¥JSõvè )yy,Ó4qÕØ)¿˜ÒðjN7ªÛÎÏ6ªw«Öæ–6Öì 3¢5ïÞnÙðqãR»L[ºxD¯ i»4ð²S1,üz }Ù£ß`ªHãþ‹™ëàö.#š·l%àqºÍˆ]kwéî߸62øÐxŠªNÔ̱ ™7$¤gHCK‹ÎcWy{{÷krµIã&V¦–£6œkgGyÁÑ~ÝúŒZ»7H™®¤µð¯Ùg¡2âNO^t­G×dŽ]ýaâ§÷ëoÉÕÉèÆL`8ÓÔë6¢Sç‘›Ú´Ù9 Ñå&Í[˜‰Ìç8§(9öúU̸aƒËòÞoŽÙ“·铨Káïâ4ëMký.ož8pfmßÙcæýÒw±æŸ?~|+üieE{†Ï²÷kºt{¤ãRû—Ocµ7¨±íŸs ¢O\K×MÖlö”í{|¡ö?•}6.kʼi¢O9h$97ï8db€‹É_aþîuÜ‘‹÷‡ÌZäÀ­e–q;|ÈBÏ–ÝV¬Y`ðìÞÙq%‹OþZiõŠôý–Š:ùiTýÅMKrœ~™ùK}UeúÁ‹©Füb¸ð£¶¿ ÌÌÜ8&&fð J¥J¥R½zõªc§VįÕZ¨VUÝ[ó9:½e«V<gÞÑkлK˹k'œÝŒ9¸-ŸÚ@Û½¡M§öÝÇO]< ,¾ÿ>×iµ"[[µúãë¹fíTª‚Ú OpºQØàña ‚¨’Êi4~›>£ÚôËå$&ìЫ»Z­V¨˜áÃGèt: 3_½º:S†Z­[¿Nn‰Dµm6¤¯ªªÊÌ£yo6[*•z4ë¾²Yw¥D"iÑ©³N§S*•Þ ÚÒh4©Tê6d]P¥Öj§.X^ÃS£Ñ „0¦mÀÜ… çõz½Fc>ØS*•Òø.ý‡úK¥R:ÏfÀ¨ 5-ªªªBa8Ó·Y×€–=jÎËd²nC{+ ³ æ4M"‘X¸4îæÅQ*•3¯®©W¥Ñûvº®Ó‘~‰ {ô–ÉdíûöP©T‚×!|TÍšT*mß«›Z­Öj B;P%©xíê ³LO ™; Tˆ×>|tûp’$ EÏ¡½är9IµéÒI£Ñ¨Õêïeøq„–ã–oùô·U¯á­ª·¦ôƒg,7v£†Ñ» ì2¤6)B£‹tìZ;Ï™ ü 'žM-lò)Wv2¡Kí‚Âzó#ÖÔ\¶ól´`m£Úœÿyëjãë”*ޱÛék7k¯o¾ÒŒÙq&.Ý:±½ÈÀ‹—ªÌ{0àã¥:³V÷¬E9eÝþ)µkÚ©•‹_¤d5î0·æ[‰kÝôöÖŽ„mÇ.o;vyMÁÑ«÷®ÅÇȵñž3Ñ@Y·nÓFùã-/dx<øFÏïP«äâ‹·lšFE7¿-G§~¸”øZÕ½û¸{—T*`ÔŒ5‡wPçÞM©µ! þÀXãÆæ&J)—S€!„èL6ÃB!“HIÀl®È®þ©ýEl-'À‘$¡S«õ!_§–k‹ÏÆå2AùéɬfIª 1Ù\—}6âµò¢sWî¸v›.`Ê^¨ôr%—Ï£!R©kõ$ÉòÙJ¹T§Gt&ƒ"IOÈÀI¹LN"ŒÅár9, @¯ªº~ãþ½ J"Á ï`KÄg“£s×îÎöŠÒªU*µ£Ñy!éår‰€ÅåóØL õªØ{QU:{‡OS«Y×i~tË’5®JÌdïÒÞæë4 •€ÏE¤^©Pè ŠÎâ ø•\F"@Ñl—…(B&•’c°¸LRúüÅS%¦P²8& GÉŠ¶Ù™°úÍ“èûŽëÍÅ9‰mÆ/mj‹9¯ÎQ!Ç‚,**Š\·’$I ÃÜÝI‚B”¶öÆ Fc† œ[ýв]6¿T›•gè€Ë¡µf.`ÍÜaæÿ}þÿÀ××÷àÁ4ŽfÉÐ?ÀÒÂbÚ´n5ç)Šª¬¬ü ‚ ~s²æðsb ñÙµZ­!^ƒJ¥R©TŸÓ×ʪM¦>!ø<È;B¨†Õ C’dí 5¥RéoNèu:]Mu5bÿ.ÿÏE2\ú£Þ¨9”H û9ì©›‹Çdó4J¥$I~mÇþkñçó†1ÌÝ~Bù8°„–,á—yþŪ¿9¾ÒûcþuÚ¿½é[» Ø"ÛÙ«¶zž^ûð/V ¹é3þŒ¿!ªÈÔÎ/èƒå2¥vp©c,p¨>ä8oÝ·÷óVüp ²ã£'ÍœŸIT•¥SšÂõ[‰\Õ¨´çqR$ìÙÑŸ,÷zÄ Þ÷n=^ïM'î›°nCeÎÚn>¼-bDÏø”BKG73£íð•m-s§,ÜŒ19õuZµi©- Š2_½ÉÏ]Ù5vûÔópcóÇ#)öÄü±CŽßϼšXØØÊó“Î]ð 1ÛÁÅcòúÃFoŸµž'2ª×°ëÖËMd✛·žO¾°ö·=„±Ù\ŸÍ0Œ*¼wëd÷Îy¹e{¾4~suâÌ•r=²pðZº-ryßî2–±ˆŽŒ<÷XWtÿhØà¶ÁaC#ç¶Ðj¤F ¸ãáÔ}Ñ3¼i®;Ðv=Ò/¸‰ÇíÙ›. {˜ÑÔöçs5htšž ß¼ÍQ*”Žyxx’$…a€ ¼¼÷jÆÒÒ²V‰?|ö¿83|ïþKž¿ôèùE²ïîþ}§˜<NÓkU*½þ?•~ñ«ž£ùC÷µ†ê_טob¹òçgþ¡ÁÊßàãì×qŒ_u ç4ëÒÿŸKò}ܵQÇèXQ›ö‡DZ zw_énEÍ7òE^éÖù[¶f¾ïùÁÏ­\¡¢>¤ª ù^žÁàdedÆsŒ}0yã­ =|²qϱÚûìÛ¶íæ­L÷nŸ¤Ž&´Š¸\4¨T¨½ÜO=[tnéē݂‹v·9bèã-çÌß5aÌøõ!®³>»vvÀ€s“ôŠ“—wðàlÝ~ìÑ3qŸFæñw/{õp±Yãö>f—TÄò½½ï¬ `ëÙ«ó"q‚ ?½{]Ë­“–v TåYS–ÝÛr3;Ì6{H—×ó'…9&Åœ»v!l€Ï†“F[;.Þºã°ãG#+nÌi>|`#š}`Ø´IýÏl^tîôƒ :MÜÒÛæŽò$«äð’m«Šû[ŠS#‹}û£Çûœ»=#Ì[^òöaŠxðú_  ïc@ãÅ¿ös—§¤Ux˜ÀO€¹™ùða#‹ŠŠ˜L6 Óîöá?‡Íæ.X°¨†þÏŸÜû¹þ©-ý{€ªYû‰ï_Qý'þž*ö#ÁÐfiN‘FSÁâ›3äy :Žt2 jE¨^SüpX½³@!DLË7€w³ÒŠb¾©eïßhêÂHWøÒ²Œ˜¸¬µË›…ÀØØ $: F i…–®`ãj$?[Žs¼Å%“Í—‹Ë0­”ïPÏ·^=Žˆ\±ÎÞÅPé¡q«^l‚D¨+zµ^ëÕZP•WVìJtz½­Ku %gñD:ÎNF²*lÝ=>æå{ §1MM7ê*ß0„î¶M¹ æÐkÍmŒ7Έ5g -c00¤“” °f…"…| @®DJÇèðêÑc®M½–NžÁNžÁž ʘÿs™êt ëôEšÿxw)ÔºkÏŠTJùÿZŸø¿F§‹¿—`ëÿh¤e®füÎç«•*R+Ù³e­Î´åÔ‘à'&¢dËšeóVïq-32ÌõÈÐ˨[odlßÉÿâ÷")Þ¥s«ˆËƒƒþ’ù°¡Ð»·/3ÊÚwïÈ®q¡§3ûýb7fßôÖŽć"*û¶ìÐ÷ìóÞ6sæç'Íš¾xø–ó-ìIN\K2š?¾#ðƒ¤¶@”J Iê@O"L¯%€Ž(D1ùƒ[ØmØ¿}D£üük[+›C£¡²| œp.OHGŠr€G/r lœªne<…¶­ÎÇetî/ʽ/–§‡‡¬R¬’èùÆŒZµRq—ö7ãöáXOè@«'p:ËÁµî»ë·dÐìàñ¤ƒ#´ŠW$)¢H!dnënlÌ.ǻԳ,_¦‘onlfy úЊ$(Q €(•B©×ëÕj%€‘W]׋ï§JFº²åe2ºƒ 'þY\ ©ã“„âðQžÅ·)­Zµa!R§<±#œÛîÞX6EèÄïÁÍ©y‘›‡æÄ]zÞhÈOO-•››3‡ônÃ…”Nzþį›wŸ41wé7lbÿ‘ãZµikΣƒëíãkÊg7Ýpæz÷¶~[ËÛ ž9£ŸÐ¾^÷Žõ'wzºhïâžÝB éÒ³U°_“†‚ë\Ÿ9³W¯Ã-zôö©cÕ&dþô¡Ã}eÔ«ßnÅÖõµ*U–'­Ú{!}7 „X\Ó&›€™{»®¥o`3‡É<=;\½et‹Kgr-Lm‚ƒ„,ŽiP³n7Þ³ïÆÌ¢ Ð^‹ ]´öÎú/!ÊÂÁóµæk¹LË€àÀÒœG}ûMæ[l^0êQ§é G ˜¦V o 4sXtèòìõóFô×rcùìí·Ãì |ðÙ97ÀÇWDEQaRm aéؾáã‰íú¯;3*¸¡§ÕóéÝÃxöÍ®j àñtæl_ŸyŽu‚—lÛÖ a  è/_s!·õêã·úvòÙ9µÍ€é«ç´oת ²rô÷6)θý&͵®UéOíá'þ0ŒÆ`I+߯X:ÙÖÖR­ÖPZ±å}~Š^ORTõ0¦Ó™"s;[@ý—L|~â?œR|™è/ûÝÂeK-]Q..1ÌZ7qy[ĺcÑÝ&ì˜=¸þ›øÃ^‡n:¸ÿÞá#]7œìX>vì RhëèàЪÿ¬–îÌ3súÇ7_¾©“›^™Õ¬A¯5wž…蓇LZ:jéš“ §è¬=¸Ò\±}ØÉ Ç…u·ðôÕV–püúnžÖ >}[š÷ã{öj¶+nH „M^aÁb4 Ž=¼#EB³ààç†Ëæ‡kÆqóI…{§1SÛ²Ç[Z,ÇÛöêÚkHc+œÐ–/5Üḇ)M„µ« T9¡û,ŠmJË´¢\ yy¹'ŽŸØ°i³a2ùê45Ý'-£ñy4¸\n°!S‡ïBOÓ¼%yØ»î9ñQ\ ÷ptåñ™,ÓÉÞN[ø0µÒüYÔöÂ;{çEU¼{²çô»¿ö^•týÒµ›P˃Pö“+¹”óì8gÑs$Ž™—û*%ôoîzrkìŸìÛ…ÒO7›rlé°C;WÏÙy-cîÄa€*Lº÷Vf´èS p¯ëèÂðXÓÉÞ–Çè´²ƒºÿÒá†ñ½*’¶Kq׃;·©žïh<{ëøcý† ž;kÌ("HÜ`Þê-SBFÍí4qT{_,qÿ®“íV÷}óÞ¾Cs&hi‹ ó§Ü}mmÏâG1a£þk$FÎ-9üãÜØgð€|þ68æ^½ûxÕæó ](þÎAÝÿϘÿ0(z_¹vÃÖöý§i$òÃÇ6<{xF)I’$)x“/ÇŒëxúÉ߉ówŸûuñ°ÿµ°ÿC ŠBBH¯'p “ÊdZ-Åæp0ÀÃÁ`³9:ÃiÔŸ9œèçPü6·P¡£ÓFùÿIÐY¬À@[g$½x—Z*Ç™›íçecχ҂Šç饅ÓÎ.¶P^¦àðÖê«´1ñé*:—F¤–ý IDAT£»y:y[Z¢}–”W¦:É·0õá/Ф-Ø 1U¨bžgiè\äkeÍ‚üܲÄ1Ig›XY4ñ€‚|õ:ïTÏ52ò«geÃÊR飤w“c!ôM¾ƒ¿ÚûÒV–UHµáÓ§ÓYP¥ÖàƒF£D¦“º! 00ÄéÄÇ Q$E#U;Pé).GªòÖý7OÔõ[ôY¥¤®âÈ‘;¿,Ü\sFhboȯT%.š9€¾¼ŠÇaaZ¹ÐÚ¤Z ƒÆ€Â’²ÊŠr€ÒK/_¸ß8|TÍ ¢H‚DL&p Bˆ¢( Ãpk3b.§$õ…Ü)%ßÂTˆ h Cô^‰ž Q8088¥=8E€U£§Ü{ ,wŸa®;ð«:ü'jð­ºî߯çß_éw‚¤|‹º!çî8jêŒÁ$¡Ñê´AÄ'çï9õPIÐ6}u¾¦EZ­Ãq Ñ ýói¿­…ÐFcЫm(’Äh4üû‰†>|ݽóX&—¢ÌØ»nݺ :]£ÕR¥T*÷ì>ll‚SQšE£á€!Ö 04™¤Èüüb•FŽc$I†á8nèLŠ¢ÐG¯ø‰$AäVp„B6Nê Œ‰»Ô±±$U7îe5öÐêeй"S{zjz¥JD½JR7 ¶Äõ: gsMLš»Â¹ó/¸.®¸ì‰NÄEf6vB¢¤X§Õ¼M.(K}ywò·?þ4߯›‘\Ÿß¬±¯‡Z“T¬i"ÈŽI³…íƒ]ŽßLvr´2#Õ×o¾©âêmõª€ÉþܯSª(ìé£Ç™¹E„:%·ÂŸÉdRÂél× _ÕîèÃ×â<-lc7_7Swoç7Ï凌112ªc~/ꦖS˜‘'Þh~«îgå%?xV$¶óh5ž»bä¥„ŽŽè}^¥ G;ßÚuf<¹Uóš~Üò (Š`˜¸Ö÷5;µéRŒMé¡GËî#lïüÑËò¨'!]€/¨$űÏÓƒ½ë’¹/ß”ªfýR“.%^Û6uóÃØ{çil#_W“˜èhJT–žSÆärYLaŸiýn˜7còý“Dòñž&¦}Övô£v Æ_}gþõÐT¦U¥ý­^Y4sÜøë™­¬ôòÙƒK×l{pÿažìŸ3þA`n" ´7‚ÉÃ(岑#FOž8uÒÄ)S'O—¿¸ÖM}jI¸kRì…I£>½´±²Y¼b_ZÊë””äÎnÄâ)¥ï3ž¬Zºxãæíq)†•æ%m]³bݺuG.=¡³Ì\ìml\àØÎ­7îÆ^¸pæÀ1OÓUêÊ{·®\±âTT…a@TìÛ²õìÙ“¼—„0ìß’aõ©¬¬üæè»woÝ»w31ñYEÅ{‹‰ã8†a…Ê+*ЇI !D!$SÃÛgÉ=c˜¬ ã®;™:º»…P.Ñè ÂÚÎ{_µ~ûµ-'ã*üºNFoã³Öì¸ñ\¬oèkO©•…~â?ŒÁäÐht0¼1 DaÎBÂ2³•f‘ Á¢39XõÀ¹X\ŠXmšú×w·Àh$ÁrI©B¦!é “ŵ°³mâʾù8΀!œN£3)ÀNÃ)•cpè4–š"ŠŠÅ"k«æÁž>ö\Z…S$ª“ßFÙÿº•*–ÐuÙ†_kŸéÜÓ Lºæ'kïV«7´úx³™°pEõocŸe>/Ù7Z[+ÿÏ=tã¶)ÿÐ-„Fgu0Rèô± ïÚ{+ÔØyoܾ³æ*×½õ޽µlYfmÃǵ (AmýºÔ’³ê¹(¨FÒà%+‚k®™ôd«÷õÚoÛÝÞpI©Òû6è4e¼!­ŽZG`ÍzŒÕ§!€®jԬŮžß ÖÅ¿TAb솭[ô‚V€ô«®Râ ¥Bîٴ瘾­´’¬ñ£§‰5ôF]‡ÍÙõðÙ‹§,4 Øìä{GϾ\1ëéÙ w5Á³»:Θ6¯HŽYp…–®® È›:uA^¹ªy·“‡u¦¶ôRž›š^ª±n’ˆõ”È¥+¾y[±jßNgòýºåK¼Êª×´×Ê…ƒ6-˜ú(¹Ä§A`Ynqß‘Œó¦ÏZY¡ÃÛ‡OÝ« ¿{™’Ý{ÚÊÇ-:álgÃæ0˜¶MÖÏ ß¼lyŠÄ´‡™$êÔƒgo[»Ž]þ«‡þÕìù«Ë”TçásGv ²*.6.lì6³O;HRò¢gÏiMZ7“ª˜“W,³Uç¯]²2KëÄãêŠ3vž¼gW'pÌìq‡¶¦ˆÙ<®W“nÃ~ š2lÖ²ƒ;ˆŒ'¯>2vÄ…Ó^¿—ë”JÏî“V k €ÒFi-‚§‡:ËË2³ân1~L/%Yýá§Sl_>=“ršV×ÕÖ֩ާµÿò‘mÑqïú êyaOd9n5©{Ö‚ù“v_ìì¦Ù±w‡ÐnCgÁ¿a5 !¤Õé) é =›Íú<É´IQ$©Õéu:0D'tPX©fñÍdM™\Ãä›ÊU‘$IÁJR^l#®ÐD‡pJGI¦À´²R£·ÃIŽÔé &,?ñŸ$…H‚ÒéÐè´$…c¸N¯Ç0šN«ÖèÁÕâcŠÓH§#¤t:B‡ë ™,*æ­ƒ¹€ªÔxXšÊ´zG!«J"•©ôÀE"K“†‚W1IY2ŠÇÁtà:=ã4­F§+À •+ ezRi&`Wê(¡%(µz`‘‰0’ú&<_7¸ÿ?jµäcðìÝ}DŒêÚkËðUò€µk€Å§þwO¶ ¶Ã·üZ“û‹5hê’j ˜ÆîžukbýÍ{ò·÷o2gb§Wq·(Õ“¸8Z`÷Èc^D}%C¦MÀ‚‡_¹r±o‡$@e~¡JU®U^™ÿòE2Tf'3ø— Ó_\Ý{=áMRB¢€•$½~£gñæEnÞ1… ÈÑÃuÙ™‹»ŒŸÙoY&6}ÆÌ–%½*øËY›~xh”RV®PÉ ‘jÍÍÍÑ›ý;JŽ\(;–Ó©ú£²r•–IÊŸ_ºpûökW.GßËÍȬ°1ççe¤HiVaÍ\”:Måâicòl»l^:ÙÍÉÉÚÊ¡e—®®6‰/SzLÞÖ¹]ØÔ! îÙ ñ ˜8°]ƒ]ë[1ã’ÿÿõŸ‰Bÿ'ÃÀ¢IP$eЯ(„Ãh††SB8N’$Bˆ$ÈÊJ©©•…€A#ÂB¦Ñ[[˜Ð²uÉÊÊÕ*Šúù÷ü«öpDQ”N­)‘ë­L8X[sªJÄ”ÐÈL©*k§‘TõlŠ(Š") Ã1ÀS³Ë8V–Òü2 ÐMØLI•TCa\‘Qó 㜄ÔWù•L¾‘N¥)RuíÌp,L˜åïËeiV×ÊX`Âç(4¥ 5“Å•HÕ©y•¶îÆyiï”'EQ߯eûßîŽö­T“oªâ0kxÖþäú¡µ¨ÏA357ò„€aà`ëÖ­sC&³Äˆ^^ðöú“’ó{€‹ öªÌ(˜ †‰±98,¦–YQYÒi)‡&ÙÎ37ýQQÊ³Ì¸Ôøk&\¡U¨½õoªTIŠ®^¹ÓmãmÐé)ÏÆA0k`w1.¯Òôo›q&³Çú6š~rÕ¢žv®qîMüÛbš‘åÅ™ƒ»úaâÂåûj†ÀÆÒ¢K S €{wï& ¬Bð­FåW+UˆÔ¼|òPÏumX½÷»d¤NþìáÝØGI!§´r5þ#²÷Y¯žfKÃ:„2¿ü>Ðß>uÔ¢y?[îŸÔ‹Hí«§±–s“`÷ÚdˆÔ=¿þê½×|¡ˆ¢ Q·1¡^Æ@h·.žsiÝßÃü7VÉ(íUÌ;©iûÐßšBÿÁoýÓÇL›ö ´ãi$9÷“ÔZx¡>2Ú®ïj–ú!BH.!€§"I’Å7kîa{=†¸€R¡bs84 0¤SpNgàH…ÅU4S…¹yZi.€ï‹Ì÷ö­|}›ŽšѨÖ^ZíÞËz%¦ÜÂ>˜ktZ¨)¾……EyR@ظܺ Ûë4¥RMè´*•ÚÄÄÂÞÙwâêùÎø”$ŸL.m÷@Z•BÍÖjÊàuZ­žÐž0;+óØÌwr€DN˜ðØY9…ÁV&™9å­<@òèn ù¨Mõ€D”>íåÊÈÕÛÝ–d½&çX¸€F£%HCÜÙÖâNæ[m0 dåR½NþôEf“¶¦¥¥U!í\¸t$`Èr¹ ¸wbËë2îÞ•­@¯®z÷Ì£Q7ãƒaƒFõ£³¿ÂÒŠÓ§N·0ëê [.ûuJ²D"¹u9ãÝ;¦ðrNŽävT¦M@É wY7ÎPQDB\ô†}ŽDYîÛ”·ýæ¨ëÃnh5aÑZ;¡† ×ÿÍõÒ¥ÎõüÃûÚ†³Û^ÞÑ.¯G‡^«wo ªgµ#Þ¡cûŽ­vX¥ÌöJ‰Íë¿f@±F+=¸jç3–8Ÿ4ׯëÚ7µ€€¢ ÐëuB!Ãh>Ók%L­VU;ú–ôÚë·^²9Œ{ö†Ng2XœÄ§É8N#r˘l.ƒÁ~ú(1öŽ›c\¸sZ§e0YJ/áõöljvföÆyÉéå—À^VÅcGvç …|SÇqsVøZAVJU»~=ÕmÓ£Z‚TÏÎΨӲ›jû”^}¯Ñôì5G&Æì]aáhf Ùø ýÅgj·îFÆ"¡•ÇÔÙSNo>òËÖhømèbqæ¼I3ž$æ”3ÂzNÒgÀÿcשׁXº~vo—îîFPD%D1E±»»ûY(b·Ø…ÝÝØñ •î .·ïÞÝýþ¸ˆ¨ïùÔÏW>~Ý=3;sfggΜsÿù¼þÝ{›˜õž0hŸÐ)ËGã±-ÚõjÁÙ:mKø¼Äú¼j¥d[üBjÓ‘«§÷B$§0cþÔ©¯3Æn¸PSütôè9O_/>£cq݇ô8å—½ú˜YØö1H‹K»¼qΫT»VZúj?1=¨©1Ȩf((OXjÜ{ÞüEVQ±Í¸ÏSóÆÏjóA¯¨Ï@PTßÚ+¬}ÔÉýëIWÝwåi¢ÑÛ²@C³Ó³=ý{˜ÙÞ¿tºN¡„vYÆrÅпÿÄ…&l€_7ž;vUMcéZxj¡`nmcVSBçéÆí‘½àZ6ýF8xìh­H¡Åç€ë¤ùHX €¸„@8R«$è-iîÅsiê‡þš4`èjT‹aÐ7* €\»yWWSj€™q¥µb’ÂäpˆCÌé€ÎR5Âår”åÏ<Yõa½–Îâ¯Û{BÍÔ ‘>m·ÄÇ#šíµË¶ÝI<]¦&hTX¶a«X$Aè³ö8ð¦Çŵq®‹¢>%> V®Åc‚@^^Éáɤ2’Ã~|x.nâIûH3|Š µÂò´Ì·-û'®žóê1›…ˆ ’$(Ñœ‚bõL¬¯]¾dª÷ÞRAyFNmº8}V’ndÏÞõæn!½,3XÖm¹Zy×yiiש³æ‡³°³Žã‰zÛ;0íÿq[´N:¶R™yÞp°á‹E›Á¤³u2)Ù8jäÏ„ÆàÀ×̺„JgpIMTÔÇ*í[=ª3Ù{>pßò¬üS—ÚZ{ìÙ—”ºuÜœ•§W†Ç¯g—”U€XB@•¸nK.WLM®ÆU*Á› c–Á0ìÕñ•³.¾)Ï;7sí…òêêÒ»ûtÏ|vî“]³ê¼{+¥?Î€è £',L>°ðJ™ÇÀ&’µ_´6‘$¿åç =;öì;aˆ“ÏŠ—yyæ.³±tÙ½7)}ÇĹ+O%ÍêJàÒ„IËÝFiÞßdå›kÏ䦮r/rÐ¥eôÍ1z§,›øìî“¡³Öºùšïz˜à+‹ºõž´» *„ ¸®˜_¨²«]¹°°g‡©û/ÅÇÖäß;”žœÞ@}þÔ•ˆ!CôC­J/_dupø)Ïd! °8|6°é gŽÈ‡™ÉãÉ†ÎæÕ/G2¸œw¨LNÃnHçðß 6ì–~ƒ?Ü ñ}éH}n†¿ÞFæÓ„ÉÕÔ‡åÐááÓõå ¨ÖÇ®>Шèût”Ó`ar5g;FÍ/Ý<÷Q% 0u¨umâûø~P •ËÓåýÃ*•Ç×Àq{cS£u@µ´(ˆÃC86÷YÜçSÍ4ò f&:CGŒ¹~ù0ƒ£3hü¹1îõ·²ŠåÃ'-ÌMÿÕØÌvõ‚šÄ?U™4¾÷ÕÇ×ÿì§|$AjœBå|Á>—$I¥RõÏ«|#ü`È?20üJ¾Ç£zIyjB*nmá.S(=ýÃ5’<.B’ÊúŒ &‹BA :&̵ \¾ž… µ2ã.0ø[wìVVO™¹äÓ‡² ·±Ló’#qÅõC OäŸ9½|w²ðÀã½oÝ©àyùøµtp€¢²JŠ (Tš¦zj â’”k)5«¶º¡–$m?Ø~Þ^Pbj¶¶V…„Ô3°ÆÅb:]òŠŠ¨eE<®I€Bqe±O ³›¿".98d€Úcð(V:£™Û§Áp~4Mú‹›ö‰Úÿóô?£œOŠâÅo úšòŠáØùsþÿÊ4RÅc#üü–“ëÎѸúË¿@áÿXÃJƒÞÄÇN¥”ÿÝiäǃ (‹Í&7[¿ie¾\öÜýý§ÿHÀÔ*Pb8…Î27Õ=,0¨ª¬ÕÓçr¸tI™À¡ÐxL´ªV-“‹JK…†¶)¢Ë[X"¨TüIË: IDATYz¸»*ƘúÓ‡Ô¥ÝJ­›¨Y]À3îLØsw÷Ë€Z+ßmȲoÑ¿­p× áx#Àñ«êª§F©4x~ㆎ{ˆÆ_{UΓ[¯ekã½4 Áq5ŽSpµŠ$ñOܪ28Æ6ú¬«¥ÐÑåÕæVV‚óOÚ_º’é2@xë¥`ø–ºœ$I1hæV÷ûÿRo#óO; ú…r¾þäÏà¥ÕTeêáqèí½´î&¿¼qã:NGQ,--ûÆôû»«ÖH#åezi©Eÿ9ß6©’W¾ ‹)­²u½r‚ƒÆA* •kÚÒù\k{;=Có™»nvÑGÃûtÒ®_Çk!q»vNØ1¼is_µ¶©¾{çÙ]Ÿ4 ´Ð¤é mó[ܰ²´äòøá£Ö®ÛÀ{'‰=¾qGÏÅ߇@`Âí›w=~™2´K7mS§Å«µ2CJÚpÔ=¯óZ­²Ð€ ·EE,í;¯²F­šè¬‰XϵäWÍ»hV”×Ï\unmÑànýЧ¬y=26îÒ­ÛóREMÑ–Uc6®Ù>ØqÙ~üºUÃ'9ÚËÒÒÒÙ3äìyßG§¶òí}ü4†4G¤á¿ á»K]o¿{÷4|±ÉÕy×}‚8ú‡n>uÀþw%Õqcc8]WŽkcóåÒðä¤-I©òE“#b£\ºqêäâåÕ¤]WÒ%,Õý¸yUæ¬y›¢',t6øŠÂÉwO¯­Ûs~ÂÊ öÜo¨‰0çj—›’“Ï~þ’Ù{8 ÿPÓ/ƽù=¤Õ…3ÇMŒ\t°=«0ýüÁ»Ô™£"4Œÿ#=³‘¯Ac™~ñâSWWG¥R¥T(÷î=Ü7¦_mm­ZýÙ×m#ÿ( ŸÏoüÿ&D"Ñ)çÛ&UlïÇ© S’Î@ÀˆÍ‡`ÐÌ5ƒf®©¿K·Ê|Uw9)#rRý-‡‘KžüPNß¹[úÎýpYß”’ÊüJqû35—ºÁÚ¤ëB+Ì>r&íopóñ¯ «wìB øÛx–'&iÒ¥‚ü"-:¶Î Õ2hÀ† €£ÛZ ëß¿a9–-‡¤g}8x¸öð¥÷u¦¾-j×cü· …¸RÂ]¿€Är²Þ¨W«9º&V&z&}–® (ÚFö6áy…/fŒœ^£ ‚L\UR*vp²•¾« tí Y™25!©Ã2R_È0BÇÈÂÆÜðó ¶ÒÊwÇ/? ³Z]\^R’+WÕ¾Íupub‘ÊÜ·YUµrŽŽ©“½i~Ö+¡ãjk©äJgš,3ã­’DõM¬¬Lt@Q[~îòu× ‘º€?yö’Å £(‚0õ\íŒ+ÊÅ1‡¹;c‚Ò‚¢Ò**ƒcåèÊ!k³2ß( ÔÐÜÖÂH4.9.ŸÑµ³ÿØ‹¾Z)zñªÞ²üMêú¸…I·Ë+KÓ ei¿îÝŒ|Z:ë¢År­5ÛWeØ8zÙß+ŸˆY£ºÊ ‹qG)iNLšwüáÓk—6Oß- Ùc4hô2goŸ’·Õu9÷Ï$®Ú~‚¯§KÒu'.[ïcø±ÝId<{HrLÚø™(*2ó‹³6-›óÔ¥¡;/õÐÉ;ÅÐÖ¡ª¬fÂʸC Gf‰Õ4í&N&6Íúêï8rE‹¯EÓ±ž±l™#в_”)u÷OÞ<°ý¬sþžv®vÒøñÍ}'×m8õâÀWA–hé›§sç-­ÂVvÝFÍT>ܹ|Ç + `š.O\c…‚¨";ùAöœË›?Q ïF@PéË⊟?òŠ]0-ÊmÑŒ1ϲE#ÛjéÙ÷îê¿eõ/».¼©©Ê%Uåƒczsì›xš³ó„ô_VN<‘t'É›I{‘à°^a6Ccú(µÍ(˜:xØ‚AM$FÅM]¹äØŠEv1‹Â¸)+w?œ=ÈÌaS†í¸ÙMpEÙæg†{]{v-_G3ïØ·»îÉÑzuµpùò¥ÁÁ­Õj¼nÅ¥AóUJ•J%IAСC†ÿ]•ü‡€ ˆL&‹¥@‘¬ÄÝÝÑÑÑ(´´TP^^^/OâÒ+'O¿-¯æpyæŽÞÁ-˜†ÿ沉?¾vêU±2¸]ë×É‚b¢* Ãþº¶5òO‚ı×ÏnÝzÊ1rèÓEw/Ÿ{üò-Í&ÞÅßÕ˜P+Ÿ'_å8¶d^¿zWÇÂ3¦O}°2²ìíËSçn‡öå¬Ï"I²<ûñÓ V¨+gÿþS2œbdãÙ§[@V]xñìÅR1îÙ<¼uS(Ìxxúb2KߪMç.Ö: qeáõËWŠª¤–®Í£ÂýáG¿»¿ÙPý›þÿº~^Âiÿ¯ç P›v/Èì¸PñrtŽJØÑÞ@9côèÔwEkVX•šÖö½¼S׺ñ¸l{07ÐÒa˜<}òhàÂ#;ÛNîØL”÷lÿ¡c|›¦[:Ÿ?yæñ³\Ÿ6 Ÿ«VÖ&ß¹mßj‚€P¦tsðÿeEQ渳—½ýJ|#g¬žßûÅŽA£~Ù>)v «·Í¬…Ïî5#M%¶iÖ1ЖuâôíÔ ¡£¿nò哆~}-*ôþ —˜¦”2<¯ï<*˜’°IB]E% ôYò-š×ÙÍË@^•=áêÓ´±…ù#ºž<÷nR”ÝÓK˜®=5§Ë>ú`Л{…ÆÍšo÷47-ê­¹©kÏ»7Ñ$옟Ò~ (âaïºjoK˜=z@¡ÚfÞò-‚—o'/f P‘²#»’‘|KÝÛrÚ–iWvÇMÉ5+qz·¦®QiÙý»š¸ï×n. 2óÎË;J”Õbräì‰æO uå29ý3zLûmP ¥Y³€˜>±¿yW,®a2™I&îJüMÿ))™I-¥>>z††&T*Ã0‚ 1 +*Jõóý¤•ÀªÖ&¬uŒŒ ÒVìZ·´×uká ©©þ|UÊË~Y¼²ÍøLºdöÜ9†· ‘ÆIÕ–ª’´™ÓçûvPþàäürêúÉt —Ã&A±fÕ*Ô­›¿+ÈÅ¥k–Ç;É/-*,xµakRTŸNš£×Jqùá=§,Û½7rŒ³>©¸°71Í2*ØšWX"´q0?±ù—b–ãôö–G·¬=“RÙ%¢Ù®u+ÐÅ[ZZã“GO4 ée"|0iÙ¾ø —ŽmßáUÏØÞW&T1Ö neòc’üÓÃÔ4òDÓûD…åJe]5 •ÍCÄÅ ¸EµlÊk–ß_Öm,$ƒA£³x`gί¨­Ô·p ê71Ì]'ª[?íÓÕ•êòŒ›÷ó¶¯ð‚.Wªr„¤ÐŒ­m ZFle­¡ÓªDR)TQÔ2=ÇfƒFŒsÑQwí3’NR•ŸxäQâ«DÀqRMÊj$˜\-CÔ HË«E5 ;œ ôM¬ëªKé,-‰PŒ 9R™[½ùú’—kàs„Jc€T…¡jX<}S½!Jj‹*JIöhl-T JA0ȵâRkU­@ÇÌd€S¨4jii)ÝFãT a#ÊܼB=¨•6žï¿A³!ÎððÀGÛQþþ#(•*(//%œ I’ ’¨‡$IƒÖǘ"ÕWmŒè=ÙÌÜœÍævŽq1¶š1%2ȧÓÕ_ï³[uÄ›§WGO˜QPnjéžøþ@¾z|#¯‚;` ‡N•¤8¥P=`z'ž6,]ÙZ0^ Þ›4½ý„ËɯFm¿ÒÖä…ão^¸Ü"–ó"GvìÖ€´¦Ëã'äç¾ í¿¬/oêÓ±+— nµéǶô*ÊœÌÔÄí«·¾-#Wd§&/ZwëQ¦”@”òÚÌ_/ Ÿ°¨X ’âÈG{j¤êNR¼_ËÁÉ5þð=A#¿þ‹!ÔÒAFVL&TK0 …ªÆÔ@’IÒx¶³úxͳ³DPþäE¦’6O‹É€”[5è˜Ð á¯å‚KÉÙ w7÷ܧg +²ÝÊÐs ò³5L¾{C,—¿N}‘W®øè¡$vzǧޫLß'h¢¾`jœBc8{ä==ök^ÎÚ©}fÍ–U×à8Aj’$¬=üLinÜÁ0åË/J…Š+Û–Øt]î@I©ÈLÏV ós²k•ØÌ·,ûáÅgo²³3ókØ>ö—Îz•’ü4MÔº³ÓËãK´GhjG’j¥8~æ°„Ý—A€|ù:ùDzé‘#ý¢ÇŽ*•€Ä«Ê‹22óå²Ú €j1F¡PÔ$I$‚ËH›þòaQ7÷‰`Ô<¿ú¶øð±'M#£qYAÜ–S—R^ælœt1»&#ùÌ™iEÍËøÖm2Õóè¼PÛ«Ÿm]}H’ø/Ψ‚$H±X,‘J¥R‰D*‘H$R©D*“Èå2’$œ@Ðú!ˆ ›4mUQaavVÆž¸IÝ£Ú)¨¬¬ ”¹Ù¯SSSÞæ•¨ß\JQáš5ëÃFm(..yúðf°½&fç•"B`²¼œ<‰Lú.#-555ë]F")ɧ-ú%_JÄU%©/^¼zY)V RSYšõúuVFzzæ;‘D”™öòUÆ;õ_8Bêhó‹ŠÞ––¼+-ÉIyù$;;½¦¦ºªª²ªªª¦¦†F¥|b´NêÔ»É÷¯Ë.ª¶à¿ù^iÙ•#ËÏ­_úkq ‘è[xìO:óîÝ˶>ÍãæeÅI%õÙ‹^ìK8ø \P™4?bt×H5=|à@`Ÿ…™¯L„i9 IÞGyd~QÑk§Ûz›HjÀ)÷FþvDeïø¶A,¡(’Pm]´H?l²ñðì ç‹4òA•JUç•IRõvÃú}Vï4abJ¹ŠÓŸ#ænï¿RËÞÞŸ´îÉܽÓ@úaikèééJ«ÕůõìƒpŽ>,PA€·GêíÃéÛ÷ßV«? ªñÿóm+U¸²êÐÎmO3²ÙF¥Æ.NêÄÍÛ^äT– tóYvÊÌ­›ö¤¿•$î==z”3õƒAŒ¢æÍ╇O–‰CÙ…=¢&>hÙT·¡¹ ‘þðÚ½_³Y:æ¡£,xÈó·ò * žYDDó;—.+•Ê™ÒÒÁÓÃNëü©‹2’æØÖßÕ\Y›»ïàmk+ÝòjU«‰Âgn¿à꘶êÐÑFïKQùn\þôÎ…ƒÇŸ{&,ZÕ¡kî½£-´©@ᇄ…›kS¬'oöš6jò¸[þ‘ƒ}½ÆŽ1Ñâ¶ý¢DŽiÓ³cÊžñ-:v÷máëääaû¶[«;¦ Ÿ2Ê=¨KÂøv9÷‡õ]àme³sEìì#§týyC¦­`Òu¯¼w÷qfQFäˆ •öŸíü$I t½ºZHù~±Š’Ëá2L•JÕ0$…B>K¾¸ó¸#S¥$ÕE×o•íÝ=]—«CÜ<µw㮓,m] §ö™×ÕßA •îæ`‘òìÆuŽ“ƒ“…>;õüÞž³Nd¥Ý+I¿9tò†i³úM½4 uK-#Ïy‹Ç¿zô´ªºtçæ}{5ß>{vžXMCkÛ:1nüÑËæ®>Ú£wÔ“{OšEvUd<}ž=ÿÌ£îö¿íùÏ@¡PP(ABR©Y÷#¢P(ä£DµZvfãò¹îaެ÷³sן=sH¤R6WK[‡Ë«—IÔ€ðéàÚgZ•8!nöš„Y«¶4ÈAq ŽÙ£¹hÛJÛg+ƒ&Ìj É·`å¸×õ]ˆJ宨q²þ[Ï}È8w0ðë¡9–ÙqдŽïkÔ*¬ØÆDt°€Ù+7ÔÝ“æÎŒOÔeFÔ¨ù€3ðv±€­û4l¬}ó.»›wye0kÅ:Í/LT8qñšf ÕÂàõ¿ì}µÁÅ%höÔ9ïoÒ‚"ê Aiœ¶=†·íÑÀDÚ[£=~x·:A¦1ûשK×»ëñ3]5¿Zô©‹ubÓvYµ6‹™Ö Òž[Ù6µ†F((E, VV6EÅ…©˜À ‹mddD§Ñ333¬­>2×C€$ 5ã$BÑÒÕ51ejVñ•¢4éÕ£MòùC·.]ïê 4®ÑÑöî;qrïæâ’Ênã– è4¡ÃŽkßJ Ó2ƒ:ö r¢±tìlÜiÃè?vp¶4í—‰ýOׯÛâÀÅkXîý„Í'ËâÆ[™ÅŒX¼nÙÈw§& Ü#I>“ôzÏÔEG/uŸÝå¯ñªT*¸\ô÷ÖÅ>Og0øKÎ_lÃ(y¦mj`1eæL`D­RöV¡R© BGä“&X;9Uî½ /RhÚfæL>ŸËÊÍ|¡nÕæúÕ§jZþ”eqEÙ)qKãŽ*Ù¾ó«äòÿìçÁOƒ¹“Ÿ¼ðH @å½ÛºN­5¡F^]?ζk£E«ßݯf¹zþV^ÿŽóf4+Ê~‹RP [Qmݪ) R–.ZáÔqÂŒho`è˜YkSï>|ØÝùuf®[ßñcŒR³6 €Ÿþ‚Ô±v u>£ëpX:,Â>|êoé7Nª¨¼Qö~^RÄÝÇàGöêeèUéæêkÈ:âWloÔNÑ=¼ÙÇ;7€j”Æ´tô S<¾{áG¯y^{pß3,¼6?ï•€ ÖæV¶‡DE4¬6ÃÁÆ)zþòPcyÆŽÕ…3÷ŸðäBÙ¸˜ä»ù®!ZM=BGDiBÌÊ脬B >áô¿t º‘ÿâïpÇÅ1nÒÚêŸûÝOG86Á­>”óÝÐø6->*§aº–­NžëøyúŸ‚ MýþÊ'þcQ«q±DšþúuaQ¾—‡—•…‡ËET*•æ?|øpêäé•U| (½yXáÑ]àÞ™M*•ÆüHÀøÚz-£G· ±iÕš§Å{ŸƒÂ2v9Ý ®\²rͲèðýËf‡ì?,ÊÙ§í8®óèV“ý‹ÆÞyQšp𸾠«  ª(×+xXŸv!••îуç20œjd`J:Ûʶ `(Aþ…^¢¾¼Ïøy_"H²,OÎ|¡PÈwˆX44ÇÍÊZÏ@ßÒ5tãÖ8:J’Ø'y¤ra+>“ÎѱÚ}*1¶Ù,o{Ccó [.²Ú…‡Lœ3ûlâ<{k=wKaÉ£îÝÇȪ£Ï°è¾ŸûUiä߈©KxïЛVVf–“¯®ìÞw»Óâ+@’ê'—n¹ÆŽ’ÄŽÆÍ¹|» ƒðˆ~«7ojÒ!¶ ¹÷SZëÇV—VÖ´ö3€'ÖnK:ç™[}hýŒ~+×Nm;uj¿®CFï›/í}DaÖ0ö°H»~ëã¿_¡À«Q:›Ë¨‹Gà8“£Ã§Èªk9l®¤†n ‚ƒÉáj××€¹ãú‹¬ÇFD4u°pÓ¸.ÐÈÏ¿È:Êcräãþßúü—À ¼ªªª™ó¦>M1L­R*E¢Z’$Q53µ°°° ‹ÈÍË-¯Ôg!‹k4¿•ª'Ô8@ós·»ÿj§V£$‚rA•ÒÎF0™ðþÝ*–¥“QnN¾­[†‰2¹ââyfSQþF­c3iÊѬµ…UÖ<BQùFöÁݹK¦_É“h#i/_{û»’$‰© PájL*5Ž¢Ý7¡ZQ(l 壘ÊQ(å4ê‡Ø—T–å¡7p€êêjÀHN‡ÑKóF/ÕÜ•Éå¡Ý&¶éI«‹i\Ç+ÏÈJŒb•RXZ_†ã^kL~ïèO$ª±p>z¢N}+¦‘>>º¦F·>¯—¯i´Rÿ9 é ^°}ð‚ ºþæów5¿ uÍ«–W/<êÑþä·œÊü¬´‚êÀVÌKP+ªý ã69´gÅžSÉb×%Àìƒè»jnÒ9¦#ÇŽ67¢Põb§LÒ>~I D§ÅO4¶ÓÒ¥ 5’(…f E{“[hÔ­CØ_4*ýg!”UYYù(†Ê¢7i·’NXwhÛôw?¿ÕûÖ/çñÖLª¾®¬>´s[…Q¿+6uê4ͤê; ÔÒ‹Ç÷>®à‡´ ü䩖嘹7ÿÍŒŸ ؃«Gß(ÜòeIÁ» £,Ÿ IDAT‡+×l é ö÷Õðô×®¼,›>wÊú"ñÚø93{ì;ü~Ï W‰ŽíÙ‘Ïñ òÚºfuD¥Å2nÑ¥G+'ƒfÐßïƒ$Ia•0?¿P¥RU kè ºT*£R)†)•A ¥’ÍbQiukáBµóik÷þ\„½w{o€ÑsÇÀ”Ŧ|T>J× í16´ÇØúDyõÛ•KWf Ì·uÒÓ&Ý.ßèö!M¯ÿÌ•u–„Lÿ#§ÏÖßqöoç ÆžÑƒ=Lš÷úþA?L)¿Cd§È/_8R~ÛÕÅ¥©ŸŸ¿ì½©8I’8þÑ©ßLÑü¨Oÿ\€xo‚ ¨s³¶[ƒ;k.Åbq£/«Ÿ’/»œüš‰Ä×'~YæÏþ·ú‹üT‘$‰ z]ûm˜ÒP€fÚdؘ&õ—V.M>d¤ëºûê~¸¤Föð!'˶uÈû[^D¿‘ þ~š÷Ä? âíƒ Óæ/k…Ì Tå†L’R¨µ¢ZŸˆA³GDÉ…é#b*äÞoJÂŒþs~™)Ÿ¼@ãèõ“³ëw$oÛsǬsòÀ•±NѽÔ^–l;[+¼*½gÌèwe¢ý§-žÒ—öÙ_°ü]jN¥rÔüXùP.©>¼_ÊëÊ]×.;«‹&sñþ+ßv7Nš9¸÷µÇ9!AÅYù#ã¶ÑÏîÑwb•‚ì1jÁ¬¡QYÞËôÜnó’r¯­ ³ÕÑֆš³¬Û\=zêÐÁ/*y¾=&¯:¸euüކNswœöÂô8¹\Œõºfzl0(¥Â»·ïEŒ9ÌûXAòÚ‚=ú–* ¶ª*túŽEm™»¬S­<ò «¤¹WWP̆öi¿zÚ~ØhÁÍÃY¥•R©Ä=¤ß‚q=ÂŒ®c j”mb&ÆÍraÃŒé I¶ž‹äê6x…É{™ž6à—m]Ì„SÆŒ»ü Í=¨ÛÎË%é·†SZ« í2rþôˆ#Æn9}áE¿¾áí"'îͨÌ{]"´zàhΉŸ²±Ý¸5>N_âðßN761!H`²9 Äp Õš_Ai :“ÃÁqÂóî»ë;Úþy&×`аqºÜï-óoÁÔÔÌÎÎþË22™L¡P|Yæ»!IR&“ÉÏ÷5ò³ð×9ÿüÂ(óåèë§™ÿ–ìߨ}`ÇSI”æ‘»UYQa‹eÇ'6§ì?îA—6 œwaI¤¹Æ•º°¸L¡¨Û¢UË…‚ŠÀk Äâ3I{ÝÚO89¹gˆ«KmÉ‚Í mF,=ÙÍΘ![NxïîÞp…ÄÉ7¯ëØyð ¦ –ÇÕYºý@溉¿$™îSQ‰:¾ywfãÈöý¦°ÂÜ}¢b{Ì[š|éÔ’±#g\@C’¢}XcGNOròîÝÒìåÃKbª]|ý•oï±öyeL—›G®Kº>i×´± U•Jà1îŸ=tüFæÓŒ,@iá» ºÍÝ52Â=º]ëýÎgcýø¹©—ÒŠX+ü9ðq÷có­Ž^»GV>2fClß -<½°yÊÞ7k™“²U–¦ jš¸¢Ümà²iaF£bß.Ž83z¨ç„ƒ—z:áàŠâ¸õÇçæùˆõˆÇt’ž‹gL›/‘jy§g??lø”­1ö•ÞÝÆžWwlpóžcüªöÏl^§=ÕƒÛWY¦þMuAUûæî“üÅÛ¶+Š„çö°ø™VªŒŒJŠ~@´ùo¡ñ-¾–ö×£©¤D"‘H$(ÜÈ¿‘Où2A Ÿ™~Õùf4o ‰HJ¥Ôõ#ËæîF /ÐÓfÊ„…oJˆ½‘æ@£h–ú5½•J¥ò8ZÀ¤S©TŠÇì›´€¾!N¹)BQYöéC…Ï.p-ýZø»}ò\‰°àÚõ'±‰K@“ö6^5&å3©RZáà× zvwÞ2çaŸ|m :êê|1‡sýàÖ—§ÀÔ%¨‰«~`Ï鎨®•‹'ÖbèëÑG.ÇÕ˜ÆÝvEeU“5múl Nã5qw€6-l 2òÁÏsçº=–ßùM%U—¼^Áúu—JLb¹’Bg›T{|ÐÝO=àŠÅâÛwneff" èG&ØŠêéêvèÐÉ@ßàçVÂWÞ¶‚¨1ÆåòÞ¾É5f”ƽ殎ŽNŸ>}ìw iäk@ˆOqÀ¢ó.^R)¨àkkÓhôÆ¡ç¿Ì†uk†©+4;ôÏîCgþáá³C}ß+z¾o|\Ê©c ðu'iQÎå“Ož:½]Süç¾y¾iñ½ÑÓí9¸¹Þ9áÇUúþ’ÿ *Ò˜Ÿ|êÄzø¹¦S—/]¤ÓéamÂ+‚_ûgÒÆ¸|á¼›‡»®®žRYOíXÒ‘^}ú®Nˆ·±5¨¬,'DÔÚÚF"‘‰‚húð»wùæv±ýú———xey¹L¥¦P(L_W‡ÂŸçõ€UUŠ$r •ŠRhläß]Ù¿Ÿ²²*  E¤ºZ@AQTã A²² w7Ÿ^¦”¥}{ö”Óy–&jšÞŒå ¾Ö<¹DRÿ]ñUGÜЯ$0á©ãn½|Ó<°¹J©2wò:f”ƒK©øµ†TKÎ%3pjâ+‹ÿ?M4ò'"®Ê™;0}åV)Æ~Zžk “U—§÷ìÜ“¢kJµi@Ìѵ««—v.?'v\m;p“§¥R¡ó^rf_Ú·mîê}Ú††V^³W¬räÕ.Ÿ7þÜã}ƒæÑ“–ŒíråìÎy+vh™²8úSÖìjiÍ“ŠÅ?p`lœT5òÍÔù‘ük_Ï’¯‘¿ÌTù NY~àSþ្ S€B©d³Øø{ÔjµZ©0•\.×ÓÓÿcH$þìî™wÕ†½»¶„oWfÆ£³›OóÌ]cÇŒw5`|G  l6·Y3;µ€æÍšjiq¤2±fðËW’nü!(ÒÔÉ-æð­(ãš1½co]?õ¨ºZKŸòŒÝ-ùN_PRØm¢Â‚š²ê«Çv?xUbçêP-¤M›;ôöñC/Rs¸Ú< oÿÜ»—Jk•†ÖÞÃGõ%« Î;+SÉò Ë­Üì (7¯Ý2r :0ÄÅΡݼ½cÂÝd‚ÌÉ#&%:·ttÄÖø¸*©oé1`HeiúÙÓ'X/ß +kZ˜Þ¾G¬FÌ]Z Ü]-þæ>L# Ú¶ÇÏÞ¿n«”Å ”Ü96ªod»æCëÿHâªlaÚ-Ðê2>²§ŽmgkvîUâ|±œþòáFÆ^çn‰ eUÊ[ÙÀ¹Clɧ1 €++z.X¼¡[Ò˜ý‡¯ŒÙruXKóEãbO¸2¥'ïÈ¥Ô´ò~ÅM°¹£†´}öäAç±› »¶ièòÙKÂŽ®ýгÏQ‡i¤‘Ÿ 4G£ ‡÷®& ‚ÀqÃ0L©0%Žã¿çî²’ij^ܹó ½A IþQ°< ¸¢pÄð鎑CüŒ¥3¦.þÿÚóçÒ2¨™—§»›µ‡»uYYÁ‹ÏÒRS22^ef¾.(ȳ¶²üÄIAey¹…i•Õb>¹g÷NŠGÛ@Oãíë·ðÝ‚:{žÙ³ùIV•²êy¶S‘CGrEY{vmajë¤>¹ó$«$¸sOW[ËfAaÝ»uRd^M¼‘Åç¡×.œy”¯ŠîÑáî±­çŸäwïßû×s»®¾•ò8ìÊ’B¡D*T0»‡{å§§#L^Pp»îÝ;£Ev\xnicãéìèÛ"¬U«f78,¢[·ŽåzXÈ×Öþî%áF~8…AR)UB­ù˜A©,®¸¶V$­}/Bæ<¿¡Û¬èL¡ R&—jnh›xéäêbeíÛgÝ„g@XðZÊ5t3×!iÚj©´²4óü½ÂØáxõ;œÁw¶6RjeiZ[Q$,J1°oÍQøz¤mD¸‡¥VfzzYúmë‰=ƒ{õµ·ñÀ××3ŽˆŠiîëÉçíݱaùòøGii/S+¹:| 3›ÞCÇ„„´ òôôiÙÞG7-%Åd Â0™RE¥Ð•ÒÊw,[¶òî³_Ÿ¿(Ñ3³442µvpótµä¤íÙ¾qEÜš_3³^eÔ°µþ ¥mÆÜ¿¶ñê–93'O;¿wUî«GOÓr˜|£æ¡¡TÅ“{·óËkYZÆíÛ·Öá2´tõP€ä+—LÝ›ˆòŸ-]³¾½Y ÃÏÎÖÎBQ”~ùÖ3®ž‰W@ +="ißicks%†z47æÑÈ¢´kwsÈË»C²Ÿ_MX³9½”ˆ§ö;ÙSûkZ~~H•‚R(TÍÌ€ Ù …N§«Tªz³7“%íÝdÒlxŵA ¾[±ã貓é=šòçô J¥¤¹žÁ¢}@Þ²çÙ‚"7€a½¢öœ÷Ù‰§«¥·óÄ~Å)†EŒR)‡âä~‹µÒÖ׳k €Ñtéh €Ùߣˆ?F&•°ÙÔßÛ)û|¢ÎdéîÏL騟qNaJœD¨$ž_$@Ér‘Xæ k ÏÕU‹D$äåË¥à˜²V*¿~fùwòÚ‘·ú.W… ‚Rq…¸J¨VáÀÀÕ5ÒZ Ç©4úûù)]½ÿÚ±ù ìÛÛ+ØA÷î/x´c̬{bœ$T*L*‹ªWϳ Ÿ|jF—ãó»©6~cü»”¾ÈioÖñg¶q¢â4š»9€ÔØL.ÈÍxWZmãÊ(®*Ø‘—KÚÛ>Ã0*W_Ÿ‰d¼É 25Î/,1nÞÅØ'Io‚·*†ŽP†óúg iyì5E.@Íõ’Ú8©jä{PŠ«~½!áðš.ùÏ̘<É*¤[+KÚc——î\›’·êLFT§ðÄÀ¿‰ƒ4/G&WhÂÔ¼I¹6wÑ™»÷/ÜOJØ% [ÉŠ_{08"ôqFE6zùÀ¦w^ÙÛZ\½vK1=¡µã£Y‰§?{ˆð,Z9sdeµÅe¹îÝ»³ÿ@ÓªŸñ›y –7o×.e÷žÂ©ó$®+•"²ûÔr/ÁýôbKcík·Œœ»ÊÛ ²ž ˆ±áŽ÷¶ œ¾æEKwc‚£_\ƹ¶+?ýÑÒ½÷ ¼Z·°¢ä¥Ý[¼x¡“•¹™®}åÝË›ÞÉjÚÄñä©Kswìtã@uiú½'ù+—¶Vem“ËDÔÊkùÉ“y‚w³f,ri,)¼v=C¼¼Ÿí/‹–z…GésXûÖt*Ôâ¤Äø‹Jf5 ”k±™µ‚²‚23ö܉SfîºÄÃëÏÞ.™Ö¢÷ÀþË÷W¼¸~å•`õĤZ|öøÕa³h:&Íš8äò›†µ7Óø§ZíüÕ N/..ºté…BÑ„ûÕ¬_! (Âãiøû×»®%Õ¡R¸´Œ¬Ë.«æê˜ñ ´]³k¥µ¤™‘½@Õ«d]Kw+iWkE­€ïlçÏPIT-B‡@¥ šÏc³µôqi.`R¨Þß¡†¯A‘zï$Ÿóy"H¢èMn5ª- Õ I$€¶¥W˜çÓ ¦ó)„®cPP€‘tD §M˜B­É$¨Út ’$®®nÏ=Š[-z~!…Ñ,©H$I$A’ „Zzeï:áË+ÕåÅt}÷a1ée¨èQ\B|ÚÕG`í‹"; §wIËÂü‚š»|%nUƃ›YZQFªÒùV’Ê]«—œ¹ýH§DI‘cc…§\9¾çÐþ‚ª² ‹7ôlÁÊzjÛq(@­Vþzùèî“§ * âãvõïéÖ¢»õ¦YCÇM&…¹¡æý½ó‹âxø»å*w”;8zï½HP± * öÞ{o‰F4ö^°÷^°7D°÷. ¢(J½Þw÷÷Ç!¢‰1ßÄ$&¿û<<<;;³ïÌÎÎî¼7óÎ;všªŠZ‘o IQ·2¶Í^½«Ï¸Ù3~øÑÖ·õÐäЉço˜“Æ” ÙvïØå{Å;OHécŠË›ŸepïúÙ-Û(ÒjX?ìíH|ëM»õJ•žÿcß ÓûŸ¾³ñvrOš™ÚÌDöjÄèü—E«7™ÿ(§~Gbmƒ\6ËÁÎ,xCÌôéÓ[ÇoÔÆV~ë´ðõ­ôcGKÁƉ"ªß•¾xUÙÔÙ¦a¾¥èÊÅ‹îÍS™5Jµ·{èÄSLގ̺Ò$ämPÛI©Sò÷\¼eÊÀ.n~v3ç<ž7/¡[ûÉOÕb#‡@kB]ö¦¨¨Lhaxñäa›˜foh´S7õ´xüžá›µù€ ËØI9%(<º}ÓØ!|Ù‚@Uý|Çíü1ËÆØÃü1]Ïž}îäqãø^ø`'€JqÝæe*ŠD)âõ­Ç²®ø¶l „äîÅŒ‘¾4³ñNiÆPkä‡×§íí‹ €GÇ¡ö Ò@XpôʳšgéK@ú ©ßÊâ÷ÑþqS{u†V¬ð.;`lR훜Ç%ŠÑÓ‹kÖºs'­#Ò*ÐYI|KÀ1 tãÑãG|S>Žc*•&33ÃÔ” Xrrײò²›7n6nRïë£3l/\I‡6C€T“L#ž«ðåë§ì ·5þÍ’¢48uGç#*fæ€âL>ß 0:ÃÈÈg;íZÕ7À’ïà´îbæ?X¿ ŽãÕÕ5FƶñQ%ÿè°€¢jkE\®àcÓB˜Î.*•ªÎfèèÁ•HDE)qN£ˆ(AT*•L(ÖjÄs¦OyZ©X»Mߘ&‰ M-Q*•fàêuænj™L+°µÕh4 ˜˜ÛS¥R© ¬Ü P®}c IR¡K¤*a8ùÔ]KQ„T*£±Mý‚í(ŠR(n¾º(‚PÊårýñý€38Á-užx)Š …&vAºX•R>gó.5N …‚Zx6r ªs…(“Éär•…kœ7( §€¸Qd ¥Z­æÛÙz°u)µZ­D"Q¡¸‹¨;†©Õj‰PB’¤ŠÁ‹j¯¥V« vM¼@­V×Öʾy#ù'•*Š-:xfÚ!!E}!Jü¶_çÖO ·ÜËÇ~õw¸VQÜ©e·äÔZØ#RúêFŸþ?ý¸åLŒ;ë÷Û—è<²üOö(ZE±½»c‹©9'S9¬ˆÐ`.¡~?yàPVİÅcZýR ŠÒš5o9w5ýT¡–ÍÝaKÖûÜ’‚ïØš"Ôe…E׳ïÕÖ–=x[e†(ét¡Õê¾s8Ç)uXôÄ!3'ôm%UҚņpMx†lôüŽ“œñ–6N òÌÉÛ÷.]Éh넯JßsÃ,øè­WÝ&Fûªsv¬^¤l]ù®Ê;´¹›óc¦”ò@Úš°Q› >œ!-h ž¡M—ïÛ‘žIÛS0tÎ.éýÝ¦ÖÆIPenïèÊZ¶pC׿·å¨è›ë—„ŽÜa@R$©sÊC€àá™ì7%Å÷nd›2¢cãbÎÍݲñàG>ÝÜÖ)¦‘Óñ=«äÁ6yoñ± /mšàÞ5ÕñCy„Ï/ܺ|ôðí¶CxÆbÈ‚á»Î;[RU5¤mÇ«³§¯8øGŒh'”€ÆrŒì6$i[hð®€S½B-=,¯îLláìitᾓ&Å·¬¼Â\íÍž½º›vê:vóJH§~êìk÷ÚÌJÿP'Ÿ¸¿úý¿ŽÏ+,|- Û´jGQTÓ蘦Ñ1Õµ•‰¸¦FHäÍ[7Z´hñá  R×ø¸HÏ1êЩ³ eÍ>¬;°íò(¯ËÇNäéìHà¸t:v°üÚÌókÆQƒ+ª×'ü>PTTÔ‘#G®\¹÷¥PÖÖVíÛÇÈduë°(ŠjhòB’¤HT·½¤F£©­­­ÂpîÎó·t¨P(¬©®©¯­V[•Žzù …ⳃú¨z>»V·ò þøwÜ´ž’$…BaÃ3*•ªÞ-‚ jœ ¹î§ŽB¡¨o:úõ@QT"•hµZŠ¢”Jåg«S ‚ø¬…4ÌÔjuMMÍ7º­_áëJ•¼º`ÉÂUoEšàæ]úv‰»s|ÛÎc—,]õ:èÕ±U'•[[™«$b¾kckõ³+ÏÞÙ:Ù×¼- ï8È“]¾nÃ.5BhÚ¡kLj]K—”tUeMPëä”Va§·-?~ã.SûFb¨Ee«­ðNè×:Ô|€0 õv6+,£-X´c›˜xjÓ*µsóî¡Ü™©Û;u‰?¾5óÖmáò9·›¹³^œ}ýkîô‡­;ëutËÒŒ›¹v>QÃÆö/ºrpÛ¡+F†\Ši1eæùP%O®:{½Q|Rl#WøåçRŸÚ•vêòc¾W×!£< ª–Ì[,ÆYáÎÖŒà.jQÑ‚µg›Æw´P×¼^²xîÚ'ìòæLaë]¼-±µû–μS¦ŠHì@*Ò·¬É¼™kç5dT_3þSذ"M+#FÏeú½~…(MUÙë7•ŒÑ£úÜËÎo3päp¾`Ú¥[O>˜÷œÓ]º4û|–gT˜:õÔg÷±›yoƒÝ"‡ ,¹r÷Aâ𠈯¿¥OóÂ}O³r'ÍÍg¤pç¦ çÎY9ñ,˜ ó¬-¼’~µêâ{ (Êï:aâ8p oÛÏß77ݧôÔ•kí§lêjý”ÖÔÔÎ|ØàC¾A×Þ‚Cœ‘ +ÏœËtðŠÄT/7(:p§Px6î˜B öaXÊ03w—QóêY~y|ÛÖ }›WXã9k¹íp–”kîѼãðѪÛsrKÆ,ßå…Tþ¸?秬¥ºòð¢¹vþj»á?·Žp€­k&¯Ùsº˜a–Ø£$…Ï=ÌÍ º¨ª|Bãµ|8™¾údN €EÙ3 —yð”oç³dûÂÙ³7‘.þSû§Ðà©©‘À ­¹4Ÿ×/.÷òæ*] G€Íãûm$ÿеoÛáᣇÕÕU7o] “+ä2™”$I©DVðò¥“£“£££µ•­.ýÿç:ôóõiûKÊ ‚¨©©ù{ÔPUXXøçJ§çÿ :§h¿3±ÎCÊ_Zž?ÃW”*RSûÓÈ¡‹÷gM·¸ü}mÙˬ¸ÎƒštèšbçåÜʦÊSû/WØ[ÛÙ^Û|°…÷q‰Z&W5°Y¶D³tfG#ž¥±±ú‡}qã=û÷îz©ŒJtýaÒd'³ÉÉC'{7ëh¡,– Ž@Ué‹Ióµo®Sª>…Ê¿v±ÊÓtÕò)Jæþg'T†Øtõ4Z±zQãä¾aÁ^†L–Gˆ›—…é{?o§«×ó½ƒÝ= îlí}/­š›'5ìecÍÚµ3çMŸ?s’Eû1£?.•b3ѲwÓ6lpj:xüÔvQž´ŸÚòœÃúŽ9sÞëÛ§û]É[5Úü‡…«“ûöx]+õe›Ð¸,MÉС½-õJàñÜ\ìÜ,͸.až¾~FFŸªì­ÛOl;yãeôÓŠÛ»» žÜ¹wß+kççÙÃ\y¯Ÿyé¿{ãv‘ ÉÊAßíJ.”ÆñkÜÖ¯qÛ§`Ô¨qœîDòÐ ¯°ñk>Õ¯¹.Ð"9 ~Œ°øÎ=¤ÄzKh®¯CS·='³êõ,º¡sËDgàºÇ¶€à–Ý‚[ÖÅú†@«€à3H2¶.NS¹ñàɆÈ¡w›æF6f0î‡O¼ <¢¦üõ!d˜2x´îˆTP˶î±ÿA391¼á…FnÑ3fG×_8æÇŸë£LÝé†*¶IºžÝÐç‡Ù>uÑ §Yóæ×V(äóìRÚÔU‰½gÔâµAÏ×`³ GFÀí;·Ž;Z[[ƒ —˵µµKèÈ`Ôµ£ïóýú;‘J¥úMýôèù†|E©RTçm?ssÈQÍüD®m„38Žì}¹NèÕ­“}úq"ø5±ypo¹£÷ v‘W/åL˜è}õólˆ™GÔ²mQË4Õó'èÔÄkSÖ‹q®tòê‘ýpîÐPJäZõ¥¬W ÖíO—â!“ÕØÌœÐuÑÛ8 ÆIFØpï×6²Î8µþ¾oÜcÂXYÑŒý58À“‡ÝƒZÞ±-+µ{òÚŸú¬kÔnàÆUieOVØ ƒÖü¹'ûòŒ‘ÐŒŒ¾AîÍÌÉáÈAY{ûo ç·¡‡ßx’¥;¦(Ê@àaðáø¯Èî?Cý®)a¡áa¡á_Jðÿ Ãx<žV«U*•õ“È8Žs8J¿‰Þ¿ Ã8Î?]Š5ÕUÕ••^ÎW”*ƒéimúîéeHlVRúŽkÈ¥Ów(zYl&pá0éj•BMi¤2%†" *¥’ µ‰â]Ñ㥠ü†­IŸÚÂÃÜ•¢H„¢”µÕR£Y˜óÙ,Î €âwUEŠ[ÙWø^!®V¼_£¶¤à­FUX­0 rdhÊÕ•Uï« Ój@¤Òj*ßT@¤€¨ª+ÔòÒ×ÀÖÂD%¾ àqQ¥¹cGIŠ‚J£ÕҘ܆ò…eù{·o9t*[¦a¬=žÓ+εa¬¥ ÏÔî\îSg€ªjòðìhqU©jkd EÀË7ïÕL`”Z¦ÔhåeÏœ °«j( €Õ¥ºuÅv–&jEI> +Í‚*zÿæ±X“óVijñþCOó¿Ì÷¶/ÍßÕë|Xö¯ŸìûßùÒV‰új¬'-mmÆùsff| €$H ÃO?%‘Hˆoê¹GÏߌÞG×€")ê˾E~?_QªXïVD‹V#V[3*á—Å(¾}rXçWù*Ó´©Ó¤ûFm_5mV¡#Gàˆ Hv>67+=elŠ©…³¿͸vùÝFýîtii/0аƒwμ8öÇÞÝŠK„rw ^Ý5bê’¢¶Œ[?¬w›Äß){GŒãï3`d€Õ'öïE‘¥ÛD#ºûPóC’‚‚è6f[&–Ó¿øñ¥i½:>Í‘,Ø8ìÏ=Ö¿yå³õ»îN˜Ð~³—RT?_´d«‰£O÷!}̾˜’ÌÞ¿ ìëÁÿmiä«û—ï–’í›yîØtnĸ_Kÿ%¨Ò¼{×rßwHjÏþT‚FöþøÉËbÛ8Ypðª7yoåÄvL6cüñÞZRvgKzѸÑÉ*qñž‡ÞTH»OŸåÎyõóeË·³­\{ `ŽüŠ|µ´ì­’¶-êcôêÂgè+ä·Ñh5?Îg&0Q«T8N_±|˦Í E½Í Ç€cmmó™á°žïýo°?À7Ѩàë6U”A£Þ³ŸEtz]©¸6b±ð]'?¸_`hëíbc ‘Ë#qg#TD3€è°&¦,gö0W:7ÖÚŠ{ûžšÆÜC`F6ó¨å;ñqAffSg{‹m‡Ž\Ï)³¶²F l0s×Fåïâ^ÁŸ€Æ6›0kÍ€9|eU×ÖßÊ`Ä,÷ØÞ×ì'3à[»ÿ¼Bɶ5Ä7õÏÌ:ý°°VËó®ñµKgî?/¸†ØòAc3ðP0ÏXg22vƒLרĕQ?Vî§{ß"Æ/_ɼÿìeYy4âÂó¾ÅJ†ã¢Å ž=»†œ»(ÍÑÙ¹ªª–ųE,J íñF\;Á'º›V%~‘÷ìá“—Lópðcn^>{ÿÅ{3—`;SÐØOÊ™E(ÇÆÑêób|‡à8Êã ¾šŒÎ2ôv4ß{òpÔÀ>f_4‡ÕžOßÅ4êëñiZeõ={ Ï(Qš––¦Sªþ¤FröøÁç*Ç”¤Ï£P124d0Y¿GEªodɸ#혒üÇJ¢ƒFÃLx 1 ½wí:r–;èLCg«mûF`Nû,oí“ÛYgÏg>)£•<ê4(È\¿ëÀí[7þøþEÿE,-­˜,æ¯FÑh´ÒÒ²§OŸÉd0ccÃKJ^‚|øÝIå=¯242tqqÕ+Uzôü¾¢Tét7 W‹ú 1ºyPÄk$ S°w©;ÇåX[„E|”e©3‰1vÖ‰âØ6ްýK7´ð«Ó¨>Q&0¦‘ŸX|p3D7õ øÔ#±s£pçúÍÒ3Ô²>Ê¡Q¸CÝyc'Wc‡Ï/2úÚºt®m£ð¥5wûhq¶ŽŽ×ÓÌ}¸Zúhðþä€OØ{G×™éóëfyNÂëL¯h¿tdÐø©Õx@¿~mIUYRÛî&G!“ø5ï³lFÿÚâaM’äÖ²ßôs†uéÖéáÃg:ê/ï½øÌñÓ{N,|PÚdãPŸØøä*ê!ˆr´ÝhÓið›Jqëž–ÍÃùE=¼Í½_""'öïÀ^¯¨(LêÜúîÃâý÷Ÿ5¢ z¤ôºp'¯Q\·}{æŽêÜ&ãÆ‹Ø&U…¥Ã—¦·Î}Ó}Ù’'‡¦…ôZáíéÎæ°˜vqö¤öï”p£ˆH¿ÑÚ@).]=ÿ§%›Ú8zÎÙ›í+9ߦëÈZ…¶ûøå‹Æ'€\\‘uázûŸÎ~ªð{çôê;7=((Â4°ÇÉuƒSZµÌ¸õ,°i—Œã›îß:zÆ" ‚UT 7Þ+µ°¹÷¤Tëðñ}{Æ¢ ~PãVÑn–( €X%vjçúm†ð¾è^B»nokåÎ~1Îíõoü ëØ‰]{&Ôê5ªzÂÂ#I’ø’CËÿ‡ (úìiΗbµ)—+QQ«4¯_• E2äƒRE§Ó0 §Ñ>¶nBY–Ô¶ëõœÖ66!ñ=,Jµb¨TJå_ú½zýäTHlN£GuéÍ®–¯Ü3«sXâìôSaæl½¡ý¿B¸nÞ¤™«8z†mϾ` XõÓ„) ¶p {MZ¾xJ@-¯ùyòp·ó¸vv5×;¨Ùõ;$¡Ú¹tÒè9[Ü-O?ßÌžAQÄõ½ Öåp–öqô‰è©Ô’þ;œÊØkð6ç|×^cŠ*%)£ç/œÒ‹¢”æ-dÝ·dï² Ý@X–;mü„7C[ôØwp­É·îm—Ÿª°ƒÿ{²þí\¾UìBSÔ÷î·æ÷€êÓéê}‹¦±E ‹[v¢Ÿ2ª_¯K%‰ëS?U6öÃTjue•Z]·\Gµ,¦p1¥ ¥=¸·Ó„mSú6nè.«x5sëŠÎ“׌K ™2dà†±’ýNž’ùÅ ç­|Ú¸àP+’X[8L?ûjç´IK·M ­4tK¬=óд„”¡Ëz%$M]à>vJöƒGKR:MË ä·dô d 6åХ؞1¶w.#y¡‘fpWXÙcáéÏÞ2|3¶•¶ó\ÖìÉód"5ðè—Žî½þ\ñ¾² jË &N]?ãÀƒ” “¾íbÖF µx~+½XaŸèðéå1©‘ë²"àUhÔÂÑKÆ \:Ôž»~ì‡Ä¤ÒÏí?¡镵³Ó:ØØ |ÜÂ,ª]†îZŠ’ÉTšÇTm­Põa±±™•ÛésÙSÃmSúŽYtjs/'¶Ï¢™fyÇ®³ÛâßíŠÑ¿™»wïR:'I :ÝÍÍÅú]CÿUHâ‹KÐÞÏãrtŽz èãâêD‘¤Z£&IR.“§­ÛÑp­;‚f\Ó,unSúvh±cïþxG TciÌÌÏ}®Aév.^Îö– QäÜ-|'å›ór<:.¸ðéÃÒòZ:‹nha+~óB¬Ô²yÖáa”B˜›“«ÑjjD ;¾òêåkshˆ¨•$IÒ0HŒO>š±j²3ò1¹…ÓQ¶!¡?|§¼JÂàòCC EÁ³'EeÕ Ž‘³‡¿­9W«Vë߈ïWÏlM¿'KˆkÜ#Z<ÉL:}ôOi¸Ü#rR‚£¤úåÓgâINLï1š]c#Gë,ûjK®LY°¯R"e¼Úg߬ɛ‚;@3.äuœ’fá®y'㦴mßwåµíc¦OžŸ8ÿÄäÖ®útZ}Àc|rȦ‡FÜÉ<›§jUݽEiRS{öâÚÁ=úÌ=³ã‡oûÙÔoSó÷ñŸy·ë<ª¼%´u_[##  ÒBC.“WŠ>§ÙÀŒ CQ 8†bB£c[háô¶ì‚SE9w7ˆó}bºF´ðû,_Iuá¥+9#/‚+W@KT  •Úx´€È¦©³_´o.RhyföÒªLQMò¯n]XŸCFµíj Û³'³ïÁÛ@QË48…!¨VÀ‘½­ Q©Lî«ËÝ—2¹¦v|ˆ ±¯.­°Ø¸æP—Õ×YE4Ž2P­š¡–i¸8¨T"[¯–ÑÔqÑú—píÈšÔµç³NEQ5/K1ô·^F]í=¿{rüŒ­MÚ´ª,¯F­ -½’{{<}Yí㪷©úHyyY¿~}[µn¦Öh(Šª©0¸y³úÊùU† ïogk¦T*ššÚ[7oˆD58Ž#B§Ó£¢Âß¿ûÄ•"F+y!R[T°Ytêmë–‡ÎYìfnpäJV‰PkÅgW)ÎOH]àùC†Œ nÙåïL¿-‘Tìß´üøõÂCÚ⬼‹ÙÕjmñ«bÉ¢]ìå³§M¢øv~Îæ÷<õ ‰äâ[·ÖÍ;u)Ü•J¥Pw\ž4K¥ò ,ž±'óá6“…ñäú©kX»º‰+ËîªÇt2Ký³ÀÙËÂÒ–aæåbÏÒêÝ~T¼ºçœ ˜o&J“,l@VùÜÞÊÂÁÅŠŸ\0 èÂCUu®FSç±³âÅ5—À$Ø{sµÒ nÕ«*šY„‡!à”F(×bÞ‘-Þjh&ñQ®ìõ¨( „ke/¬ª 3 T²êêJŸÈðk§8pè›ß©^©Òó )J¥Ð‚I$mÑÒÏl×¥Š1‚Új!ÇȧÑ!å"c Ó™(¥Ô<{þsåYÌï¾Íˆ¼ð ($ÙE`çdž0¬KTŸ»‚£ž\Ú¯µˆ ý0â Öh@¡Ö 8ÃÜÒ¦üÊ€Æ'NçùÅÐ(* ‚ ´’$yk¶¹UtŸ¡q¦ .mªb¶4 ­† H‚ÐÂ/:][ËS÷rÞ˜U#>{–ÿ<ÒÞòá“Ò˜ÎÞòW‡óe6œtK´Ê‹'P–¡-"½@'VK ˆ™•ýÅk·ÂŽŸÊõj5ÄÏÓveÍI?ù…êD Ø4I€) ‚b8Š€LÀ‡7y7Ìüzü0eÀÞ‰7Ž*už¦QW3ЫSŸ€xûx Ôµ à5 §•¯[¿.33S.—×§ˆíÔ±³¾Ò ¦ºöý»BF£›bFQÌÀ ÎäðÊ*!‚|2³¬Q‹–ôO8lj p MJl–¹áøø‰#¥Ûîo¹pܤ ¹vý>Ë(ÃÈ+yãÊÔš[®?"ÀÊÂ2exŸñÃÛK*_U^cË„JDRtðäã.S\líF,ÜÔþçQI„oâÌ~‘«†µÉ¸ü<º³‡n3lSc¾X¤‘\jdRcen „ZvéÔž¼IX §rEé¬,èÙ›"QïÈÖÝ:·55é·ÞÖMφÒÈ–1€g  àêšE3¦wJo 3Ò¯FÍ8§K¯Tª?Ž«¥(› Åt×Öääñ]Ü­€ÔÊOlY~à)ãÌR¢òJgj‚D(R·í¬JS¿ÿ,EF¹§© Ó;ÕÛaèùŸ!Uµ{×þÒbL~þ儃ӯ¿·±¶ÁP”Á`àt£1;VÑÄÙÅuÒŠý€£&ö®þ®F]<­g¸ãÚF@/ŽX)7uðNH|||^ëÖ­Õ¾©ƒëÈq=¶ŽnæèèŸÐ'OüI¦„ª|Ѳ·L×ÕMè`8É2pˆèŪȴ²²Ìî™\SY¢“É@Œgôóôä™­üœÚ÷]$‘-]pxèÁõ:©8NgÐp§áÆd2_\õ l•~êØèñ?®¿Ü~ps/<ÜÁ>,2ö!å;kJï3R\]ƒmÛÍJöÔ[»¯Ù÷±„ù‘ÝO^Ê:A§á8†a›Í‰ïcT“eeeyºÒgó —Ý6œÍΞžÒ¦mçžWÞT-›9:"aN^ÎÉ®ƒ¦Þ-Ò +©güML‡Ìߦ0¶q ö5ïám5u÷õ6½F)óv·Mè˜UXm$¨ÛmZïUè3Q©Õ¹Ïž?yò,'ç ‡âlmyžvº?g«»vþé|(µJE~Ákƒ­F£Ñh¿s«y…RN§Óètš û¤SøeÓ¢Ñ8cÓö;qzÝŠYl†BY%oŠ™FP¼¶’Ëa"@aZË ä€Ñq”Àh,“N¤ow©½dùønÁ²Š÷8ÉÒHUÛ„ÏçË'?þ¬Bí¼’âÂÛ·knI'•ê&óÉa±ÏŸ0jDêâ-'-fÑöï[o®ÌOi6sÅQ®‘‘n XÏ?Ž©¥­²¢Ä•%(ÃØ ñâ‰Ã©½VÍê*áÓ e=~ÑêøVªÊ «.#p–-Àƒg%NξªÌýii‡ïmË8„(ƒ‰’Uµr¨Ö›Ö­ùª…àtcciU Ô¼+fšX[ ô#Uzþ8“×=ìþâ IDAT}ÄOÝGüôñT¬n‘ãì¥ëÀåþ³ü±·×Ôu½¦ê¢`áÎS „™í<Ò0èwöÊí†yÕ¿]ïó®:4÷á$ß!þxf<ø÷˜·Vî=³òÃU#ÇÏ€˜p€Ù?‡Àµ{uQŠ’lÔµ]纕X£þk4€äv1ðäÅ'{kôœ¸¤çÄ%BÑWï>ÒIJ®Svу}ÐúBÒÙü´£7êÒØ qÇÂ`áöã ëeNZÞsÒòú,¢ç¬ž0guÃL}ÚϨ¬žñ¡ö ’'¬NžP—àð…‹¿Z?z¢RªÞWÔTW‰ètZE…P­")@€’¢P LŒÿdjIA\Óî“¶Oô·þÕ”V²lÖ´7j›´¥Óþd^)ù[zᯀ`lc(e2¹HB„@àäÌÉØ°iW#žôu¥fHTŒ›¨MMM¿âRxâ\•XªÛ£‚Ðh(#Øoù¦àÉÆƒwðè¾EQµŠ¤hj jÔ Ñúqµ°J¥¬0–ÉTªÓy8“Ó¸iÌüí[Où² TÕ*ŠèiôðQ®K`ãÁ„ìIeVï±é»Á%¸=_;-íX–ìÊÞØqi°hòàu§Ÿ¸ïþ£g¶Nâ;gL›ögPï^æÜ¸}S$]%в5%y]zð<#ç`HŸ9t€WïÞÇ&6€;[ºœ3ïÀMÉëÂ*Šëëб±Ë¶• ™‰O jŒŒ€§×³ïæ¼(V‰o½…;EG5I]äØE£ËÛ2ÚL9û›¥þ#è•*=ÿ3ä?áÅ:0eM Ôçû‡sgÙÆ­]÷g$èàÚ6^¸¨qC9zýæûA«% ÎN4Eíìmš6ˤ©% ’¢¨—/ßlÞ²·Ar²8ÿÞáô³ŒiëÛ¢G÷ãGÎÅ%ÚšÐ^;Q"á ²Z•F”_PæmN—]¾tÃ)´]Jg7—Â;k.Kù~‰bX×Oí>3×ÄÚ­C÷NÆl[K…ºÀÖóˤ¤™OJ·#Ú÷ÖTH€_wvBQ¡Õ ÈÇnŹÉݻ۰IÝ&µ(Ó|èa4`;Ì3aÁª×J8ƒf­Œ´…$ˆg/ÿ>l· ³'ͤsŸy—^µ<(ŠxóäfFÖ³ƒz^Ú½ÓaØpO妭s\¸›ï³iT›wy—e2ÌÛ€ÉÀû÷í_|mÿª“5-Gû:›¶3I–¶1ýÔµ~©[Cù ½}þŒ5ts0>yètø”î®M’ÆÊ‰‡ö„ö\Ø#òã¯âo…^©Òóïà{ó„þõ‹z>ÁÉÉ>©Kë—/^`¦V«ïß»#•J(ÝDœ³³GCá„ZqúàÆgå–ý{7U¡ eç§ 9ž}Zú¤-^ìÑcÚõ+¸¡íRB­—ÎìÛª{ˈ€•ógØï¥ôîó÷#¶nÙFXtP›:eUZõÍã§”Ù0ÃP @øjÚ¾K«ÒæK$Ž|W‹4U*••••B¡Ôé4,EQrù[¶ÁG¯~(—0`€J¥’Éd€³ìûñ‹Åj‚` ¼S—Ô ‹D"­Zróú=“YQ«î5q$„Ƶ¡PH7q2ù‡z™B¡°u×.jµZ.—7iÕ• ±Xܨý’$E"EQ<+¿"Äb±V«5¶ðëÑŸ#‹;õï!—ËÅJ46iPlÒ (©T:mîòúû’ËåßO=ÿ¿å{4[²ºY]ãu0­sƒhû^£ÍúÙ·ìµ³Ûy{%UÖ”ÿ±RQ¤úÔæÅ[/îØ¿ÅèÓ(yUÞ„)Ë{Lœ×ÄKðuÅ…"nœØúó†Ó«ÇKò²//S¸ÒÉ„¡®ýa@¯7v±øíý{²ï“efaÝaãõ¿n†ó ”57Ù¦QjŠÒ¹_TÉksß:pæ^³‚#xt½^õëh4­VC£áöE/þõ 4vò¸´äï–ÍšlØ2S,,ëÜrY—Y3Gl>ÿ ”J 4-T!t–n»OG(À)Ь,¯s~u­˜Ãu4å‰HB5Åå†\>xá¶(zûêØ–yÁνV¾ÕÞ›ÿý(Ä)ÉÝR’»}5ÙÑTŒÅ·ä`HhTzôü’ÿéuøNÞ/¡Wªôü(ä]úÁôâJ‰“t»V.:æÔ¤• GyúÌÅàVI¶lцÕ„škpl§fÍË«^§Ž™¢»ò}ñÓ+7Šºvk_póÄckb0÷öÝ•JT`njáâ Ù±nK…DãÒ¬mlú‹÷GX–ÿ¨°vôO+†÷¬M-Ò3Ïç¿ï6¸Ÿ€ž<|àYa…µgD·ÎMÎìÝòüÄÖÍA\QÙ®§§r×®Ã"-иeóp/@V[z'çe1 ïÌÙz^À3¦10ŒçÕ'!"ûôï8OwÙƒkYo>aó,[vígEíÝ›^«BÅ&6 qµB|ãúõ6ç83aó±ÇÎ_ [„ÞŒ‰lãg e¹×^ËŒ×ý<vÂËÁƒ6nÜ´€§ï; ­)\ý‚-˜òŒ WQ–qHL»Æ~ve¹×¶ÌäšZE¶hìÄu·q;yé›'…í{ô²DjŽî;xëñÉ4ó àñ¿¯‰¤ï /;Uÿ¤ÆµââñmKT>A]ºx0Ü»…ö_z½ò õ–$IB Aè̲ I E­èHÚ"U#«{ùU=ìjjWj.œºÂ@öâv³!³éj­€’Vå¬XÂÕQÒ¥‡¹ÿ¯½íÿ‘¿n\‡¢(¥Rù ×£ç»E¯TéùŸ!TÂ#»·Ýy%iÖ"êâÉM2Œk«*œ1mþ¨f§.‹â“ñ?M>óš3²_¢ah¤…¯ÄS%(yykㆳ]»µzaÏ6q k‰öô¥Ýû$œÜþ¶3¦:˜–öü6²‘Û…C“ùíL>Q(í“;7P#‡H;DV.ª¨.W`//]c0Ðîõ†íGzvfâ ’,»xA‰³÷Ÿ¸8¬oÓí¶¸ÖVcü£c»6°‹¢œEywJ«±)ÖOÛ¦*Þ›­dY¾È¿Ó=¡±™²|Çž 6]Míñ—w³/ßÝ©³¥‰¸ú]Öþ5÷Þ!­cü6Îÿ³q˜)Ô”>¹þ |ýb?…¡/(È[Êp5äriæ™Ë~­’u›Õ¾È}÷¾ô öÈ« ÍOõi?ph§8œÍ¥ÊÎÞQ.©Ü½aoáü]sç«Ãº„y82X @ÐQEµš^™seõ˕ÛØÛÙzj¹nÎŽèׇ`þ?ó[n&´ZuCm Å0'·Z¢ŠeÈ[¼)€Ò²˜ôSv`@™Ìü9ÕÌŒ˲4€>cf*­ ÄrÑÓü¢1³xØ€õžKN\Îá‡OŠ÷€–»+h–lV¥‡³“kÒw\wA]áþº;ÿ!‘H$É?]=ßEétzCWøz¾ŠB¡À¾…ޝ(Ub‘H·ÖêÏç¤ç_ŠV«E?õþ§¾Í<òÎJ*«)Sj%aõé5õö¥VÃfË?¾Ê„ê%Û.ì}óÆçCzMƒ‘6“ieé¦Fl½zu«ÕÀ¥‰ñæ/ö­«-¸ùðü©ÜjFeYÁÛ·•5ŸX]€Z.¼œ}ѧS Ti}=Â{Æ5m*Ìuë^)ù6°õ„nâ#ˆÌ„e[ÕÚÉ˺|Aî­R&åªDËò«¸\Q#% óÈç¶s¹4 ðcg³'LüÝ> @xÇ.—K0  çÑC︑}»€¦&wÃó²~ ÷7¶â»3Ÿ†uó¹x`³uË)´)½ß>,QøG9r º8÷QÁ»ä>íY¥†y»úùabàÑù[ûvî”kµb‚[)gøÙ²7Ýyعu;/;>(Þ8X¹ вò¦¿h×wè ë»%ÁÁÍ ¾îù;D«%pÝÄ„ÿ«Ûz*•ŸxÙF0ºS`s§Àº Zönéì);Ž?¹¿å8x»ÝÊÑM—†ç `çÀpô𯅠<;vñ¬šX9›Ø'u³¯?ù>²‹ÙYí:f]8à€¢¨J¥:~üØ‘#GL†Î¦ÊÕÕõ§Y³kkkõ‹æþÕèÖg蕪ÿ ‚ ¿A©½FõÿGã†`êîÜwíšè:÷“ .{W,õóö¼ùbÍÁI ¨ PÐj´Ž# Ej耢( $ˆD §–*å ÐjPóˆ>3ÕµAÃnؽyváæ3͹4s (@0ÔÈÎ4ä°Ô*”VJLŽEŠ%r (I­Ø€I7 è´`ÞDƒrDÅY;Î>\î $j•L*W+I9db™B®kõ†6EQ$  Tª8laµl8 •ÚȈP°pûƒ:?Y8ÓÐØÕ+V·]8Ej®ggà|ÿÆÖŸß…:Û„MÔ^3sÆ›×<øqx•RJ,Ï4¸ëóÐC›‡³˜\¨I0Œ8×®uœÝ/+GO=<>ÏÃÓ{ê”…l6ûó8D÷™8irý¹Ïª‘n`1mÑŽi‹~%êßNû„Žb‘0&&–¢(Vƒ¢haQÑÔ#]] %¡%gÏ^šÿ"_&•Ö¯ŽÄi4KKK¶Þ(ê_Š¢†5\âªç«|+?±_WªÄ"QNNN£}É‹š‚ )É/Æêù·#UhnS,ž}l¸ïºYßFkÊ€°Æ7÷­ò°zz'ªkÏþvÛ·®ü±ëÐîCÞ´‰Â m»&Æ™ LðÝ?/WõîèìÍ"¯?t4ï^!+Á-"œ=gý惄WÖÓòä™MÓ^Ì>."ÄW¡$›'ôò¶aÔOÿQ¤tÏúm-f¦×ƒ$ HÅé¾-*·/_šVòô\Õ„Å+Ê3×›Ù˜PIðmýãÆŒéï,QÒ:$uÊN[ß:õ8@E‘õNÉkU’cëwæä=3LßE¶êмMë ·V¥.^mkjè߸Y‡ßÝç¼v”«¬{´±<±tD“Ñëtýög½¯JVqîì•V 2¿T«Iè~F² ùq‘Áç·º)ͽù¬¤²Ó÷ÔH›4‰•ZG`@$AéöOD~eÐEÏ/¡Óè©?Íùj²/Uã¸z?|àæîž——+˜ãl6ë]ù»'O’iÈåúx­\µEQÝ) (†Hêܵe|«êêêºøzôü øÚôŸXœy!“Ëá5Rk4¿ÜíÅ‹·sK­¼c4j½Mâ ÂDOíw0nh®EvNQ…¡“—§»£IÏñ~ü`ñì±¨Š°í0i6ëxa­ÆÆÕ•ÆvÃÇOȾù§ÑØæ?ü8öQ‰¼Å¼Õ„ƒßÀkPwf©Pµùà#/0÷0Ú0ãE¹ÐÑÍÍÞ† z¸ÊÎ?RÝÜÆÕ4±ð_¶r1øÅ÷žNx€}ÎŒa7ŸÇ-ÛèbTÊêjhfFg³ ¸\.IÕMð$©ÕhŠÄ(vv6õûêà8®Tª EýÏ*RS»nѲ¯ËŒMxža-{vëÈE”õ4SEñƒ±cS,¬q ÷ŒŠeKµýÆ$­™¹‘Ö>ã¼C£½ŒnùÁ¸å“®ÑܹéÕëƒËÒ§né§KoëÛØÖ·q]v.àë`ïÞ¢]xÝ¥ò ·.h÷Í&ÄÀÊŒí ]× o–eäþ!ÄoÞ¶î0?’qîKÆâaI§3†6<ƒ°­}|ën ¡™¸x›Ô#ÓÒ-ÞÒ­>%'´™óǺcø†{fêæoúyèÑó?aii) ÍÌÌT*U½YGÖ… …Ã0ARR:º¹ºÑè4•JE¤\.ß²yŽ7è&HÙÃOã\ÕÖR:uúbž­sß¶!Úšj’$AÐßµ•"IÀ0ô÷·d¥ôTF.\¿ÓL^© Tï kUÍí›·âÆ³i4ô÷(UU¯ï]|ôbb÷ºÁéº[ÑHO;,¡[Ï_3öðâ±k×î[¹ ÿºi?>S¸­Ûµü}I ´À —¿ypýéû6©X¬ûþ08NŽŽª]|õ¶ŠŽ!2BvpóÆ×*Þ²U“çM»ÃÔq\çÀ’ª‚“G²Æ÷þ‰«zÓ¯‡Ÿ>¨„T.†•==>ÝÑãs^ï=aÒüÌÝóä²ê¬“È&ƒ8l´Kÿ±¡Av‹Ç ›°Ð{ç´V —®¨1n´jj‹i“S -»EÒG™3tËYç⣳¦Ì:wbãÉ[K&+·/Ù2uàœ4»¥#šÕÖÖ~Ã'û¥ªªº*®YsP(êö°$IR.—pP¥(J"«U*‘H$•J>7½ÑóŸ@«%~õÉþÝß:oïÎAØ8ö-ä`Xƒò|Ãý4¶a²/ëÑó—’qîlÛö Ù2ƒê=xMŸ6ÞËËE®P B¡¨¬´¤¨¨ŒN§ t:=%¥S~~ñGbf̳rõòpy;˜½Íϰìݲub"ÏÊÑÑ@µaë~:Ë ²mï©ÓÆrd/û¦ôz^*ôôv¼ïU¡´,µWÒþÓ7B#ÂÛ&^Ú¸¢¸Jijp]{ô„*·gò` UUÕ{÷`?ÇSÇNZxïÊÌ(Þ«T*à ¥b1&ËÅO~˜¾ýøí½>ŽcŹW§N›ó¼ø½!ÏrÜÒ]ñN’‘ƒFßzúÚÆÁ½ÿ¤%Ýâ=Å5Õ©¼}ù®c£ÆÎÖ&G–ŽÜu®pɺŸÔǪãŒÎ!å·*«*+-8†¨+îm>ù`÷³#âÒç&|£è<+OLJٙ­Â!ˆDÿ¶þa Œ¬:öíõúY>‰" •ä¼z×yÊg{‹NmBž?®ìXñê*eÕØ¨&ñΕ+Õ}EÅ¥9Ö¾ AfÛ&îœZ óHñ›¼ú¸1SHvR«èg?/¯ˆ¢œ"ÑÄSì¬f¡.¯d5· ©èCbw._£Ñ Pà8F§ë&û(‚ ª«ÅŸ Ñjç·¯)å*J¤´îq‘wgÿ˜žî_~-¾Óô£Ï+ÃËzwœ‘}?ÆäªŒPYy0ïô‚Îb€Y›ó»]´zfÿêw…ÍíLU(zaû’e[3w÷w²2·é2sn僧)±»%e›L‰9pþÉô¶®êª*2· góâ¹LmÞ5ÂÑÎÅP 5òó'öb¦þûÓÆ¿ÌÞ¼iþ¼˜½´4³½™#|ìU±XX¤VQ!’ñlÍq { «­Y>¦g_–#6Oíž}hõûwVÌÏC(fß0o黣,6sçÜiÚªRðÇÏšéÎã˜p ï*Å„‰aúE‘-ATWÕHärÝ·Q«¢1YIR@jÇ@­¸“q)°ßFŠDâa}m»‡u0Û²º{Æä$… ˜³g\ö/$_3wG€ H’$ ‚ ¢ àeÁËM¢›b¦Ñh‚ HŠ¢@KP„þï?ú÷?¼7¿†ô݃¾ÉI“ZTþÁPã× ¯šuìá{ä·Òè Ÿ^<ºjë!±¸pÆÄ9¿#ý— ^ÝË^½~»ôTâ’u+×ä–ˆ~§ð²ü{kW­)W"‚<}pñêýbA>L…k¯X·úðUA®ÞÕ«k§nÝzeäÊ~C2¡,›2|¼èË ÔòʳY?qïzþŸrõòE‡óàÁ}‰X\?©§Q«™L“É`0L&ã“É>Ý:óO›Àd±m]Gþ8/ÔÅ0¶àé£G R[V\iomªVÈTµåÿ¶$fÌ3æp´ ¥1¸V|ãj…*#}Ý¢5;ßV‰yL…PƒÓé,ßÇYPõ®ÀÂÁÑÉÊ^¬þ{gEÒ4ðšYßlVânÄ J "8äp‡ÃÝç€ÃÝåp9‚;‡;A„—ͺ½6ä‚gÏ;ÿß~Øéé©îží魩³ãµZ+‘‘ÓrÇέ{vo·ektUzE`&­R`ëV–ŸËqˆ˜={¢…Èoh¯´³;– èÑmÏé;\¡€"E(C¸îŽÂ‡Ïž×ëØU]ùúà‘3Q&fü’1¸µÏꉣ˜öΔÙsñŠÝ»Wˆ´…W¾àòßúqûóüöoÂds9 T§US@©Õ*ž…˜ ŠS×+Z7·3EÕô:ÀsˆÚ°jº+Ÿ\/ÐÝ=D ðôY›§Ÿ˜˜ÉuóôO«÷÷[~8‘d¥Ñi)×hµ–b‘µ¦*%Œzµ‰B­%̈ÝÆôk'dsãÃ%b{Þ§ß`²¹,Åæ1YÜÏéŸåR$ Š"¥Réý»w¢c¢Ù\žR¡¨Š¾ $N£É`2!È·Ù‘H󯂤àϘª,mjìßyɺÝ%8ý®¿JâÑ ¬Úƒ[ÿÞù·`ºò‡ŽŠbzpÑÊ“'OÍ]<íëjE˜”§O-ç…Y~pŠÍDÇÄ8ÚŠ?GEo\:ö,—5ˆS†"m…¥DNi›^­¢AôòœU›wXÙ]þúúì×Z¼=®'»äSvf —g;6rÍ’_”}}þü5¸•Û¯gÎFõ—ü/ xBóï'<"Ç ïZÞ–BQµ¥ Ã1ëwðÿDL^ã^C;y[TVVÊ‹¤&ÃÄ® È¸™•#(ñº¸yŠ¿»¤ xÙ¾|C××ï—JËXÀ¢0ÌD’¤´(Û5¢sßîßM=¶šre BQ`ÐiIÒÂ`ÄÁh Iˆ5B QúÄ•…þ·š°¹¾¾¾/³Ù‰ii,uå«Å:¥¡vtrX\Òá­K.Ÿ:Ó&=U))&ÇN,,VÉLUœó0ãÒ³åKgï׿Õ%VB~qIa™F—••-´öº„Æù¹=É,ö·ÎÑ›0k[{ŠÄe*¥ÐYÀeâjÚLõÍà|Dׯ^néùx IDATãpóÚ}÷¶Ë¯Ïlë(?X$g4êqw¯ïÖêéѤ‹ˆ×eRßÐ06ÀË7fÍ_ÝnÊÖN Õj5WâêgÃ>}îj¢Khæ“W1ƒÆ‹=M<í¢„Ãۈ_m*+ô>õ’êÅÊÛ'-­7ùW&À'ª3¬ˆØd†›0'(­Zù‡Mûc5È<\¯× --#"d¿Ì–V”™LF½^¯×ëM& Áq'(úó?ùùHŸ MÏ®ŒªàêêÒ²}ÿ¬Š²¾íÒæï¹TøäLÓÄÆwʨÒ'Gm$"¯^“Vb,qpH £ØÂ¬rß8µ>²~"‚ »¦~×|èréË µ\½}C2Ë1kWûÜÛGBü½ÝÝÝÛ?±A>0ÆPÙ÷®–éÙ]:5BMú×yRR‹Å6GòLšâ1¡þÖÖÖõcZ>-~Ó$º6“É œ¿ûÎãK»ü¼Ü==½zùQŽ ‚”¼z”ù¬°ÓÈî×ÖõEÄÛË#($ nRAz¤4MhÖê¥@Qò|d¯Ö¶66õ#fdižÙêæhïêêÚkÌb=‚ ¢–å;~¥Ãô)(À¨Q=NlÝ®æ9·o@a×2[…5kã+8wb¯KHZZ¸—ÄÊ9"@|vÏJo7gww÷ïúNV ˆôÕ•ÄÈ;{û´nÃrtüZÖC;¤ÚÚØŽ_}Aé¾³±¶ò¯üÓÑLߘŽ-êß±É2¶Kz°5ÐóŒ4_‚\.7ôƒA¥RV{åA>å‰2;.©AXžˆºR*•* 8~>~(|û°Ÿ¦u_Ò/%½ûØØ>³Û4ñ纥 hjãå¶ëzŽ‹_Pk;‰Äñ¤ÔŠ;k“š479¹ûÕòÀ0ÊÙÅYÈc“bgïh%â8aëàl+â8®ÐËË‹“E±¹–^ž^€#žž^|6¯Uç¡A¼—!..ᑎ=–Qúü¾ÛDÇ&Ÿyl2kS§"IaðÔ÷-Ê{Q!×\?{< Ù°C&LI³üÞÑS&Êoí©åq±ÈyÁ–Ù bþ´qæ¦î11M{‡t™Ô1!¨²(/¿´Ì·~"Ÿ4Ò»ÿþ ji^‹†‰›ŽÛðàæ]¦Š¬=OÁ}uÔ×Óߦñ„qö®:3x8„aÿÂqþaíŒ,c³úIçžÉ¸<"-©¾ÀÒî1·Ñ¡…Ýrî]+ÑžF“éÁ•C7>[6®£““Óð';SçOŸ[æÔyNŸd7½Þiß–1ƒÂ‡.ûuÙÑm\€‡—ø¸X;†ÆO<2©©X©T~b5t+fÛ·yÙ/;WŸ<°Ydíð‡-ý K$IR…„“³3†™?zZ7Ìd2QeA$ARBïÿ«¼;A¦Ì_¿~kdêÀ_FtZ3mðžŒ7›®o‘Ð핇íÈuç" !iäOYªŽUù+J¯„–šq¶pßô¡›v;†—†5y}Á€ó :³ºß€A˶øŽštíÔ¡^]ÚLº‹k†-ÙÕ&;퇥§¯–vŠu¸})âVÓ&Üc"ý×Ýèãú°rvÃ5ÀŽ3¿Î™´Œëú©#帣´²4¯FÎ8²êbajmö°ŽÍ¶œí8¸±ËÓËû â¨x .=züîÚ_vf?Cî?Éo覗œ¿ù¢Iï UVXØÅU5ƒ"$¥Ïh<æ´Aýw^+Bní½äâ€$Àµ/Uͬ½7¨3{ŽÝ™yO³bÏ¥«…š ˇ,ùipêºr½`Ùî_D†²+¯µ ½,hKÍç“ÿæMdTô«WÙ~¾þÕ¯+†I$BA?ô> ˆ^¯CQNµÇaÞæÍ2™L©TGðsƶüü|=Ž[6ÍÌê F£±4ÿ Ž|Bâׯo›s÷|žM36Àw}‡€T*µrÜw2Ý,ÇñÂÂÂqsæh4µZÝ}ÈT’$¥Ri»1+‚J¥E¹ø%¬ßÞ¥  Àd29ù4Z·µK~~þâ-«ËÊÊ&þ˜7M\­V«P(Îßx`–\YY©T()ŠBÔ)8Ü÷fIa¹|ô‚mF£1///¦Ï¼F,–L&ûùøEsþ²²2•VËáø<ÊÎ5W¬¸8_.-eóœ6 Ö)‹éíÏ`åèsëEAõ¡R©d²Ý–îÈX *E‘Ø-".ÊËh4"»ÿ¼ýçm¬™ù^vÕµz½ÞH¡Q±-=¸`Pê»ÙÔmÌ¦êœ …‚Í÷Ùq´ê7U«Õ8Ž»Fõ((íaN‘Ëåqi} Úö¯–üi]™Ëã[°Õ„aFŽãdeyѶô”*AuzÏ3/ÓëõÎÎ.VVV8†8òvMI$=ãü?ɇ aT3Y(ÓBøìÑ“¤ýB# Ú^·éVÁr_x£5²âËovÁ`î huG!p‹!´r€Ø §’ò7€Ïd<}þ¦uÏQþõj¿[,¨¤¯.\y>õÌ* H°µqaDp@H•k,øÙÉ3 ™¾J-ßB¢®(ƒŒãèÉ#Osu= wmûùÒ¸‹‹€ €£\KƒÐ,µ•Rs³õ£WízæÒY á$Ö<6Ôñ·S”«`Õª#Ü`Úô27Hƒmø(ÀËÌ[JÌ">Ü$kÝ˪§ÓWîX·üI)Ú¸YC+e2 ~tR•3JòxVÞŠ7Å<¶¾8ËÉ7^—¥äø-[Ú• ü6ÝúÊä*+‰°êסzšÏ&2*Z.« 2¯‘‡ã'ΉÅÂw–`WÛ LjfÍSª=Q‘$™››[Çñœœów•J¥RÕ\ÕNI+Ê ó+¬½ã‡§'TUû>ÐétÕW™),,4)++3)--­>«×ë«ó ó÷ׯ_›SòóklNxO2P…3lzöê(EXùùùæ¶TË/µ|3";·v=%X¥Þø~Ô,š/ð÷¢wa|{W„ßqøp£Ñhö"öaœïš)žÑuêóÌúЇ9 Ã{^6ÞËón/ýLFã®uó-D6$(ƒß¼ûáüR%‹¤ÖVVä[mÎD’l6ç­™ª*l)m©ú_†z_Yæ mk¹ºé<ÕÀ+å†û–ÞCêí_4ù‡ÍK§÷nî²æ—œéµ*Ê+EV› ©‘X‡ÃcPzÀý‡Ì`['çkož4:zãUb×ÚöööÿÈä8g LJ-ùîô4uïô¶oÛ!M`0á(‹íèâQxî @ìÞê¥Át¯qBD8E‘6î|‰H’Ô¸&££²Î®6:4ŽåPIć¾m½=œ_¿[}]€ªT!¶ÌOÆx¸ÝºWÚ¥ŽâñÖbVPª5@•Ç)„ɵ1?Q„I{åâi·¨~æhÊ!Q›3ögª†… ñG÷Ÿ–ËU»ÿÔ:ÂrÝäÑö6˶îünN_µR‰ëuŠ‚ HÒ%$Ñšñ¤~P3(¨¼¼Êi¡Y£¢Õ)š/åøÑ#-SÒ.ž?d^Þ²E«ØØ†æÞ[­æÏårÅb‰Ù.õE (§Yׇ¢(…¢äs¼Iýu$0bk’üR?¥¾DÀbéÕ´‡ª¿Š¢>T~óÒ£¿´>Õ0ŸøÊÂp‚¤“A÷Ç—|útDdäþ½û^¿Î©_¯¾þíSaî^æ‡P­Vá8ÎB) ½Jýü]™)p8eÜð^CܯܝU'rü´±§/¯Ý½ÜLgÏL8v·xüî-b£=FÈšöž´jZ_±{Ʊ¾ÝCÝ;.Ø;¯S»·c5Ž ² i“r Ïà¦ûçû†GÕòqOÝ-µë]_F4]¸yƒ¨F¡å‹ OnÌÊŠ¢˜l¯XZÙ»»¸¸†ÇùíêæäèТÇÌÍc[íØ¼\({zº1ÙßÐÆƒ‡Å§4Š™[R/ñ»›üÑnÜŠ"„ÖŽ.  m€-òðôx“ýkrJo&Ot¿W‹¸vSgîÓG.K¨åieï>{ï¥é?ŽNkÕgù(鸕çZû@¯fÛFì¹X]Úî¬øþ¡S/ö߉6zÇ´Ÿ9Bàl¢ 2²F €^#ÛocYD·Ns”ðô›yet;ûÉq­º-X<6,¸. áYºººX¹5Ù»°ÀÃÑo)I¾|ñàÆ@«S4_KË”4µJ‹ ¨9Ì9ƒÁp°wøÄž&Š¢ŒFãWxñù¢¿É¿s+¾âÂj“ÍÿOµwp2a¸ #’2èÿX©B>êxã‡éSgÌœ-­(g³Ù½þøñãA|tËù¶•z½ÝôUAæôIcr™yp9q4£C§Îl.ïï)½Z¾8¹í,1fh ¼5 }@¬üöÒ9cGuú“Ó–Üßz0kð.ð1-çÍÝݧ³ýúw®÷'kû!´FU“S'O°Ùì¤äÆÒŠrAn^¿Ÿ$°´¤ïÒ{œ:~, (P­V{xxæçç[[[ÿrè`÷^}èˆ~ÿ“0 “ÉôOWä¿Dîëœò²Ò؆q…ÅåÓ×dté5¨¨¸È¼º ÃN$E$AR–|næíK›gö¬”V@^^w-^²Ô<ìü¥ Ã0Ÿß«Oß¿£M4ÿJÖ®~nz7°£ùÿêoþß²õo1Æÿ·r¿ºt–]ÄØQF‚ ǰÁCÂ~OŽGxçþẶ44ß—Çåò †@`‰¢(—Ë}òäÉåË¿²Ø,óf@g—Þ½ûh4ÚËåƒÁd2ßs9Fói¸\î7±J~ê¦çæ¾æp8ŸÈ@óÿµZÍ@ÿù¹]Ú£: ÍŸÄÝÃS¡;99™ýEÀÅK­¬ùþþ>&“Ñh4íÝs¨wï>$I~è4?ô»ÜWð­æy?®TÙÛÛOš0Ö‚oñoˆOBóÏ‚a&“ýÕ—“˜&'§€Éº¸»°ÿ9¯,Α³€ý‡NY©0€½ 0¿ÜÃË ¾vì0h2ÉÁÑžñ^0>ÂXZV)²¶·à2?G²I¯©”«­œ8 ЪpÅ–ÜϯEšÊKKårµƒo „]U­²B©g:;Z›´Z½…HÄ GIš?ÇÉÇZ¥¦_8w&4´*ö›Írv±+)}Cà¸X,qr¶‰ªvaE%ŠÆ×èß³@ІæßÌÇ•ªƒ‡þÍõ ùײiÃ:Ãglyø=´û÷R‰±¶Þ½ö»/Óä¾m­úî™×Ö÷ÓŽ—H\½ý§·u.‹ÆÇÇDu**ËûºZQ„áø¶e‡(6nZÊ÷”Q•¿vŶÔïÇÔ¯%ùc/P~ýø¶å»®­Ø¿»ôê¥;®–¨\{önãeg Ÿ§aÚ²ýÛV›¼hÊCjVpUâÃ+{gZÎÚ*òŸ­]½»ß¢N_ÕRšjZ¥¦+äò¸¸FÄ[[Š¢V&S J¥º~ýÀ  ?€ª8K(‚•üæ^ˆ"t¿ž:•W¡âñN^Q‘u9€ÑK¹iþ£N~þhF~¥1.½}€³ŒêòË.TjkÇÄÕ÷CÜøøöe¾W§òŹ˷DNuÒÓ’˜U)U‘÷ôÔÙÑßu÷–p)Š’¾~)寸ò<©'¶nuҚǢzeñ…3ʵdíz y@ñ«{§ÏßâZ¹Æ6iê*bkdÅW/ýZ"×9ùÖkÚ0¾õËêÇ•*ú…˜¦³ãw¡ •¹«W­{Y( ŒMëÕ-qïŠõ©]Cĺ-Û&uâ#¨˜—Ž™3Ý™nZ¹ìÖ³‚Zõ›œ¶uñ¬»Ï+ýBëT–·ì=®®°hÞ‚UrŒÛªKÇf€º²àî“W†®2>?9pá!W;—É´¯?±_‹k6hÁÆÑE¸QuéØþƒ'¯YÚ¹w>Õ‹x¶pÑ©ÚõoŸ FòÚ•kÍûÍuaƒ×Oömß/öo^fb{½{¿tŠ’½[×ßzVhåPgÖ¬Qê‚'«Vm,×3½¼êµ4tÒBoYöÍ*«0qãжýŸÄ´™ ò¼kãÆÏ»rã¡ÊÚ5©EzË—#[—¿üPâ0ræX„~Ti¾€ë×®„‡Gd>|àîæ^½àæé“Wµe  <®H,±f2†S…cxaAIxýèj $&3sŽ{bz”›ÅÊCøËSxir’"@>Ó-EQ(ŠÒ]÷ÿ&½ü»_S:× p®”áôžÞNLŽÛøÓ,ôõMýùuÉÂfõÚvQøúÅ›çÏ\^ß8í±y¸IS±{˲³6miÔÍ[@ŽmY{Ͼy¤ƒøîý'®^.?NÊçìžèphò=×rÇ×_½hÌ^ÓÀ;x˜EP¢=ùè\fѦ9Îذáà­mÓ­_ e¯èióm](Ó Ùh¾ÂPùóÆJ¦ó¸©¶/ž±YàìÏ2|ÂÈö®‹Å¬™? ˜'H\<­SA©Pç¾Ñh«ÜŠH‹²Nž8;ldŸüçN©þ¦+¯ì±ã:öiyÄ“Ôn\²†íVoL¯àõ+–ï³õíçüNw§°·®±¬|"@]¤Ôê5~‰m4÷F/Þse°OþñËY?¬\°aä€l6«¨ØËÓf÷î‹s¦¦í^³ü2_ãÚÓ_°bíVkg¿&A¢œ§7J•¼æ¡¢{ëög+xu¤JÄíaÆqS¿”æÍbNÏÚ—W‚9{°ž^=±jó¡ï§-ö³a4Ek×®0XM«7mÂdkߣI® Í¿wã‘l÷rOÅ›|iJûô µCeE¸¸Ô¸aTvæ•3 _ºÞÚ¤ÅömßRbŽÕ¾EtÒàˆá ½áMeµ?:4¸AÜ“—·N;ó]bo±}­Þ]Û€‘Þk@°=óåý›ö\™³iÍëŒÅ#Ælسø{Ú£:ÍçD’”«‹›H$®6/5líïçcö8`ook2ée²ò·3€Ç­é¤ A7¯. –´“ò¼Î·.ïÛ2å²»Ÿ3ËÎ79Ô~Þ¬ŸLL~»StkN*K—N¶çìÉ1%…ÈÑã›6-˜”qú–£}ÝVm¯oYþº\Y«^óM›æ‘Ù³§ÌWê”÷g×KnîÉY³z‹|×µ«F-5¶ ´äé‹Ü€ØV %«–oðŒl³uÓœ’;§'O_]TY?©ý¤™“íí×yL ùÛÚxN_±ÆÅf¦‘ÓºäÌ¥»íÇ®èÚÐõ:æØÎŸ›Îþ^UþØÀôIv`@J·ˆ–aI‘#Þ®?¢žÞ¹S‚ è2À’Ë“¶$ó Öctk¡–­N€fÖÙ߯ß9!}ü€¢¬@R;qçªÉ£¦CdOÈÚA¦•(ÛÞÖ¬íŒY¥pÿô/Þ'™skʥƷ»ÎuŠ7«WnëµæÀåcM:-€ $ë>åè ©]™oÒÚû#Omýc„+v¶°µµy#/“åÙx'P–6,*Ïuüv_?ò¦‰ãö½—Mƺ߼´REóŰ¹¼À ˆŽ W4õ­J){~áV>Ò.-éàCEÛËú^.w¤àf½Åá ( 3pÁ`¢€@y¹šaiame•#/ä”*$q`hüðѳBkhS5Í0¯3O¾’YÿŒ€Tªu,ßÚJ¢zV÷—¸4ÀM˜Z­'pL£ÖŠE"gïä sÇÛ¿•SñâÈ…ÇØÍD˜ô­Þ¨'µ@hu“Ñì7ÂÎF¢Í•¸j-&²à•E8‰‹ËTn î °çBÙõåžÕ5tt¨σéÕ¯²s¬\}ì%öþq[Žœ}y;#6!àÇÌÂZ®Ž¥’ǯË™,àrF•€o¶<©µúj÷îjNgÐ)¬Ü<¼zݵvvú·ýAiþŸõìil\£›7®Õ©€ UJ•Þ`` & ÃAQEÕù1èõòÎߎi¶N~×Û+m처[¬=c¹ðúÚ-‘kHŒ©·ˆ ö’–HüS¦@@ƒ0/·€Xâí ÷Ïïž6w«½§—©$ßhOX8:z5Mm ”.¦aFp#ˆµ{RTéæZ;*¥ €1ºa„Æ#Ùœ~êáCެèéñý™gY,«„f±´;¦?EQ8N¾Ö˜,6‡ÅÔéÔÖj†Ë·€ý¿¼~¡–93Aü¦"ß<øÓµS¯Â›—Üó?s²ƒÿwYr=ýƒÌËa‹³oOŸ4£ÞÀÕí% /Q3¨V ­NÇå[ El¨B«%²!ªÇÄrùÜá=ú„Õ‹ð,ðü ¦Z©¢ùb8b×–IQsft=ëæÊdð·lyqû–†c6HÔvî8Ü{Õ†• û·éÖú~x°m­ðA=ZIì¬Y? ÒaèÄÔ€0±oê¢e²Gü¶~q EsVþTè~/_ùODsqÞä~Ý\Ý]qçþ£ÕÓ[¡Ü¼êç–³/UWƒ¢H0»Öd°kǵT®Ÿ:tÔ-ùk|úºá™{ÙºY™=jIœ‚Û&yŒèÖÛÓÍÁwìÙ¿÷Ñå›SÂ,¡:ØY¹²hÙÔy×o?6®šW–Ö3­MÛËwæ<ÂÉÞ®i§¾]R£æ/sÝA„ëµIì˜>:aüFUeÞŸƒS–¼˜1aBÊØU]üòž][½r›ÐÑ¡[›Îu=… B§®œ›{Ç3OIÔ0 1%~õ¤ö† MiÞàäæu§ÎœÏS>ßô‹w‡´(k{ O?oäÔôZ¥õÛ}bÆÀWù(Ðx@ïæ~åïLó¿Fl\#¹¬244Œ ˆê?-ƒAo)@Ï?í‡é®xé…s‰lyá½É XÙ"úÊÌךPvYnAYLÓZnéÆ3&hýøÞ“J…” @bÐk €Ç·Îø$^3£Íái­7´@R UÊq£@«7qôZÐ0&‹ChU ãkô&£A ZféââNƶ˜Ð-ÆtZ£I­¦'Áÿ[p„vî6‚÷礪5}xÿ±KäD0Þ.cù~ÔpäÔhp_¿›ç/ëM˜RZ©È«PzÄ„@eá“y3ç¹%ô›Ú3¸bgwóêíQi¾Y/rýÚb:b ùò,Ë癤ȵ§óˆ:€ÅCR<âG|ó¦ÑJÍ—ƒZø't^é× _ªáZÚººÙ‡xýdã` ×-Â,Ø¢=w­Š,Ó"'6œGÍXðüµ”çáÌÀÒ5Ëse˜ë÷M"{>øÌžá&Óaz÷C,ç×~¹[Ýb…ÞBâäîÈ€ÊJÉ“ã×sÙ?…9ÑÆµÁ¾CqÖnÔJ†°aõšÅÙ…rGŸ>„öʱà…q„B|úœ(¾ò“gH…³´vµPÝ·ÞšIQ;-èv}Ó8ÿN;¼dÚˆÅJSZÛóÿÔ ²íHÙÓܸQ„¦â’ Yl¸\8´låÎ_" §w/Žýî‡ùÃG ïÐqÈýs°„¶ƒº7ñ€9ÓÒ||]­<…Ü?¿÷û¡Sµ$ÚªËøEÝ|¾yK?¦††¦šË–ô0À 7T‡©iß©3ço SSs@Gª:¾ò· üòçäPÕ ¡>ªT}Žäݳ{š¬îñõ7óÿù :¦æ31‡©qs÷Ô¨Õ(вX¬Í×÷4dÆÓCëúJÄ\“ɸ%D$/¯”ÉõìÑËì§ EA,¶2’ W)H’d2(¡ÈZ­×F£X$€·…£ÉdÐ ZµžÁ~tbõºGµö/é $(C©Rñ9Lç7O&J b¥×ª„–a¨•J‘G"l'A§Q -ùÂR©”"—D9(‰ZU– ½VI’$ý£WÃ`0D"Ñ¿ê† |ØÁj`šÝP³볡æÐú.Ué/n;|âù„Æ|«º™oTöËùoòâ%üµajhh~¿ý‰ýM£ú3¥Wë:ßV·ü¡dA:MÙò™™ihþï­øó|óžC+U4ÿ þ[ajþΚÐÐ|&Iñù8Žó-,ÌݯeËV·nÝ$‚Åâô’ˆŸØ˜X½¾Ê% EQ5#ØPU­‘$Y3‚/ƒ)øqg†ù;†a*µº¦Ø:@µÌjGï9ý0ý= 4ÿr¾h¸ûhæÏOü3Eÿyh¥Šæ+Q¼¹øý˜½¬ƒOöZeÁÕVm†»Õm8wÃ2ß͉¯›:˜Ólj¯X×OK ngìÈxnœ3~Àå¬e¼røˆK³^Õ—Ï.gìù5{ô´qâÈ!N®žz[جwµãTæÈ #ø_ùdb'Öοɛկ•^ñzç/÷;tn'dÃÔœ†æÛáëç'—ËÝÜÝ 7÷@w7w?ß?Xœ§×ë«•ªÏ‡¢(:² ÍÿCh¥Šæë pÔfÄ(ó¬6)—JI!IŠÍˆ-ù$a’–•cÊ­ÝbNÚ4~êÂJ<˜€™tjµÉÚFbPË´ÏÊ‚)­¨ÀH¼èõs.f e%Añ,Db‘òòaP?jâ¸KO ²²ç¤¡°IÃ$‰o#"ĵW¯››{fSz¿¹ž¾þ¡»þ8¡ÊdòY „p÷ÂŽá“÷ܹéàü>[ñûzµh?ÂÞ݇­×´²gžÝ9|Ú2`ñ½Ãf,_ê/€w•òùkT’Ö¸lZ4bü‚ý»uÒjÙ“g÷ض»á0X’uÎêÏî0~_,±u [°bê¡e‹n½zc˜5¦"6®ÿ Î¶EÙ™/re“æ5{¯aož\hÝ~ÏÖM`,óK âr®ó¹¬GwÎÏ=sÐÊý¬ÝC§¯YÛx&ü¸thtpW½7 Oè9c|'×€ŽýK CRûMïÛÿõÈQ¸61‹:­N\´¿Ùæ“Éîïìj¤¡ùÛHIk­TÈãã‘$e0ÊÊÊž>}š_Ï@«\*X …ÁAÁ&“‰îŸÿi FÍ™YšÏA­V3˜ß@#¢•*š/Ô#¢É¡ÓüäfÛE‘zþµ[®;”jOŒøýýÜ’eÓV/}Qþv‘†R§'ɪ1ZdiQÛ/ ÜíÅÖ|·kW¯ ZxrpºßÀÆõU7lÛÒ¤K‡ÆÛ×o8ᕪwÍrq£æêå_kÅô”…9ØŠ;˜»~ñPøyÉаc—Ìì~se÷уf·÷׎Zy¶{¢ÛÜ~ÍvŸUü0w;§0b×Ú8 (âúÙ_¬‚ÛzUþöÿñðÚဤ1?¯¼ep«ëÊ Ê¿uîà©ã'¶<¾Ž˜zϺ¸èØófÞè„^©ûŽf¶ŠŽïrø@4†ôtûq@Bpô†u«º›Í:®‘÷mi©*ºú 7yðH(/-µö š¼¬y¨…,Kæ^Ûê/ý}hh>Îí›7BBC?~äììÂd2Y,ÖÙ³g**Kœ ‚À1üùóœÛwÊdòš §hþs0 6›ýO×â?ó[hT@+U4_YQæ UL–Àš  ~Ãd¢¦&)ËàË^«6t¼=4¡H HœÍfò,¬ ØËºTV"´uˆNpvOž»ÜÖÅÞEYþâÂÕW‹gÅEl®¥«³'i"L:Ø{ÕÇJù‘n|žx{Kn–Ë H®*Ñð¢(\Ÿ¿e×µEV¤m&µ¥s¸:‰À¤£(ÜB`U?ÔïÊ%ix‚‰oi%@ÜÝDr•žË·VÊe¡¸‰àñ¬Ø¸BÍ¢HÀ«GwÔ¤(>XvnuìÜê„)>ÚLEóQËÛAP;;{‘Pdž¢€JMmÌ`R˜ ³°ä=ªæf:‘HÔ®Ýwµ¼j},°: Íû ÿthþƒP$E`•2ŽÀ€‘‚`&(ФH¶À½oÏù«ÎÀ«œ\ŸÉ¤ Ÿk@ ”0IUÀåÛ¯Y—ZžµŠ³®˜@±ÿÊK‰w½:Î6% ­‡»;Å”•ïŸ .î[k?¢Ê¼@`˜exùøç?kÂáKOñ ŽÐdŸ×yøòsŠc2¸‘Û.Ù´âЈ¦gÞ0Ý­¯\;/+yó~iýIJŠì3˲sä:ðö÷,,yºç–yû™ÈÕÏàÔÑ_ü[ ¼-hŠæŸ…Íbi4j‡£ÕUiH £¼¼âñã§ÏŸ¿ÈÌ|˜–šY§Aƒ ¨è¨è¨è`yüø‘@P:ŠÔ(å•ÒJ™\®Ö(äw]±ÓÐÔ„" ¥LZQ^aÀe B‘¸Z!“J¥jP‚ ”^«2â¤É •V”Ëj`0«;nÒWVT Êí7êTZI˜dÒŠŠŠ …RcB˜R^)­¬ÔêMÊ@3ê*+Êer%FP(Š’¦RȤR©J£GŒoÞi¥Šæ‹!1õÑŸW ¿ÙÕ…×kàè‹OtMZ´°0-¨VßÖ’“:÷gÖÕåQQ1›OÜb0ÁÒ¥vÛ”èE}šÎ?’éܪIÛáí»Š‚4ŠŽªÛ¤3»ì×>݆5ïÒ5Ä×¶Û¸ ò‹k4h0~ÎZ-ÿóµ¦üÁ¢·fÌHƒ*×#¨«‡`m³ŠÅˆMí“쇧D4@NœÞÄ.¹][4çtÓ¸¦M§f|ç0iѸCýGÏ[S /˜½üʼuƒ Z¿!1ƒJf ÙP;¡wz=ªûwBĆÖâ[9úùyp/œzlõîÓ§”]ØØ¦Ã€6³~iâå¨'ªK«ûN\ÓeÚÚÚöbÖš¿÷É«§`²;Ç~-éßÚ§ºê-ç/ECS“;wîXYÛ<žÅ@(£jð—Je¥²Š eYY廟=Ë}þ"ïÙ³œ§O_eeå––V …Âê?ÂP”Þ¢Y³V­öÿ~ЈÉ/åÀ³üuz­±ýÏ ¬Èîõ]Šmÿ•g‹Å"âñåCmÓS;th?däÔ=Çç5åc¾ïrì¹ôÒž5Ñ ""Ä©EB‡„IsxÃ_ÿ=/Tb±?¹~æð…[+sÎ5IJJKMMIm{Cʱ´äÝ9½£Uóæ:v=õÇ"Ü‚Í&WL’˜Ü¸c‡N+~¹%‹ïœÝÒ¼i§Nú ùDÉåëLOÿÑ|1LŽ8µû¨Ôî£~KЬ öz2'¬ÚQóŠð”Á¿¦ 6t9¯ÃÈêSüqs–ÖÈi9w펚eUk!¤Á4zþöêpÅÊ®›Ð©îÛ<ÓªÛ¸…ÝÆ½=ÍuŸµrÛ¬š‚ܛ߼ÛpÙ“¾“–ÖÜDηr¿hÃ[¹–í†Îl7´údhg×P`{§/_°tËÞªŠé_RˆÅ¼ë…æê©ó…BÇ%?®0Ÿ-/—˜·…44ÿ"“Ëe²ððH‚ ¼Ú38(Ììÿ).>ÊJ"Òë5$ER$e2aGž¯9ñ‡ hDí ®{/¦Ø©wìréÊ­ú½šÝ;~ä×ÛY¡}b›ŽaVeožŸ>~2_ªq®’Ú!]÷òƾC§LL~ÝØfñÑÁ/Îþœ…vm^¯<÷Þ®ƒwz êzûØÞ[_s…Žé=z»óuÇ©TëI 1”×´mAöýS·^2ºuO1¨ôkɉױ3'w®\¯ãs(\W¾߉ï&nß)få¤ÛÖíZ1£§VþÊH8´‹ógŒJïÓ¦eãIÕVŸü¬Û™¯ÊZ%¦ÛZÛn>ÌÖôš4À׋Ì*èÊ81§ÝÌ)KZî¼uûñ¡. ˆw›6¸Ëý§G·³ÜväΣò"IÙ¹ äéßwktëú•ôakg n|zEŸ™ãfžÝ3G¯û–{±iKÍC¾kzùK©.TäÓ>½|`øùððÓ0­Û·k5Ôµ¯«ÂqZ¾aÿ­”ï³÷àžjvþM[%¸=ÙGóoâÜ™Sb‘èÎ[Z­¦zYnrr\LLpDdÈÈ:ÒŠâ{÷n?xpïñ£‡OŸ>ÊËËñöö|ÏII‘ůs‹^>VjôÖvŽò7WWm̈i™^ÇN_€9{wÜÏ‘%6O ð÷4ÊsW-_g{D‡zý²mÍÍçR[KÅÆ%KQk›¬+'/>x)sóЉ;q-R}l4s¦-f[qOeº“¯‹U>;{þ&SÀ9¸iÓ°¶°ø¸{Ršÿƒ‹°™ÒrÃÀ¤­4˧V-ðôõÖ–äjÞd^‡w \ž¬¼B§¯Z³K`šC»ö„µî_ß×Á¨Q€²è…šoèbE±­p­VZúâÂÝ’†í{Šœ- ðrJëåá¢*Í—dÚy'HtL‰5ÏUᮎֲ²B=Àãg¹ŠÒ7ZÆÛ8˜ßÚREóßà[i'ßLËAöž‚ß2Ä.¾b¨¡ó}›Rhh¾É›êtÚP.—W½¿O£ÖÊ*†™ç@åpªfÞQU©UïMŽ˜ Љ­"¦#TD«~ééñÏÖvþõÆ%çƒzy±Ik)ÓÊ Jå©ß/k›à&“U¾º}°ÜÈ;r‚/»ðùý»OŸ_UŠP¯˜2>né¾&ÖU)$®;±{Ãù×ÈÚ³>qÙ³¶_È^»}ðó «¦+™3i¢m‡éaž¼;·XÙ<."0)ÕWüÇ×ÒÐ|sAQ”¢(E‘·ú‚ üî÷áÃHRdavž–#ê˜Ò`æŽ-²Ýýã÷ÃËmøâְ¡é„ì9°¬èa-‰µS‹´¸Ä Ì?Nq·æ¨XŽmš4ZÏOçŒ NëídÃå4hq`þ¶A£ï Y¤Ä¹~·ï€$ ŠäÚ5 ¶Ùw¬tJï°J©”6Sý§1je»W/>yýޤú}džiM£îZ?õÉ Y³×ˆvÏŽÏpoÞ‹€ãÆÇ2vý¥HZ´zåÞŽ-“ZYÚrPÓzõŒZ¸­‘w BšX—¸}fëÔ¥ÛúMZ4ÁGß„­C;¶j°xÝܼ JÊ®]Z#†PÑ!Þm\ïÁ¶Te|ßI.÷¯þy×a#•¦Éû:yþSÐJÍW@æß¿8oÑb¥0(L:cì:S£VÄwÒ¥±Añ²O÷!4¦u¿é۬ݶrÚˆiæ1<;óì–Ýwçý8éúîùg “Ûx 4ºP¸r-í½¼@ýzàÀq¹RmB›£û¥1?Ó¥¹O^–êú,ç¯Ýºû²ƒHe,Z?gõØ!gnfÆwX9{Hù«;ÓgÌY,oÞsò°.¡‹§O^´çð Æ°Ç öèó €ºâÍý§9íÆì`½Û°×ϯßv}Áâi7÷þxJÜ«¶§K6†LÜ·^ßÙñV…ÃFÍ(×’éý¦ h:¢W—0X±)=;5uiÕ¨£ýP­é0xl£:Ôë»|¯EíºRY~ö¹Ã§¼"§ÐÍ¿ Š"?f2Ÿ¢p«iXe°¬ÇM›ÎvâÉZïÈ&“¬ë[Š|ç-˜zùîsŠÁ–¸‘8#¦y;;÷À7*K7 CçïûyÞ~¬–X¬“5©‘i¼cOØÃñ §TR+¨Ù3îgåƒåàS€ÕkÐ Ž½£N£! ($±ëè6(uº¿ë~Ðü%0Yìºõ¢âÓQ x8…„5m÷ƒ³ÿË"EíÈÆÞ¦)G%OήMš·ïÞo¼\‡Zò¹3Ôw6œÅ4dÞ»£×sêør :]pDòÉcq‹­Ój¬=Üp’Û¶‡È34OfŽjì!• =wî¹_ﳬÜÄ…ë z[{çÄ„x#Kž”⊂\þw?ÐJÍW€º…%ÌR”Ôã À( õïÝ©7víÂ8^¿ásïµLÌÉ >m£¡ù{@QT¥VÛÚº2¿M®Õ˜Ó•JÅáH~ë«(Ï#(Èh4FÃ2¨®£N§CD®ÍSü1™L*¥EÙþõ¢ƒÙlÇU*•ž!ŒMnŠ¢¨Á`ÐÈ*H’dr„>‘1&“I«V!"rökአˆÁ`Ðjµ®ÞÞ†éµò£?/_»ûÚ’c·•šöðþ_‡Å&´ëQ}(—ËQ”å_?®NJ„F¡œ±f+)*•Ja¸×mèÅ5çÔétF£‘¢(åy‡F«Ð„¤c“Ñhtòõ ®ÊI’¤R©DQvHLR(‚à8®R(H’4qì›§·3‹2§Z-}CÇq¹\ýÍÇaZ©¢ùJø€g.޵š'‡»ØVÄ–¿¸ô@ºïçdp²ãAðö]˜ÍbIDÖ à²ÙF–R¥¨›< @Ø;¹vÞË›ÅÙr3ßÜ?kźøÕr{¯D¼ðرKm—ŸŠ0YÙ8¦´mk e¯³êÄ÷ç ë”0}}P‡:±  [Z­3—ôŠLF`óªä¸r뺟“æ^û°Q,S"² .‹Ç°¦HåžÛ­Üüºµ!˜rÔ8»CÓÀ„f±.On?¯ã‘ÖÀ€²¶@Š‹¤ñÉ,€Ú®†ò£•Œ—÷³Ë´cÛD€wHÒèÄ÷s¢ÂªïÐÍ¿˜˜˜={ö`öñ P¸¸¸ôìÞ¶Ú«EQ*•Êü ¥R †éjX’ªÓ«e2YM©8ŽW§P¥Õjkzm0xf°,úŒ^4j¦H¡PȤ2úyù¯C’ä{Ý€ …BaþŽ †¢„Zi^Þ¤ÓétØ&)Š2÷+ƒe†QõaN‚ äryÍó;@õ¡Édz¯&ßZ©¢ùJ FSut’¢4J,ôIp,lâk;φÞ> Qk¹|>ƒÁ„2ê,€Éd¡FäÊvB;Ûg¥¹!·_–º§:Fõ0;¶F|ššƒéË;GeˆS³Gu5tfkcó ï@ÔùË9‰ÝżWy¹Ï!,ìNf±Gûº¡XEQE<“Z¯ ¬’5îÞ¸ù¹Û2™l0  HŽ,e÷4aPÇn­zíËXf/²ÈÎÉw´yñª¢N7Ÿ;g÷_Ï6yÚJU£ƒLQøÀ9·#Xœ¿|ÎÆ¯q-¤ªAÌý÷@óï!¬n½ÆÉM>±¥œ ©TjVtþN(Š’J¥R©ôo.—æ¢(“ÉôÇù€ ˆo»´üÛB+U4_ iR=¾sõº /_–O˜¿²iR¬X$®^•rìf.Ý9ö´»»oTÊÔa­œ\ÝØãSšw™º¬_LŒ¹³ÏÐѬ¼R‹ÚIɶ»G.u׿•ÜâÚѹ´wß–K¹b{¯aSçÛÿ6MFâ²5Ë÷¶^}ã·zPeŽÓŒ°¢Z™´¸Uêa>S²ìP$óØà)SÎïdºz&Žoa íZGôiѨ¼ï ^½ÛnX¼½ÝªsÕbôÊ’¥ófwž3ÖÝÖÁÅO„lí;d;¿˜í:&—ïšÚ=üè¸É['ôJ=uàAÓ74¥Srý³s¥{§÷=bP7éàY!/Y..¨âp^Ç«FuÎ;ˆÇãåå$<ŠgV$Å4oðè68<`*¥Z(öíÓ ª8œ—Á¥©á¼&UúÉ&M»þkÒ•²'g=]­ºöK{QIë7÷\{öÉK¤p¡/ì];~Æ’-©Î8Õ¤I—¿+~ü‹·»B*•:ØÛ×k7¬ìië¯ç<ã_Ëkr.uêÐ×ø²¹e˜‹?};þËEÃhSéÞ½»ï$—ryi8ï8‹™bF§Ó''§ä—ä””¨Tå|ˆ$ÿ<6!uQüÚu‡äŽŽ†•ÜtrrªR¯A“-ç3_ûhG¬éàÖ5+¿Ý#vt|z—ýϬ†4W—€À ˆˆÈa“—1rÇ ¿ìœ¿`½Ðѱjsârª\Yab«ºÁ8ŽoŽKµÕÙwz·kâêêúÉ‚í,†af5—/ûräO72OlY( š´êcª8y"æÆÑïíä“Ë0 @·­9wcqÊ 7g™LÖºç83†aVš~¹u£HO/ïÙëÛ6t~ÿ2¹LÙ å™t†a¹/wiÞÀU¡ôéø89s-Uœ×€hCy¡š¿|ÍF–JIH´b8M[휽j(J÷àÎ=K:zø…ø·/(I™=þ - @‚AS’« ¯©ÎM.bƒ=D ÷ãõ4²4|‰;Äß¾®§Xgÿš~îØ_Z€ô¥éGÎÞî2ù[xtûšÅiŠ’{ûóõÿ°Z$ËZ3=ºxê5¢?ª÷(Ñ MÓ2g¯À°žyE= ãÜž)"G(Ï{ûû£‰››ÕÙ7åÊì$¬•’¹Õ tå”0s¾Zd›4”eÌÙi)y%™£kPp0®/¸ÿ0Ê|‚j{8J@_šqôì­ŽWá :`˜I$Ÿ¹ÞaÂì¡Mjp³}rÞMW¯Ü“HÄ€6fôÐààšˆ¢(†e z÷ßþP¹bô¿[ùíÞãF¢±“Æ  idÌ•— èáÑhˈhSJb’ÆHI\ƒj qF[š—˜A ¥î¾þRZUduˆr7”ë¨W§ß¼]8aE€(x’qøG­š~f}ööâÌ»ûŽñt àÉ€µªŸxÐiÚ· *GU.ØóEÇv#Ö]Ùùù´Ïæµýßþ ÝC>ÜcÍAß)ýœz^˜d4xŸªÕºSâã«»~XÚ}ÎÉ˽/ØkÒ†£?©Ú“3Tq^Re'ïÚ´r×™üÒ‚G,U4jÈ@÷­Ã]ˆL5å+“|ûź_›6kâÖ:Èߨõ„¢hœxtóèó]»ùÛÙæìд^ÛÏiÒ”Õᦧ©»ËE—oYÿã)g…‹‰LZ°¾ûŸgGLÒë ólUÇXͱŸöd•«wï=:fÞâ½[¾Ž;çk´ ºhø€A“·LÛ¿~õþDUq2²6Ä)²YAj ¾jÛW]qêæ'݈¥î\¼èÚ¤±üÝï%¾^Š^N¥läÙ3îXÿݑĂ ñò/³oQøyø†öwt>»vîÉ„"Og¹_£E ?·H¾wƒ*Z7p0 Ðå»GkõÖ®I àîrÞU“§Lðõu3›Ì R•]¿~µ¬LÅã‘ Z¶Œ)**¯(l5äí=|ù›ƒ?¡L÷>Õ(×êµ4M3t¿Xè]Ûpûä=‡Î;¸¹é´ÆvC¿ØÂqÉœ™ž¿oîý|K|±½ð^Ü7íýlíå#LŸ2ÍÉ/Xnç<*¢®L"æ±+]<Ö°wÅ3-û~ãÿHÎjµ†iõe_¯ß&+IÀœÛE×:µ÷›#WʾÛ<´]çŽÓ—¯¶¤ÝͱznÞ¼L‚*Ï÷Èyë¾LæFèµe°h ¯GËÖТu㟮=èXxAÑGN”:…a¬¶F$–1ýzà§À&y:<¶L"]QJ)éÒ4Ä0 ZK„µê·(°ò:· €fÑá²’tÙ((¬³'¸ø»óÑc¼\²d@ ð¢¤;U¾§\PÅyeà®!Ñ+7κßíGÀq,"0¸çòÍ3&Nz”‘»jÍþå÷Ú<+o­t¥+“ˆýÃÀÓÙΞïvïîõ!sŒïæ7½[cMÖí½ûñœÃG×:sâôÝY Ü}+o—¶è.Åŵ˜&\>{õ÷ gvÔíò¿±ý¶f?³ÿhÞÛ#&´ò¬Ñ¤_ãHh0õ|Â6Àp,<°V§¥›ººÓ3'Ž}œ ®Þ›Ÿ[ÂLmh¦ò‚«·âëõY >žÓ¾[K¦Üí=ªí¸_ˆ¿úîëÄ¢Í,€éØÏ'ê ˜ùõèö Í¾päbúé[%P0bÐôût‹òòùßZL‘0£‹Oè§_Õ‘ =(€ìÿû;áp^“J©.*Ì´Z)ÛX^‚À¥R±í%‚ ”Ê2 ûãNœ@âþåø>[çÌÌ/%‹º4¯ƒcp7ñæçC†•&Þï4k³hæ8hÔl×0úþÅ·._oç+ÿ=^’|X„½ƒ×¦±'3 n]NûxéwiçÖºÖ¹{íÇ Ñ”>2› ðîþŸNœ[½m'•{o×þ3Åð?oÏjµ œ=¼Ý%†¬4uFF‰›£ÜE!æñxu#¦NŸâJ'vë2/_c •ó¸ ê‚BVƒ•¦mA0VŒ$Ë€•Á1–€ß_j2ù¸íu“ÉR‘æ;7þ×3wÊb Ÿ9dP †€ÌøÇŽ5=0–1žÚ½~×ì—%¬òÆ" €a1ÄЈ227+.ÀY Ý{öþâ« Coì&M¥®Š†U¾§\PÅye¶C]“_b±<Í@")è 0ÆÌ#ì|*—GØ“/<€ŠE€X€ÏJÀ×]V¢S)|jýU«`é°cÿºÝ²¢Ä ײ·-„ØÇW/Þtü›#‡À=À%vËÞ:^‚K×N94ºì¢JÕK1Œñ´·ã~uíi›¹='í~z.5¥¹°,˜Lz¾•)Siù8€¥ Ô`´ÍŽhEߣ†×Ó¢´RG/ 4j;©€¦SÏ\Êܱ¬§:M[¿Ñ§ÌáT ³Ù$ñþ©c ûçVŒgß}ôgÍ"¾YúÛ¥›yü/:ÛEE|µyGþöé«ÏŸ ?²wpi=hÆ€Ö~}?ê%:fý¾ÈÁ¯!Ør’à˜Ûèþ¡ë'O ª9 q͇wTrïPÛ«ˆ¡ ÀhKê´™ôÉðÁ¥E­ÇOqAV¶"Br°sØ»½7´—eÞ¸aÏò J@(r”ª…$Î׉ñ]ÇIÅåP廯uTç¼:–Ö( <Ì0Ô¹VPj)’$i+ ±ñìç¨?c꺌œì«·YXØÙ‹pûä½R3¸¸zñYåõìÜ㓊€¨ˆ¨Œ?§ç&üx!Ù9´EtâÔ¯±¹EÅ×®^Ï,üóh#DÅ~·2tÄ70¨ŸLüdfX¿™NFs~™Á/¨vvvúéC¾_òs“)­™ïŧé e¹VPj(’$+e›‡Ç lݶ¸‹bͶm‰±J@#@,˲,‹X«¸Ÿ¢ÕªåkÊ ‚þ]Ùµñ^jÖí»ô¢ :îØ÷‡ãΜºÄ\#j‘?­ú*tÄFw[e+ªjúçÿË2/ƒPåɳ3òÕb™<È׋Ïb8.É<%D×O—Õ¥®LÛv·KÛFWNïOÎ*Hzô(!>% º“»)aãÉëIII©yÅ4ƫ۪ÇóÇȰ6ÁRkÜ&óìÚã×ãÆÇ›í¶cÍ6ƒ$Ê».>*.)¼ðûeв¤Ç_;tô´•'2[Œq÷2âΉ½ú$º{/«Å‚ží À²R½{«9ñÎÍ'EÙ©²Õ4)¯Ù!:ø—=ßß¼{ýì…„–CǨïí’E• evê›ñ:“îAmd ÿüC¡ÄÇÉ3 Ú—x3L²rkÖkw~ÛÑwܼ¡ ¶JTê´Ür¾sD·hŸmëVÝ»yönRQ‹^AÒ©žB¿ìçK‡výìÛz˜€ª óêÅ).Ì_ñÛÄï¦Vùžóçϯò•rþKnݼQ¯~}š¦m‘AZjJHmÿ×Ï:y;42ò὇ž!¡>5B‚ìy€c<¿à& ¯üùØ9Ü¥ft˜_êàaŽîÛoP„5«S׎É?~òjã–ÃbzDG'];éA×^Ýë4oØ¢Qƒ¬«¿îÞXm6jÝPVi@&ïêŒ7o+´%Š2¤Æ?áú³'O!ïú-£ü}}C´ììÊòÚ¶l˜ð[lìñ›áQ‘ïÅË=BÂ}=½k×t„_H¨=•i°ú ˆ©eYç¬:ýýÚ)$BHÀú;;øøyJq;B¤úq×ñÀÚµS?$Ã6oæË+Ù°i‡ÒˆÕmß©W—ˆƒë7æèùŸ,Xè§ÿl˦móD\÷©j”žžF„¿€ÑhÀ0,/7××ÏŸ/¼íz½sÒÓR® ‘H\‘4-)1±^ýqã<<œù|üo“©a¦Ñxu¢ê˜L&dº}þ̱Óç§u5¹[ó «¾X‡¦õÁ`jÓ¬Æí›¥ö±!ãæ?ÌÈQÔkèØ·]­ KW&$?±ó ò¡Ê‹-juÌ€)Á.„Ô©†¿½eÏνIEõ;÷p%­v ?ÿ€ÈžÍjl[÷íõ[÷-RßMËÓf›"D±EÏžzŸÜ¨ï³úÔ+W•JüÈ(‹jV3ë1L&ë‡<)ŽãB¡ðm×âO̺’ÍË—$.ȨJÎaZE×ò¯‰òî=tº÷º†KX¼¦Öøyõ$BôÝû6ï?׸Uó;—nDE»J @´Ú¨µ8ùD97/^Mê9¸“  íªž–—fÞÿõÈa“$¢Q¨[í&õ4 WbÏÜ6çû>Þ£Sؾõ4ÒÚKMâ¦Ýû~ýÆ‹w2gï8ãð쌭V©4šr__?­Îw'%¢N['!–E ‹²]!ƒ€Gågõhe2 ¼¼×>ºžTšBýÍWn*¾ŸP\£Q¤ó®Ê¬L|%‹®ï \PUNŸ:ÉçóÛ´m§,-Á0ìÆµ«-Zµ‘ÊdÜWðœÓ'އ†‡9::UÜG;üsìØñ¿úz~X¸¿¯[åi¨*x RÅ"çÃGªÕjÀqÜÞÞØÜ£¿šzö¨©×ëY–µ³³ÓétV«U"‘£Ñ(?í•Ų¬F£‹Å‚g‘n~JÜèq³£ÚŽ˜=wi4Z­V¹\n{Él6óx<‚ ÊËËe2YÅ, Z­V$ñx<½^_9ûŠ^¯<O§ÓÉd2­V‹’ËåF£ñŸU‹ ¹\þNýüÛÛ’~+Þ£aCñ?¼\ùœ¯Sf—ªÀ¿–Ï?”ýÇ7þSÛƒ´Ô”œì¬-[å”ÌÿþØà‘ó òša­V†¢†E Ã2,’‰…nÅm_0–Ö&+ëɾ½ûV­Yk[×§ŠóÊØ·1]¯H­€ŠíVÕÖÅnu¹ý±Ú×&t‰v®‚õp8Õ)00pû¶>öwóBB“'Q±€eYµZc–-]´:CÓ![¼ƒÁ`0Às1MÅr;úmØ´Í5°6ÏJ›,–Êo¯L£ÑT~Z‘õù¹ÂE=·üoׯyë^|bÄ0A`Æ/.Vñ’ÌÙGö*'Ûj>'sAçýðÿôQU«åb)ÎûhÈà¡Cý×beee•Ÿ" IÜZÑ%à•`<¹O ‡Õj´ET¯úvÎÒ+ ïøaÃU‡óz.Zú'MÊd›àà5°,[ÑìÄáü÷pA‡Ãá|pX–5 Z­ömW„Sõ‚÷F9/Ãl6{üqA‡Ãá|pX–5™L'Nÿå—_ø¾­Wx@@ÀìY³ËË5ܦ÷Žã‹åµ[?L4Mÿ}ÏÂWÄU‡óÁÁ0 ÇñôŒôO>äg2›i+³xÑ·¹¹¹:½ž¡ŸÎ°@’¤³³³P(üÛ98ï&‚ ‚ྲWò·9¼_Tq8ÎJ$©Õêã'†‘ËìÂÂk.øfŽØ³Ä ,‹ôÔ¾]•Jõ–ëÊἸÕ9çÃE3 e±² [VV. BBj×®Y»vÍððÚþƒÇã=-˜´;çfO5ö÷ûH$'pìÞù“ÆŽš³`õ&‹PÖ“›]Ð>˜2~ôØ1c»‡•;’$©/M]9ƧŸO‰=w—9žrëø¤1#gÍ_þ¸Ô,’HÊ R×-šóÉĉßýx ·w¬8¨ª Tq8·ëâ…›÷î¥ÝÏß¿G÷n}z÷ìÚµSçÎbš5--QþQJ³lSýÞcg͜޺i}±/ÞŽÓÅ3Ç/ßNÚÉ_)¨Âp>aΊ=pVìèÀ^=þSšZèâl_1ÑÚ¸óÊ1ŸÏÝý@¬ñöµ«e‘XüÜòs¿ž52¤XŒ;†B襲 2¦¼%‹—ÐÎn$Oå €7æpœ«-d-IÙ%|Ö*:Ÿ0åóûX=û—…vr¹Y_tøÀI™pT£V³çÎÑ»ù¯Û×]yTL@É—³W¶?·E Óôi‹„B!€å÷_Nër1oÙeÀ¼ùÓÊïüß>7¬Ù²Þ‘{—ηrãk‘ÛänÑ ã-\ûñ#U¹Ù ‰h=9ÂÏ¿¬©Ç´¾CD&5âZª8¯{Ó&w‡óîÐêô.œ½rù•+îÞ½QX˜‹ãÃ0 M[­V­F[ù§Ý±Fý/F´]6aÀ'Ÿ|ºlíÙåÌO'X\zæ×Û#–oצÝï=nÙýŒŒCÛ¿&ö“ÆŒœ·òÇã7ü¾uQžÁñÈ…+ëf÷\óÅ$ÌEáé$o>amn~^ˆXÅxµÍ+,ié¬Úî±ÌÎ$ÎÁQ~¢ ço<øçÁO?ÙsNIiéôÎÎó§Ì+\p ÃsKœÞ8AHE¤›g(Gz9Ùwš³·°ø¡´,Ã%f\Q©ÒŸN:xäð–=§Æóýáý?Ø›óÎ]¸Å—ÚUþ¤"^‡ƒ·<|ÿØ´mÿ›^¿ó°9£‡Nùvß‚‰woÛÙuâÚâ’’1Mø‹f/µsu tw»õLì[£œ ü_Æ“ìÖ>ÖR"4;¿hH8¾ýxBÏA£voß±uÓÊòG'OÜ*ýå¤Q­;ûéà6$Cã~ù·Øš-'(•ª-ŸÖ:l¬ÌÃÍÇUÑcÞ¶ —⚈¯œ¿)HŸÿžÞs,˪ÕeZƒž¥ÄÐétjeÁÁmë“Êœ‡ 5›õ×O‹94ekÌþlöJwý _¾^™'v …°ðJŠR’¤~AÞθÉdR«J®žØ;WòÂ͵9Ù<©ÁZYFO|Öj¡´ÅRpИ0{^ÒæM›<:»uóîÝÛß‘Ø9[þ=…ΫáZª8¯„XnÐ5ç­ãÂ7g¥(€ãÿ»BÓtåŸÄ­OnÓgô[—¯ØrØÕÖ¨6Ýšºí^µÖLx}Öªž}^YÒÁóÓÇŽ¤Å‹ŽQk´tAN©>²¼$ŸïžÇdñ\±j¹E­Ëuƒ=”…i>Aµ$^~Z“ÎËC–§§l}\H±CTdÀÅËGóKÚOYˆR·¸Ôª©7›=ƒƒˆ3‰œÄ10 Ã0[Ó‹a†Ü½N cQFb@pX›«J]XÃÃNS/¶w¢ÊŠï&0mºõ«Y?Êb4TÞM‚*\¼(€b ²JÊ©´\ƒrŸçYIéê¨5}B‚ˆ»…‚°·÷ð—C~V*"¤µ¼Uª|©{°ž2¹ºJÒ ³÷í<ú8ß^ÛÇÎNl1°Z½QkÔ>–(ÃX–2ºÕjh¢i§  ÂøØˆódvŠ¡²0ƒ Çö_kï@!øSÃ˘Îܼã—NŸò(Ugœ¹ZòÍjwŠ¢¾ãò‡–Cá§Ã§:ÕÏß‹Ö% ³^Ãép31§†_ˆœ³Å”zçܪõû†¯Œmʃ" ͤ7ËÓ b©½ÜIlÕ]g(£ÖŠpG{^½n#XÁé;©E[6ÐÇ» LUÚ"ÈUœWCÓô}û0 û/5MsÞSO²²Ú´m÷¶kñ~£¬”˜üÇ+õç®áMšÂß&Ö©W§fí 9à8ÒºÑuY0pÉ^¾Ví5~êTeÊÙ.ý¾šºbª€GZh V;,ìþ IÇþýí,º”Ç9¬Aǰ¬Ù¨gÒl±ò-f†a,V'žþ$!\R»öΟWçé\NhtaÑö´ûµ}.þvÕ-²¯C,b1 %f]©™¡Sî'Rd†ã,Bf£ž!Y eµšM,+¡hÆÞ+Ð=é‘}X«±-ý r²u&š¶<·£ˆ¦)†+Íà^Gò„g{Yꃫ†Öîqg¯»…”bk5›!° Ø-´•²íƒ,å…c×Qs;Ö6žÜ³“àñ1œ$€±Ð Â0ÀpÒËËóúݳÊ!q'ãÜ£:Ûá,Ͱ”Ù„b–aP•M'ï"ÄRWŽîX¿ïòú“çƒùPVV¦L¿jq®  *+Ê)Ñy!ÐdÅ+Uš†.î.µZ åÞË*Àn\‘øGûì+R´­#ÈH»µhéÆÎS6Žhé«ÓéöÞAN¼³q×[yE$@,55‡¦ù#†,**’$===IòéÕxaa¡ª yïŽ=&‘ïÄùó¤Ú;;;©T ÃäääÈd2gggÈÏÏW(¶„‹¥°°ÐÓÓS«Õêt:…BAÓ´R©tww·Z­J¥Òöõ‘$éããYYY<ÏÓÓÓ¶Ñüü|«Õêëë[TTÄ0LÅrÈÉÉñöö.))1›ÍnnnƒA£Ñ¸»»›ÍfŠ¢ …­Xii©N§Cá8îíím0(ŠrttÌÍÍ%IÒËË+;;[¡PØ*# =<<žÛtAAÅbñðð0ååå...PRRâêêŠaMÓvvOûliµZ¥RéååÅçóM&“H$*))¡(ÊÓÓÓÖVPP@Q”···Z­Öëõ …‚eÙÒÒÒ79† ‚pvv~§ÒÔðx<™LVñT¯×ÛŽ“g »–m¨7aFM!e6›+>=Ðét4M;88T,ytã̃Çå½F÷g´ÚÊ% ¼¼\ TÌX¡×ë­Vkå÷–——K¥ÒŠX«ÕVtQÏÉÎ*/S7m“WP2ÿûcƒGNÌ/ÈGh†µZŠf1 ˰H&>¸·}Á•²²²žìÛ»oÕšµOÛ*ù¼8. ç¼;¸àé )ŠßÎ^tt”³Ìÿ×=‹¯2™-mÛtªøá¡i:;;»òXÚ(¶÷ê2p¬£Õ™Í&“©¸¸¸âUF£Ñhlsrr*¿±âiaa¡íAAAAå4MgddØ3 SñئâésË333mòòòlòóómþšÈ™eيݱ¥A¬ØPEeL&Ó?mºb»l = ´´´ò[rssŸÛtE=m²²²ž[ÃŒÕjU«Õ•—T~ŠaØð3(ŠÒëõϽô×ÂQ-¢…¶xè¯%M&Ós­×Ï•ùÿÎwÉU‡óêÜ©Kà Y„*÷F¯Ü`%‹]ŠŠÀè¯ìÝçÍim0´*%ãr^Bè¯áÑ?1F£ñÿµ>o‚ ª8¯&!á!ŽaØ?ŒâpªSqq‘‡‡ç¿—ãü@àïð‚!å¡§æµZ­Ïµ0q82.¨â¼š–­Ú¼í*p8OY,V“éݽf}—a&‰L&Ó¿N{hËÎ[=µâT Ç×ÿõ•$Y%M­\PÅy5%ÅÿÍ[þœ÷QYYYEïcÎ+Á \$¥¤$_»vÇçÙ~€ÝÝ݇ ª×ë¹®“ï5‚ x<^åüBœ%‰ª$}Tq^ wÍÊywܵøkCgÏK?/Š¢,êàÁýCeY¶ ³vpªz6kùÛ®Èû¤ª2rA‡Ãá| x<^` ·Þ ¢­´½½ƒÂÕ©e«æ•»XÉíí§O›Þ¤qÓ²²²·XOç}ÁU‡óÂq\«Õ©Õ¥€JUÖ¬Yýúõ#lÙ_Žá99ù™™™M›4³•g­Úk—î…Æ´PHø&mñ›#šÅ8 I®e‹ób”Isí÷³ùeÖ†í»Ôt“€Å º}ízA™Ñ'¸~à €¥©äø›¢Qü²Ì+·îËAÚ7%ž6¹!U^ê¥Ë÷ëvùÈW.@•å<~\&¨ï':}*ÎÄŽí[6À̺âk—®©Ll@XúÁÞPœ•pé꾃{Øî2ž¡¼øÎų›_xó†!PÕMz\ã9‡Ãá|¸RRžddägefe•––ðHBˆX‚ep‹…ÎÍ-ª<¿"¥}2eÊäT)‹ªÇ_Nÿ2ËÌ‹Å/·)U¼–ó¾°JÏü;gæôC7Òå÷ØmKV—”’øíÒ¯.¤[À¤+\-;kçºobïk€K¨Ìáp8œªR¯^Ý  ÛìÛ5¼=ÆZ^^Ša¸í^*•Tž¤Š ðZÞ~"‰x$é_Ã_(%ÝüuòôÅVR3lË¢1iâV­X÷0£ v£Î3Îs1=Øwr­º!˜ÄÉ‘¤Îý~IâàÖâÂ!Bµee\¿Ÿ„‹ÿÒ­;j.øÆ,à¥+4™ ½Á Jৃ&µfè?&õ¸qhÅå v‚õɕԗâw}|/ݯv¸-w`ñ“ûógÌ ²r¤˜ 4$v`4š…"‰LF0V°F# ˜”Ñcæ,™×·mÇÆ1õ¤þU¾§\PÅáT•ÖœšŸ™WleÞ¿ÔÔ†I%¢šþ^µ}2!<×7™eÙü¼¼ÌÌLIJնkOždÖðñ©žmýW™ÌF™g˜¿òŸënÂ:ø8 ¯\o×§A^F#°s±ãùÆ :Ðø£óG6Ô#Y•Ñ®ã¼;e¦uªk Ë0Ìñó¯—-Ì[³dîâ+‰±?­ ÍæªÇÎy¿ì5œ¤ñîu¯Ó6áÁc¯:_€õnXÿï û6ÐË5v×OJ­6;%¹ ºf«´¾Ñu@]´lÁb—ÆÃNhB{o;âú‡Ñ]“Ó³{ãyÒ¸ú»4qÊCÖÎ3@4z֊ѳ`ãÔ^L“IU¾k\PÅáTƒÙš–Sš˜žÕ§Gg!ŸxïæWdY¶D©Š»zG"j\ÓËŽÀñŠ]@•–”\»v­]»v2;»jû¥¤ÆÖˆóÚðöÒ}®-¸NŸ6dä¬Ï›®ÅEQÿYß{óaÛâé;Ç×ðvðé*gôéÕiW³2™Sßç~ÔXj[EYþ£/­ÌQYÜ\]{šDp ½:wÒ ~'o';_ŠO¹7yý¼#G~ôñ—+Zì\Þ°ã7ÃÞÜ5«fÿ `=þÝ¢™ëväk Ÿ¾à›…õc†Ì‰@yÛϟæ@å—¨×÷€ G¾]³óç] ÓÙ=ºÏøj|óÏ'õ>ý³_Wc Ú Ô1æéYÇÍÕuäÊŸq€q‡>Ÿ¾ÐÀ1G®Zå{ÊUN5Qj©[÷Ï™2”ÿ¶kòÚ‚<ånŽv{öÒÛž Ã0ׯ_8p ƒ“suÖÇÉÙÙl6Wçÿ{†Æq†ý}pEQf—U2GÏÝg.ü1É»cHûØãíŸ=£OÆåõÛÐÈngw›ôÕ_ÖÁ,ܹ ’oÜ"x€Þ“Ö¡IßW.äZ¯×éó½*/éòÙÊ.Ÿýñ4ªeÏ‹·{W.PµÇ!Tq8Õ„a¡X¥±ETï]3• †aÞÎY9y8)À°JbÒ”—Û"ªêÜ5³Ù,ªmsÿ=R©499Í××»rƒß³Ÿ„ÊÏ/ŽŠò­ôÒ,|>Çq³ÑhµZY–%! †ÖéÌ4MSF’$A,CÑ4Í0`0hŠ pœG`˜Åüô\PõÁ€dã ¿Þ±žÅ7öü…'BÞ¾=8ºûüèîÏþË%*Bè/3&Ïx™ê¿<.¨âpª †“|¨º|ÕÏv†Â ÃþÜk ÃH’¬(ójÆy-;w¹|ù’Riú»ôSV·Eó•›B‹¥r9š¦+w°C=wO¶¢aO±‹Ù‚à R `€ËPŠA@òx| Q3Ͱ$_ÀçñB¬•¢h†ýã-Uz4r-UNuB`4L&ãËþY(µºP Pž›$â‹x8FIë•ÀjH’Pf@bj¾£‹/ næ¼xú ÷._ÇHLóæýPú,Xóã™ýß~þãÿz¸ûÖs¬Фqû3W/üzâÌÃGwƵòÓëu¯V“ñYh^uUü™Íör‰I[ é¹e,cAø‚Œ 4jN$‘ÅnPç–ð ÒV©Xð÷C9/­¬\[ZZ¦VkKJÔ7oÜø05áQÚÃø”øøäÇÒKJÊd2Ù³o=ümGÏ>ÃÌ"gUæ­†õê.;'uv>þãª1ÉJŒƒz 4¹zÿÓmCÉÃaƒ†ÞÉÕËìì^þö7ç¿ÁXž?küÀzuë´mÛé—ÇåR³yùŒˆ°°F¢ÇM]T„;„B‹¾dòˆ=,<½k•¿Ÿ_Xxƒ½@ `(í7S†EEF¶mÛqÇÅ æä¦¹£¿ú¾$ådTDxýúõ[¶érO'•JE÷ÏíiݪUËOf­({Ï8®og/oïÏo8Ø8–—tmhï.MG÷ì3,A/K¥U{4r-UÎ[ðòÿÆbîÅÿ¾bå·Ê»'cF.¯øV“™aÀåíÚ¶ZºuáŒüº)ׯ®¼ ;„§.?ôÅ/ÝB.-ÎQk;7Æ}ѦßxÑ-[ƒ‚d1EïØkPLýƒ‰è=tdT‹ùUÇýså_ûôDüíº.ÚøÝjIjò©k§Î„°~§(—o& v”ãz6£ §X–F϶$ž_ÃnFnðÚ›1­þÁ Ë`íÚ6wQ8YÌF†eËZ,ԡç+æSÀ0<"º¾÷Owr5zNˆÄbƒÉ¢))hÒc’›ãÈ!#”öö¥³éÙÅ1ŸbEm{ôtæ[3îÅ:sËÓKæêdﬠ eŽÅ>Î*¶wê3¸*Jº|/½QLë’„73aððž—÷ï÷hÑYVžvâÔE†ø„µîÕ±®A£áÚ·Þ_Ù‰çg³EÅ%æ‡?„ 6"ñòØ©ßÌ]µ €Ñ9&öØýyCëä—¥›­®}[‡ñ#¦õ׿kûY¶X^såøÅŒ\¥R˜ë×~ü”^w.{¢=kb-6G9\ˆÁñ…ͱâúOŸü°íÈäí—&µöýß„Aœž7¾Ó®Ø£w/Ÿü-™–6Å9=à·«_^Ù1å«É³ÎZeªÒá>\PÅáT7ô*“Ž7èqýl #£)õžØ£wƪù|Æ—"Ÿ”•jÖeÄ2·ÐøÌ¢î½Ç6ƒ^çÕÿÒ馵`ß⣛—;†˜(&<,T\h´sp:£;útÁÂàSqùZÊ^áïéæDSTÎþÚÓdc€…7í½P˜\ o6wÃ$YL?·ÖéËz$×ö#;0¨bbøö¡{öì&,„Á |3]º´·— lós–——fg§›LÛ\ |>?88(;«ØV!DÚy×rį%fÖ×+Ú¶×)Õ%ii¥­F5±¨–­]ÖjÚ¨ËgÅ— '6òìþ-?ó¼'uwž3oUÇ ÓDªùyù_xsïΧ›øÛÁåzÙ¢a>Çu ix~ÏÖ³%N1½šmßy`jûžwOÎÕ(:·«‹„rÄMƒöžSç%*j·äV“Ô+³j¸zèt:Uî=E„Ö€¬ûçåõú:ÅÒ²‚ƒQo{oYö}×àöB“ÜKÂSä%Z‘K¸·3&D¬É¨Ôæ_KPF÷Äj2h¾<"È!ÐßëaQ6 ²·/.,´R΀±š4ZµWèp ¤~8¬àU™‰• ª8œêƲ¯0fMìÝVj{l6›tf¶fºVŠ2èõ$)ŒlÑ%²€V«a†åyÄt6 ”Ä»a3‰Á gIó.ƒše)3ø|Y»^m+4 f“å¶ü^Ðõ&¹G̘ ´qÛÐg?Ÿååe|¾s×~C!V£)BѤ•½FSŽñÑÍåZM¹ÅLq]¨ßN«W)s­VÊöµb&>Íüƒã¸N«­üu#BÞ<Òeˉ߃B•>mû?”œy¯H+¨%7ç˜ÝÜ ‚tU¸wlÞ£KÇŽ{3^ag¶ ™7axQ’ëõÛ´Ù˜-´ÓÔ Ã{æÕÖ7°ž·à|¸»Sú“j­]ÿá¯:äÖ¦±¿[Ád¥§™»õlFi•oçÓáTY)“ñpŒ (ʨ͛óùô½–ö®i4E§Þn±v5X,Šúcü)(à‰ÀD!@fBšK@ zƒÙ Ú³fñ=:ätŸâä$œäÑÅ0,M# XÌfšy3a€pXÀÀDæ Ú×ÿTq8ÕEé #‹ÙlùsÎàò²2Û+E©Uú±©¼Äl2Ù¨Uª¯° ±,ûÚ3rÒ,û\UÍ&SÅ^ØTì킪™¯ïCf0èDb¡—: ðê´ŠJüýC¢mŸq1Eñ6lÙì1  -OSÐ`8c¬åz½Ádåóø"±H$–”j EQ€\&²hôÊš—¯tpòÂ0»faÎ+÷ouÜ:P6y¦fóvJHñØ/WõÌOß¼nÙ—ãSãó“Qv67ÑÃûËÝ7Øtø  $í‘ÀÑ× 0ëÁì)Óåmg®˜ÔZ§ÓióïÜÌ,óÝ_2m+üÂ,…?h³S0±"`[rn@øGr€¢òÂ]ß.»–Å;qx˲„ØÁY9Í<œò ]ët˜*­à Ý\\r’LÐ<;9Aîá ­ÒÖn.¨âpªË0ðÏ}½ß}/H¶ƒª7K.ú§œ9œ—†aÿø•=÷e"„îab¦<™ ®%Q„ÛůoL>| g–ý#3Ë2Ù¦¨Æ\BZ×u¾¸xåzIyRn±šŠë4íðó’ØeKŸäß¿Ügæ&Eù†û>üzSãÃÇÜÈãz“)( †Y[úë¾-™JÆ/4²-I¿+Ù8¯É«vëÚÎ×ç~9åÇ÷ž÷Ìë—~uîAΑ䊵ß7nÙ |ïÓh€¦©¤+§:V¨.ܱãøG­ÖîÔªö‰9_Ì—¥vüb…”*,ÕhÃÂë°·Îîš»f×į7lܸÑÙ'º׈~ë­ß²´èªwe?¶{kYOíÚ{2N‡+¶ü:fPë¦1-.oˆ]ðuqÑ£”ócUž ª8œj‚ØFQaïó4è,‹p’økX…c¸Ål‚jNŽñ¾~Šï„Øšx !DÓÔs7GÐgß¶ï ž1TY¹CpÛŸ÷Öˆ«‹‹ ¡ÇÒåKÅ:Cû}Y©‡E¯‹lÒÅ«ž«Ùâ0gî¤c—“܃;Ôk=ÊÏäò³§Êï¥äÕý¿6Ôy92¿Æ?íÜå)’iÂؾSá+3ʽkø@#Êg.îÇS+)®óÜûLä0ñÜß.ÅÛuèÖ±¯VßgèØ˜^<£É q÷ŠÑñ m'O„ÜÎ.¬Nóö=F•™p>Ÿ‡xНV-:qîŽØ­sÛö ^2©ì?|¸r8Ï©’ÓTq8ΊeÙĤÇC¯¼¢-éii'תmoïðþŽTý`Ù ¹ñ’¯¤ª>..¨âp8œ—íøQâC¥R‰8Ë0,BQuü9v´}»ŽApíUÎKz§uæp8ΛB„B‡‡»››››‡›L&»xþÜùßCBC“ …вVCöÇCF%k‘D"yÉÕ³”jÿŽŸ™ýŸfmàTeîí/7—†1®«0 3}=e¨D,öööööö_óÓm Ã0 Ë}t±aHÐ’½çlOmïµh2;4‰ttpøhü7Ô³å´)«mƒè,†!Íü‰CFL[Y”sÇqÿ€€ÀÀZ w^®¼† eسfº§›k³ö}Ÿ`š¹Ÿ’J$ÞÞÞÞ5×ÿ|Ã0Æ¢[;w|ì­'§·/uv´oÓe˜éU1wNlóñð8œZŽaºìÛl)I=íïí¡pQt8ÙŠa†©žÜèÚ¢Q@`ͯ7·ÕäÊáµ —FÍ;]xbÀ0¬ åzß1¾>5FN[ó·U}C\PÅáp8.[+”J­JMM}ò$óIæ¥R)’ˆnNv2YFF†@ ¨øáÁÀ¢+×&>NÌ)T¡ÿcï¼Ã£¨¾>~ffg{O%½Ré½#‚ €HGŽR-ˆ€XPª¨T©ŠôÞ;¡·Z!½'Ûë´ûþ±!†þ_a>OÈÞ½sïÙÙÙÌwÏ=÷\€*ÎÍHMIIKϲ³A”Ý|/íæõÔÔœÂr†Âëã'¿Yj.7 òrs²2n\¿ž]P\VZt35%3¯'…³îöõÔÛwÒ3sruF;ŽQ7RRsr²rŠÊ8L€ÿ—k:ýëxøÇÝË/.++ûã“Ø¾o½ :oÍnÏËËë\½U] ìº•Kç[‘Rªô¬<¥Jº·y»Æ°5zƒaëâÉ¢ûírtº2³cú§#³ðˆU³'8Ê‹z¼; óÞ½ŒŒä_>}÷Ìã̸wmï¬E{ï—ìúªnǺï¦þö×uV›-//·c|hõæ‰`.¿{òÔ½–õBš½Ó/+ã]˜[¹ð\š•²fë.‘Pr p´açŽóM:uñ ޽|·°´¬´‹ûí·†,€Ï?\{Äo÷2ÒKO,ýeë5€ÜÎý§œ(-;ÿǰQ];!€U‹ç4–“û–ò\÷qË€/¨ÌóêA2èõÿÁ <¯9N§ƒ¿ãþp€H$òõó%!W™jL&—øÑÌ¡ª1Ù’~›-¢ÌJ:áûÙ5Ä÷õ¨ô am¶ZÝÇÿ0´íŽ•³%íóö÷³;‰‘?ü¦ÍÌ$ ÁŸK–4¨é¿æçŸY¥»FˆŠôÖ˜„ø²ìôœbzþ¶MçWÌÿcïùÈ)WRÞ¾•׆ ›#j7Nõª¹ˆX2ác§‡Ã~ëÆùÅ[÷8nhÕaP­ÇYQxçlhžbq£tÑ 3€.íú1ÃéÓÖ çÆ!yTwðôaôÙUì‘æXÇ®?×&¾ý‘*’±Ù$æâ´"pï hYÝÜþÊD€B ’wy»4m›’~Í’A‘­Ã °†;د3àå®`<@­å?ðÑË=ß¼¨ây6Ikþ0›M¼÷žçÿ¥¥¥5¢¢ÿm+þÛp,ˆCE HTdÃ0åD¨#ºM[Ñ#æ}>òÐÁ#¿«ÂÀ{·ÿd»™ÕáËχ6<}þúðYÛ{5õYýÝÀµ‹×¬ùµwíowý6u ‚#üQoâ’!ª}>°“²VÏßæÖý¶_›6›²í ÷ŸMPÃ×CºIjR(ˆ Šž:}²€Ùlæ#åÿB´Óq ‡ is €7p¬iõ¢MM&]扟\rpïѹShuµÊK9öèÁÓ~–ô©vþ°re”²â)–uÞ<µù’÷çp-"#‘T­9 5ƒ²à²0J !0—¨âÓK·6w9o9Þ`ÔnW«ÍQYÍ;ÿúÞ½gŠ×n›=¹w/–e 3åº&<ÒàXû¡µ¿-9éØt¤.*;‡‹$ÀqbÖa%U¾&&Mßé<ñû_†¦üé,É÷ð¨ÿRO6/ªxžÉÐá#l6›H(ü· ááy˜#‡óɾÿG\N „Åb‰D€*T•V«AÜ#I+„"¥Ÿ8ƤˆŠËÙôêÐv`H¤ÀÊ2R…B*‘€[5O"ÇAÝæ°ÈËtB‘*ÐCÂÚ 4Þ>îj ¨Õ¢r³UîéGP@Y‹=ªyØh'B„Bî!æÕËÃpï¸Áãã lWH~ßôºæüóÛÏŸÝ¢€['÷ì9v¸Cýºù™™åŽÈ6ouõk½Tå˜þ]”€Î~÷ä쨮•2±H5o÷Æå#ÞSŒÝ·Y(Nâ› éÒ ÃÄÎ ~ßš=½KàCeã5žÞÎòl°é‹0Ræòz™òÏî¾Tzþ-P–{û/þŒxd#Þ¹ë6mß”Óàö½´ õÍÎu/ÉÊÕ‡Ô%Ø3»VÏZqhîž 'ˆ€3Z(pÍ&…›‡ªš›S·(‹Îˆj^±W.k˜¯³ßMžÿÛíK?ἨâyB¡H(=»Ï+G,‘>”ÛçEq‰*Vëááã8&$I2 Ã2Ì£ûþ̦ü%¿­Ñ¾íwæVnÛÑŸÄÈ‹þõØ­Ø›I;ÂZö÷!Ü‚½UÇv¬ –6ß²ýLí“„€¹ÉÉ+÷Êj°VŒ (‡qE3 MͰê (Õͽ;·o¡¢ÄÛvj;~ŽQ,Ç1|ÌïË 4ûÒéëúð`÷£IâÚ~ ôÎe+c?øÎ õAõ;þPÔ¹«g]?zúö»vÛ¹Áo»RÞõÊ1q¢„˜@¸/w`~~áëNlôno˜µºµ—²° ór™C˜}<%Ï>¼Ù£Ž*K|[2ïëçÓŒ»ÖÕê÷=ˆÚ¾tE|Ÿ™È8½S^û‹rnÜH·9mw,+ƒn“7¢ÉÓ>Øèçåaà\Ÿ“׸õH9¶¶÷ˆi߬?ˆ3y%?Ïè¶ >«-ðí]÷Ò¼^?u¥úñ׃×ü3vø4x?ÀP’—“W*gÌ=8jïå—~ÂyQÅó ød'<ÿo¡(§÷¡þop ”ƒ*Öë\_Ÿär¹§—'†q!„XþÃIeÏ^œDöäéÛÚõû콆ˆÒΟ9cìðЄ–+`YfÀÇC~™¿xê—‡; ˜=¸ƒŸÕ¢ûñ«^ãFêÖ¯ß[ïvõ×JäH¬Ó@ ¥9&¾~3KtÂÛ-5#'þ4ÿ$[×ÛÃ'©nïwÇù?>/Æ®û}η4)oÜyëâ¡ÀѲúÄÏ>„Ðý0m‡WP„ÔK 6SIvv.â‹~Ôý£i¤zÐÌÕuïghǪ>={*À§á®ß¿þzÍÖ_½î#Ÿ>¤ÆácWŸK ðÈ{'«VgÞŒ¡c¾Ø~ù¤6À1²úgŸ¾kÙ·ãr›+!úÂîu3“v7jÛèçq_ŒùzZ¢¯€3$4hà΀îîe‹j†c`5Ô®]ïðò©›f–5|ÿëÏúÔê9iRٔ￘ræã©Ë›û€p톟>ò¥6¼þ¢ßÆ@qVêßÌ1üË—›>ÎÔÿŒäááù²oï¡Pتu›²Ò ÃΞNnÖ¢•\¡àoƱo÷®è˜šZ­›ÓétµlÙ´qðÐá:nÝú¤ÃG•–K$WÔ?‡MÑ@’äÊ•+FŽ­×ë]§ÇqµZ]9¬ÙlfY¶²…¢(«Õ*‰¤R©«Åd2±,«Ñh*¡(Ên·«T*ŽãŒF£F£Aˆ¹´oóÉ«wœ emïqcÂ$ˆÀ0“ÉÄ0Ì+9=¯A¨T*×[öÐÖ¶**ê1ñ<;à;àSzVåfp¶›Éš6SBÀ°M³òbYŽåB*¾zþØòoú»ÊÚdee®MZ;{î<×P¼§Š‡‡‡çÍE&“qˆõp÷pÝ]0) ¼¼<…âÛÇq:î¡jq8‡ã)ª¶èt:äÓÂ=ˆI½C¢ÕR0Œü·ý—Å£úæ±_9žÿ{ÈßûÆòt30BÝ´ÙÓ¯|Jå¦òzK^ñW,^Tñððð¼¹´lÙzΜÙV«U  äʨøùúõìÙËjµþ£·% Ãå^1A Ã(в¼¢zÓx¡ ìÿ¹šU<<<éY«ÕZ¹bø¢(ŠßpÀózÀ‹*ž7Žãl6›Åb±X,åååÿ¶9›Ç6oíì)Zì>pÔ˜èjÜü¾/£Œ†!?OKiÁµ{E"’@œmÛêeçï™ú ìunëÂ?ÝRªTNkц?6 |‚Ä"Y­Z!;Ö'Ù à0nÙ¼vÉêÕY”X(²Úº³ˆŠ :ºNóAƒ>Ì=¾fÊ¢ãJ¥ô_¹aõïõΤ_÷_È‹,ãÆL m×·ŽõÍÔY íÝ´ââ]cÿQ#‹Ï­›tV¥R=O:Óç‡÷Tñððð¼¹°$IúøVÃ0Ì•¨* À_@’!¡öxà–ƒaÕÜÜýkIJî 1È•ùW68ܶ~7l¶Ú5RN÷\8ç—âË9“–on¡0E‡( éf-ûä亂ìÖÝ×ë4nUÏOxïên³nY¯ETLPD$ŲŒMG…û‡DÅYòœ p¦â#‡&´ûxÌ ìfSí„„€(ßïtYHㄸ²²2³ÙŒÒ¨TÒðØÐÐtµXj’“"‘©äFf±ðÛi£/œ<Ø"¾¯\*f¬%“‡ î2yþ¸.……ŠzCÜT •Hƒ}}BkPéw8ÜÏó”ó §ˆþnKhßÇ-‚Ér’¤( Çq¹ˆ\8i¬Uš°ké—ˆ5‹„¢ ê5"ªG–^È¥ áßrÅÈÕ~ýGÒ•³Ncþ¥Y]>ýµiÃÀòkûwìÙ6ðí¨òÜ ¬*±¥·uï•à¹þÓS!öÜ‘ÝfÎcÜà‘Ä€Ã}³Ò¾µw5ÁˆàðÔ¤/Ï]¾p«pø’ß ¯ßñØ‘fž¢R‡úóÛÉmö’a7uF“±<á­1â¸f>ã—Ïÿªo}‚ ^bî~^Tñðð𼹸äƒV_ïó§_põ¸z]{ô” ïäeLìÖ^ÂQÊj‰óæŒb)Ê#0Úî¤JKË\6 „ få°˜Ìf³Ër–¶ýùË´K{WÛít·±ó$¬ýàî~-G÷kÚuð‚¼^ï ÜÑÓ§š˜þû¸.ÅÅÅ»ýÍQT p Š öm«”+ý²î‹*§±Pê&F¢Åj€¢(”Y\p&Õ°jßo ×ëYƶ~É·©g¶›ô†îW0/¾^ÆqHo0š,V‡€£l¸P, 8B$Êæ¸°÷@LÿŸÀl±˜Ê ì}‡¢¡àÚÚG'mزaÔP†r@Þ­Ti`x ‡Àn·;ì–ôK¾ZvcÚéÕæœc©J„qk‰ÄŒÙf×—)üÀä$4B\ÇIׯ»`ûÊ5ÊüCω¥uìÏWxçùá—ÿxxxxÞ\8–‰Tâæî¦usÓjÝ´Z­V« …,ËrÜâJ$VÏØwñèñS_¼³bÎ÷Šj>ŽÒ †a,ã¼›Q“(b-åå8Ž#ÄÞK/TW —<˜›Eâ&݆Ÿ=qà«Q¤^½a¡PÕŠHØyüðÉ“'wÿù3a1Éå}IÅ"×DŽã+†BùèÙI‡Nš9êÀ¢ÉWÒn&m>[T|uê¬?2o_9r§X¥ÒtjÙ¦¶¶|„ٜLý²6yýW¸qzSr¶âæÍg÷ûuçø’$@©ÖЦb°[ 'ÕmGÀ¢ü{v­÷ÓÈ)wôŒX,&òOg¬Ú¿goòÖùG~ýäxšU*“½¨¹_œ»â$…"‘€°YÌ ‹Ù$‘©`Þ{²èÝwý(Šzð½æ.í]u25cÇ‚{ÏÛ¾ta ™%þ!‘jP”=ýʱŸæ®ìùúb°ÚCV›q¬Åj“ÈUj77ÆRÂ8mfá°É» îÚ ëæíZMëz¸ûJ^vŠö7ëòâáááá© ‡8‹% ¹B&•Édr™L.“ÉHRÈqœKr=Ô¿(+SW–/·'åõß–žMÚuY¤ß9¿óÐUýãÆ%xØÌþSjLE·—l8Ûê£áËrUn]”µüDò™{åTõ¸ÄZÞÀ²!pÐÎ,ddd€@Z+>öêÁ¤óéÅ"‘0óæ•”ëé„DZy+GœóÆ¥‹'p¢¬(¯ÔlI»›‰+¿}”Ö&ü8yDó6]¾ÒrïŠ$‘R©ñûÕXìö® S‹Ü½‚xUg÷ßG&&†ÕÜMM“Ë4€‚{wÒ³ }¢šp%)ç vú¤2¬Qàý}  Pzõ2:Q–÷qÿa¸Ü“ ð²â<@êÍÛ,‹Hé +ªG*½B=çÏž±9­Î] ¨ÕŠÎ>bVÅ&¸âå«€5í4tëÆ?šÔŽ ÷óoÚZÍš2KÊBâEw/ΘñkÓÓ‡¼a±XDjßP-yäÄ9Ún¸~+#(¶&¸È|÷P1ä¦^·ÐèMLƒö~5yص—bº ø@už—ƒK61]XPXZRZZRêtRžž^¢ q†Tr úüíÚ±ñõÖœ-Ÿ0}ºÆ-ú×¹c×}Õ'6²ú× -I¾UËM2pâ„DQF°›ºû€‰µ|7©O’ÂB'Õjµ„b¬ÿ\ܸN\B½–—,þŸL+p؋ʲ;‡ú$&&¶hýNž< Õû ¯ÖðŽ j}¿x‹È·†ˆ$…b™Z¥â8àèòŸ¾ùæ¥pS“ ¾”Uã³y[?üò{ÛÍôFÿnÙ¨Qã¼î&ß² J’û/X±Àr!iÖâ­û›ã¯ŠnÚ¯SéîÖyøï3^ÒìÛ°2i÷q÷ˆÓ>iÙ)Ü}ÂÒ« ö-„ûž*'µZ7àÓŸæ½íSÖí£¯<=–|;:ÌßÿÃÑszNÿ£µ?¸ö¾沬ö[Ì[»eþçßî3Uí2ä“Áæó„FàqýÆöNܵ|{âÀáÀ²Î3'FÄw-0´«ÛöðM½Wõ† 6hÑ¢IThX­†õ ?½Lg‰¬NÓôÙl=tbá´!þþþ£gìÖxÕ˜8yxÚ†o¢›ø´ùd`ûP'°xf¿>Ñnƒ¿Ý1%)Ipùè¦Ð/­6<°û¼)]|Í/þZžŸØ‡‡ç¿Ê¾½{„Ba«ÖmÊJK0 ;{:¹Y‹Vr…â šyöíÞSS«u«LްqÃúî={éõú}û÷Œ>ª´¬X*•â8€±G9†‘á’¥‹û÷h0\§ÇñàààÊa ƒ^¯W*•nnn®½^o0Ë!IJlAAEQB¡Ðßß¿¤¤Äb±áíí-‰\G•––Úl¶ÀÀÀÊ‘ N§»»»\.wµ”••™L&µZ­Õj333 88¸´´T©TVŽc·Û]e¡óòòB®Íf³B¡ÈÉÉÁ0Ìßßßn·½Þ7>‚ ÜÝÝ]ejR©¬|Ê`0¨Õj0™LUÛõz½ë-&IR¡PØl6†aªv¨Äd2ý°n×°•F£P(”H$®‡c~ÒÜ¥õÇL’Ò‹E£ÑT=ÖeŽã.ËS’w_L)ï9¼/óàKp½:‰DRy=X,š¦«Žf0är¹kG!˜ÍæJÿ\Nv–A¯kÔ¸I^AÉ”…;z ž_0,GÓ,Ű,‡X–c9¤Š¯ž?¶ü›þåe¥••¹6iíì¹ó\'Tçáááys!…$¨ÕÄq€a˜€)^WyrÁq\FFÆC# ƒÁPµ…¢¨¬¬¬‡ºQUy,Ã0yyyuxtäââââââª-z½^¯×Wío2™žôÒ*g,))yÒ¯= Ãètºª-•jwAÓôÓ;ü=ªëÂn·W®ôa˜tÈÔ©E™Í–'ÍËqœ«=²V«¸Fb—¶{´§ÕjµZ­U[êó” ¦*&’B,‡8. pËr‡üiå›xQÅÃÃÃóæR£zÔò•ËX†Á°Š[äŠ'‹%5v8¼çç!ôüÎf³Ùl¶Ôž’¼۶Д`ý‹Cˆü‰Ú‰U<<<~ðhð1Ž$I:ŽÁ,ž ¡ëÚ$¬u›: ËÞÿL`€¹jTüs&ùÔ“çEφab±Øµº'‹»Ÿ !ÄqÜßHŸÍóï‚ãøyðyžW4 Œuœ“eh¨’p¡ª·–uXž8Î?h#Ï«åì™d…BY|ÊãB¯×‰„DG.•JY–µXÌYÙ™€a˜ë¶A HOOO­F ð´Øžÿ·¸D¯†_‰DR™[ã8†a\ñøý.þδk©Wããù@užç‡U<<<¯ 4Mó /Š+ f^~^QQ‘€pG9þ~qq ;vlóôôòöªV™Ý ±¶”K·BkiÄ‚çMVÄQy9ža!8˼qÆ1¶ô´L­op5ËT“Ó—•…Z)CO¨ióÂP–ËÉ§ŠŒtL£æ!žJd./¸z%ÕÊ€oXlL˜<àÓâ2¯&§•bmÛ6!\íˆÍNO³áò¨ð@}ѽÔÛ1uâoŸ=Ub¥EB¡ÂÍ·VíšbìÙ^1Úi¾œœ\l¢ãš´rWâJò2R®Ý 0e›·[ ]ul8æÞ­«"Ÿ(¡1çbÊM™{P“ƵÊZzòØi+‹)Ü|k׫¥BÈX˜žnÆø‰O?ëä•WPãz18€ÓZvåÂeFÆF‡T€²¼´ —o‘JÏØ:uçøŒ=÷‹±_PJEeÆÅÿÊœ7oæÉ7ŠäU’IG›·­ûãøÅÛ¥ê¥Lôzà°”¬]¶ðãÁ­9š ¬S¿fÁÌïfÎ?~âØœï¿9“ JEaCîÅ!C‡öìÑ­2 ±ÖµKæÎ_¶À1aä°›Ù¾cç÷Vî9zâèÞ¯ÆÛq­ž£&±ÝT´vù‚A X{2 ˜ô+§Wý>¿c‡VEõ)œ:~Ü5vçÂñÅ f÷éÙÛe†©èÚ‚‹Ü?{êç3Öœ@ÔÎ%sïH¶—^[¾|Õ{¦Ž¾ú¢ö¬þí«og=¼wÆ·ß]-ê«Ñ׬ÝòçŠß¦ÿò'Ù¶ìóIÓŽ%Ÿú凯wÜ´=ñ/ï©âáááysq‰*šf8ŽÃ0@‰DÑÑQb‰D«qÓëõU+å8ê[mï©ónÜmñÞÈaý$Ž’Uó¾Ùxà’od­±ßþï)̸zbÎÜß2Š : óiß+Û÷ì9¾÷‹q“›Õ¯/Ô]_±~ÚÓ¿ËG“z´8¾nþ³ÖÄÔ­‹ãšÁŸ~*3œùü‹…~Anwóã¦Y?ã»R¤™8cN½`­ÝjAˆI=ºqÒO«B¢Â±§‡O€Eodß¡¥ä[½áÔ&3Ù—Ö­]ƒö'ß¾™ñÕ¤ÁŒÑørkºýGQhNZ[{ÆO‰†Üó)wGÎÝÑ1J°|ZŸ?–®i0µ«'K•/›¿¢Ý°Ê5gþRøx¸é¬Ö±ûhž9ö}€¢ÄÈ_Íñ˜2°ÇÜ"ˆó|¦*ùIë¦o 0a£wz7êÔº$¾{¥›ÈX’êÀƒ;I!hh“nM[×ûÔåõq¨³y÷ÈÛ;ý­ï—~׿1c/ºrÏ9pN/;¬ßÒ®,é7bÎ’þkÇlÜ‘r”ÙTD…¹}JØAã7ï kœ±`íÉi×®/?¬ïðËGW._ºTÓa÷šÞŸvë8Kâýã .õfîš>{:\ß7vêåi þŒ PZl\qÚÁá_ü¶êJºôÒª†Í~oô"Kîåë·¦lÈЭÙ»KßcwÒSýì÷Õ›š|7ÂiÇù—ÇOY8eÓÑêÆý†ÿDã™ÊkòwsTîò­?O¾xçïãZui×VÓ¨÷àîMvÇë]ˆæÀl™®gËT^ê‹g޶j¸ïàyaý†Ýs~ÏÆ|2pNÿNÛæþYu†6Íþö§?î]?¶°6ƒÙZïë'DLhÝ®vŒ}n3˜RNÊÞ_5ÆpLvËýO ºq|—ïÛ£\¬%e§½â)R NKÁÒ?/¼=b”ÞM¡ÜýkºWi×ß”Úké* o!±*Ø‹Ÿj^K ô¹ðV˜Ò]ÀfRì¿'5Ù qÛwŸ³£^ü„>^Tñðð¼>X,–äS§ô:†ã8޽º€‚°Ð°¸„ǹ*1=¡ÒÒÒü¼\W¼×«Í„Äb±¿€¯ŸŸÃá|üæp–•ZåæîV锓¢÷°³G­ö8 N°Íb‚S²îfOÖìú} >ïµ3s|޳œBdûÎý¥a¯Mî=ÁŒBà¤@暨š3mb˶·h‰ßKñŽ{§¾sÆÆ´jÚgh‘DÞºõ‡µÝú†á‘qª(NÔ¬¶a·žÀ0L—y]Þ´cuwÖÕ¢Q-Öj.Ϲ=zè§H¦Â­…ÖšM”„P,‰I«“®¯!„šãËëCȃ¦Í˜úÙØÉ–N­S·™ÜÛ×ÕÍXxû÷µ;k¾=ê©c¥ÆÒh ¯È²!Hzvj£?»å¼¾]] `¦”{¤g\Rì˜1zÔ‹7þ0ä¹Ì éªJ!Ä2,Ü›8ºluÒ•QÇk»žb¶jO«>ï—ï¾.Ð6ZúA\>{38*ÎUö¯,÷ú” Ý;3"ì:A8lJ°;œ"±D&ÄRÀÙ0©:Ÿ–3yB£Ä:-ÚÖ ‡½Œsü¼¨âááyM`Y6õÚ5O/Ï{÷yõ³#ŽMZ³F"•ÔŒ‰­Œì»Ý^\Xþê­*.,HK»íæîA’¤Ë÷‡€c¹rC¹t-ÍxzzË>ìï±Ù '“³âêËîæy4l¢Ûy’†Ž×.¤¨½Â¼E¥Dx7í:S3þÂ…›Þ¡Ýˆc,bç³æ—æÜûñÛI]\9«g¸X `ÌÊJÏÈÄ$8v'€Îdç8L‡@@2¹YÙrO’¹žË€ ¨è^VvA¤ŸÝ ï¶kÕ„óKG~~ÖË:6ÞCõ ä¡?.]_î\-®ä¤ß)”Ú6:yýЪ³96Ê~ùRFýf¡ò#ëv7¢™°~«†ß'íy«†’C( †Ñfõis=7Æœ³ÅDHÝÇ=ÅRÆE?N»ç¨¶ø·Ï€²f낚$€¡èÎÌéßÊãºý8¶#ˆÔ>¾ âÜÕõÚ…ÜÉÈì8PèÇ€nq&€øN*«¨V@:júo£¦Ãï_vƒ:Ï–ƒ/ /ªxxx^8Ž»sçÎØñÀõÍøú*0 Ãp¢yóû÷ïOH ¨¿Òf2 c4ƒCÃNÇ+Þœˆa˜J£Q©Õ:]¹ŸŸ¿Óé|ÔOV¨. „€0¹\îéåewØY–Eèa‰BprùôãsKÕb>ïÜ*ó‹$Çwlÿ¶ÐÐ¹ë„ |»uƒyh5©q ÿMw–.nW«ZÿNÝßiÙ¤4÷úݛƭã‡i|üû6ÝõNãæ‘Áî6L!’H@_Qtýµ! €¾àÆ·_O›´b逖·G÷êâ%Ê70"©¼zbsvÉ’±Ët·/bAM &Âûç…Ón^¼8kæxš©»©hÂÀ^Û“¯kO^ÏN9ç‹îG6.ùþ—•œ@Ô²ëÈam¼ôŸ 2hëÑwOz ëÆ…F4 …*Ûâ z3é÷þêïúÞŸ;UJ2£Úuò$Y IÆ.[øëë…+ïæ” ñͤ/?ó7ï›ðÓ²†í{ôí×OæÛtñwõ&k³Ú!plÛ/?-ÝЮ›´ûûÞúä‹ F~Üùã)cþ&¬Qû­:DÀê}а±·VÝûûU€Ô“Û?›ü“Ãcë¿3wHÂK?á¿ðÌÃÃóeßÞ=B¡°Uë6e¥%†14”´fìø‰®ú*¯Þž’ââm[74hÍf¯¼£›M¦Ë—/v}¿‡Ùd|õ•C0 Ëȸ+Kü,K¥[6mí×wÂl‚ ‚p5r¬sϺùsm«¬žf-½1vÔ„{F¶²ÏSxº|qyÙÌ%Ç÷ï=|êìÙ ¥F@’ÈEôOþ•Ü>× O˜BÇ1” È›·ï`˹ràä¹.=Þ¦)}ÅÝý€'ךEÅÊEåFå OKËðÀ8•mO± ðؘت>+qùœøpožç‡U<<<¯~£'ñŽÇO4éøñ'ý7^×=~Ó‚¯{~2ÃÝÃ3,"¤yÏ‘0°CÃÕû/ÄÆGYèj*MŰϜ÷™¶á.–Ê€‰¬ƒÐåg¤ÞH§1\¢p¯_KEê6þ±;8:´¬¸Ü3 ³ê‹ÊM¾a1±ÑAiçŽßÉÓÉd Ÿ°šQa>WïøÖNŒò-¸|6mÝ$üÂéó ¥ê¨„ºÞj]¹èöˆ¦b¦¸¸X¯×3 cµY«ú³p ‰Ä÷òü÷ÀqœeY¾æ a2™\Y!³Ù\\RB?9PÝI?f9Û/ªxxxÞ8œæ²Ó§/$|²cPè¶ ãæuß¹q÷éÍ©lךø„÷ëéz{Þ†­§³BP°7ºÅd'ûWÐìß j¨BœI_rëúucnZµê bp¤§ßó ‹ ®vïVJQA±{„ÜÃݳQë·Åöüm;ϵ|¿«’5œ>“¢+ÎÊÓ z÷ë¶Â=¯R¡~¡‘‘™Ezyæe–ÔnÑ¥(ý„Ü3´QlD~ú•¼Ì\ßúÑ”ÓY9ûC`F’$I’¹y92¹DH 9¹20,g3X¥R™§‡WÕ€*žÿ AðšøE© uç8® °Èé¤\KÒ®u}Žãë‚!TTTòÄq^•Á<<<<¯Šz|öðJòïY¶ót-ûàCdaöͳÛîø«Ä8Ôjs\WnοÜ Xh"À×ÇiPý䯧.šÁ»2‡Ð4ã8ð„@ T«Á ³¤ÍX ¸P©[­V¯€•›cË#„2!@IQ©Tå)Ç¡0+a8rØenPXª'1B¡P,CL^aN¶™ÓÔwŸ¿hQøÛ¬f•W¨¿ÆƒrØ)ŠÂqœc¹§¬õnÝ€c8qËq‡Ãj¶¤ßM/F¨š·ÏccÌyx^c"«G¶hÕúáV„8×g„eÓÒÒžt,/ªxxx^7†yº¨JÞ¹®q‡‰ÇwýXR”×{ɰŸ§.ïîuëú•a[¶·ìèSÇtû3 @zjêÛ×9¼bاÏËrì“å ry}8„÷??‡„=uôl¾{…LÂVc³˜å¥8gG€Ñ4ÃrGsòˆ©ZK§eé«Ñ@ãB©ˆ´â!ÎÞ½zMÕ’A Ž©´nN³É¬/#HqBˆ{$ç`W¯]Í/( –c9–«]«Nóf-W®^¡Vk„¤«ây£p:)p8˺Rà:N†a8ŽãX–㸧|"øÝ<<<¯Ï\±:r¢`ìúqoo¿‡ÉJ3{}2hÿüOÔo%­Ñ²n\ ¶Ý?ÿ?µfúªãuZõÖJ€{¤ Þc'†'ŠªûK¡D*uÐŒƒ!£c‹²t‰õk—¥]ܳkǹ Wì,!I•*%€¤\!Ç0\ •ɤjÿÚq^ûÖÿy7ߒج¥„sÒáé®&Dê˜ho›ÞZ£&Uœ¾wû¶ä3çMN‰D.…÷˜ÔUMæ8ɤ‘‘¡a!aa¡Õ|¼öîß³bÕ²Ö­[]¿~M,Wv¦Ìéoµhb@ …ÂÕ¢/8÷n‡îœ’z¿Cç³ùV•JõÌ“T’ò^ûm;t;vtIÿa3ܪ‰†ôî½õìµÖíÕ'G}íɼ²5ÀÏ7  Ný‡sVë:É7÷¸Úk×k¶ûŽU{¿qΫfÕŠŽŒŒŒOølÆ ‹ˆíܼ¾››[bbbúͶœÏ•iµ8þbâ¡$óB«ú‰ÁÁÁ±±qcfí¬œ®Æ–=qÔç¬Vû`Æ©Çc.¼ðVË·ÎåÚžçzû_(++Ã0 Ãp Æ1 $IbØ£©èþ‚÷Tñðð¼np÷ôìµ:÷¼ùó·Éz=Ȥ²)ߎñT‡ïºÜŒ)y9y‰‰b/w?ÇD"eçazÆÈèÂìk—.›î;›k+Ëʽc²H”Õ¼|:ĵìùþûw|°Òâú¿ƒù\Ût¹ì!ã$m5Z°®ï´ ð€ç; ¼ö¯?rÁšßiÀðå©’‚VئAÁX(¾ví³ išrš÷%-Ú›N¬þ¥5EÓ®«‚aš²Þ¸lÇ zéþN÷Ã8­¤:îMêÐçk¥gànôÐxté=tHóÐÍ¿ÿzzÿ¶·êÄ éª°:=¶ÿ™°sÿ¡µó>_´|Ӊ݋œõÄá}&š+,*¿™E7mÚÐÏ3èƒÆqœ¹©4B‘ÇqOw­B¥!I{½Ø–¿Ó¬ðô/åDð×#P'Ï^,¦ޱé×ÏÝ,0òÒL§r†Lhøa£ˆÝGN…5DŒ0àýVavÛÿT°è½÷Þ“ËdíÚ· KOO€üÜœ§Â{ªxxx^#0€çȨ^VZòÐÏ3yN¿ûï¾v»Ýn4L÷±Z­®eÁø”<Žå€rR¹Ù¹…E…ùEEE……E€a€aì#; R*AŽ¢¢R!Ã+ÇG€Y庲b«Ý)–Êä²e—/Z°~ÓžÛ·OYi,ÂI émŽâRÕfÕØ2½qÝðX–±Z¬ŠÀf§ÏHT†uk=cávÒ=rØðV¿ îS"õmÚ¸aõ&ÃO\U|jEýè9H&‘H€ç©°´mçê_Wí½±tßF/«ÅL9ìv'…8jÿºEK6Ÿÿu÷–³ÙµºU¯×Øpc{: 1­?Î+*ièÎå[8‘Pâáæéß(.øîÅS¥& Iò?ˆÕNiBj?eßñ•¹çwȼ»`ÙÆSÖìØµ÷ý6ƒ(ÚIÙˬV',ŲC[-V„á€a¤PFX­v‰ÊJõ&ŽcãvüñC‰²å®=ûim)Ì€Î}Û^Z»xÕêõ ‡ÏQ¸²Šümvû[íÚ —¢*ÌÚŸ ÞSÅÃÃóº€aR‰´´¸˜ÿJ 8«Õ*“ËúïÊe4è+ã•Pÿ˗駃BàIS¸öªTj¹\!`"±H¡P8×v§Gä†F8B®¨üû/…¦Œ;W-±Gi®æš»ti%žÿÅøo )²Ñêµ Xöi»2u·V.ÝÒ®[Ç-ÎÉ”ªNÚÉ}¼†Õ„4ÒâK8ÏØ_òìæ 'o–ED·o%yÑXé7“ìÔ½Æýðñ”EÇ×o@Bß:×>²yM¾Ð¿}Ökô7&ýz~ËÆ“àѧ{s‹^ÒVïÜ¿Íé/o\7‹Ä§F¤Ÿ6Ûa:´r>wÉ?åôñ¸Ö=|TÀX^ìó••²Åö”ˆˆàòœ”Æí6ö»˜rd‹9žëTÚ‡ïiA(Œ¨&û}ŦVqMâü¶¬\œínÞ-H”*0–cX¿¸V5esæ®þSTpî^~±@"Ž;š‘»}׿u«’Å­æžðA<ùˑҫ Õ‡ãï}²*?§ÇYÌf…Ba³ZL&Se¹§DªS¦LùSòðððüëܽ›NDHH¨ÍfÅ0 ~-%% 0œã¸—åz&‡ôzÝåK—êÔ©#è™v @IDAT“ÉY®jb`Y¶¼\§Ñh\_å)Òëtv›-88„eÙª éæëÑ1±‡ãîÝô:uêzÊIaÆqÇpR@"yéòÅø¸x‡£âë>A!Á5ü}IÄzyù†„(%¢°Ðx_ÏðÀðÄHïâr[ówûÔ ¡œ‚¶¢‹ò '}#Â=Õ UPX„\L$åé©•ȠꪹK¼Ý½‚ƒ$`ç8a@¨Ÿ¾ ßdg‚k6lÛ¶9XuVSyQfF½ÆEi¥4;;¯ ”m?PÝSb·ðŽãR©ÔÂÑÆè˜F!>J“®\êéC™ôR¯ -Q;Ü_cÔ•K´¡ñÕ}œ µh×J Á"–ißñe\5©·F.À€å°˜íúôé f烥¬Ÿ‰DˆÙÍžAñ#'öÀñ`w›Å$’ºõØ/"(@)$Ædf•ø†Æ¼Ó®®® DîQ­MÇn±5‚eB""¢ž_˜/Bò·šT¿u++¨Flûw>Œ ò QN“ïÔ½síZ ýÜ›ÕRpéöÖOÝëV³X,Ïy‡ÃhµZÏŸ;W¿~¡PèZu:‡Ãõ÷ÊÉÉÉÎÎjÖ¼…ÝfƒÁššúV»v®q° 7ãáááù¾½{„Ba«ÖmÊJK*Ü?:u2';‡¦é§ly{éà8®T*ÂÃ#YŽ{(QŽcwîÜ)ÈÏãXîi…ûþ«Ü=Üãâ„B‘Ùl®:óÆ ë»÷ì¥×ë÷îß=røèÒòb±H„a8€†@°xñ¢ý?2 è~€—J¥²Z­ ÃÈd2–e)ŠR«Õz½^©TA’$EQF£‘eY™L¦P(0 s:z½^"‘`fµZL&3›ÍAÈårƒÁ P((ŠbF©TÚl6¹\.X–5eYÉ=ûNlÚ}âô#œ¹¹A¨Õjp8F£‘¿=‚ ÜÝÝ]Y[%IÕER½^/—Ëq§(ê¡öJ÷ I’r¹ÜÕŽ2›ÍJ¥²²§Ãá°Ûí/ªeE"‘L&sýNÓ´Åb‘J¥"QE¹Óé´Ùl*• Çq†ahš®´Íét:N¥R麫eµZBUM-ÎNù~ê”c—J§žVÑ´Åü컕ädgôºF›èõú3gNs×ñNˆCEqGQyªŒFÃáC‡7i§×ë ++smÒÚÙsçUœ@þ¢äááùò¨¢iŠãHH24ª³{5`¸@€¢iæÁÚ0AZ$–¼b“€¦½Áá°ã^uò­›7á8€©TÊÒÒ2¯,ØüW„‡‡»Édþ½A¤÷õñE8QXPÀçvn\ácînZwÏœœ«ÕòB—­H$lРa`pˆÍfÅqüÄñ.\`Ç  Ç]»I4jM«Ö­âbãì»ë$?$ªø˜*ž×³ÙlµZI’$pâé úþ B®„ËýK^\\ô¯H \™ >ݺ÷€û©AŸb†aÿúêŽcýƒag¯'oæéª¸Ü!6>þ…φ٬ÖÜÜ ¢AíZ·"‰Ûc9Öj±¦ßM ¤á1Ê•U<<<¯ þþ"‘˜¿ûòððü<ž«—FãŽéÊË»U‚U<<<¯ ‹Åü"Q<<<<'m>åEÏk†=f‹‡‡‡ç•Áçùàáááááááy ðž*ž× îÁ”Tÿ10 Çñª…wªÂq˲ÿUoäý—Û€8Žùo¿4W¥Ç”›D±,û_Ç0W%¨ç©¤ ¼¨âáááy`æÚµk7o\çX¿_%ð¿†\&ŽŽ®Í>" †I¿sçÒ¥‹,ËÿÁ—&•HkÔ¨á¸+3g%,Ëædg'''³,Cü× Ecb‰8""¢fÍIV$ɼ˲ùùùgÏœù¿öî4ºŠ"íøSÕ}·„$$ I€€,‚ÊaG•Á\3zæuF_EFqÆñtœY=¬’H°/! Â’í.Ý]]Uï‡ÆkHÜK"pãóûÀI.NýoÝCUÕO{=5ȇÛÜpÀáp4oÞ¼]‡öN§‹s~ÕqUˆuB¡ªÎ=š›“3õ¹nt[®QYiÉâ/¾ ”$'·ÓtÝ BädgïÝ»7t£ù¼Þe_-1˜qW×®>߯OP‘Bœ²mÑ”©/ø`î¼Ùo¾±.#*í|ú­õëLýܦõ?Òª*bÉLÿfú‹/€ª*5e­;z0T}ÀÍL°¢÷Þœ¹|s&Tñ>Ha®þrÞ;ï ¿MoÖÔ)KOn3v(Üܽ€ƒ*„ªmd0ûW$@½è°#«ôëy{×>LÕ¦žÉ;mP(:±gdÿî-[¶^øÃÑ?}ýþ­â~¦¢5m¼61±É­]ºöZ~ DUéœW§ Jí÷ð˜‡=øXWUµ†wîM0÷©“§M€ñ#†=6îá!Lziv±ªª*ýèõIÅuêÜmÙÁ¢f qMÛ (îÛs°¡ªFñ‘g&LÊÈ*½pò˜M›þuÁ†ŽwO€ÜÝëÞsW«ÛZMøó;Åjͧ ”|ßúO“š¤tìòÆÂ5 ª;¾›7üÁ LU?úŸ§þôÆg*O;¤iBBßA£ó÷jš]¯AÑ©õ ZUÕ¢¬5½z WU5kûê;¤ÜÞ5ã¤Ñ¢C/ÈX1¯QƒºtóUšªªWŒÙæk\äÌý·§GÄ7ŠaöÒæ­†À‰ÌõCî횘ÐdÂ_æª*¼GþÐñö)S&¤öéûʼ¥RU™§`Æóãš%&Üywß%×µ›pP…BµPPKL+%1-Ö¬ÝøæØ&“F¿$*c×Î>_ð®£iêñãGçžñ¸ÎŒ2ë?ûÏoølæÙ#»¼’ž?ôŸ×þùÝÓùßÎìñÉôLî‘°V}¾XüõˆÎðÚôàÊKoU®¶T;—”ÒÔ/¤mßHöeîn8â•5ëÖÒ3?¹l'wñÓ¯~xøÜùÌÝ?õo[Ræ€óî*Ü9dz½àJ[õ^F69yêä€f~A.<÷âÌÏÌ>–µ¯¡wÏŒ7>¿†t5ÒkE9úŽš¾ôÀéŒï¦-š»bÇé»?GsîOíûÉ–soýuÜÜW_ȧ·ÌËÛ°vI´¯Ð` LωcDzôüƒ‡öÀŒ7ßKôÎþ}{Š÷î(2Äî^£¦gnZöö3_JË7)¥5-ؾó;–þáœÏ7=wö©®ñgN3ÞjÕÊ©¼Ó4óßO¿²ØéÚ¾/óžY Ö?/ã›O·åèÛ×}°r[ááSyKf ½/¹5TsúªÑüðî?„ªm8篹HÎí®˜~»@RJ2,;@£Ã#ÓŠ.œ¿ãþw`¤Á?¼~Ú—¿£qò½}bÌø!ý»ó҂û–;bÚĉeEçþãt ¶~“f]S Q“FüØBq•’‰\a»=ç<ð³I)£Âc Y“ÖÃRÛøâc£|î’ìô•]F/Œ€*4Ͱ.œÑ‘õ%€”¢NxEUòíîôÐ[0bÜÀ—¿ZÉÅÑcÙGòÿõööo£ލÁ½ºZí‘2¨b—f"0Zþô‚âÂwž}‚{Knm‘rkb™òä}mL±SÜpüxn¯‡çX‡IÍ*v@)Œ¨o½ÕàqFÜÓ·Lž0àëìü3;24Cû߉Ýç:ÞÙ3ºž ÁöÝe£Uç<Ù;6wûôܳáß÷I0˜TR?ïûÊ¿7Üݺq«¡u4¥~g‰×s*}̓͞'<¥•?9ù E›ÊdÀ»íqP…Bµ®koŽ‘\g&/uÀÙ ¥ U !…× þƒé0hÐ?ì2d WlõòBý3¹»36½_6oÛ.åîèçÏ·Nåqçy5]Ÿ€’R¯Bë€aèW­C]¡U6^×µÀ+ÌRè`0΋ι¡±Û§iN)Ú&ŸûÛ"Œ—ž’2Ÿir.„ÉÁ¡p_@ø™ü¼üÓLÈ&Í 6lHÙ²q—O÷)ô–ö­Ûž±ü¡v*pŸ—3\4ÿ—2MÆg-&îÖÖ-:ýããêY1Mqöäþ÷,5dÈögæÏÖ,¡afú·rУeÅ>Í#„0MƒØÂ ïy/@î¡cnŸ "2Ì™uèp¯Äæë7í1št‹MHJLHYðKoê&w»½ÁÔ—’—ýÔ™¦É˜¡ëzÀç¹DB‹æÅ«Ö ßùSæ…â|öºá޽;¶ kß}íÚŒ¸Vã ÆÙ€¨’Rf2ÁyÛöú¹à_‹ïû ÃÔµ &œ*—V.ÿ!„ëÎwòËVeBˆâtòÀÁU³† ¾xëɘ¸V Þ>ièmc{þaò˳Nñ°Èºqݦöl|ºY󤔔”‡&ͯӈrñõ7EIˆ¿A`µ‰‰h7eDŠÒ¶yR¯ÞC¿;^V/&€Ø ÉŸÇÞ>´Cç—f}\Âΰ°;û?n?¹¼ßàûæ~½;²A{€˜çŸâã;vìØïþÇ꛿š°¸Á+VôPwþé…yO=ÿâÙ­´iÓvø˜‰ç#¹œN“Ø-¹AY¿>þ±ô§°è–¶1Cºý÷õ'FŒ½-ËyK¼Ò`Ä[înÐ ®c§NG>sŽ)‡#Èl5/©ÇãÂŽôê7èíÿn²G$À##®ž;©mÛÖ;/4ùËìG¸»@QîCGº=0µ^Ѫ¤-SRR¦ÎXâ²Û«¹?ðŸ&¡[ä!ô;÷ÝšÕv»½wŸ¾çÏ]ç{ÐnNŒ±O?ùdúK/]( f‹±Œ w›ËãõØ ³¹êjºÛ騣sa:µÙ9—ÖS~5]s:àj]¿4Íëtº@ TÑ5Ën—Ôæñ¸Ãl’Ú#<^¯i=E1ïý¹Ó¦Móiºi^¬¢nšæ’Å‹'O™ZZZ"¾l© ¨§3ŸÃæ2„dž’ððpImšÏër9AH Šƒƒ»©½ŽO×\Ep…*8€Ð½6‡“siÝn¦ºÓF€¨ 58èÞRä$Ü?çÏ{öÙg fú'¥8ç«V~;rä( Ô l¦ŠRY$PIˆÎL+UÂb¬·Jªƒš„”TÑsÙÔðyœ.—ïÔ Ý©HPlB¥ ø4ŸËé)€(ˆn0Ãç |‚çã… }ô»Ã¡ë†õŠà|Ãúõwwï^/6Vóù‚z£üTU ¯Sç×^`†“ Pì %oç.mv{ùL”Ò¤¤¤ô´´­[¶ØŽê׽RÓ|ÉÉÉ‘̼ú†wT!„P-A) ïß¿^^žÉXè•™ ÄépÄ7nìt8­"àþ¿¡”ºÂÂRSSsssC4šÃnkÔ(ªn]“_Pâ sõîÓûxVcìz?¡ºú±Ûl 6ŒŽ‰á¼âü¢ÓéìÞ£{NNŽÏç#! Àf³Õ¯_ÿ–z±\pœ©B¡ß.DÝèèz±õ)­‰*“×›”Bš¦i\îf~!Dxˆ))!ATc¼YH)¥iróÒÁ¢Eaw8Û§¤PBC0X&¬hå;NJ)lvG›¶É Õhœ›&3\OÇABÕÖ?úŒ™Œ…êVn¿ 0ë[ëÊ}ƒZTc.sîó…ÜêXE•‡V}¬jÖ¡¸¸ U!T«„Ü^àÀa´PT‹£UÚ¥LB!„n8¨B!„ª8¨B!„ª8¨B!„ª¸Q!Ú ÎÉÏWåF7!ô»Cà’Êb8¨B…*Bèœ9ï6¨__‚5™B!OQ”öíÛù¿%TE¡›PÖgFÕBþ²8S… U¿«ú7¡›nTG!„ª8¨B!„ªÿ²†2TÉŽøtIMEÚ Û~›÷—zTXtRaw profile type APP1xœ]ŽKÃ0D÷œ"GàgÇ=mÜ(RÔV¹ÿ¢ØT‰Õ' Œa­¯zlés¼ŸÛ^ajeÐ4—|Ë]tAdà¸Y÷€ÂÔ¼ûž‰Û¢ ™ð™#éoÞIÆy¸®ŒhùûÁY Š¿!æ’‰] s/Zì·á 6 !ÚêkzTXtRaw profile type iptcxœãò qV((ÊOËÌIåRc .c #K“ „CKÃd#s 3ÅÜÈÌÔÜØÌÒ<ÑÌ(dlfafhnd”« ÉÒ·ö—&Ãï>³iTXtXML:com.adobe.xmp 757 991 ­jIEND®B`‚dkopp-6.5/dkopp-6.5.cc0000644000175000017500000050216212343020444013075 0ustar micomico/************************************************************************** dkopp copy files to / restore files from BD/DVD media Copyright 2006-2014 Michael Cornelison source URL: kornelix.com contact: kornelix@posteo.de This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***************************************************************************/ #define dkopp_title "dkopp v.6.5" // version #define dkopp_license "GNU General Public License v.3" #define debug 0 #include #include #include #include // v.6.2 #if defined __linux__ // v.5.9.3 #include #endif #include "zfuncs.h" // order important // parameters and limits #define normfont "monospace 9" // report fonts v.5.2 #define boldfont "monospace bold 9" #define vrcc 512*1024 // verify read I/O size #define maxnx 1000 // max no. include/exclude recs. #define maxfs 200000 // max no. disk or DVD/BD files #define maxhist 200 // max no. history files #define giga 1000000000.0 // gigabyte (not 1024**3) #define dvdcapmin 0.5 // DVD/BD capacity sanity limits, GB #define dvdcapmax 50.0 // (Blue-Ray dual layer = 50 GB) v.5.3 #define modtimetolr 1.0 // tolerance, file equal mod times v.5.1 #define nano 0.000000001 // nanosecond #define gforce "-use-the-force-luke=tty -use-the-force-luke=notray" // growisofs hidden options // special control files on DVD/BD #define V_DKOPPDIRK "/dkopp-data/" // dkopp special files on DVD/BD #define V_FILEPOOP V_DKOPPDIRK "filepoop" // directory data file #define V_JOBFILE V_DKOPPDIRK "jobfile" // backup job data file #define V_DATETIME V_DKOPPDIRK "datetime" // date-time file // GTK GUI widgets GtkWidget *mWin, *mVbox, *mScroll, *mLog; // main window GtkTextBuffer *logBuff; GtkWidget *fc_dialogbox, *fc_widget; // file-chooser dialog GtkWidget *editwidget; // edit box in file selection dialogs // file scope variables int main_argc; // command line args char **main_argv; int killFlag; // tell function to quit int pauseFlag; // tell function to pause/resume int menuLock; // menu lock flag int commFail; // command failure flag int Fdialog; // dialog in progress int clrun; // flag, command line 'run' command int Fgui; // flag, GUI or command line char subprocName[20]; // name of created subprocess char scriptParam[200]; // parameter from script file char mbmode[20], mvmode[20]; // actual backup, verify modes double pctdone; // % done from growisofs char scrFile[maxfcc]; // command line script file char backupDT[16]; // nominal backup date: yyyymmdd-hhmm char userdir[200]; // /home/user/.dkopp char TFdiskfiles[200], TFdvdfiles[200]; // scratch files in userdir char TFjobfile[200], TFfilepoop[200], TFdatetime[200]; char TFrestorefiles[200], TFrestoredirks[200]; // available DVD/BD devices int ndvds, maxdvds = 8; char dvddevs[8][20]; // DVD/BD devices, /dev/sr0 etc. char dvddesc[8][40]; // DVD/BD device descriptions char dvddevdesc[8][60]; // combined device and description // backup job data char BJfile[maxfcc]; // backup job file char BJdvd[20]; // DVD/BD device: /dev/hdb double BJcap; // DVD/BD capacity (GB) int BJspeed; // write speed, x 1.38 MB/sec v.4.5 char BJbmode[20]; // backup: full/incremental/accumulate char BJvmode[20]; // verify: full/incremental/thorough char BJdatefrom[12]; // mod date selection, yyyy.mm.dd time_t BJtdate; // binary mod date selection int BJndvd; // no. DVD/BD media required int BJval; // backup job data validated int BJmod; // backup job data modified char *BJinex[maxnx]; // backup include/exclude records int BJfiles[maxnx]; // corresp. file count per rec double BJbytes[maxnx]; // corresp. byte count per rec int BJdvdno[maxnx]; // corresp. DVD/BD sequence no. (1,2...) int BJnx; // actual record count < maxnx // DVD/BD medium data char dvdmp[100]; // mount point, /media/xxxxx int dvdmpcc; // mount point cc int dvdmtd; // DVD/BD mounted char mediumDT[16]; // DVD/BD medium last use date-time time_t dvdtime; // DVD/BD device mod time int dvdnum; // DVD/BD medium sequence no. char dvdlabel[32]; // DVD/BD label // current files for backup struct dfrec { // disk file record char *file; // directory/filename double size; // byte count double mtime; // mod time int stat; // fstat() status int dvd; // assigned DVD/BD number int inclx; // include rec for this file char disp; // status: new modified unchanged char ivf; // flag for incr. verify }; dfrec Drec[maxfs]; // disk file data records int Dnf; // actual file count < maxfs double Dbytes; // disk files, total bytes double Dbytes2; // bytes for DVD/BD medium // DVD/BD file data struct vfrec { // DVD/BD file record char *file; // directory/file (- /media/xxx) double size; // byte count double mtime; // mod time int stat; // fstat() status char disp; // status: deleted modified unchanged }; vfrec Vrec[maxfs]; // DVD/BD file data records int Vnf; // actual file count < maxfs double Vbytes; // DVD/BD files, total bytes // disk:DVD/BD comparison data int nnew, ndel, nmod, nunc; // counts: new del mod unch int Mfiles; // new + mod + del file count double Mbytes; // new + mod files, total bytes // restore job data char *RJinex[maxnx]; // file restore include/exclude recs. int RJnx; // actual RJinex count < maxnx int RJval; // restore job data validated char RJfrom[maxfcc]; // restore copy-from: /home/.../ char RJto[maxfcc]; // restore copy-to: /home/.../ struct rfrec { // restore file record char *file; // restore filespec: /home/.../file.ext double size; // byte count }; rfrec Rrec[maxfs]; // restore file data records int Rnf; // actual file count < maxfs double Rbytes; // total bytes // dkopp local functions int initfunc(void *data); // GTK init function void buttonfunc(GtkWidget *item, cchar *menu); // process toolbar button event void menufunc(GtkWidget *item, cchar *menu); // process menu select event void script_func(void *); // execute script file int quit_dkopp(cchar *); // exit application int clearScreen(cchar *); // clear logging window int signalFunc(cchar *); // kill/pause/resume curr. function int checkKillPause(); // test flags: killFlag and pauseFlag int fileOpen(cchar *); // file open dialog int fileSave(cchar *); // file save dialog int BJload(cchar *fspec); // backup job data <<< file int BJstore(cchar *fspec, int ndvd = 0); // backup job data >>> file int BJvload(cchar *); // load job file from DVD/BD int BJedit(cchar *); // backup job edit dialog int BJedit_event(zdialog *zd, cchar *event); // dialog event function int BJedit_stuff(zdialog * zd); // stuff dialog widgets with job data int BJedit_fetch(zdialog * zd); // set job data from dialog widgets int Backup(cchar *); // backup menu function int FullBackup(cchar *vmode); // full backup + verify int IncrBackup(cchar *bmode, cchar *vmode); // incremental / accumulate + verify int Verify(cchar *); // verify functions int Report(cchar *); // report functions int get_current_files(cchar *); // file stats. per include/exclude int report_summary_diffs(cchar *); // disk:DVD/BD differences summary int report_directory_diffs(cchar *); // disk:DVD/BD differences by directory int report_file_diffs(cchar *); // disk:DVD/BD differences by file int list_current_files(cchar *); // list all disk files for backup int list_DVD_files(cchar *); // list all files on DVD/BD int find_files(cchar *); // find files on disk, DVD/BD, backup hist int view_backup_hist(cchar *); // view backup history files int RJedit(cchar *); // restore job edit dialog int RJedit_event(zdialog*, cchar *event); // RJedit response int RJlist(cchar *); // list DVD/BD files to be restored int Restore(cchar *); // file restore function int getDVDs(void *); // get avail. DVD/BD devices, mount points int setDVDdevice(cchar *); // set DVD/BD device and mount point int setDVDlabel(cchar *); // set new DVD/BD label int mountDVD(cchar *); // mount DVD/BD, echo outputs, status int unmountDVD(cchar *); // unmount DVD/BD + echo outputs int ejectDVD(cchar *); // eject DVD/BD + echo outputs int resetDVD(cchar *); // hardware reset int eraseDVD(cchar *); // fill DVD/BD with zeros (long time) int formatDVD(cchar *); // quick format DVD/BD int saveScreen(cchar *); // save logging window to file int helpFunc(cchar *); // help function int fc_dialog(cchar *dirk); // file chooser dialog int fc_response(GtkDialog *, int, void *); // fc_dialog response int writeDT(); // write date-time to temp file int save_filepoop(); // save file owner & permissions data int restore_filepoop(); // restore file owner & perm. data int createBackupHist(); // create backup history file int inexParse(char *rec, char *&rtype, char *&fspec); // parse include/exclude record int BJvalidate(cchar *); // validate backup job data int RJvalidate(); // validate restore job data int nxValidate(char **recs, int nr); // validate include/exclude recs int dGetFiles(); // generate file list from job int vGetFiles(); // find all DVD/BD files int rGetFiles(); // generate restore job file list int setFileDisps(); // set file disps: new del mod unch int SortFileList(char *recs, int RL, int NR, char sort); // sort file list in memory int filecomp(cchar *file1, cchar *file2); // compare directories before files int BJreset(); // reset backup job file data int RJreset(); // reset restore job data int dFilesReset(); // reset disk file data and free memory int vFilesReset(); // reset DVD/BD file data and free memory int rFilesReset(); // reset restore file data, free memory cchar * checkFile(char *dfile, int compf, double &tcc); // validate file on BD/DVD medium cchar * copyFile(cchar *vfile, char *dfile); // copy file from DVD/BD to disk int track_filespec(cchar *filespec); // track filespec on screen, no scroll int track_filespec_err(cchar *filespec, cchar *errmess); // error logger for track_filespec() cchar * kleenex(cchar *name); // clean exotic file names for output int do_shell(cchar *pname, cchar *command); // shell command + output to window int track_growisofs_files(char *buff); // convert %done to filespec, output // dkopp menu table struct menuent { char menu1[20], menu2[40]; // top-menu, sub-menu int lock; // lock funcs: no run parallel int (*mfunc)(cchar *); // processing function }; #define nmenu 44 struct menuent menus[nmenu] = { // top-menu sub-menu lock menu-function { "button", "edit job", 1, BJedit }, { "button", "clear", 0, clearScreen }, { "button", "run job", 1, Backup }, { "button", "run DVD/BD", 1, Backup }, { "button", "pause", 0, signalFunc }, { "button", "resume", 0, signalFunc }, { "button", "kill job", 0, signalFunc }, { "button", "quit", 0, quit_dkopp }, { "File", "open job", 1, fileOpen }, { "File", "open DVD/BD", 1, BJvload }, { "File", "edit job", 1, BJedit }, { "File", "show job", 0, BJvalidate }, { "File", "save job", 0, fileSave }, { "File", "run job", 1, Backup }, { "File", "run DVD/BD", 1, Backup }, { "File", "quit", 0, quit_dkopp }, { "Backup", "full", 1, Backup }, { "Backup", "incremental", 1, Backup }, { "Backup", "accumulate", 1, Backup }, { "Verify", "full", 1, Verify }, { "Verify", "incremental", 1, Verify }, { "Verify", "thorough", 1, Verify }, { "Report", "get files for backup", 1, Report }, { "Report", "diffs summary", 1, Report }, { "Report", "diffs by directory", 1, Report }, { "Report", "diffs by file", 1, Report }, { "Report", "list files for backup", 1, Report }, { "Report", "list DVD/BD files", 1, Report }, { "Report", "find files", 1, Report }, { "Report", "view backup hist", 1, Report }, { "Report", "save screen", 0, saveScreen }, { "Restore", "setup DVD/BD restore", 1, RJedit }, { "Restore", "list restore files", 1, RJlist }, { "Restore", "restore files", 1, Restore }, { "DVD/BD", "set DVD/BD device", 1, setDVDdevice }, { "DVD/BD", "set DVD/BD label", 1, setDVDlabel }, { "DVD/BD", "erase DVD/BD", 1, eraseDVD }, { "DVD/BD", "format DVD/BD", 1, formatDVD }, { "DVD/BD", "mount DVD/BD", 1, mountDVD }, { "DVD/BD", "unmount DVD/BD", 1, unmountDVD }, { "DVD/BD", "eject DVD/BD", 1, ejectDVD }, { "DVD/BD", "reset DVD/BD", 0, resetDVD }, { "Help", "about", 0, helpFunc }, { "Help", "contents", 0, helpFunc } }; // dkopp main program int main(int argc, char *argv[]) { PangoFontDescription *deffont; // main window default font GtkWidget *mbar, *tbar; // menubar and toolbar GtkWidget *mFile, *mBackup, *mVerify, *mReport, *mRestore; GtkWidget *mDVD, *mHelp; int ii; main_argc = argc; // save command line arguments main_argv = argv; beroot(main_argc-1,main_argv+1); // if not root, restart as root zinitapp("dkopp",null); // get install directories Fgui = 1; // assume GUI clrun = 0; // no command line run command *scrFile = 0; // no script file *BJfile = 0; // no backup job file for (ii = 1; ii < argc; ii++) // get command line options { if (strEqu(argv[ii],"-nogui")) Fgui = 0; // command line operation v.5.0 else if (strEqu(argv[ii],"-job") && argc > ii+1) // -job jobfile (load job) strcpy(BJfile,argv[++ii]); else if (strEqu(argv[ii],"-run") && argc > ii+1) // -run jobfile (load and run job) { strcpy(BJfile,argv[++ii]); clrun++; } else if (strEqu(argv[ii],"-script") && argc > ii+1) // -script scriptfile (execute script) strcpy(scrFile,argv[++ii]); else strcpy(BJfile,argv[ii]); // assume a job file and load it } if (! Fgui) { // no GUI v.5.0 mLog = mWin = 0; // output goes to STDOUT initfunc(0); // start job or script unmountDVD(0); // unmount DVD/BD ejectDVD(0); // eject DVD/BD (may not work) return 0; // exit } gtk_init(&argc, &argv); // GTK command line options mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create main window gtk_window_set_title(GTK_WINDOW(mWin),dkopp_title); gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(mWin),800,500); mVbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(mWin),mVbox); // add to main window mScroll = gtk_scrolled_window_new(0,0); // scrolled window gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0); // add to main window mVbox mLog = gtk_text_view_new(); // text edit window gtk_container_add(GTK_CONTAINER(mScroll),mLog); // add to scrolled window deffont = pango_font_description_from_string(normfont); // default font if (! deffont) { printf("monospace font not found \n"); // v.5.9 return 0; } gtk_widget_override_font(mLog,deffont); logBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); // get related text buffer gtk_text_buffer_set_text(logBuff,"", -1); mbar = create_menubar(mVbox); // create menu bar and menus mFile = add_menubar_item(mbar,"File",menufunc); add_submenu_item(mFile,"open job",menufunc); add_submenu_item(mFile,"open DVD/BD",menufunc); add_submenu_item(mFile,"edit job",menufunc); add_submenu_item(mFile,"show job",menufunc); add_submenu_item(mFile,"save job",menufunc); add_submenu_item(mFile,"run job",menufunc); add_submenu_item(mFile,"run DVD/BD",menufunc); add_submenu_item(mFile,"quit",menufunc); mBackup = add_menubar_item(mbar,"Backup",menufunc); add_submenu_item(mBackup,"full",menufunc); add_submenu_item(mBackup,"incremental",menufunc); add_submenu_item(mBackup,"accumulate",menufunc); mVerify = add_menubar_item(mbar,"Verify",menufunc); add_submenu_item(mVerify,"full",menufunc); add_submenu_item(mVerify,"incremental",menufunc); add_submenu_item(mVerify,"thorough",menufunc); mReport = add_menubar_item(mbar,"Report",menufunc); add_submenu_item(mReport,"get files for backup",menufunc); add_submenu_item(mReport,"diffs summary",menufunc); add_submenu_item(mReport,"diffs by directory",menufunc); add_submenu_item(mReport,"diffs by file",menufunc); add_submenu_item(mReport,"list files for backup",menufunc); add_submenu_item(mReport,"list DVD/BD files",menufunc); add_submenu_item(mReport,"find files",menufunc); add_submenu_item(mReport,"view backup hist",menufunc); add_submenu_item(mReport,"save screen",menufunc); mRestore = add_menubar_item(mbar,"Restore",menufunc); add_submenu_item(mRestore,"setup DVD/BD restore",menufunc); add_submenu_item(mRestore,"list restore files",menufunc); add_submenu_item(mRestore,"restore files",menufunc); mDVD = add_menubar_item(mbar,"DVD/BD",menufunc); add_submenu_item(mDVD,"set DVD/BD device",menufunc); add_submenu_item(mDVD,"set DVD/BD label",menufunc); add_submenu_item(mDVD,"mount DVD/BD",menufunc); add_submenu_item(mDVD,"unmount DVD/BD",menufunc); add_submenu_item(mDVD,"eject DVD/BD",menufunc); add_submenu_item(mDVD,"reset DVD/BD",menufunc); add_submenu_item(mDVD,"erase DVD/BD",menufunc); add_submenu_item(mDVD,"format DVD/BD",menufunc); mHelp = add_menubar_item(mbar,"Help",menufunc); add_submenu_item(mHelp,"about",menufunc); add_submenu_item(mHelp,"contents",menufunc); tbar = create_toolbar(mVbox); // create toolbar and buttons add_toolbar_button(tbar,"edit job","edit backup job","editjob.png",buttonfunc); add_toolbar_button(tbar,"run job","run backup job","burn.png",buttonfunc); add_toolbar_button(tbar,"run DVD/BD","run job on DVD/BD","burn.png",buttonfunc); add_toolbar_button(tbar,"pause","pause running job","media-pause.png",buttonfunc); add_toolbar_button(tbar,"resume","resume running job","media-play.png",buttonfunc); add_toolbar_button(tbar,"kill job","kill running job","kill.png",buttonfunc); add_toolbar_button(tbar,"clear","clear screen","clear.png",buttonfunc); add_toolbar_button(tbar,"quit","quit dkopp","quit.png",buttonfunc); gtk_widget_show_all(mWin); // show all widgets G_SIGNAL(mWin,"destroy",quit_dkopp,0); // connect window destroy event g_timeout_add(0,initfunc,0); // setup initial call from gtk_main() gtk_main(); // process window events return 0; } // initial function called from gtk_main() at startup int initfunc(void *) { int ii; char *home; time_t datetime; menufunc(null,"Help"); // show version and license menufunc(null,"about"); strcpy(userdir,get_zuserdir()); // get temp file names sprintf(TFdiskfiles,"%s/diskfiles",userdir); sprintf(TFdvdfiles,"%s/dvdfiles",userdir); sprintf(TFfilepoop,"%s/filepoop",userdir); sprintf(TFjobfile,"%s/jobfile",userdir); sprintf(TFdatetime,"%s/datetime",userdir); sprintf(TFrestorefiles,"%s/restorefiles.sh",userdir); sprintf(TFrestoredirks,"%s/restoredirks.sh",userdir); datetime = time(0); printf("dkopp errlog %s \n",ctime(&datetime)); menuLock = Fdialog = 0; // initialize controls killFlag = pauseFlag = commFail = 0; strcpy(subprocName,""); strcpy(scriptParam,""); strcpy(BJdvd,"/dev/sr0"); // default DVD/BD device strcpy(dvdmp,"/media/dkopp"); // default mount point v.5.1 dvdmpcc = strlen(dvdmp); // mount point cc BJcap = 4.0; // default DVD/BD capacity, 4 GB v.5.3 BJspeed = 0; // speed (0 = default) v.4.5 strcpy(dvdlabel,"dkopp"); // default DVD/BD label v.5.1 strcpy(BJbmode,"full"); // backup mode strcpy(BJvmode,"full"); // verify mode BJval = 0; // not validated BJmod = 0; // not modified strcpy(BJdatefrom,"1970.01.01"); // file age exclusion default v.4.8 BJtdate = 0; BJnx = 4; // backup job include/exclude recs for (ii = 0; ii < BJnx; ii++) BJinex[ii] = (char *) malloc(50); home = getenv("HOME"); // get "/home/username" if (! home) home = (char *) "/home/xxx"; strcpy(BJinex[0],"# dkopp default backup job"); // initz. default backup specs sprintf(BJinex[1],"include %s/*",home); // include /home/username/* sprintf(BJinex[2],"exclude %s/.Trash/*",home); // exclude /home/username/.Trash/* sprintf(BJinex[3],"exclude %s/.thumbnails/*",home); // exclude /home/username/.thumbnails/* Dnf = Vnf = Rnf = Mfiles = 0; // file counts = 0 Dbytes = Dbytes2 = Vbytes = Mbytes = 0.0; // byte counts = 0 strcpy(RJfrom,"/home/"); // file restore copy-from location strcpy(RJto,"/home/"); // file restore copy-to location RJnx = 0; // no. restore include/exclude recs RJval = 0; // restore job not validated strcpy(mediumDT,"unknown"); // DVD/BD medium last backup date-time dvdtime = -1; // DVD/BD device mod time dvdmtd = 0; // DVD/BD not mounted if (*BJfile) { // command line job file BJload(BJfile); if (commFail) return 0; } if (clrun) { // command line run command menufunc(null,"File"); menufunc(null,"run job"); } if (*scrFile) script_func(0); // command line script file wprintf(mLog,"\n Searching for DVD/BD devices ... \n"); g_timeout_add(1000,getDVDs,0); // blocks GTK until done return 0; } // process toolbar button events (simulate menu selection) void buttonfunc(GtkWidget *item, cchar *button) { char button2[20], *pp; strncpy0(button2,button,19); pp = strchr(button2,'\n'); // replace \n with blank if (pp) *pp = ' '; menufunc(item,"button"); // use menu function for button menufunc(item,button2); return; } // process menu selection event void menufunc(GtkWidget *, cchar *menu) { static int ii; static char menu1[20] = "", menu2[40] = ""; int kk; char command[100]; for (ii = 0; ii < nmenu; ii++) if (strEqu(menu,menus[ii].menu1)) break; // mark top-menu selection if (ii < nmenu) { strcpy(menu1,menu); return; } for (ii = 0; ii < nmenu; ii++) if (strEqu(menu1,menus[ii].menu1) && strEqu(menu,menus[ii].menu2)) break; // mark sub-menu selection if (ii < nmenu) strcpy(menu2,menu); else { // no match to menus wprintf(mLog," *** bad command: %s \n",menu); commFail++; return; } if (menuLock && menus[ii].lock) { // no lock funcs can run parallel if (Fgui) zmessageACK(mWin,0,"wait for current function to complete"); return; } if (! menuLock) { killFlag = pauseFlag = 0; // reset controls *subprocName = 0; commFail = 0; // start with no errors } if (! *scrFile) { // if not a script file, snprintf(command,99,"\n""command: %s > %s \n",menu1,menu2); // echo command to window wprintx(mLog,0,command,boldfont); } kk = ii; // move to non-static memory v.6.0 if (menus[kk].lock) ++menuLock; // call menu function menus[kk].mfunc(menu2); if (menus[kk].lock) --menuLock; return; } // function to execute menu commands from a script file void script_func(void *) { FILE *fid; int cc, Nth; char buff[200], menu1[20], menu2[40]; cchar *pp; char *bb; fid = fopen(scrFile,"r"); // open file if (! fid) { wprintf(mLog," *** can't open script file: %s \n",scrFile); commFail++; *scrFile = 0; return; } while (true) { if (checkKillPause()) break; // exit script if (commFail) break; pp = fgets_trim(buff,199,fid,1); // read next record if (! pp) break; // EOF wprintf(mLog,"\n""Script: %s \n",buff); // write to log bb = strchr(buff,'#'); // get rid of comments if (bb) *bb = 0; cc = strTrim(buff); // and trailing blanks if (cc < 2) continue; *menu1 = *menu2 = 0; *scriptParam = 0; Nth = 1; // parse menu1 > menu2 > parameter pp = strField(buff,'>',Nth++); if (pp) strncpy0(menu1,pp,20); pp = strField(buff,'>',Nth++); if (pp) strncpy0(menu2,pp,40); pp = strField(buff,'>',Nth++); if (pp) strncpy0(scriptParam,pp,200); strTrim(menu1); // get rid of trailing blanks strTrim(menu2); if (strEqu(menu1,"exit")) break; menufunc(null,menu1); // simulate menu entries menufunc(null,menu2); while (Fdialog) sleep(1); // if dialog, wait for compl. } wprintf(mLog,"script exiting \n"); fclose(fid); *scrFile = 0; return; } // quit dkopp, with last chance to save edits to backup job data int quit_dkopp(cchar * menu) { int yn; if (! Fgui) return 0; // v.5.5 signalFunc("kill job"); // v.6.4 if (BJmod) { // job data was modified yn = zmessageYN(mWin,"SAVE changes to dkopp job?"); // give user a chance to save mods if (yn) fileSave(null); } if (dvdmtd) { unmountDVD(0); // unmount DVD/BD ejectDVD(0); // eject DVD/BD (may not work) } gtk_main_quit(); // tell gtk_main() to quit return 0; } // clear logging window int clearScreen(cchar * menu) { wclear(mLog); return 0; } // kill/pause/resume current function - called from menu function int signalFunc(cchar * menu) { if (strEqu(menu,"kill job")) { if (! menuLock) { wprintf(mLog,"\n""ready \n"); // already dead return 0; } if (killFlag) { // redundant kill if (*subprocName) { wprintf(mLog," *** kill again: %s \n",subprocName); signalProc(subprocName,"kill"); // kill subprocess } else wprintf(mLog," *** waiting for function to quit \n"); // or wait for function to die return 0; } wprintf(mLog," *** KILL current function \n"); // initial kill pauseFlag = 0; killFlag = 1; if (*subprocName) { signalProc(subprocName,"resume"); signalProc(subprocName,"kill"); } return 0; } if (strEqu(menu,"pause")) { pauseFlag = 1; if (*subprocName) signalProc(subprocName,"pause"); return 0; } if (strEqu(menu,"resume")) { pauseFlag = 0; if (*subprocName) signalProc(subprocName,"resume"); return 0; } else zappcrash("signalFunc: %s",menu); return 0; } // check kill and pause flags // called periodically from long-running functions int checkKillPause() { while (pauseFlag) { // idle loop while paused zsleep(0.1); zmainloop(); // process menus } if (killFlag) return 1; // return true = stop now return 0; // return false = continue } // file open dialog - get backup job data from a file int fileOpen(cchar * menu) { char *file; int err = 0; if (*scriptParam) { // get file from script strcpy(BJfile,scriptParam); *scriptParam = 0; err = BJload(BJfile); return err; } ++Fdialog; file = zgetfile("open backup job","file",userdir,"hidden"); // get file from user if (file) { if (strlen(file) > maxfcc-2) zappcrash("pathname too big"); strcpy(BJfile,file); free(file); err = BJload(BJfile); // get job data from file } else err = 1; --Fdialog; return err; } // file save dialog - save backup job data to a file int fileSave(cchar * menu) { char *file; int nstat, err = 0; if (*scriptParam) { // get file from script strcpy(BJfile,scriptParam); *scriptParam = 0; BJstore(BJfile); return 0; } if (! BJval) { nstat = zmessageYN(mWin,"Job data not valid, save anyway?"); if (! nstat) return 0; } ++Fdialog; if (! *BJfile) strcpy(BJfile,"dkopp.job"); // if no job file, use default file = zgetfile("save backup job","save",BJfile,"hidden"); if (file) { if (strlen(file) > maxfcc-2) zappcrash("pathname too big"); strcpy(BJfile,file); free(file); err = BJstore(BJfile); if (! err) BJmod = 0; // job not modified } --Fdialog; return 0; } // backup job data <<< file // errors not checked here are checked in BJvalidate() int BJload(cchar * fspec) { FILE *fid; char buff[1000]; cchar *fgs, *rtype, *rdata; char rtype2[20]; int cc, Nth, nerrs; BJreset(); // clear old job from memory nerrs = 0; snprintf(buff,999,"\n""loading job file: %s \n",fspec); wprintx(mLog,0,buff,boldfont); fid = fopen(fspec,"r"); // open file if (! fid) { wprintf(mLog," *** cannot open job file: %s \n",fspec); commFail++; return 1; } while (true) // read file { fgs = fgets_trim(buff,998,fid,1); if (! fgs) break; // EOF cc = strlen(buff); if (cc > 996) { wprintf(mLog," *** input record too big \n"); nerrs++; continue; } Nth = 1; rtype = strField(buff,' ',Nth++); // parse 1st field, record type if (! rtype) rtype = "#"; // blank record is comment strncpy0(rtype2,rtype,19); strToLower(rtype2); if (strEqu(rtype2,"device")) { rdata = strField(buff,' ',Nth++); // DVD/BD device: /dev/dvd if (rdata) strncpy0(BJdvd,rdata,19); continue; } if (strEqu(rtype2,"dvdcap")) { rdata = strField(buff,' ',Nth++); // DVD/BD capacity, GB if (rdata) convSD(rdata,BJcap); rdata = strField(buff,' ',Nth++); // DVD/BD write speed (x 1.38 MB/sec) convSI(rdata,BJspeed); // (0 = default) v.4.5 continue; } if (strEqu(rtype2,"backup")) { rdata = strField(buff,' ',Nth++); // backup mode if (rdata) { strncpy0(BJbmode,rdata,19); strToLower(BJbmode); } continue; } if (strEqu(rtype2,"verify")) { rdata = strField(buff,' ',Nth++); // verify mode if (rdata) { strncpy0(BJvmode,rdata,19); strToLower(BJvmode); } continue; } if (strEqu(rtype2,"datefrom")) { rdata = strField(buff,' ',Nth++); // file mod date selection v.4.6 if (rdata) strncpy0(BJdatefrom,rdata,11); continue; } if (strcmpv(rtype2,"include","exclude","#",null)) { BJinex[BJnx] = strdup(buff); // include/exclude or comment rec. if (++BJnx >= maxnx) { wprintf(mLog," *** exceed %d include/exclude recs \n",maxnx); nerrs++; break; } continue; } wprintf(mLog," *** unrecognized record: %s \n",buff); continue; } fclose(fid); // close file BJmod = 0; // new job, not modified BJvalidate(0); // validation checks, set BJval if (! nerrs && BJval) return 0; BJval = 0; commFail++; return 1; } // backup job data >>> file int BJstore(cchar * fspec, int ndvd) { FILE *fid; int ii; fid = fopen(fspec,"w"); // open file if (! fid) { wprintf(mLog," *** cannot open file: %s \n",fspec); commFail++; return 1; } fprintf(fid,"device %s \n",BJdvd); // device /dev/dvd fprintf(fid,"dvdcap %.1f %d \n",BJcap,BJspeed); // dvdcap N.N N (DVD/BD capacity, speed) fprintf(fid,"backup %s \n",BJbmode); // backup full/incremental/accumulate fprintf(fid,"verify %s \n",BJvmode); // verify full/incremental/thorough fprintf(fid,"datefrom %s \n",BJdatefrom); // file mod date selection v.4.6 if (! ndvd) for (ii = 0; ii < BJnx; ii++) // output all include/exclude recs fprintf(fid,"%s \n",BJinex[ii]); else { // output only recs for one DVD/BD medium for (ii = 0; ii < BJnx && BJdvdno[ii] < ndvd; ii++); // of multi-volume set for ( ; ii < BJnx; ii++) if (BJdvdno[ii] <= ndvd) // matching includes (dvdno = ndvd) fprintf(fid,"%s \n",BJinex[ii]); // + all following excludes (dvdno = 0) } fclose(fid); return 0; } // backup job data <<< DVD/BD job file // get job file from prior backup to this same medium int BJvload(cchar * menu) { char vjfile[100]; BJreset(); // reset job data mountDVD(0); // (re) mount DVD/BD if (! dvdmtd) { commFail++; return 1; } strcpy(vjfile,dvdmp); // dvd mount point strcat(vjfile,V_JOBFILE); // + dvd job file BJload(vjfile); // load job file (BJval set) if (BJval) return 0; commFail++; return 1; } // edit dialog for backup job data int BJedit(cchar * menu) { zdialog *zd; ++Fdialog; zd = zdialog_new("edit backup job",mWin,"browse","done","clear","cancel",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=8"); zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=10"); zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog"); zdialog_add_widget(zd,"vbox","vb4","hb1",0,"homog"); zdialog_add_widget(zd,"hbox","hb2","vb1"); zdialog_add_widget(zd,"vbox","vb21","hb2",0,"homog|space=5"); zdialog_add_widget(zd,"vbox","vb22","hb2",0,"homog|space=5"); zdialog_add_widget(zd,"hbox","hb3","vb1",0,"space=8"); zdialog_add_widget(zd,"label","labdev","vb21","DVD/BD device "); // DVD/BD device [______________][v] zdialog_add_widget(zd,"label","labcap","vb21","capacity GB"); // capacity GB [___] zdialog_add_widget(zd,"label","labspeed","vb21","write speed"); // write speed [__] (x 1.38 MB/sec) zdialog_add_widget(zd,"comboE","entdvd","vb22",BJdvd); zdialog_add_widget(zd,"hbox","hb21","vb22"); zdialog_add_widget(zd,"entry","entcap","hb21",0,"scc=5"); zdialog_add_widget(zd,"hbox","hb22","vb22"); zdialog_add_widget(zd,"entry","entspeed","hb22",0,"scc=3"); zdialog_add_widget(zd,"label","labxmb","hb22","(x 1.38 MB/sec)","space=5"); zdialog_add_widget(zd,"button","bopen","hb3","open job file"); // [open job] [DVD/BD job] [save as] zdialog_add_widget(zd,"button","bdvd","hb3","open DVD/BD job"); zdialog_add_widget(zd,"button","bsave","hb3"," save as "); zdialog_add_widget(zd,"label","labbmode","vb3","Backup Mode "); zdialog_add_widget(zd,"label","labvmode","vb4","Verify Mode "); zdialog_add_widget(zd,"radio","bmrb1","vb3","full"); // Backup Mode Verify Mode zdialog_add_widget(zd,"radio","bmrb2","vb3","incremental"); // (o) full (o) full zdialog_add_widget(zd,"radio","bmrb3","vb3","accumulate"); // (o) incremental (o) incremental zdialog_add_widget(zd,"radio","vmrb1","vb4","full"); // (o) accumulate (o) thorough zdialog_add_widget(zd,"radio","vmrb2","vb4","incremental"); // file date from: [ yyyy.mm.dd ] zdialog_add_widget(zd,"radio","vmrb3","vb4","thorough"); zdialog_add_widget(zd,"label","labdate","vb3","file date from:"); zdialog_add_widget(zd,"entry","entdate","vb4","yyyy.mm.dd","scc=10"); zdialog_add_widget(zd,"label","space","vb3",0,"expand"); zdialog_add_widget(zd,"label","space","vb4",0,"expand"); zdialog_add_widget(zd,"hsep","sep2","dialog"); // edit box for include/exclude recs zdialog_add_widget(zd,"label","labinex","dialog","Include / Exclude Files"); zdialog_add_widget(zd,"frame","frminex","dialog",0,"expand"); zdialog_add_widget(zd,"scrwin","scrwinex","frminex"); zdialog_add_widget(zd,"edit","edinex","scrwinex"); BJedit_stuff(zd); // stuff dialog widgets with job data zdialog_resize(zd,0,500); zdialog_run(zd,BJedit_event); // run dialog return 0; } // edit dialog event function int BJedit_event(zdialog *zd, cchar *event) { int zstat, err = 0; zstat = zd->zstat; zd->zstat = 0; // dialog may continue if (zstat) { if (zstat == 1) { // browse, do file-chooser dialog fc_dialog("/home"); return 0; } if (zstat == 2) { // done BJedit_fetch(zd); // get all job data from dialog widgets if (! BJval) commFail++; zdialog_free(zd); // destroy dialog --Fdialog; return 0; } if (zstat == 3) { wclear(editwidget); // clear include/exclude recs return 0; } zdialog_free(zd); // cancel --Fdialog; return 0; } if (strEqu(event,"bopen")) { err = fileOpen(""); // get job file from user if (! err) BJedit_stuff(zd); // stuff dialog widgets } if (strEqu(event,"bdvd")) { err = BJvload(""); // get job file on DVD/BD if (! err) BJedit_stuff(zd); // stuff dialog widgets } if (strEqu(event,"bsave")) { BJedit_fetch(zd); // get job data from dialog widgets fileSave(""); // save to file } return 0; } // backup job data in memory >>> job edit dialog widgets int BJedit_stuff(zdialog * zd) { int ii; for (ii = 0; ii < ndvds; ii++) zdialog_cb_app(zd,"entdvd",dvddevdesc[ii]); // remove mount point get/stuff v.5.3 zdialog_stuff(zd,"entcap",BJcap); if (BJspeed > 0) zdialog_stuff(zd,"entspeed",BJspeed); // v.4.5 if (strEqu(BJbmode,"full")) zdialog_stuff(zd,"bmrb1",1); if (strEqu(BJbmode,"incremental")) zdialog_stuff(zd,"bmrb2",1); if (strEqu(BJbmode,"accumulate")) zdialog_stuff(zd,"bmrb3",1); if (strEqu(BJvmode,"full")) zdialog_stuff(zd,"vmrb1",1); if (strEqu(BJvmode,"incremental")) zdialog_stuff(zd,"vmrb2",1); if (strEqu(BJvmode,"thorough")) zdialog_stuff(zd,"vmrb3",1); zdialog_stuff(zd,"entdate",BJdatefrom); // file mod date selection v.4.6 editwidget = zdialog_widget(zd,"edinex"); wclear(editwidget); for (int ii = 0; ii < BJnx; ii++) wprintf(editwidget,"%s""\n",BJinex[ii]); return 0; } // job edit dialog widgets >>> backup job data in memory int BJedit_fetch(zdialog * zd) { int ii, ftf; char text[40], *pp; BJreset(); // reset job data zdialog_fetch(zd,"entdvd",text,19); // get DVD/BD device strncpy0(BJdvd,text,19); pp = strchr(BJdvd,' '); if (pp) *pp = 0; // remove mount point fetch/save v.5.3 zdialog_fetch(zd,"entcap",BJcap); // capacity GB zdialog_fetch(zd,"entspeed",BJspeed); // speed, x 1.38 MB/sec zdialog_fetch(zd,"bmrb1",ii); if (ii) strcpy(BJbmode,"full"); // backup mode zdialog_fetch(zd,"bmrb2",ii); if (ii) strcpy(BJbmode,"incremental"); zdialog_fetch(zd,"bmrb3",ii); if (ii) strcpy(BJbmode,"accumulate"); zdialog_fetch(zd,"vmrb1",ii); if (ii) strcpy(BJvmode,"full"); // verify mode zdialog_fetch(zd,"vmrb2",ii); if (ii) strcpy(BJvmode,"incremental"); zdialog_fetch(zd,"vmrb3",ii); if (ii) strcpy(BJvmode,"thorough"); zdialog_fetch(zd,"entdate",BJdatefrom,11); // file mod date selection v.4.6 for (ftf = 1;;) { pp = wscanf(editwidget,ftf); // include/exclude recs. if (! pp) break; strTrim(pp); // remove trailing blanks BJinex[BJnx] = strdup(pp); // copy new record if (++BJnx >= maxnx) { wprintf(mLog," *** exceed %d include/exclude recs \n",maxnx); break; } } BJmod++; // job modified BJvalidate(0); // check for errors, set BJval return 0; } // perform DVD/BD backup using growisofs utility int Backup(cchar *menu) { strcpy(mbmode,""); strcpy(mvmode,""); if (strcmpv(menu,"full","incremental","accumulate",null)) // backup only strcpy(mbmode,menu); if (strEqu(menu,"run DVD/BD")) BJvload(null); // load job file from DVD/BD if req. if (strcmpv(menu,"run job","run DVD/BD",null)) { // if run job or job on DVD/BD, if (BJval) { // and valid job file, strcpy(mbmode,BJbmode); // use job file backup & verify modes strcpy(mvmode,BJvmode); } } if (! BJval) { // check for errors wprintf(mLog," *** no valid backup job \n"); goto backup_done; } if (strEqu(mbmode,"full")) FullBackup(mvmode); // full backup (+ verify) else IncrBackup(mbmode,mvmode); // incremental / accumulate (+ verify) backup_done: if (Fgui) wprintf(mLog,"ready \n"); // v.5.0 return 0; } // full backup using multiple DVD/BD media if required int FullBackup(cchar * BJvmode) { FILE *fid = 0; int gerr, ii, zstat; char command[200], Nspeed[20] = ""; char *dfile, vfile[maxfcc], *mbytes; double secs, bspeed, time0; dGetFiles(); // get files for backup if (Dnf == 0) { wprintf(mLog," *** nothing to back-up \n"); goto backup_fail; } vFilesReset(); // reset DVD/BD files data wprintx(mLog,0,"\n""begin full backup \n",boldfont); wprintf(mLog," files: %d bytes: %.0f \n",Dnf,Dbytes); // files and bytes to copy if (! *dvdlabel) strcpy(dvdlabel,"dkopp"); // if no label, default "dkopp" v.5.1 for (dvdnum = 1; dvdnum <= BJndvd; dvdnum++) // loop for each DVD/BD { if (dvdnum > 1) ejectDVD(0); // eject prior DVD/BD if (! *scrFile || dvdnum > 1) // if script file avoid dialog v.4.8 if (Fgui) zmessageACK(mWin,0, "Insert DVD/BD medium no. %d \n" // ask for next DVD/BD "and wait for desktop icon", dvdnum); // if GUI mode v.5.0 unmountDVD(0); // no mount for full backup v.4.7 BJstore(TFjobfile,dvdnum); // copy job file (this DVD/BD) to temp file save_filepoop(); // + owner and permissions to temp file writeDT(); // create date-time & usage temp file fid = fopen(TFdiskfiles,"w"); // temp file for growisofs path-list if (! fid) { wprintf(mLog," *** cannot open /tmp scratch file \n"); goto backup_fail; } fprintf(fid,"%s=%s\n",V_JOBFILE +1,TFjobfile); // add job file to growisofs list fprintf(fid,"%s=%s\n",V_FILEPOOP +1,TFfilepoop); // add directory poop file fprintf(fid,"%s=%s\n",V_DATETIME +1,TFdatetime); // add date-time file Dbytes2 = 0.0; for (ii = 0; ii < Dnf; ii++) // process all files for backup { if (Drec[ii].dvd != dvdnum) continue; // screen for DVD/BD no. dfile = Drec[ii].file; // add to growisofs path-list repl_1str(dfile,vfile,"=","\\\\="); // replace "=" with "\\=" in file name fprintf(fid,"%s=%s\n",vfile+1,dfile); // directories/file=/directories/file Dbytes2 += Drec[ii].size; } fclose(fid); if (BJspeed > 0) sprintf(Nspeed,"-speed=%d",BJspeed); // v.4.5 mbytes = formatKBMB(Dbytes2,4); // v.5.2 wprintf(mLog," writing DVD/BD medium %d of %d, %s \n", dvdnum, BJndvd, mbytes); start_timer(time0); // start timer for growisofs sprintf(command, // build growisofs command line "/usr/bin/growisofs -Z %s %s -r -graft-points " "-iso-level 4 -gui -V \"%s\" %s -path-list %s 2>&1", // label in quotes v.5.6 BJdvd,Nspeed,dvdlabel,gforce,TFdiskfiles); backup_retry: gerr = do_shell("growisofs", command); // do growisofs, echo outputs if (checkKillPause()) goto backup_fail; // killed by user if (gerr) { if (! Fgui) goto backup_fail; zstat = zdialog_choose("choose",mWin,"growisofs error", // manual compensation for growisofs "abort","retry","ignore (continue)",null); // and/or gnome bugs v.5.9.1 if (zstat == 1) goto backup_fail; if (zstat == 2) goto backup_retry; } secs = get_timer(time0); // output statistics wprintf(mLog," backup time: %.0f secs \n",secs); bspeed = Dbytes2/1000000.0/secs; wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed); wprintf(mLog," backup complete \n"); if (BJndvd > 1) wprintf(mLog," (DVD/BD medium no. %d) \n",dvdnum); ejectDVD(0); // DVD may be hung after growisofs sleep(5); verify_retry: if (*BJvmode) // do verify if requested { mountDVD(0); // test if DVD hung if (! dvdmtd) { zstat = zdialog_choose("choose",mWin,"DVD mount failure", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; } Verify(BJvmode); // verify this DVD if (commFail) { zstat = zdialog_choose("choose",mWin,"verify error", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; wprintf(mLog," backup is being repeated \n",dvdnum); commFail = 0; --dvdnum; // repeat same DVD, salvage job continue; } } createBackupHist(); // create backup history file } wprintf(mLog," backup job complete \n"); return 0; backup_fail: commFail++; secs = get_timer(time0); // output statistics even if failed v.5.8 wprintf(mLog," backup time: %.0f secs \n",secs); bspeed = Dbytes2/1000000.0/secs; wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed); wprintx(mLog,0," *** BACKUP FAILED \n",boldfont); wprintf(mLog," media may be OK: check with Verify \n"); // v.5.3 return 0; } // incremental / accumulate backup (one DVD/BD only) int IncrBackup(cchar * BJbmode, cchar * BJvmode) { FILE *fid = 0; int gerr, ii, zstat; char command[200], Nspeed[20] = ""; char *dfile, vfile[maxfcc], disp; double secs, bspeed; double time0; mountDVD(0); // requires successful mount if (! dvdmtd) goto backup_fail; dGetFiles(); // get files for backup vGetFiles(); // get DVD/BD files setFileDisps(); // file disps: new mod del unch if (! Dnf) { wprintf(mLog," *** no files for backup \n"); goto backup_fail; } if (! Vnf) { wprintf(mLog," *** no DVD/BD files \n"); goto backup_fail; } snprintf(command,99,"\n""begin %s backup \n",BJbmode); wprintx(mLog,0,command,boldfont); wprintf(mLog," files: %d bytes: %.0f \n",Mfiles,Mbytes); // files and bytes to copy if (Mfiles == 0) { // nothing to back up wprintf(mLog," nothing to back-up \n"); return 0; } if (! *dvdlabel) strcpy(dvdlabel,"dkopp"); // if no label, default "dkopp" v.5.1 fid = fopen(TFdiskfiles,"w"); // temp file for growisofs path-list if (! fid) { wprintf(mLog," *** cannot open /tmp scratch file \n"); goto backup_fail; } BJstore(TFjobfile); // copy job file to temp file save_filepoop(); // + file owner & permissions writeDT(); // create date-time & usage temp file fprintf(fid,"%s=%s\n",V_JOBFILE +1,TFjobfile); // add job file to growisofs list fprintf(fid,"%s=%s\n",V_FILEPOOP +1,TFfilepoop); // add directory poop file fprintf(fid,"%s=%s\n",V_DATETIME +1,TFdatetime); // add date-time file for (ii = 0; ii < Dnf; ii++) { // process new and modified disk files disp = Drec[ii].disp; if ((disp == 'n') || (disp == 'm')) { // new or modified file dfile = Drec[ii].file; // add to growisofs path-list repl_1str(dfile,vfile,"=","\\\\="); // replace "=" with "\\=" in file name fprintf(fid,"%s=%s\n",vfile+1,dfile); // directories/file=/directories/file Drec[ii].ivf = 1; // set flag for incr. verify } } if (strEqu(BJbmode,"incremental")) { // incremental backup (not accumulate) for (ii = 0; ii < Vnf; ii++) { // process deleted files still on DVD/BD if (Vrec[ii].disp == 'd') { dfile = Vrec[ii].file; // add to growisofs path-list repl_1str(dfile,vfile,"=","\\\\="); // replace "=" with "\\=" in file name fprintf(fid,"%s=%s\n",vfile+1,"/dev/null"); // directories/file=/dev/null } } } fclose(fid); if (BJspeed > 0) sprintf(Nspeed,"-speed=%d",BJspeed); // v.4.5 start_timer(time0); // start timer for growisofs sprintf(command,"/usr/bin/growisofs -M %s %s -r -graft-points " // build growisofs command line "-iso-level 4 -gui -V %s %s -path-list %s 2>&1", BJdvd,Nspeed,dvdlabel,gforce,TFdiskfiles); backup_retry: gerr = do_shell("growisofs", command); // do growisofs, echo outputs if (checkKillPause()) goto backup_fail; // killed by user if (gerr) { zstat = zdialog_choose("choose",mWin,"growisofs error", // manual compensation for growisofs "abort","retry","ignore (continue)",null); // and/or gnome bugs v.5.9.1 if (zstat == 1) goto backup_fail; if (zstat == 2) goto backup_retry; } secs = get_timer(time0); // output statistics wprintf(mLog," backup time: %.0f secs \n",secs); bspeed = Mbytes/1000000.0/secs; wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed); wprintf(mLog," backup complete \n"); vFilesReset(); // reset DVD/BD files ejectDVD(0); // DVD may be hung after growisofs sleep(5); verify_retry: if (*BJvmode) // do verify if requested { mountDVD(0); // test if DVD/BD hung if (! dvdmtd) { zstat = zdialog_choose("choose",mWin,"DVD mount failure", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; } Verify(BJvmode); // verify new files on DVD/BD if (commFail) { zstat = zdialog_choose("choose",mWin,"verify error", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; } } createBackupHist(); // create backup history file return 0; backup_fail: commFail++; wprintx(mLog,0," *** BACKUP FAILED \n",boldfont); vFilesReset(); return 0; } // verify DVD/BD disc data integrity int Verify(cchar * menu) { int ii, comp, vfiles; int dfiles1 = 0, dfiles2 = 0; int verrs = 0, cerrs = 0; char *filespec; cchar *errmess = 0; double secs, dcc1, vbytes, vspeed; double mtime, diff; double time0; struct stat64 filestat; vGetFiles(); // get DVD/BD files wprintf(mLog," %d files on DVD/BD \n",Vnf); if (! Vnf) goto verify_exit; vfiles = verrs = cerrs = 0; vbytes = 0.0; start_timer(time0); if (strEqu(menu,"full")) // verify all files are readable { wprintx(mLog,0,"\n""verify ALL files on DVD/BD \n",boldfont); if (Fgui) wprintf(mLog,"\n\n"); // v.5.0 for (ii = 0; ii < Vnf; ii++) { if (checkKillPause()) goto verify_exit; filespec = Vrec[ii].file; // /home/.../file.ext track_filespec(filespec); // track progress on screen errmess = checkFile(filespec,0,dcc1); // check file, get length if (errmess) track_filespec_err(filespec,errmess); // log errors if (errmess) verrs++; vfiles++; vbytes += dcc1; if (verrs + cerrs > 100) { wprintx(mLog,0," *** OVER 100 ERRORS, GIVING UP *** \n",boldfont); goto verify_exit; } } } if (strEqu(menu,"incremental")) // verify files in prior incr. backup { wprintx(mLog,0,"\n""verify files in prior incremental backup \n",boldfont); for (ii = 0; ii < Dnf; ii++) { if (checkKillPause()) goto verify_exit; if (! Drec[ii].ivf) continue; // skip if not in prior incr. backup filespec = Drec[ii].file; wprintf(mLog," %s \n",kleenex(filespec)); // output filespec errmess = checkFile(filespec,0,dcc1); // check file on DVD/BD, get length if (errmess) wprintf(mLog," *** %s \n",errmess); if (errmess) verrs++; vfiles++; vbytes += dcc1; if (verrs + cerrs > 100) { wprintx(mLog,0," *** OVER 100 ERRORS, GIVING UP *** \n",boldfont); goto verify_exit; } } } if (strEqu(menu,"thorough")) // compare DVD/BD to disk files { wprintx(mLog,0,"\n Read and verify ALL files on DVD/BD. \n",boldfont); wprintf(mLog," Compare to disk files with matching names and mod times.\n"); if (Fgui) wprintf(mLog,"\n\n"); // v.5.0 for (ii = 0; ii < Vnf; ii++) // process DVD/BD files { if (checkKillPause()) goto verify_exit; filespec = Vrec[ii].file; // corresp. file name on disk track_filespec(filespec); // track progress on screen comp = 0; if (stat64(filespec,&filestat) == 0) { // disk file exists? mtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; // yes, get file mod time diff = fabs(mtime - Vrec[ii].mtime); // compare to DVD/BD file mod time if (diff < modtimetolr) comp = 1; // equal dfiles1++; // count matching disk names dfiles2 += comp; // count matching names and mod times } errmess = checkFile(filespec,comp,dcc1); // check DVD/BD file, opt. compare to disk if (errmess) track_filespec_err(filespec,errmess); // log errors if (errmess) { if (strstr(errmess,"compare")) cerrs++; // file compare error else verrs++; } vfiles++; vbytes += dcc1; if (verrs + cerrs > 100) { wprintx(mLog,0," *** OVER 100 ERRORS, GIVING UP *** \n",boldfont); goto verify_exit; } } } wprintf(mLog," DVD/BD files: %d bytes: %.0f \n",vfiles,vbytes); wprintf(mLog," DVD/BD read errors: %d \n",verrs); if (strEqu(menu,"thorough")) { wprintf(mLog," matching disk names: %d mod times: %d \n",dfiles1,dfiles2); wprintf(mLog," compare failures: %d \n",cerrs); } secs = get_timer(time0); wprintf(mLog," verify time: %.0f secs \n",secs); vspeed = vbytes/1000000.0/secs; wprintf(mLog," verify speed: %.2f MB/sec \n",vspeed); if (verrs + cerrs) wprintx(mLog,0," *** THERE WERE ERRORS *** \n",boldfont); else wprintf(mLog," NO ERRORS \n"); verify_exit: if (! Vnf) wprintf(mLog," *** no files on DVD/BD \n"); if (! Vnf) commFail++; if (verrs + cerrs) commFail++; if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // Reports menu function int Report(cchar * menu) { if (strEqu(menu, "get files for backup")) get_current_files(0); if (strEqu(menu, "diffs summary")) report_summary_diffs(0); if (strEqu(menu, "diffs by directory")) report_directory_diffs(0); if (strEqu(menu, "diffs by file")) report_file_diffs(0); if (strEqu(menu, "list files for backup")) list_current_files(0); if (strEqu(menu, "list DVD/BD files")) list_DVD_files(0); if (strEqu(menu, "find files")) find_files(0); if (strEqu(menu, "view backup hist")) view_backup_hist(0); return 0; } // refresh files for backup and report summary statistics per include/exclude statement int get_current_files(cchar *menu) { char *bytes; int ii; dFilesReset(); // force refresh dGetFiles(); // get disk files if (! BJval) { wprintf(mLog," *** backup job is invalid \n"); goto report_exit; } wprintx(mLog,0,"\n files bytes disk include/exclude filespec \n",boldfont); for (ii = 0; ii < BJnx; ii++) { bytes = formatKBMB(BJbytes[ii],3); // v.5.2 if (BJfiles[ii] > 0) wprintf(mLog," %6d %9s %3d", BJfiles[ii], bytes, BJdvdno[ii]); if (BJfiles[ii] < 0) wprintf(mLog," %6d %9s ", BJfiles[ii], bytes); if (BJfiles[ii] == 0) wprintf(mLog," "); wprintf(mLog," %s \n",BJinex[ii]); } bytes = formatKBMB(Dbytes,4); // v.5.2 wprintf(mLog," %6d %9s TOTAL %d disks \n", Dnf, bytes, BJndvd); report_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // report disk:DVD/BD differences summary int report_summary_diffs(cchar *menu) { char *bytes; if (! BJval) { wprintf(mLog," *** backup job is invalid \n"); goto report_exit; } dGetFiles(); vGetFiles(); setFileDisps(); wprintf(mLog,"\n disk files: %d DVD/BD files: %d \n",Dnf,Vnf); wprintf(mLog,"\n Differences between DVD/BD and files on disk: \n"); wprintf(mLog," %7d disk files not on DVD/BD - new \n",nnew); wprintf(mLog," %7d files on disk and DVD/BD - unchanged \n",nunc); wprintf(mLog," %7d files on disk and DVD/BD - modified \n",nmod); wprintf(mLog," %7d DVD/BD files not on disk - deleted \n",ndel); bytes = formatKBMB(Mbytes,4); // v.5.2 wprintf(mLog," Total differences: %d files %s \n",nnew+ndel+nmod,bytes); report_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // report disk:DVD/BD differences by directory, summary statistics int report_directory_diffs(cchar *menu) { int kfiles, knew, kdel, kmod; int dii, vii, comp; char *pp, *pdirk, *bytes, ppdirk[maxfcc]; double nbytes; if (! BJval) { wprintf(mLog," *** backup job is invalid \n"); goto report_exit; } dGetFiles(); vGetFiles(); setFileDisps(); SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first SortFileList((char *) Vrec, sizeof(vfrec), Vnf, 'D'); wprintf(mLog,"\n Disk:DVD/BD differences by directory \n"); wprintf(mLog," new mod del bytes directory \n"); nbytes = kfiles = knew = kmod = kdel = 0; dii = vii = 0; while ((dii < Dnf) || (vii < Vnf)) // scan disk and DVD/BD files in parallel { if ((dii < Dnf) && (vii == Vnf)) comp = -1; else if ((dii == Dnf) && (vii < Vnf)) comp = +1; else comp = filecomp(Drec[dii].file, Vrec[vii].file); if (comp > 0) pdirk = Vrec[vii].file; // get file on DVD/BD or disk else pdirk = Drec[dii].file; pp = (char *) strrchr(pdirk,'/'); // isolate directory if (pp) *pp = 0; if (strNeq(pdirk,ppdirk)) { // if directory changed, output bytes = formatKBMB(nbytes,3); // totals from prior directory if (kfiles > 0) wprintf(mLog," %5d %5d %5d %8s %s \n", // v.5.2 knew,kmod,kdel,bytes,ppdirk); nbytes = kfiles = knew = kmod = kdel = 0; // reset totals strcpy(ppdirk,pdirk); // start new directory } if (pp) *pp = '/'; if (comp < 0) { // unmatched disk file knew++; // count new file nbytes += Drec[dii].size; kfiles++; dii++; } else if (comp > 0) { // unmatched DVD/BD file: deleted kdel++; // count deleted file kfiles++; vii++; } else if (comp == 0) { // file present on disk and DVD/BD if (Drec[dii].disp == 'm') { kmod++; // count modified file nbytes += Drec[dii].size; kfiles++; } dii++; // other: u = unchanged vii++; } } if (kfiles > 0) { bytes = formatKBMB(nbytes,3); // totals from last directory v.5.2 wprintf(mLog," %5d %5d %5d %8s %s \n",knew,kmod,kdel,bytes,ppdirk); } SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore ascii sort SortFileList((char *) Vrec, sizeof(vfrec), Vnf, 'A'); report_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // report disk:DVD/BD differences by file (new, modified, deleted) int report_file_diffs(cchar *menu) { int dii, vii; if (! BJval) { wprintf(mLog," *** backup job is invalid \n"); goto report_exit; } report_summary_diffs(0); // report summary first wprintf(mLog,"\n Detailed list of disk:DVD/BD differences: \n"); wprintf(mLog,"\n %d new files (on disk, not on DVD/BD) \n",nnew); for (dii = 0; dii < Dnf; dii++) { if (Drec[dii].disp != 'n') continue; wprintf(mLog," %s \n",kleenex(Drec[dii].file)); if (checkKillPause()) goto report_exit; } wprintf(mLog,"\n %d modified files (disk and DVD/BD files are different) \n",nmod); for (dii = 0; dii < Dnf; dii++) { if (Drec[dii].disp != 'm') continue; wprintf(mLog," %s \n",kleenex(Drec[dii].file)); if (checkKillPause()) goto report_exit; } wprintf(mLog,"\n %d deleted files (on DVD/BD, not on disk) \n",ndel); for (vii = 0; vii < Vnf; vii++) { if (Vrec[vii].disp != 'd') continue; wprintf(mLog," %s \n",kleenex(Vrec[vii].file)); if (checkKillPause()) goto report_exit; } report_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // list all files for backup int list_current_files(cchar *menu) { int dii; if (! BJval) { wprintf(mLog," *** backup job is invalid \n"); goto report_exit; } wprintf(mLog,"\n List all files for backup: \n"); dGetFiles(); wprintf(mLog," %d files found \n",Dnf); for (dii = 0; dii < Dnf; dii++) { if (checkKillPause()) break; wprintf(mLog," %s \n",kleenex(Drec[dii].file)); } report_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // list all files on mounted DVD/BD int list_DVD_files(cchar *menu) { int vii; wprintf(mLog,"\n List all files on DVD/BD: \n"); vGetFiles(); wprintf(mLog," %d files found \n",Vnf); for (vii = 0; vii < Vnf; vii++) { if (checkKillPause()) break; wprintf(mLog," %s \n",kleenex(Vrec[vii].file)); } return 0; } // find desired files on disk, on mounted DVD/BD, and in history files int find_files(cchar *menu) { int dii, vii, hii, ftf, nn; cchar *fspec1, *hfile1; static char fspec2[200] = "/home/*/file*"; char hfile[200], buff[1000], *pp; FILE *fid; pvlist *flist = 0; dGetFiles(); // get disk and DVD/BD files if (dvdmtd) vGetFiles(); else wprintf(mLog," DVD/BD not mounted \n"); wprintf(mLog,"\n find files matching wildcard pattern \n"); // get search pattern fspec1 = zdialog_text(mWin,"enter (wildcard) filespec:",fspec2); if (blank_null(fspec1)) goto report_exit; strncpy0(fspec2,fspec1,199); strTrim(fspec2); wprintf(mLog," search pattern: %s \n",fspec2); wprintx(mLog,0,"\n matching files on disk: \n",boldfont); for (dii = 0; dii < Dnf; dii++) // search disk files { if (checkKillPause()) goto report_exit; if (MatchWild(fspec2,Drec[dii].file) == 0) wprintf(mLog," %s \n",kleenex(Drec[dii].file)); } wprintx(mLog,0,"\n matching files on DVD/BD: \n",boldfont); for (vii = 0; vii < Vnf; vii++) // search DVD/BD files { if (checkKillPause()) goto report_exit; if (MatchWild(fspec2,Vrec[vii].file) == 0) wprintf(mLog," %s \n",kleenex(Vrec[vii].file)); } wprintx(mLog,0,"\n matching files in backup history: \n",boldfont); flist = pvlist_create(maxhist); snprintf(hfile,199,"%s/dkopp-hist-*",userdir); // find all backup history files ftf = 1; // /home/user/.dkopp/dkopp-hist-* nn = 0; while (true) { hfile1 = SearchWild(hfile,ftf); if (! hfile1) break; if (nn == maxhist) break; pvlist_append(flist,hfile1); // add to list nn++; } if (nn == 0) wprintf(mLog," no history files found \n"); if (nn == maxhist) wprintf(mLog," *** too many history files, please purge"); if (nn == 0 || nn == maxhist) goto report_exit; pvlist_sort(flist); // sort list ascending for (hii = 0; hii < nn; hii++) // loop all history files { hfile1 = pvlist_get(flist,hii); wprintf(mLog," %s \n",hfile1); fid = fopen(hfile1,"r"); // next history file if (! fid) { wprintf(mLog," *** file open error \n"); continue; } while (true) // read and search for match { if (checkKillPause()) break; pp = fgets_trim(buff,999,fid,1); if (! pp) break; if (MatchWild(fspec2,buff) == 0) wprintf(mLog," %s \n",buff); } fclose(fid); } report_exit: if (flist) pvlist_free(flist); if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // list available backup history files, select one to view int view_backup_hist(cchar *menu) { cchar *fspec1; char fspec2[200], histfile[200]; char *pp; int ii, jj, nn; int zstat, ftf; zdialog *zd; pvlist *flist = 0; wprintf(mLog," available history files in %s \n",userdir); snprintf(fspec2,199,"%s/dkopp-hist-*",userdir); flist = pvlist_create(maxhist); ftf = 1; nn = 0; while (true) { fspec1 = SearchWild(fspec2,ftf); // file: dkopp-hist-yyyymmdd-hhmm-label if (! fspec1) break; pp = (char *) strrchr(fspec1,'/') + 12; // get yyyymmdd-hhmm-label v.4.7.1 gcc if (nn == maxhist) break; pvlist_append(flist,pp); // add to list nn++; } if (nn == 0) wprintf(mLog," no history files found \n"); if (nn == maxhist) wprintf(mLog," *** too many history files, please purge"); if (nn == 0 || nn == maxhist) goto report_exit; pvlist_sort(flist); // sort list ascending for (ii = 0; ii < nn; ii++) // report sorted list wprintf(mLog," dkopp-hist-%s \n",pvlist_get(flist,ii)); zd = zdialog_new("choose history file",mWin,"OK","cancel",null); zdialog_add_widget(zd,"label","lab1","dialog","history file date and label"); zdialog_add_widget(zd,"comboE","hfile","dialog"); jj = nn - 20; if (jj < 0) jj = 0; for (ii = jj; ii < nn; ii++) // stuff combo box list with zdialog_cb_app(zd,"hfile",pvlist_get(flist,ii)); // 20 newest hist file IDs zdialog_stuff(zd,"hfile",pvlist_get(flist,nn-1)); // default entry is newest file zdialog_run(zd); // run dialog zstat = zdialog_wait(zd); zdialog_fetch(zd,"hfile",histfile,199); // get user choice zdialog_free(zd); if (zstat != 1) goto report_exit; // cancelled shell_ack("xdg-open %s/%s-%s",userdir,"dkopp-hist",histfile); // view the file report_exit: if (flist) pvlist_free(flist); if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // file restore dialog - specify DVD/BD files to be restored int RJedit(cchar * menu) { zdialog *zd; wprintf(mLog,"\n Restore files from DVD/BD \n"); vGetFiles(); // get files on DVD/BD wprintf(mLog," %d files on DVD/BD \n",Vnf); if (! Vnf) return 0; ++Fdialog; zd = zdialog_new("copy files from DVD/BD",mWin,"browse","done","cancel",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5"); zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5"); zdialog_add_widget(zd,"label","labdev","vb1","DVD/BD device"); // DVD/BD device [___________][v] zdialog_add_widget(zd,"comboE","entdvd","vb2",BJdvd); zdialog_add_widget(zd,"label","labfrom","vb1","copy-from DVD/BD"); // copy-from DVD/BD [______________] zdialog_add_widget(zd,"label","labto","vb1","copy-to disk"); // copy-to disk [______________] zdialog_add_widget(zd,"entry","entfrom","vb2",RJfrom); zdialog_add_widget(zd,"entry","entto","vb2",RJto); zdialog_add_widget(zd,"hsep","hsep1","dialog"); zdialog_add_widget(zd,"label","labfiles","dialog","files to restore"); zdialog_add_widget(zd,"frame","framefiles","dialog",0,"expand"); zdialog_add_widget(zd,"scrwin","scrfiles","framefiles"); zdialog_add_widget(zd,"edit","editfiles","scrfiles"); for (int ii = 0; ii < ndvds; ii++) // load curr. data into widgets zdialog_cb_app(zd,"entdvd",dvddevdesc[ii]); // remove get/stuff mount point v.5.3 editwidget = zdialog_widget(zd,"editfiles"); for (int ii = 0; ii < RJnx; ii++) // get restore include/exclude recs, wprintf(editwidget,"%s""\n",RJinex[ii]); // pack into file selection edit box zdialog_resize(zd,400,400); zdialog_run(zd,RJedit_event); // run dialog with response function return 0; } // edit dialog event function int RJedit_event(zdialog *zd, cchar *event) { char text[40], *pp, fcfrom[maxfcc]; int zstat, ftf, cc; zstat = zd->zstat; if (! zstat) return 0; if (zstat != 1 && zstat != 2) goto end_dialog; // cancel or destroy RJreset(); // reset restore job data zdialog_fetch(zd,"entdvd",text,19); // get DVD/BD device strncpy0(BJdvd,text,19); pp = strchr(BJdvd,' '); if (pp) *pp = 0; // remove fetch/save mount point v.5.3 zdialog_fetch(zd,"entfrom",RJfrom,maxfcc); // copy-from location /home/xxx/.../ strTrim(RJfrom); zdialog_fetch(zd,"entto",RJto,maxfcc); // copy-to location /home/yyy/.../ strTrim(RJto); ftf = 1; while (true) // include/exclude recs from edit box { pp = wscanf(editwidget,ftf); if (! pp) break; cc = strTrim(pp); // remove trailing blanks if (cc < 3) continue; // ignore absurdities if (cc > maxfcc-100) continue; RJinex[RJnx] = strdup(pp); // copy new record if (++RJnx == maxnx) { wprintf(mLog," *** exceed %d include/exclude recs \n",maxnx); break; } } if (zstat == 1) { // do file-chooser dialog strcpy(fcfrom,dvdmp); // start at /media/xxxx/home/xxxx/ strcat(fcfrom,RJfrom); fc_dialog(fcfrom); zd->zstat = 0; // dialog continues return 0; } RJvalidate(); // validate restore job data if (RJval) rGetFiles(); // get files to restore else wprintf(mLog," *** correct errors in restore job \n"); end_dialog: zdialog_free(zd); // destroy dialog --Fdialog; return 0; } // List and validate DVD/BD files to be restored int RJlist(cchar * menu) { int cc1, cc2; char *file1, file2[maxfcc]; if (! RJval) { wprintf(mLog," *** restore job has errors \n"); goto list_exit; } wprintf(mLog,"\n copy %d files from DVD/BD: %s \n",Rnf, RJfrom); wprintf(mLog," to directory: %s \n",RJto); wprintf(mLog,"\n resulting files will be the following: \n"); if (! Rnf) goto list_exit; cc1 = strlen(RJfrom); // from: /home/xxx/.../ cc2 = strlen(RJto); // to: /home/yyy/.../ for (int ii = 0; ii < Rnf; ii++) { if (checkKillPause()) goto list_exit; file1 = Rrec[ii].file; if (! strnEqu(file1,RJfrom,cc1)) { wprintf(mLog," *** not within copy-from: %s \n",kleenex(file1)); RJval = 0; continue; } strcpy(file2,RJto); strcpy(file2+cc2,file1+cc1); wprintf(mLog," %s \n",kleenex(file2)); } list_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // Restore files based on data from restore dialog int Restore(cchar * menu) { int ii, nn, ccf; char dfile[maxfcc]; cchar *errmess; if (! RJval || ! Rnf) { wprintf(mLog," *** restore job has errors \n"); goto restore_exit; } nn = zmessageYN(mWin,"Restore %d files from: %s%s \n to: %s \n" "Proceed with file restore ?",Rnf,dvdmp,RJfrom,RJto); if (! nn) goto restore_exit; snprintf(dfile,maxfcc-1,"\n""begin restore of %d files to: %s \n",Rnf,RJto); wprintx(mLog,0,dfile,boldfont); ccf = strlen(RJfrom); // from: /media/xxx/filespec for (ii = 0; ii < Rnf; ii++) { if (checkKillPause()) goto restore_exit; strcpy(dfile,RJto); // to: /destination/filespec strcat(dfile,Rrec[ii].file+ccf); wprintf(mLog," %s \n",kleenex(dfile)); errmess = copyFile(Rrec[ii].file,dfile); if (errmess) wprintf(mLog," *** %s \n",errmess); } restore_filepoop(); // restore owner/permissions dFilesReset(); // reset disk file data restore_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // get available DVD/BD devices // the lshw command blocks everything for several seconds int getDVDs(void *) // overhauled v.6.4 { int ii, dvdrw, contx; char *buff, *pp; char command[20] = "lshw -class disk"; // better than udevadm dvdrw = ndvds = 0; contx = 0; while ((buff = command_output(contx,command))) { if (strstr(buff,"*-")) { // start some device if (strstr(buff,"*-cdrom")) dvdrw = 1; // start DVD/BD device else dvdrw = 0; continue; } if (! dvdrw) continue; // ignore recs for other devices if (strstr(buff,"description:")) { pp = strstr(buff,"description:"); // save DVD/BD description pp += 12; if (*pp == ' ') pp++; // (assume description comes first) strncpy0(dvddesc[ndvds],pp,40); continue; } if (strstr(buff,"/dev/")) { pp = strstr(buff,"/dev/"); // have /dev/sr0 or similar format if (pp[7] < '0' || pp[7] > '9') continue; pp[8] = 0; strcpy(dvddevs[ndvds],pp); // save DVD/BD device ndvds++; continue; } } for (ii = 0; ii < ndvds; ii++) // combine devices and descriptions { // for use in GUI chooser list strcpy(dvddevdesc[ii],dvddevs[ii]); strcat(dvddevdesc[ii]," "); strcat(dvddevdesc[ii],dvddesc[ii]); } wprintf(mLog," DVD/BD devices found: %d \n",ndvds); // output list of DVDs v.5.1 for (ii = 0; ii < ndvds; ii++) wprintf(mLog," %s %s \n",dvddevs[ii],dvddesc[ii]); return 0; } // set DVD/BD device and mount point int setDVDdevice(cchar *menu) { cchar *pp1; char *pp2, text[60]; int ii, Nth, zstat; zdialog *zd; if (*scriptParam) { // script Nth = 1; // parse: /dev/dvd /media/xxxx pp1 = strField(scriptParam,' ',Nth++); if (pp1) strncpy0(BJdvd,pp1,19); pp1 = strField(scriptParam,' ',Nth++); if (pp1) { strncpy0(dvdmp,pp1,99); // increase to 99 v.5.6 dvdmpcc = strlen(dvdmp); // bugfix v.5.5 if (dvdmp[dvdmpcc-1] == '/') dvdmp[dvdmpcc--] = 0; // remove trailing / } *scriptParam = 0; return 0; } zd = zdialog_new("select DVD/BD drive",mWin,"OK","cancel",null); // dialog to select DVD/BD and mount point zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5"); zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5"); zdialog_add_widget(zd,"label","labdvd","vb1","DVD/BD device"); zdialog_add_widget(zd,"label","labmp","vb1","mount point"); zdialog_add_widget(zd,"comboE","entdvd","vb2",BJdvd); zdialog_add_widget(zd,"entry","entmp","vb2",dvdmp); for (ii = 0; ii < ndvds; ii++) // stuff avail. DVDs, mount points zdialog_cb_app(zd,"entdvd",dvddevdesc[ii]); zdialog_stuff(zd,"entmp",dvdmp); zdialog_run(zd); zstat = zdialog_wait(zd); if (zstat != 1) { zdialog_free(zd); return 0; } zstat = zdialog_fetch(zd,"entdvd",text,60); // get selected DVD/BD strncpy0(BJdvd,text,19); pp2 = strchr(BJdvd,' '); if (pp2) *pp2 = 0; zdialog_fetch(zd,"entmp",text,39); // DVD/BD mount point strncpy0(dvdmp,text,99); // v.5.6 strTrim(dvdmp); dvdmpcc = strlen(dvdmp); if (dvdmpcc && (dvdmp[dvdmpcc-1] == '/')) // remove trailing / dvdmp[dvdmpcc--] = 0; wprintf(mLog," DVD/BD and mount point: %s %s \n",BJdvd,dvdmp); if (Fgui) wprintf(mLog," ready \n"); // v.5.0 zdialog_free(zd); return 0; } // set label for subsequent DVD/BD backup via growisofs int setDVDlabel(cchar *menu) { cchar *pp; if (*dvdlabel) wprintf(mLog," old DVD/BD label: %s \n",dvdlabel); else strcpy(dvdlabel,"dkopp"); pp = zdialog_text(mWin,"set new DVD/BD label",dvdlabel); if (blank_null(pp)) pp = "dkopp"; strncpy0(dvdlabel,pp,31); wprintf(mLog," new DVD/BD label: %s \n",dvdlabel); return 1; } // Mount DVD/BD with message feedback to window. // overhauled v.6.4 int mountDVD(cchar *menu) // menu mount function { int err, reset, contx; char command[100], mbuff[100], *pp; cchar *pp1; FILE *fid; struct stat dstat; if (dvdmtd) { err = stat(dvdmp,&dstat); if ((! err) && (dvdtime == dstat.st_ctime)) return 0; // medium unchanged, do nothing } dvdmtd = 0; // set DVD/BD not mounted dvdtime = -1; strcpy(mediumDT,"unknown"); *mediumDT = 0; err = reset = 0; vFilesReset(); // reset DVD/BD files contx = 0; while ((pp = command_output(contx,"cat /etc/mtab"))) // get mounted disk info { pp1 = strField(pp,' ',1); // get /dev/xxx if (strNeq(pp1,BJdvd)) { free(pp); // not my DVD/BD continue; } pp1 = strField(pp,' ',2); // get mount point if (! pp1) { free(pp); continue; } repl_1str(pp1,dvdmp,"\\040"," "); // replace "\040" with " " v.6.5 dvdmpcc = strlen(dvdmp); wprintf(mLog," already mounted: %s %s \n",BJdvd,dvdmp); // v.6.5 dvdmtd = 1; } if (dvdmtd) goto showpoop; mkdir(dvdmp,0755); // create default mount point sprintf(mbuff,"mount -t iso9660 %s %s 2>&1",BJdvd,dvdmp); // mount the DVD/BD err = do_shell("mount",mbuff); if (! err) { dvdmtd = 1; goto showpoop; } zmessageACK(mWin,0,"mount DVD/BD and wait for completion"); while (true) { contx = 0; while ((pp = command_output(contx,"cat /etc/mtab"))) // get mounted disk info { pp1 = strField(pp,' ',1); // get /dev/xxx if (strNeq(pp1,BJdvd)) { free(pp); // not my DVD/BD continue; } pp1 = strField(pp,' ',2); // get mount point if (! pp1) { free(pp); continue; } repl_1str(pp1,dvdmp,"\\040"," "); // replace "\040" with " " v.6.5 dvdmpcc = strlen(dvdmp); wprintf(mLog," %d %d mounted \n",BJdvd,dvdmp); dvdmtd = 1; } if (dvdmtd) goto showpoop; // mounted OK sprintf(mbuff,"mount -t iso9660 %s %s 2>&1",BJdvd,dvdmp); // mount the DVD/BD err = do_shell("mount",mbuff); if (! err) { dvdmtd = 1; goto showpoop; } wprintf(mLog," waiting for mount ... \n"); for (int ii = 0; ii < 5; ii++) // 5 secs between "wait" messages { if (checkKillPause()) { // killed by user commFail++; return 1; } zsleep(1); zmainloop(); } } showpoop: dvdtime = dstat.st_ctime; // set DVD/BD ID = mod time snprintf(command,99,"volname %s",BJdvd); // get DVD/BD label fid = popen(command,"r"); if (fid) { pp = fgets_trim(mbuff,99,fid,1); if (pp) strncpy0(dvdlabel,pp,31); pclose(fid); } strcpy(mbuff,dvdmp); strcat(mbuff,V_DATETIME); // get last usage date/time if poss. fid = fopen(mbuff,"r"); if (fid) { pp = fgets_trim(mbuff,99,fid,1); if (pp) strncpy0(mediumDT,pp,15); fclose(fid); } wprintf(mLog," DVD/BD label: %s last dkopp: %s \n",dvdlabel,mediumDT); commFail = 0; return 0; } // unmount DVD/BD int unmountDVD(cchar *menu) { char command[60]; vFilesReset(); dvdmtd = 0; dvdtime = -1; sprintf(command,"umount %s 2>&1",dvdmp); // use mount point v.4.8 do_shell("umount",command); if (Fgui) wprintf(mLog," ready \n"); // v.5.0 commFail = 0; // ignore unmount error return 0; } // eject DVD/BD with message feedback to window // not all computers support programmatic eject int ejectDVD(cchar *menu) { char command[60]; vFilesReset(); dvdmtd = 0; dvdtime = -1; sprintf(command,"eject %s 2>&1",BJdvd); do_shell("eject",command); if (Fgui) wprintf(mLog," ready \n"); // v.5.0 commFail = 0; // ignore eject error return 0; } // wait for DVD/BD and reset hardware (get over lockups after growisofs) int resetDVD(cchar * menu) { if (*subprocName) { // try to kill running job signalProc(subprocName,"resume"); signalProc(subprocName,"kill"); sleep(1); } ejectDVD(0); // the only way I know to reset sleep(1); // a hung-up DVD/BD drive if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // Erase DVD/BD medium by filling it with zeros int eraseDVD(cchar * menu) { char command[200]; int nstat; nstat = zmessageYN(mWin,"Erase DVD/BD. This will take some time. \n Continue?"); if (! nstat) goto erase_exit; vFilesReset(); // reset DVD/BD file data sprintf(command,"growisofs -Z %s=/dev/zero %s 2>&1",BJdvd,gforce); do_shell("growisofs", command); // do growisofs, echo outputs erase_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // Format DVD/BD (2-4 minutes) int formatDVD(cchar * menu) { char command[60]; int nstat; nstat = zmessageYN(mWin,"Format DVD/BD. This will take 2-4 minutes. \n Continue?"); if (! nstat) goto format_exit; vFilesReset(); // reset DVD/BD file data sprintf(command,"dvd+rw-format -force %s 2>&1",BJdvd); do_shell("dvd+rw-format", command); format_exit: if (Fgui) wprintf(mLog," ready \n"); // v.5.0 return 0; } // save logging window as text file int saveScreen(cchar * menu) { if (*scriptParam) { wfiledump(mLog, scriptParam); *scriptParam = 0; return 0; } wfilesave(mLog); return 0; } // Display help/about or help/contents int helpFunc(cchar * menu) { if (strEqu(menu,"about")) { wprintf(mLog," %s \n",dkopp_title); wprintf(mLog," free software: %s \n",dkopp_license); } if (strEqu(menu,"contents")) showz_userguide(); // help file in new process return 0; } // construct file-chooser dialog box // note: Fdialog unnecessary: this dialog called from other dialogs int fc_dialog(cchar *dirk) { GtkWidget *vbox; fc_dialogbox = gtk_dialog_new_with_buttons("choose files", GTK_WINDOW(mWin), GTK_DIALOG_MODAL, "hidden",100, "include",101, "exclude",102, "done",103, null); gtk_window_set_default_size(GTK_WINDOW(fc_dialogbox),600,500); G_SIGNAL(fc_dialogbox,"response",fc_response,0); fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); vbox = gtk_dialog_get_content_area(GTK_DIALOG(fc_dialogbox)); gtk_box_pack_end(GTK_BOX(vbox),fc_widget,1,1,0); gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),0); gtk_widget_show_all(fc_dialogbox); return 0; } // file-chooser dialog handler (file selection, OK, Cancel, Kill) int fc_response(GtkDialog *dwin, int arg, void *data) { GSList *flist = 0; char *file1, *file2, *ppf; int ii, err, hide; struct stat64 filestat; if (arg == 103 || arg == -4) // done, cancel { gtk_widget_destroy(GTK_WIDGET(dwin)); return 0; } if (arg == 100) // hidden { hide = gtk_file_chooser_get_show_hidden(GTK_FILE_CHOOSER(fc_widget)); hide = 1 - hide; gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),hide); } if (arg == 101 || arg == 102) // include, exclude { flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); for (ii = 0; ; ii++) // process selected files { file1 = (char *) g_slist_nth_data(flist,ii); if (! file1) break; file2 = strdupz(file1,2); // extra space for wildcard g_free(file1); err = stat64(file2,&filestat); if (err) wprintf(mLog," *** error: %s file: %s \n",strerror(errno),kleenex(file2)); if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard ppf = file2; if (strnEqu(ppf,dvdmp,dvdmpcc)) ppf += dvdmpcc; // omit DVD/BD mount point if (arg == 101) wprintf(editwidget,"include %s""\n",ppf); if (arg == 102) wprintf(editwidget,"exclude %s""\n",ppf); free(file2); } } gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); g_slist_free(flist); return 0; } // backup helper function // set nominal backup date/time // write date/time and updated medium use count to temp file int writeDT() { time_t dt1; struct tm dt2; // year/month/day/hour/min/sec FILE *fid; dt1 = time(0); dt2 = *localtime(&dt1); snprintf(backupDT,15,"%4d%02d%02d-%02d%02d",dt2.tm_year+1900, // yyyymmdd-hhmm dt2.tm_mon+1, dt2.tm_mday, dt2.tm_hour, dt2.tm_min); strcpy(mediumDT,backupDT); fid = fopen(TFdatetime,"w"); if (! fid) { wprintf(mLog," *** cannot open /tmp scratch file \n"); commFail++; return 0; } fprintf(fid,"%s \n",mediumDT); // write date/time and medium count fclose(fid); return 0; } // backup helper function // save all file and directory owner and permission data to temp file int save_filepoop() // all files, not just directories { int ii, cc, err; FILE *fid; char file[maxfcc], dirk[maxfcc], pdirk[maxfcc], *pp; struct stat64 dstat; fid = fopen(TFfilepoop,"w"); if (! fid) { wprintf(mLog," *** cannot open /tmp scratch file \n"); commFail++; return 0; } *pdirk = 0; // no prior for (ii = 0; ii < Dnf; ii++) { strcpy(dirk,Drec[ii].file); // next file on disk pp = dirk; while (true) { pp = strchr(pp+1,'/'); // next (last) directory level if (! pp) break; cc = pp - dirk + 1; // cc incl. '/' if (strncmp(dirk,pdirk,cc) == 0) continue; // matches prior, skip *pp = 0; // terminate this directory level err = stat64(dirk,&dstat); // get owner and permissions if (err) { wprintf(mLog," *** error: %s file: %s \n",strerror(errno),kleenex(dirk)); break; } dstat.st_mode = dstat.st_mode & 0777; fprintf(fid,"%4d:%4d %3o %s\n", // output uid:gid permissions directory dstat.st_uid, dstat.st_gid, dstat.st_mode, dirk); // (octal) *pp = '/'; // restore '/' } strcpy(pdirk,dirk); // prior = this directory strcpy(file,Drec[ii].file); // disk file, again err = stat64(file,&dstat); // get owner and permissions if (err) { wprintf(mLog," *** error: %s file: %s \n",strerror(errno),kleenex(file)); continue; } dstat.st_mode = dstat.st_mode & 0777; fprintf(fid,"%4d:%4d %3o %s\n", // output uid:gid permissions file dstat.st_uid, dstat.st_gid, dstat.st_mode, file); // (octal) } fclose(fid); return 0; } // restore helper function // restore original owner and permissions for restored files and directories int restore_filepoop() { FILE *fid; int cc1, cc2, ccf, nn, ii, err; int uid, gid, perms; char file1[maxfcc], file2[maxfcc]; char poopfile[100]; wprintf(mLog,"\n restore directory owner and permissions \n"); wprintf(mLog," for directories anchored at: %s \n",RJto); cc1 = strlen(RJfrom); // from: /home/xxx/.../ cc2 = strlen(RJto); // to: /home/yyy/.../ strcpy(poopfile,dvdmp); // DVD/BD file with owner & permissions strcat(poopfile,V_FILEPOOP); fid = fopen(poopfile,"r"); if (! fid) { wprintf(mLog," *** cannot open DVD/BD file: %s \n",poopfile); return 0; } ii = 0; while (true) { nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file1); // uid, gid, permissions, file if (nn == EOF) break; // (nnn:nnn) (octal) if (nn != 4) continue; ccf = strlen(file1); // match directories too if (ccf < cc1) continue; while (ii < Rnf) { nn = strncmp(Rrec[ii].file,file1,ccf); // file in restored file list? if (nn >= 0) break; // (logic depends on sorted lists) ii++; } if (ii == Rnf) break; if (nn > 0) continue; // no strcpy(file2,RJto); // copy-to location strcpy(file2 + cc2, file1 + cc1); // + org. file, less copy-from part wprintf(mLog," owner: %4d:%4d permissions: %3o file: %s \n", uid, gid, perms, kleenex(file2)); err = chown(file2,uid,gid); if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); err = chmod(file2,perms); if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); } fclose(fid); return 0; } // create backup history file after successful backup int createBackupHist() { int ii, err; FILE *fid; char backupfile[200], buff[230]; char disp; snprintf(backupfile,199,"%s/dkopp-hist-%s-%s", // create history file name: userdir,backupDT,dvdlabel); // dkopp-hist-yyyymmdd-hhmm-dvdlabel snprintf(buff,229,"\n""create history file: %s \n",backupfile); wprintx(mLog,0,buff,boldfont); fid = fopen(backupfile,"w"); if (! fid) { wprintf(mLog," *** cannot open dkopp-hist file \n"); return 0; } fprintf(fid,"%s (%s backup) \n\n",backupfile,mbmode); for (ii = 0; ii < BJnx; ii++) // output include/exclude recs fprintf(fid," %s \n",BJinex[ii]); fprintf(fid,"\n"); if (strEqu(mbmode,"full")) { for (ii = 0; ii < Dnf; ii++) // output all files for backup { if (Drec[ii].dvd != dvdnum) continue; // screen for DVD/BD number fprintf(fid,"%s\n",Drec[ii].file); } } else { for (ii = 0; ii < Dnf; ii++) { // output new and modified files disp = Drec[ii].disp; if ((disp == 'n') || (disp == 'm')) fprintf(fid,"%s\n",Drec[ii].file); } } err = fclose(fid); if (err) wprintf(mLog," *** dkopp-hist file error %s \n",strerror(errno)); return 0; } // parse an include/exclude filespec statement // return: 0=comment 1=OK 2=parse-error 3=fspec-error int inexParse(char * rec, char *& rtype, char *& fspec) { char *pp1, *pp2; int ii; rtype = fspec = 0; if (rec[0] == '#') return 0; // comment recs. if (strlen(rec) < 3) return 0; strTrim(rec); ii = 0; while ((rec[ii] == ' ') && (ii < 30)) ii++; // find 1st non-blank if (rec[ii] == 0) return 0; if (ii == 30) return 0; // blank record rtype = rec + ii; // include/exclude while ((rec[ii] > ' ') && (ii < 30)) ii++; // look for next blank or null if (ii == 30) return 2; if (rec[ii] == ' ') { rec[ii] = 0; ii++; } // end of rtype if (strlen(rtype) > 7) return 2; while ((rec[ii] == ' ') && (ii < 30)) ii++; // find next non-blank if (ii == 30) return 2; fspec = rec + ii; // filespec (wildcards) if (strlen(fspec) < 4) return 3; if (strlen(fspec) > maxfcc-100) return 3; if (strEqu(rtype,"exclude")) return 1; // exclude, done if (strNeq(rtype,"include")) return 2; // must be include if (fspec[0] != '/') return 3; // must have at least /topdirk/ pp1 = strchr(fspec+1,'/'); if (!pp1) return 3; if (pp1-fspec < 2) return 3; pp2 = strchr(fspec+1,'*'); // any wildcards must be later if (pp2 && (pp2 < pp1)) return 3; pp2 = strchr(fspec+1,'%'); if (pp2 && (pp2 < pp1)) return 3; return 1; // include + legit. fspec } // list backup job data and validate as much as practical int BJvalidate(cchar * menu) { int ii, err, nerr = 0; int year, mon, day; struct tm tm_date, *tm_date2; struct stat dstat; wprintx(mLog,0,"\n""Validate backup job data \n",boldfont); BJval = 0; if (! BJnx) { wprintf(mLog," *** no job data present \n"); commFail++; return 0; } wprintf(mLog," DVD/BD device: %s \n",BJdvd); wprintf(mLog," capacity GB: %.1f \n",BJcap); if (BJspeed == 0) wprintf(mLog," write speed: default \n",BJspeed); // v.4.5 else wprintf(mLog," write speed: %d (x 1.38 MB/sec) \n",BJspeed); err = stat(BJdvd,&dstat); if (err || ! S_ISBLK(dstat.st_mode)) { wprintf(mLog," *** DVD/BD device is apparently invalid \n"); nerr++; } if (BJcap < dvdcapmin || BJcap > dvdcapmax) { wprintf(mLog," *** DVD/BD capacity is apparently invalid \n"); nerr++; } wprintf(mLog," backup %s \n",BJbmode); if (! strcmpv(BJbmode,"full","incremental","accumulate",null)) { wprintf(mLog," *** backup mode not full/incremental/accumulate \n"); nerr++; } wprintf(mLog," verify %s \n",BJvmode); if (! strcmpv(BJvmode,"full","incremental","thorough",null)) { wprintf(mLog," *** verify mode not full/incremental/thorough \n"); nerr++; } wprintf(mLog," file date from: %s \n",BJdatefrom); // file age limit v.4.8 err = 0; ii = sscanf(BJdatefrom,"%d.%d.%d",&year,&mon,&day); if (ii != 3) err = 1; tm_date.tm_year = year - 1900; tm_date.tm_mon = mon - 1; tm_date.tm_mday = day; tm_date.tm_hour = tm_date.tm_min = tm_date.tm_sec = 0; tm_date.tm_isdst = -1; BJtdate = mktime(&tm_date); tm_date2 = localtime(&BJtdate); if (tm_date2->tm_year - year + 1900 != 0) err = 3; if (tm_date2->tm_year + 1900 < 1970) err = 4; // < 1970 disallowed v.4.8 if (tm_date2->tm_mon - mon + 1 != 0) err = 5; if (tm_date2->tm_mday - day != 0) err = 6; if (err) { wprintf(mLog," *** date must be > 1970.01.01 \n"); nerr++; BJtdate = 0; } nerr += nxValidate(BJinex,BJnx); // validate include/exclude recs wprintf(mLog," *** %d errors \n",nerr); if (nerr) commFail++; else BJval = 1; return 0; } // validate restore job data int RJvalidate() { int cc, nerr = 0; char rdirk[maxfcc]; DIR *pdirk; if (RJval) return 1; wprintf(mLog,"\n Validate restore job data \n"); if (! RJnx) { wprintf(mLog," *** no job data present \n"); return 0; } wprintf(mLog," copy-from: %s \n",RJfrom); strcpy(rdirk,dvdmp); // validate copy-from location strcat(rdirk,RJfrom); // /media/dvd/home/... pdirk = opendir(rdirk); if (! pdirk) { wprintf(mLog," *** invalid copy-from location \n"); nerr++; } else closedir(pdirk); cc = strlen(RJfrom); // insure '/' at end if (RJfrom[cc-1] != '/') strcat(RJfrom,"/"); wprintf(mLog," copy-to: %s \n",RJto); pdirk = opendir(RJto); // validate copy-to location if (! pdirk) { wprintf(mLog," *** invalid copy-to location \n"); nerr++; } else closedir(pdirk); cc = strlen(RJto); // insure '/' at end if (RJto[cc-1] != '/') strcat(RJto,"/"); nerr += nxValidate(RJinex,RJnx); // validate include/exclude recs wprintf(mLog," %d errors \n",nerr); if (! nerr) RJval = 1; else RJval = 0; return RJval; } // list and validate a set of include/exclude recs int nxValidate(char **inexrecs, int nrecs) { char *rtype, *fspec, nxrec[maxfcc]; int ii, nstat, errs = 0; for (ii = 0; ii < nrecs; ii++) // process include/exclude recs { strcpy(nxrec,inexrecs[ii]); wprintf(mLog," %s \n",nxrec); // output nstat = inexParse(nxrec,rtype,fspec); // parse if (nstat == 0) continue; // comment if (nstat == 1) continue; // OK if (nstat == 2) { wprintf(mLog," *** cannot parse \n"); // cannot parse errs++; continue; } if (nstat == 3) { // bad filespec wprintf(mLog," *** invalid filespec \n"); errs++; continue; } } return errs; } // get all files for backup as specified by include/exclude records // save in Drec[] array int dGetFiles() { cchar *fsp; char *rtype, *fspec, bjrec[maxfcc], *mbytes; int ftf, cc, nstat, wstat, err; int ii, jj, nfiles, ndvd, toobig, nexc; double nbytes, dvdbytes; struct stat64 filestat; if (! BJval) { // validate job data if needed dFilesReset(); BJvalidate(0); if (! BJval) return 0; // job has errors } if (Dnf > 0) return 0; // avoid refresh wprintx(mLog,0,"\n""finding all files for backup \n",boldfont); for (ii = 0; ii < BJnx; ii++) // process include/exclude recs { BJfiles[ii] = 0; // initz. include/exclude rec stats BJbytes[ii] = 0.0; BJdvdno[ii] = 0; strcpy(bjrec,BJinex[ii]); // next record nstat = inexParse(bjrec,rtype,fspec); // parse if (nstat == 0) continue; // comment if (strEqu(rtype,"include")) // include filespec { ftf = 1; while (1) { fsp = SearchWild(fspec,ftf); // find matching files if (! fsp) break; cc = strlen(fsp); if (cc > maxfcc-100) zappcrash("file cc: %d, %99s...",cc,fsp); Drec[Dnf].file = strdup(fsp); err = lstat64(fsp,&filestat); // check accessibility v.4.6 if (err == 0) { if (! S_ISREG(filestat.st_mode) && // include files and symlinks v.4.6 ! S_ISLNK(filestat.st_mode)) continue; // omit pipes, devices ... } Drec[Dnf].stat = err; // save file status Drec[Dnf].inclx = ii; // save pointer to include rec Drec[Dnf].size = filestat.st_size; // save file size Drec[Dnf].mtime = filestat.st_mtime // save last mod time + filestat.st_mtim.tv_nsec * nano; // (nanosec resolution) if (err) Drec[Dnf].size = Drec[Dnf].mtime = 0; Drec[Dnf].disp = Drec[Dnf].ivf = 0; // initialize BJfiles[ii]++; // count included files and bytes BJbytes[ii] += Drec[Dnf].size; if (++Dnf == maxfs) { wprintf(mLog," *** exceeded %d files \n",maxfs); goto errret; } } } if (strEqu(rtype,"exclude")) // exclude filespec { for (jj = 0; jj < Dnf; jj++) // check included files (SO FAR) { if (! Drec[jj].file) continue; wstat = MatchWild(fspec,Drec[jj].file); if (wstat != 0) continue; BJfiles[ii]--; // un-count excluded file and bytes BJbytes[ii] -= Drec[jj].size; free(Drec[jj].file); // clear file data in array Drec[jj].file = 0; Drec[jj].stat = 0; // bugfix } } } // end of include/exclude recs for (ii = 0; ii < Dnf; ii++) // list and remove error files { // (after excluded files removed) if (! Drec[ii].file) continue; // bugfix v.4.6 if (Drec[ii].stat) { err = stat64(Drec[ii].file,&filestat); wprintf(mLog," *** %s omit: %s \n",strerror(errno),kleenex(Drec[ii].file)); jj = Drec[ii].inclx; BJfiles[jj]--; // un-count file and bytes BJbytes[jj] -= Drec[ii].size; free(Drec[ii].file); Drec[ii].file = 0; } } for (ii = 0; ii < Dnf; ii++) // list and remove too-big files { if (! Drec[ii].file) continue; // bugfix v.4.6 if (Drec[ii].size > BJcap * giga) { wprintf(mLog," *** omit file too big: %s \n",kleenex(Drec[ii].file)); jj = Drec[ii].inclx; BJfiles[jj]--; // un-count file and bytes BJbytes[jj] -= Drec[ii].size; free(Drec[ii].file); Drec[ii].file = 0; } } for (nexc = ii = 0; ii < Dnf; ii++) { if (! Drec[ii].file) continue; if (Drec[ii].mtime < BJtdate) // omit files excluded by date { // or older than 1970 v.4.8 jj = Drec[ii].inclx; BJfiles[jj]--; // un-count file and bytes BJbytes[jj] -= Drec[ii].size; free(Drec[ii].file); Drec[ii].file = 0; nexc++; } } if (nexc) wprintf(mLog," %d files excluded by selection date \n",nexc); ii = jj = 0; // repack file arrays after deletions while (ii < Dnf) { if (Drec[ii].file == 0) ii++; else { if (ii > jj) { if (Drec[jj].file) free(Drec[jj].file); Drec[jj] = Drec[ii]; Drec[ii].file = 0; } ii++; jj++; } } Dnf = jj; // final file count Dbytes = 0.0; for (ii = 0; ii < Dnf; ii++) Dbytes += Drec[ii].size; // compute total bytes from files nfiles = 0; nbytes = 0.0; for (ii = 0; ii < BJnx; ii++) // compute total files and bytes { // from include/exclude recs nfiles += BJfiles[ii]; nbytes += BJbytes[ii]; } mbytes = formatKBMB(nbytes,4); // v.5.2 wprintf(mLog," files for backup: %d %s \n",nfiles,mbytes); if ((nfiles != Dnf) || (Dbytes != nbytes)) { // must match wprintf(mLog," *** bug: nfiles: %d Dnf: %d \n",nfiles,Dnf); wprintf(mLog," *** bug: nbytes: %.0f Dbytes: %.0f \n",nbytes,Dbytes); goto errret; } // assign DVD/BD sequence number to all files, under constraint that // all files from same include record are on same DVD/BD, if possible ndvd = 1; // 1st DVD/BD sequence no. dvdbytes = 0.0; // DVD/BD bytes so far toobig = 0; for (ii = jj = 0; ii < Dnf; ii++) // loop all files { if (Drec[ii].inclx != Drec[jj].inclx) jj = ii; // start of include group if (dvdbytes + Drec[ii].size > BJcap * giga) { // exceeded DVD/BD capacity in this group if (jj > 0 && Drec[jj].dvd == Drec[jj-1].dvd) ii = jj; // if same DVD/BD as prior, restart group else toobig++; ndvd++; // next DVD/BD no. dvdbytes = 0.0; // reset byte counter } Drec[ii].dvd = ndvd; // set DVD/BD no. for file dvdbytes += Drec[ii].size; // accum. bytes for this DVD/BD no. if (ii == jj) BJdvdno[Drec[ii].inclx] = ndvd; // set 1st DVD/BD no. for include rec } BJndvd = ndvd; // final DVD/BD media count if (toobig) wprintf(mLog," *** warning: single include set exceeds DVD/BD capacity \n"); SortFileList((char *) Drec,sizeof(dfrec),Dnf,'A'); // sort Drec[Dnf] by Drec[].file for (ii = 1; ii < Dnf; ii++) // look for duplicate files if (strEqu(Drec[ii].file,Drec[ii-1].file)) { wprintf(mLog," *** duplicate file: %s \n",kleenex(Drec[ii].file)); BJval = 0; // invalidate backup job } if (! BJval) goto errret; return 0; errret: dFilesReset(); BJval = 0; return 0; } // get existing files on DVD/BD medium, save in Vrec[] array // (the shell command "find ... -type f" does not find the // files "deleted" via copy from /dev/null in growisofs) int vGetFiles() { int cc, gcc, err; char command[100], *pp; char fspec1[maxfcc], fspec2[maxfcc]; FILE *fid; struct stat64 filestat; if (Vnf) return 0; // avoid refresh mountDVD(0); // mount with retries if (! dvdmtd) return 0; // cannot mount wprintx(mLog,0,"\n""find all DVD/BD files \n",boldfont); sprintf(command,"find \"%s\" -type f -or -type l >%s", // get regular files and symlinks dvdmp,TFdvdfiles); // add quotes in case of blanks v.6.2 wprintf(mLog," %s \n",command); err = system(command); // list all DVD/BD files to temp file if (err) { wprintf(mLog," *** find command failed: %s \n",wstrerror(err)); commFail++; return 0; } fid = fopen(TFdvdfiles,"r"); // read file list if (! fid) { wprintf(mLog," *** cannot open /tmp scratch file \n"); commFail++; return 0; } gcc = strlen(V_DKOPPDIRK); while (1) { pp = fgets_trim(fspec1,maxfcc-2,fid); // get next file if (! pp) break; // eof cc = strlen(pp); // absurdly long file name if (cc > maxfcc-100) { wprintf(mLog," *** absurd file skipped: %300s (etc.) \n",kleenex(pp)); continue; } if (strnEqu(fspec1+dvdmpcc,V_DKOPPDIRK,gcc)) continue; // ignore special dkopp files repl_1str(fspec1,fspec2,"\\=","="); // replace "\=" with "=" in file name Vrec[Vnf].file = strdup(fspec2 + dvdmpcc); // save without DVD/BD mount point err = lstat64(fspec1,&filestat); // check accessibility v.4.6 Vrec[Vnf].stat = err; // save file status Vrec[Vnf].size = filestat.st_size; // save file size Vrec[Vnf].mtime = filestat.st_mtime // save last mod time + filestat.st_mtim.tv_nsec * nano; if (err) Vrec[Vnf].size = Vrec[Vnf].mtime = 0; Vnf++; if (Vnf == maxfs) zappcrash("exceed %d files",maxfs); } fclose (fid); SortFileList((char *) Vrec,sizeof(vfrec),Vnf,'A'); // sort Vrec[Vnf] by Vrec[].file wprintf(mLog," DVD/BD files: %d \n",Vnf); return 0; } // get all DVD/BD restore files specified by include/exclude records int rGetFiles() { char *rtype, *fspec, fspecx[maxfcc], rjrec[maxfcc]; int ii, jj, cc, nstat, wstat, ninc, nexc; if (! RJval) return 0; rFilesReset(); // clear restore files vGetFiles(); // get DVD/BD files if (! Vnf) return 0; wprintf(mLog,"\n""find all DVD/BD files to restore \n"); for (ii = 0; ii < RJnx; ii++) // process include/exclude recs { strcpy(rjrec,RJinex[ii]); // next record wprintf(mLog," %s \n",rjrec); // output nstat = inexParse(rjrec,rtype,fspec); // parse if (nstat == 0) continue; // comment repl_1str(fspec,fspecx,"\\=","="); // replace "\=" with "=" in file name if (strEqu(rtype,"include")) // include filespec { ninc = 0; // count of included files for (jj = 0; jj < Vnf; jj++) // screen all DVD/BD files { wstat = MatchWild(fspecx,Vrec[jj].file); if (wstat != 0) continue; Rrec[Rnf].file = strdup(Vrec[jj].file); // add matching files Rnf++; ninc++; if (Rnf == maxfs) zappcrash("exceed %d files",maxfs); } wprintf(mLog," %d files added \n",ninc); } if (strEqu(rtype,"exclude")) // exclude filespec { nexc = 0; for (jj = 0; jj < Rnf; jj++) // check included files (SO FAR) { if (! Rrec[jj].file) continue; wstat = MatchWild(fspecx,Rrec[jj].file); if (wstat != 0) continue; free(Rrec[jj].file); // remove matching files Rrec[jj].file = 0; nexc++; } wprintf(mLog," %d files removed \n",nexc); } } ii = jj = 0; // repack after deletions while (ii < Rnf) { if (Rrec[ii].file == 0) ii++; else { if (ii > jj) { if (Rrec[jj].file) free(Rrec[jj].file); Rrec[jj].file = Rrec[ii].file; Rrec[ii].file = 0; } ii++; jj++; } } Rnf = jj; wprintf(mLog," total file count: %d \n",Rnf); cc = strlen(RJfrom); // copy from: /home/.../ for (ii = 0; ii < Rnf; ii++) // get selected DVD/BD files to restore { if (! strnEqu(Rrec[ii].file,RJfrom,cc)) { wprintf(mLog," *** not under copy-from; %s \n",Rrec[ii].file); RJval = 0; // mark restore job invalid continue; } } SortFileList((char *) Rrec,sizeof(rfrec),Rnf,'A'); // sort Rrec[Rnf] by Rrec[].file return 0; } // helper function for backups and reports // // compare disk and DVD/BD files, set dispositions in Drec[] and Vrec[] arrays // n new on disk, not on DVD/BD // d deleted on DVD/BD, not on disk // m modified on both, but not equal // u unchanged on both, and equal int setFileDisps() { int dii, vii, comp; char disp; double diff; dii = vii = 0; nnew = nmod = nunc = ndel = 0; Mbytes = 0.0; // total bytes, new and modified files while ((dii < Dnf) || (vii < Vnf)) // scan disk and DVD/BD files in parallel { if ((dii < Dnf) && (vii == Vnf)) comp = -1; // disk file after last DVD/BD file else if ((dii == Dnf) && (vii < Vnf)) comp = +1; // DVD/BD file after last disk file else comp = strcmp(Drec[dii].file,Vrec[vii].file); // compare disk and DVD/BD file names if (comp < 0) { // unmatched disk file: new on disk Drec[dii].disp = 'n'; Mbytes += Drec[dii].size; // accumulate Mbytes nnew++; // count new files dii++; } else if (comp > 0) { // unmatched DVD/BD file: deleted on disk Vrec[vii].disp = 'd'; ndel++; // count deleted files vii++; } else if (comp == 0) // file present on disk and DVD/BD { disp = 'u'; // set initially unchanged if (Drec[dii].stat != Vrec[vii].stat) disp = 'm'; // fstat() statuses are different diff = fabs(Drec[dii].size - Vrec[vii].size); if (diff > 0) disp = 'm'; // sizes are different diff = fabs(Drec[dii].mtime - Vrec[vii].mtime); if (diff > modtimetolr) disp = 'm'; // mod times are different Drec[dii].disp = Vrec[vii].disp = disp; if (disp == 'u') nunc++; // count unchanged files if (disp == 'm') nmod++; // count modified files if (disp == 'm') Mbytes += Drec[dii].size; // and accumulate Mbytes dii++; vii++; } } Mfiles = nnew + nmod + ndel; return 0; } // Sort file list in memory (disk files, DVD/BD files, restore files). // Sort ascii sequence, or sort subdirectories in a directory before files. int SortFileList(char * recs, int RL, int NR, char sort) { HeapSortUcomp fcompA, fcompD; // compare filespecs functions if (sort == 'A') HeapSort(recs,RL,NR,fcompA); // normal ascii compare if (sort == 'D') HeapSort(recs,RL,NR,fcompD); // compare directories first return 0; } int fcompA(cchar * rec1, cchar * rec2) // ascii comparison { dfrec *r1 = (dfrec *) rec1; dfrec *r2 = (dfrec *) rec2; return strcmp(r1->file,r2->file); } int fcompD(cchar * rec1, cchar * rec2) // special compare filenames { // subdirectories in a directory are dfrec *r1 = (dfrec *) rec1; // less than files in the directory dfrec *r2 = (dfrec *) rec2; return filecomp(r1->file,r2->file); } int filecomp(cchar *file1, cchar *file2) // special compare filenames { // subdirectories compare before files cchar *pp1, *pp10, *pp2, *pp20; char slash = '/'; int cc1, cc2, comp; pp1 = file1; // first directory level or file pp2 = file2; while (true) { pp10 = strchr(pp1,slash); // find next slash pp20 = strchr(pp2,slash); if (pp10 && pp20) { // both are directories cc1 = pp10 - pp1; cc2 = pp20 - pp2; if (cc1 < cc2) comp = strncmp(pp1,pp2,cc1); // compare the directories else comp = strncmp(pp1,pp2,cc2); if (comp) return comp; else if (cc1 != cc2) return (cc1 - cc2); pp1 = pp10 + 1; // equal, check next level pp2 = pp20 + 1; continue; } if (pp10 && ! pp20) return -1; // only one is a directory, if (pp20 && ! pp10) return 1; // the directory is first comp = strcmp(pp1,pp2); // both are files, compare return comp; } } // reset all backup job data and free allocated memory int BJreset() { for (int ii = 0; ii < BJnx; ii++) free(BJinex[ii]); BJnx = 0; *BJbmode = *BJvmode = 0; BJval = BJmod = 0; dFilesReset(); // reset dependent disk file data return 0; } // reset all restore job data and free allocated memory int RJreset() { for (int ii = 0; ii < RJnx; ii++) free(RJinex[ii]); RJnx = 0; RJval = 0; rFilesReset(); // reset dependent disk file data return 0; } // reset all file data and free allocated memory int dFilesReset() { // disk files data for (int ii = 0; ii < Dnf; ii++) { free(Drec[ii].file); Drec[ii].file = 0; } Dnf = 0; Dbytes = Dbytes2 = Mbytes = 0.0; return 0; } int vFilesReset() { // DVD/BD files data for (int ii = 0; ii < Vnf; ii++) { free(Vrec[ii].file); Vrec[ii].file = 0; } Vnf = 0; Vbytes = Mbytes = 0.0; return 0; } int rFilesReset() { // DVD/BD restore files data for (int ii = 0; ii < Rnf; ii++) { free(Rrec[ii].file); Rrec[ii].file = 0; } Rnf = 0; return 0; } // helper function to copy a file from DVD/BD to disk cchar * copyFile(cchar * vfile, char *dfile) { char vfile1[maxfcc], vfilex[maxfcc]; int fid1, fid2, err, rcc; char *pp, buff[vrcc]; cchar *errmess; struct stat64 fstat; struct timeval ftimes[2]; strcpy(vfile1,dvdmp); // prepend DVD/BD mount point strcat(vfile1,vfile); repl_1str(vfile1,vfilex,"=","\\="); // replace "=" with "\=" in DVD/BD file fid1 = open(vfilex,O_RDONLY+O_NOATIME+O_LARGEFILE); // open input file if (fid1 == -1) return strerror(errno); fid2 = open(dfile,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700); // open output file if (fid2 == -1 && errno == ENOENT) { pp = dfile; while (true) { // create one or more directories, pp = strchr(pp+1,'/'); // one level at a time if (! pp) break; *pp = 0; err = mkdir(dfile,0700); if (! err) chmod(dfile,0700); *pp = '/'; if (err) { if (errno == EEXIST) continue; errmess = strerror(errno); close(fid1); return errmess; } } fid2 = open(dfile,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700); // open output file again } if (fid2 == -1) { errmess = strerror(errno); close(fid1); return errmess; } while (true) { rcc = read(fid1,buff,vrcc); // read huge blocks if (rcc == 0) break; if (rcc == -1) { errmess = strerror(errno); close(fid1); close(fid2); return errmess; } rcc = write(fid2,buff,rcc); // write blocks if (rcc == -1) { errmess = strerror(errno); close(fid1); close(fid2); return errmess; } } close(fid1); close(fid2); stat64(vfilex,&fstat); // get input file attributes ftimes[0].tv_sec = fstat.st_atime; // conv. access times to microsecs ftimes[0].tv_usec = fstat.st_atim.tv_nsec / 1000; ftimes[1].tv_sec = fstat.st_mtime; ftimes[1].tv_usec = fstat.st_mtim.tv_nsec / 1000; chmod(dfile,fstat.st_mode); // set output file attributes err = chown(dfile,fstat.st_uid,fstat.st_gid); // (if supported by file system) if (err) printf("error: %s \n",wstrerror(err)); utimes(dfile,ftimes); return 0; } // Verify helper function // Verify that file on BD/DVD medium is readable, return its length. // Optionally compare backup file to current file, byte for byte. // return: 0: OK 1: open error 2: read error 3: compare fail cchar * checkFile(char * dfile, int compf, double &tcc) { int vfid = 0, dfid = 0; int err, vcc, dcc, cmperr = 0; int open_flags = O_RDONLY+O_NOATIME+O_LARGEFILE; // O_DIRECT not allowed for DVD/BD char vfile[maxfcc], *vbuff = 0, *dbuff = 0; cchar *errmess = 0; double dtime, vtime; struct stat64 filestat; tcc = 0.0; strcpy(vfile,dvdmp); // prepend mount point repl_1str(dfile,vfile+dvdmpcc,"=","\\="); // replace "=" with "\=" in DVD/BD file err = lstat64(vfile,&filestat); // check symlinks but do not follow if (err) return strerror(errno); // v.4.6 if (S_ISLNK(filestat.st_mode)) return 0; if (compf) goto comparefiles; vfid = open(vfile,open_flags); // open DVD/BD file if (vfid == -1) return strerror(errno); err = posix_memalign((void**) &vbuff,512,vrcc); // get 512-aligned buffer if (err) zappcrash("memory allocation failure"); while (1) // read DVD/BD file { vcc = read(vfid,vbuff,vrcc); if (vcc == 0) break; if (vcc == -1) { errmess = strerror(errno); break; } tcc += vcc; // accumulate length if (checkKillPause()) break; zmainloop(10); // keep gtk alive v.6.3 } goto cleanup; comparefiles: vfid = open(vfile,open_flags); // open DVD/BD file if (vfid == -1) return strerror(errno); dfid = open(dfile,open_flags); // open corresp. disk file if (dfid == -1) { errmess = strerror(errno); goto cleanup; } err = posix_memalign((void**) &vbuff,512,vrcc); // get 512-aligned buffers if (err) zappcrash("memory allocation failure"); err = posix_memalign((void**) &dbuff,512,vrcc); if (err) zappcrash("memory allocation failure"); while (1) { vcc = read(vfid,vbuff,vrcc); // read two files if (vcc == -1) { errmess = strerror(errno); goto cleanup; } dcc = read(dfid,dbuff,vrcc); if (dcc == -1) { errmess = strerror(errno); goto cleanup; } if (vcc != dcc) cmperr++; // compare buffers if (memcmp(vbuff,dbuff,vcc)) cmperr++; tcc += vcc; // accumulate length if (vcc == 0) break; if (dcc == 0) break; if (checkKillPause()) break; zmainloop(5); // keep gtk alive v.6.3 } if (vcc != dcc) cmperr++; if (cmperr) { // compare error stat64(dfile,&filestat); dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; // file modified since snapshot? stat64(vfile,&filestat); vtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; if (fabs(dtime-vtime) < modtimetolr) errmess = "compare error"; // no, a real compare error } cleanup: if (vfid) close(vfid); // close files if (dfid) close(dfid); if (vbuff) free(vbuff); // free buffers if (dbuff) free(dbuff); return errmess; } // track current /directory/.../filename.ext on logging window // display directory and file names in overlay mode (no scrolling) int track_filespec(cchar * filespec) { int cc; char pdirk[300], pfile[300], *pp; if (! Fgui) { // v.5.5 printf(" %s \n",filespec); return 0; } pp = (char *) strrchr(filespec+1,'/'); // parse directory/filename v.4.7.1 gcc if (pp) { cc = pp - filespec + 2; strncpy0(pdirk,filespec,cc); strncpy0(pfile,pp+1,299); } else { strcpy(pdirk," "); strncpy0(pfile,filespec,299); } wprintf(mLog,-3," %s \n",kleenex(pdirk)); // output /directory wprintf(mLog,-2," %s \n",kleenex(pfile)); // filename return 0; } // log error message and scroll down to prevent it from being overlaid int track_filespec_err(cchar * filespec, cchar * errmess) { if (Fgui) { wprintf(mLog,-3," *** %s %s \n",errmess,kleenex(filespec)); wprintf(mLog," \n"); } else printf(" %s %s \n",errmess,filespec); // v.5.5 return 0; } // remove special characters in exotic file names causing havoc in output formatting cchar * kleenex(cchar *name) { static char name2[1000]; strncpy0(name2,name,999); for (int ii = 0; name2[ii]; ii++) if (name2[ii] >= 8 && name2[ii] <= 13) // screen out formatting chars. name2[ii] = '?'; return name2; } // do shell command (subprocess) and echo outputs to log window // returns command status: 0 = OK, +N = error // compensate for growisofs failure not always indicated as bad status // depends on growisofs output being in english int do_shell(cchar * pname, cchar * command) { int scroll, pscroll; int err, gerr = 0, contx = 0; char buff[1000]; const char *crec, *errmess; snprintf(buff,999,"\n""shell: %s \n",command); wprintx(mLog,0,buff,boldfont); strncpy0(subprocName,pname,20); // set subprocess name, active if (strEqu(pname,"growisofs")) track_growisofs_files(0); // initialize progress tracker scroll = pscroll = 1; while ((crec = command_output(contx,command))) { strncpy0(buff,crec,999); pscroll = scroll; scroll = 1; if (strEqu(pname,"growisofs")) { // growisofs output if (track_growisofs_files(buff)) scroll = 0; // conv. % done into file position if (strstr(buff,"genisoimage:")) gerr = 999; // trap errors not reported in if (strstr(buff,"mkisofs:")) gerr = 998; // flakey growisofs status if (strstr(buff,"failed")) gerr = 997; if (strstr(buff,"media is not recognized")) gerr = 996; // v.5.9.2 } if (strstr(buff,"formatting")) scroll = 0; // dvd+rw-format output if (scroll) { // output to next line wprintf(mLog," %s: %s \n",pname,kleenex(buff)); zsleep(0.1); // throttle output a little } else if (Fgui) { // supress output in batch mode v.5.0 if (pscroll) wprintf(mLog,"\n"); // transition from scroll to overlay wprintf(mLog,-2," %s: %s \n",pname,kleenex(buff)); // output, overlay prior output } if (killFlag) { // v.6.0 sprintf(buff,"pkill %s",subprocName); wprintf(mLog,"*** %s \n",buff); err = system(buff); } while (pauseFlag) { zsleep(0.2); // v.6.0 zmainloop(); } } errmess = 0; err = command_status(contx); if (err) errmess = strerror(err); if (strEqu(pname,"growisofs")) { // v.5.9.2 err = gerr; if (err) errmess = "growisofs failure"; } if (err) wprintf(mLog," %s status: %d %s \n", pname, err, errmess); else wprintf(mLog," %s status: OK \n",pname); *subprocName = 0; // no longer active if (err) commFail++; return err; } // Convert "% done" from growisofs into corresponding position in list of files being copied. // Incremental backups start with % done = (initial DVD/BD space used) / (final DVD/BD space used). int track_growisofs_files(char * buff) { static double bbytes, gpct0, gpct; static int dii, dii2, err; static char *dfile; if (! buff) { // initialize dii = 0; dii2 = -1; bbytes = 0; dfile = (char *) ""; return 0; } if (! strstr(buff,"% done")) return 0; // not a % done record err = convSD(buff,gpct,0.0,100.0); // get % done, 0-100 if (err > 1) return 0; if (strEqu(mbmode,"full")) { // full backup, possibly > 1 DVD/BD while (dii < Dnf) { if (bbytes/Dbytes2 > gpct/100) break; // exit if enough bytes if (Drec[dii].dvd == dvdnum) { bbytes += Drec[dii].size; // sum files matching DVD/BD number dii2 = dii; } dii++; } } else { // incremental backup if (bbytes == 0) gpct0 = gpct; // establish base % done while (dii < Dnf) { if (bbytes/Mbytes > (gpct-gpct0)/(100-gpct0)) break; // exit if enough bytes if (Drec[dii].disp == 'n' || Drec[dii].disp == 'm') { bbytes += Drec[dii].size; // sum new and modified files dii2 = dii; } dii++; } } if (dii2 > -1) dfile = Drec[dii2].file; // file corresponding to byte count snprintf(buff,999,"%6.1f %c %s",gpct,'%',dfile); // nn.n % /directory/.../filename return 1; } // supply unused zdialog callback function void KBstate(GdkEventKey *event, int state) { return; } dkopp-6.5/desktop0000644000175000017500000000034712343020444012535 0ustar micomico[Desktop Entry] Name=dkopp GenericName=Copy files to DVD Comment=Full and incremental backup to DVD with verify Categories=Utility;Archiving Type=Application Terminal=false Exec=/usr/bin/dkopp Icon=/usr/share/dkopp/icons/dkopp.png dkopp-6.5/debian-control0000644000175000017500000000117712343020444013766 0ustar micomicoPackage: dkopp Version: 6.5 Architecture: amd64 Section: utils Installed-Size: 1328 Maintainer: Mike Cornelison Priority: optional Homepage: http://kornelix.com/ Depends: libc6, libgtk-3-0, binutils, growisofs, genisoimage, gksu Description: Back-up files to DVD or Blue-Ray disc. Full or incremental backup with full or incremental verification. Choose files and directories to include or exclude at any level. Incremental backup updates the same disc from a prior full backup. Recover files using Dkopp, or drag and drop using a file browser. Dkopp is a graphical front end for growisofs and genisoimage. dkopp-6.5/zfuncs.cc0000644000175000017500000142417612343020444012773 0ustar micomico/************************************************************************** zfuncs.cpp collection of Linux and GDK/GTK utility functions Copyright 2006-2014 Michael Cornelison source code URL: http://kornelix.com contact: kornelix@posteo.de This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . These programs originate from the author's web site: http://kornelix.com Other web sites may offer them for download. Modifications could have been made. If you have questions, suggestions or a bug to report: kornelix@posteo.de ***************************************************************************/ // zfuncs.cpp version v.5.8 #include "zfuncs.h" /************************************************************************** Table of Contents ================= System Utility Functions ------------------------ printz printf() with immediate fflush() zpopup_message popup message window, thread safe (no GTK) zappcrash abort with traceback dump to popup window and stdout TRACE trace() and tracedump() implement the TRACE macro catch_signals trap segfault, crash with zappcrash() beroot restart image as root, if password is OK timer functions elapsed time, CPU time, process time functions /proc file functions parse data from various /proc files zsleep sleep for any amount of time (e.g. 0.1 seconds) global_lock reserve and release a global resource (all processes/threads) start_detached_thread simplified method to start a detached thread synch_threads make threads pause and resume together zget_locked, etc. safely access parameters from multiple threads shell_quiet format and run a shell command, return status shell_ack "" + popup error message if error shell_asynch "" + return immediately and query status later comand_output start shell command and read the output as records signalProc pause, resume, or kill a child process runroot run a command or program as root user fgets_trim fgets() with trim of trailing \r \n and optionally blanks samedirk test if two files/directories have the same directory path renamez like rename() but works across file systems parsefile parse filespec into directory, file, extension cpu_profile measure CPU time spent per function or code block pagefaultrate monitor and report own process hard page fault rate String Functions ---------------- comparison macros code legibility improvers strField get delimited substrings from input string strParms parse a string in the form "parm1=1.23, parm2=22 ..." strHash hash string to random number in a range strncpy0 strcpy() with insured null delimiter strnPad add blank padding to specified length strTrim remove trailing blanks strTrim2 remove leading and trailing blanks strCompress remove imbedded blanks strncatv catenate multiple strings with length limit strcmpv compare 1 string to N strings strToUpper convert string to upper case strToLower convert string to lower case repl_1str replace substring within string repl_Nstrs replace multiple substrings within string strncpyx convert string to hex format StripZeros remove trailing zeros (1.23000E+8 >> 1.23E+8) blank_null test string for null pointer, zero length, and all blanks strdupz duplicate string and add space at end clean_escapes replace 2-character escapes ("\n") with the escaped characters UTF-8 functions deal with UTF-8 multibyte character strings Number Conversion and Formatting -------------------------------- convSI string to integer with optional limits check convSD string to double with optional limits check convSF string to float with optional limits check convIS integer to string with returned length convDS double to string with specified digits of precision formatKBMB format a byte count with specified precision and B/KB/MB/GB units Wildcard Functions ------------------ MatchWild match string to wildcard string (multiple * and ?) MatchWildIgnoreCase works like MatchWild() but ignores case SearchWild wildcard file search (multiple * and ? in path or file name) SearchWildIgnoreCase works like SearchWild() but ignores case in file name Search and Sort Functions ------------------------- bsearch binary search of sorted list HeapSort sort list of integer / float / double / records / pointers to records MemSort sort records with multiple keys (data types and sequence) Misc Functions -------------- pvlist_create, etc. functions to manage a list of variable strings rand functions int and double random numbers with improved distributions spline1/2 cubic spline curve fitting function Qtext FIFO queue for text strings, dual thread access Application Admin Functions --------------------------- zinitapp etc. initialize application directory and data files locale_filespec find a locale-dependent installation file (translate-xx.po etc.) showz_userguide display user guide (optional help topic) showz_logfile display application log file showz_textfile show application text file (README, changelog, etc.) showz_html show a local or remote HTML file zmake_menu_launcher create desktop icon / launcher ZTX(), etc. translate GUI and message text strings for non-English locales GTK Utility Functions --------------------- zmainloop do a main loop to process menu events, etc. zthreadcrash crash if called from a thread other than main() wprintx etc. printf() to window at specified row or next row wscroll scroll text window to put line on screen wclear clear a wprintf window wscanf read text from edit window, line at a time wfiledump dump text window to a file wfilesave wfiledump with save-as dialog wprintp print text window to default printer textwidget funcs intercept mouse clicks on text windows, get clicked text get_mouse_device get the screen and mouse device for a widget menus / toolbars simplified GTK menus, toolbars and status bars popup menus create popup menus gmenuz customizable graphic popup menu Vmenu vertical menu/toolbar in vertical packing box zdialog, etc. simplified GTK dialogs write_popup_text write rows of text to a popup window popup_command run a shell command with output in a popup window zmessageACK popup message, printf format, wait for user ACK zmessLogACK same with parallel output to STDOUT zmessageYN popup message, printf format, wait for user Yes / No zmessage_post popup message, printf format, show until killed zdialog_text popup dialog to get 1-2 lines of text input from user zdialog_choose popup dialog to show a message, select a button, return choice poptext_mouse popup message at current mouse position + offset poptext_window popup message at given window position poptext_killnow kill popup message popup_image show an image in a small popup window move_pointer move the mouse pointer within a widget/window zgetfile simplified file chooser dialog print_image_file dialog to print an image file using GTK functions drag_drop_connect connect window drag-drop event to user function get_thumbnail get thumbnail image for given image file zmakecursor make a cursor from an image file (.png .jpg) gdk_pixbuf_rotate rotate a pixbuf through any angle parameters functions to manage a set of numeric parameters C++ Classes ----------- xstring string manipulation (= / + / insert / overlay) Vxstring array of xstrings with auto growth HashTab hash table: add, delete, find, step through Queue queue of xstrings: push, pop first or last Tree store / retrieve data by node names or numbers, any depth ***************************************************************************/ namespace zfuncs { char zappname[20]; // app name/version char zprefix[200], zdatadir[200], zdocdir[200]; // app directories char zicondir[200], zlocalesdir[200], zuserdir[200]; char zlang[8] = "en"; // "lc" or "lc_RC" char JPGquality[4] = "90"; // JPG file save quality int open_popup_windows = 0; // open popup window count pthread_t tid_main = 0; // main thread ID int Nmalloc, Nstrdup, Nfree; // malloc() strdup() and free() calls int zdialog_count = 0; // total zdialogs (new - free) int zdialog_busy = 0; // open zdialogs (run - destroy) PangoFontDescription *monofont = 0; // zdialog widget fonts PangoFontDescription *widgetfont = 0; } /************************************************************************** system-level utility functions ***************************************************************************/ // replacements for malloc(), free(), and strdup() with call counters void *zmalloc(size_t cc) { zfuncs::Nmalloc++; void *pp = malloc(cc); if (! pp) zappcrash("malloc failure, out of memory"); return pp; } void zfree(void *pp) { zfuncs::Nfree++; free(pp); return; } char *zstrdup(cchar *string, int addcc) { zfuncs::Nstrdup++; if (! addcc) return strdup(string); char *pp = (char *) malloc(strlen(string) + addcc); if (! pp) zappcrash("malloc failure, out of memory"); strcpy(pp,string); return pp; } // printf() and flush every output immediately even if stdout is a file void printz(cchar *format, ...) { va_list arglist; va_start(arglist,format); vprintf(format,arglist); va_end(arglist); fflush(stdout); return; } // output a message to a popup window // works like printf() // separate shell process, no GTK // returns immediately and message dies after 5 seconds void zpopup_message(cchar *format, ... ) // v.5.5 { va_list arglist; char message[400], tempfile[30], command[100]; FILE *fid; int cc, err; va_start(arglist,format); // format user message vsnprintf(message,400,format,arglist); va_end(arglist); printz("popup message: \n %s \n",message); // message to log file stdout cc = strlen(message); if (cc < 30) { strcat(message," "); // lengthen short message message[30] = 0; // (no truncate box title line) } sprintf(tempfile,"/tmp/zpopup-%06d",getpid()); // write message to temp file fid = fopen(tempfile,"w"); if (! fid) return; fprintf(fid,"%s",message); fclose(fid); strcpy(command,"xmessage -buttons OK:0 -center -timeout 5 -file "); // create popup with message file strcat(command,tempfile); strcat(command," &"); // return immediately err = system(command); if (err) return; // stop compiler warning return; } /**************************************************************************/ // Write an error message and traceback dump to a file and to a popup window. // Error message works like printf(). // Depends on library program addr2line(). void zappcrash(cchar *format, ... ) // rev. v.5.8 { using namespace zfuncs; static int crash = 0; struct utsname unbuff; va_list arglist; FILE *fid; int ii, err, nstack = 100; char message[300]; void *stacklist[100]; char *arch, **stackents; if (crash++) { printf("zappcrash re-entered \n"); // re-entry or multiple threads crash return; } uname(&unbuff); arch = unbuff.machine; va_start(arglist,format); vsnprintf(message,300,format,arglist); va_end(arglist); printf("*** zappcrash: %s %s %s \n",arch,zappname,message); // output message to stdout nstack = backtrace(stacklist,nstack); // get traceback data (can fail) if (nstack > 100) nstack = 100; stackents = backtrace_symbols(stacklist,nstack); // traceback records in memory for (ii = 0; ii < nstack; ii++) // output traceback records to stdout printf(" %s \n",stackents[ii]); fid = fopen("zappcrash","w"); // open zappcrash file (can hang here) if (! fid) { perror("zappcrash fopen() failure \n"); exit(1); } fprintf(fid,"\n*** zappcrash: %s %s %s \n",arch,zappname,message); fprintf(fid,"*** please send to kornelix@posteo.de *** \n"); for (ii = 0; ii < nstack; ii++) // output traceback records fprintf(fid," %s \n",stackents[ii]); fclose(fid); tracedump(); // output TRACE data v.5.7 err = system("cat zappcrash tracedump > zappcrash2"); // combine zappcrash and tracedump err = system("mv -f zappcrash2 zappcrash"); err = system("xdg-open zappcrash"); // popup zappcrash text file if (err) printz("*** xdg-open failure \n"); exit(1); } /**************************************************************************/ // application initialization function to catch some bad news signals // the signal handler calls zappcrash() to output a traceback dump and exit void catch_signals() { void sighandler(int signal); struct sigaction sigact; sigact.sa_handler = sighandler; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigaction(SIGTERM,&sigact,0); // v.5.6 sigaction(SIGSEGV,&sigact,0); sigaction(SIGILL,&sigact,0); // SIGILL/FPE/BUS added v.5.3 sigaction(SIGFPE,&sigact,0); sigaction(SIGBUS,&sigact,0); return; } // catch segfaults and produce backtrace dumps on-screen void sighandler(int signal) { const char *signame = "unknown"; if (signal == SIGTERM) exit(1); // v.5.6 if (signal == SIGSEGV) signame = "segment fault"; if (signal == SIGILL) signame = "illegal op"; if (signal == SIGFPE) signame = "float exception"; if (signal == SIGBUS) signame = "bus error"; zappcrash("fatal signal %s",signame); return; } /**************************************************************************/ // Implement the TRACE macro. // v.5.7 // Trace program execution by function and source code line number. // tracedump() dumps last 50 uses of TRACE macro, latest first. namespace tracenames { char filebuff[50][100]; // last 50 TRACE calls char funcbuff[50][60]; int linebuff[50]; void *addrbuff[50]; int ftf = 1, ii; }; // Args are source file, source function name, source code line number, // caller address. These all come from the GCC compiler and TRACE macro. void trace(cchar *file, cchar *func, int line, void *addr) { using namespace tracenames; if (ftf) { ftf = 0; for (ii = 0; ii < 50; ii++) { filebuff[ii][99] = 0; funcbuff[ii][39] = 0; linebuff[ii] = 0; addrbuff[ii] = 0; } ii = 0; } if (line == linebuff[ii] && strcmp(func,funcbuff[ii]) == 0) return; // same as last call, don't duplicate if (++ii > 49) ii = 0; strncpy(&filebuff[ii][0],file,99); strncpy(&funcbuff[ii][0],func,39); linebuff[ii] = line; addrbuff[ii] = addr; return; } // dump trace records to STDOUT void tracedump() { using namespace tracenames; FILE *fid; int kk = ii; fid = fopen("tracedump","w"); if (! fid) { perror("tracedump fopen() failure \n"); return; } while (1) { if (linebuff[kk] == 0) break; fprintf(fid, "TRACE %s %s %d %p \n",&filebuff[kk][0], &funcbuff[kk][0],linebuff[kk],addrbuff[kk]); if (--kk == ii) break; } fclose(fid); return; } /**************************************************************************/ // This function will restart the current program with root privileges, // if the correct (sudo) password is given. It does not return. // "argc" and "argv" are passed in the command line. // Use the original argc and argv (optional, may be omitted). // argv[0] is omitted to avoid passing the program name twice. // argv[] parameters are enclosed in quotes to avoid gksu eating them. // bugfix v.5.6 void beroot(int argc, char *argv[]) { int cc1, cc2, ii, err; char command[1000]; if (getuid() == 0) return; // already root strcpy(command,"which gksu > /dev/null 2>&1"); // Debian err = system(command); strcpy(command,"gksu \""); if (err) { strcpy(command,"which beesu > /dev/null 2>&1"); // Fedora, just to be different err = system(command); strcpy(command,"beesu \""); } if (err) { printz("*** cannot find gksu or beesu \n"); abort(); } cc1 = strlen(command); // gksu (or) beesu cc2 = readlink("/proc/self/exe",command+cc1,990); if (cc2 <= 0) { printz("*** cannot get /proc/self/exe \n"); abort(); } command[cc1+cc2] = 0; // gksu or beesu for (ii = 1; ii < argc; ii++) // append command line parameters strncatv(command,990," ",argv[ii],null); strcat(command,"\" &"); // return immediately printz("%s \n",command); err = system(command); exit(0); } /**************************************************************************/ // get time in real seconds (since 2000.01.01 00:00:00) // (microsecond resolution until at least 2030) double get_seconds() { timeval time1; double time2; gettimeofday(&time1,0); time2 = time1.tv_sec + 0.000001 * time1.tv_usec - 946684800.0; // v.5.2 return time2; } /**************************************************************************/ // start a timer or get elapsed time with millisecond resolution. void start_timer(double &time0) { timeval timev; gettimeofday(&timev,0); time0 = timev.tv_sec + 0.000001 * timev.tv_usec; return; } double get_timer(double &time0) { timeval timev; double time; gettimeofday(&timev,0); time = timev.tv_sec + 0.000001 * timev.tv_usec; return time - time0; } /**************************************************************************/ // start a process CPU timer or get elapsed process CPU time // returns seconds with millisecond resolution void start_CPUtimer(double &time0) { time0 = CPUtime(); return; } double get_CPUtimer(double &time0) { return CPUtime() - time0; } /**************************************************************************/ // get elapsed CPU time used by current process // returns seconds with millisecond resolution double CPUtime() { clock_t ctime = clock(); double dtime = ctime / 1000000.0; return dtime; } /**************************************************************************/ // Get elapsed CPU time used by current process, including all threads. // Returns seconds with millisecond resolution. double CPUtime2() { struct rusage usage; double utime, stime; int err; err = getrusage(RUSAGE_SELF,&usage); if (err) return 0.0; utime = usage.ru_utime.tv_sec + 0.000001 * usage.ru_utime.tv_usec; stime = usage.ru_stime.tv_sec + 0.000001 * usage.ru_stime.tv_usec; return utime + stime; } /**************************************************************************/ // get elapsed process time for my process, including threads and child processes. double jobtime() { double jiffy = 1.0 / sysconf(_SC_CLK_TCK); // "jiffy" time slice = 1.0 / HZ char buff[200]; double cpu1, cpu2, cpu3, cpu4; FILE *fid; char *pp; fid = fopen("/proc/self/stat","r"); if (! fid) return 0; pp = fgets(buff,200,fid); fclose(fid); if (! pp) return 0; parseprocrec(pp,14,&cpu1,15,&cpu2,16,&cpu3,17,&cpu4,null); return (cpu1 + cpu2 + cpu3 + cpu4) * jiffy; } /**************************************************************************/ // convert a time_t date/time (e.g. st_mtime from stat() call) // into a compact date/time format "yyyymmddhhmmss" void compact_time(const time_t DT, char *compactDT) // v.5.5 { struct tm *fdt; int year, mon, day, hour, min, sec; fdt = localtime(&DT); year = fdt->tm_year + 1900; mon = fdt->tm_mon + 1; day = fdt->tm_mday; hour = fdt->tm_hour; min = fdt->tm_min; sec = fdt->tm_sec; compactDT[0] = year / 1000 + '0'; compactDT[1] = (year % 1000) / 100 + '0'; compactDT[2] = (year % 100) / 10 + '0'; compactDT[3] = year % 10 + '0'; compactDT[4] = mon / 10 + '0'; compactDT[5] = mon % 10 + '0'; compactDT[6] = day / 10 + '0'; compactDT[7] = day % 10 + '0'; compactDT[8] = hour / 10 + '0'; compactDT[9] = hour % 10 + '0'; compactDT[10] = min / 10 + '0'; compactDT[11] = min % 10 + '0'; compactDT[12] = sec / 10 + '0'; compactDT[13] = sec % 10 + '0'; compactDT[14] = 0; return; } /**************************************************************************/ // Read and parse /proc file with records formatted "parmname xxxxxxx" // Find all requested parameters and return their numeric values int parseprocfile(cchar *pfile, cchar *pname, double *value, ...) // EOL = 0 { FILE *fid; va_list arglist; char buff[1000]; const char *pnames[20]; double *values[20]; int ii, fcc, wanted, found; pnames[0] = pname; // 1st parameter values[0] = value; *value = 0; va_start(arglist,value); for (ii = 1; ii < 20; ii++) // get all parameters { pnames[ii] = va_arg(arglist,char *); if (! pnames[ii]) break; values[ii] = va_arg(arglist,double *); *values[ii] = 0; // initialize to zero } va_end(arglist); if (ii == 20) zappcrash("parseProcFile, too many fields"); wanted = ii; found = 0; fid = fopen(pfile,"r"); // open /proc/xxx file if (! fid) return 0; while ((fgets(buff,999,fid))) // read record, "parmname nnnnn" { for (ii = 0; ii < wanted; ii++) { // look for my fields fcc = strlen(pnames[ii]); if (strnEqu(buff,pnames[ii],fcc)) { *values[ii] = atof(buff+fcc); // return value found++; break; } } if (found == wanted) break; // stop when all found } fclose(fid); return found; } // Parse /proc record of the type "xxx xxxxx xxxxx xxxxxxxx xxx" // Return numeric values for requested fields (starting with 1) int parseprocrec(char *prec, int field, double *value, ...) // EOL = 0 { va_list arglist; int xfield = 1, found = 0; va_start(arglist,value); while (*prec == ' ') prec++; // skip leading blanks while (field > 0) { while (xfield < field) // skip to next wanted field { prec = strchr(prec,' '); // find next blank if (! prec) break; while (*prec == ' ') prec++; // skip multiple blanks xfield++; } if (! prec) break; *value = atof(prec); // convert, return double found++; field = va_arg(arglist,int); // next field number value = va_arg(arglist,double *); // next output double * } while (field > 0) { *value = 0; // zero values not found field = va_arg(arglist,int); value = va_arg(arglist,double *); } va_end(arglist); return found; } /**************************************************************************/ // sleep for specified time in seconds (double) // signals can cause early return void zsleep(double dsecs) { unsigned isecs, nsecs; timespec tsecs; if (dsecs == 0.0) return; isecs = unsigned(dsecs); nsecs = unsigned(1000000000.0 * (dsecs - isecs)); tsecs.tv_sec = isecs; tsecs.tv_nsec = nsecs; nanosleep(&tsecs,null); return; } /**************************************************************************/ // Lock or unlock a multi-process multi-thread resource. // Only one process/thread may posess a given lock. // A reboot or process exit or crash releases the lock. // fd = global_lock(lockfile) returns fd > 0 if success, -1 otherwise. int global_lock(cchar *lockfile) // v.5.2 { int err, fd; fd = open(lockfile,O_RDWR|O_CREAT,0666); // open or create the lock file if (fd < 0) { printz("*** global_lock(), %s \n",strerror(errno)); return -1; } err = flock(fd,LOCK_EX|LOCK_NB); // request exclusive non-blocking lock if (err) { close(fd); return -1; } return fd + 1; // return value is >= 1 } int global_unlock(int fd, cchar *lockfile) { int err = close(fd-1); remove(lockfile); if (err < 0) return -1; else return 1; } /**************************************************************************/ // Start a detached thread using a simplified protocol. // Will not make a zombie out of the calling thread if it exits // without checking the status of the created thread. // Thread should exit with pthread_exit(0); void start_detached_thread(void * threadfunc(void *), void * arg) { pthread_t ptid; pthread_attr_t ptattr; int pterr; pthread_attr_init(&ptattr); pthread_attr_setdetachstate(&ptattr,PTHREAD_CREATE_DETACHED); pterr = pthread_create(&ptid,&ptattr,threadfunc,arg); if (pterr) zappcrash("start_detached_thread() failure"); return; } /**************************************************************************/ // Synchronize execution of multiple threads. // Simultaneously resume NT calling threads. // from main(): synch_threads(NT) /* setup to synch NT threads */ // from each thread: synch_threads() /* suspend, resume simultaneously */ // // Each calling thread will suspend execution until all threads have suspended, // then they will all resume execution at the same time. If NT is greater than // the number of calling threads, the threads will never resume. void synch_threads(int NT) { static pthread_barrier_t barrier; static int bflag = 0; if (NT) { // main(), initialize if (bflag) pthread_barrier_destroy(&barrier); pthread_barrier_init(&barrier,null,NT); bflag = 1; return; } pthread_barrier_wait(&barrier); // thread(), wait for NT threads return; // unblock } /**************************************************************************/ // Safely access and update parameters from multiple threads. // A mutex lock is used to insure one thread at a time has access to the parameter. // Many parameters can be used but there is only one mutex lock. mutex_tp zget_lock = PTHREAD_MUTEX_INITIALIZER; int zget_locked(int ¶m) // lock and return parameter { mutex_lock(&zget_lock); return param; } void zput_locked(int ¶m, int value) // set and unlock parameter { param = value; mutex_unlock(&zget_lock); return; } int zadd_locked(int ¶m, int incr) // lock, increment, unlock, return { int retval; mutex_lock(&zget_lock); retval = param + incr; param = retval; mutex_unlock(&zget_lock); return retval; } /**************************************************************************/ // Format and run a shell command. // Print a status message to stdout if there is an error. // returns 0 if OK, +N if error int shell_quiet(cchar *command, ...) // v.5.5 { char *cbuff; va_list arglist; int cc, err; cc = strlen(command) + 1000; cbuff = (char *) zmalloc(cc+1); va_start(arglist,command); // format command vsnprintf(cbuff,cc,command,arglist); va_end(arglist); err = system(cbuff); if (! err) { zfree(cbuff); return 0; } err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 if (strnEqu(command,"diff",4)) { zfree(cbuff); return err; } // no diagnostic for these if (strnEqu(command,"cmp",3)) { zfree(cbuff); return err; } if (cc > 200) cbuff[200] = 0; if (err == 127) // special case v.5.8 printz("%s \n *** %s \n",cbuff,"command not found"); else printz("%s \n *** %s \n",cbuff,wstrerror(err)); // log error message zfree(cbuff); return err; } // Format and run a shell command. // Print a status message to stdout if there is an error. // Also pop-up an error message window if error. // Thread safe: GTK is not used. // returns 0 if OK, +N if error int shell_ack(cchar *command, ...) // v.5.5 { char *cbuff; va_list arglist; int cc, err; cc = strlen(command) + 1000; cbuff = (char *) zmalloc(cc+1); va_start(arglist,command); // format command vsnprintf(cbuff,cc,command,arglist); va_end(arglist); printz("shell: %s \n",cbuff); // v.5.6 err = system(cbuff); if (! err) { zfree(cbuff); return 0; } err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 if (strnEqu(command,"diff",4)) { zfree(cbuff); return err; } // no diagnostic for these if (strnEqu(command,"cmp",3)) { zfree(cbuff); return err; } if (cc > 200) cbuff[200] = 0; if (err == 127) { printz("%s \n *** %s \n",cbuff,"command not found"); // special case v.5.8 zmessageACK(null,0,"%s \n %s",cbuff,"command not found"); } else { printz("%s \n *** %s \n",cbuff,wstrerror(err)); // log error message zmessageACK(null,0,"%s \n %s",cbuff,wstrerror(err)); // popup error message } zfree(cbuff); return err; } /**************************************************************************/ // Start a shell command from a new thread and return immediately. // The thread waits for the shell command and gets its status. // If there is an error, it is logged to STDOUT. // The calling process can get the status asynchronously. // The command format works like printf(). // // handle = shell_asynch(command, ...) // Start the command and return a reference handle. // // err = shell_asynch_status(handle) // Return the command status for the given handle: // -1 = busy, 0 = complete OK, +N = error (use wstrerror(err)). // // The status MUST be queried for allocated space to be freed. namespace shell_asynch_names { char *command[10]; int status[10]; mutex_tp mlock = PTHREAD_MUTEX_INITIALIZER; } int shell_asynch(cchar *Fcommand, ...) // v.5.5 { using namespace shell_asynch_names; void * shell_asynch_thread(void *); va_list arglist; static int ii; mutex_lock(&mlock); // block other callers for (ii = 0; ii < 10; ii++) if (command[ii] == 0) break; if (ii == 10) zappcrash("shell_asynch > 10 calls"); command[ii] = (char *) zmalloc(2000); // allocate memory va_start(arglist,Fcommand); // format command vsnprintf(command[ii],2000,Fcommand,arglist); va_end(arglist); start_detached_thread(shell_asynch_thread,&ii); // pass command to thread status[ii] = -1; // status = busy return ii; // return handle } void * shell_asynch_thread(void *arg) // thread function { using namespace shell_asynch_names; int err; int ii = *((int *) arg); // capture handle mutex_unlock(&mlock); // unblock other callers err = system(command[ii]); // start command, wait until done if (! err) { status[ii] = 0; return 0; } err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 if (err == 127) printz("%s \n *** %s \n",command[ii],"command not found"); // special case v.5.8 else printz("%s \n *** %s \n",command[ii],wstrerror(err)); // log error message status[ii] = err; // set status for caller return 0; } int shell_asynch_status(int handle) // get command status { using namespace shell_asynch_names; int ii = handle; if (status[ii] == -1) return -1; // return busy status zfree(command[ii]); // free memory command[ii] = 0; return status[ii]; // return completion status } /************************************************************************** Run a shell command and get its outputs one record at a time. The outputs are returned one record at a time, until a NULL is returned, indicating the command has finished and has exited. The new line character is removed from the returned output records. Use contx = 0 to start a new command. Do not change the returned value. Up to 9 commands can run in parallel, with contx values 1-9. To get the command exit status: status = command_status(contx). If the command is still busy, -1 is returned. To kill a command before output is complete: command_kill(contx); ***/ FILE * CO_contx[10] = { 0,0,0,0,0,0,0,0,0,0 }; int CO_status[10]; char * command_output(int &contx, cchar *command, ...) // simplify, allow parallel usage { FILE *fid; va_list arglist; char buff[10000], *prec; if (contx == 0) // start new command { for (contx = 1; contx < 10; contx++) if (CO_contx[contx] == 0) break; if (contx == 10) { printz("command_output(), parallel usage > 9 \n"); // v.5.6 return 0; } va_start(arglist,command); // format command vsnprintf(buff,9999,command,arglist); va_end(arglist); fid = popen(buff,"r"); // execute command, output to FID if (fid == 0) { CO_status[contx] = errno; // failed to start printz("command_output: %s\n %s\n",buff,strerror(errno)); // v.5.5 return 0; } CO_contx[contx] = fid + 1000; CO_status[contx] = -1; // mark context busy } fid = CO_contx[contx] - 1000; prec = fgets_trim(buff,9999,fid,1); // next output, less trailing \n if (prec) return zstrdup(prec); // return output to caller CO_status[contx] = pclose(fid); // EOF, set status CO_contx[contx] = 0; // mark context free return 0; } int command_status(int contx) // get command exit status { int err = CO_status[contx]; return WEXITSTATUS(err); // special BS for subprocess } int command_kill(int contx) // kill output before completion v.5.5 { FILE *fid; if (! CO_contx[contx]) return 0; // context already closed v.5.8 fid = CO_contx[contx] - 1000; CO_status[contx] = pclose(fid); // close context and set status CO_contx[contx] = 0; // mark context free return 0; } /**************************************************************************/ // Signal a running subprocess by name (name of executable or shell command). // Signal is "pause", "resume" or "kill". If process is paused, kill may not work, // so issue resume first if process is paused. int signalProc(cchar *pname, cchar *signal) { pid_t pid; FILE *fid; char buff[100], *pp; int err, nsignal = 0; sprintf(buff,"ps -C %s h o pid",pname); fid = popen(buff,"r"); // popen() instead of system() if (! fid) return 2; pp = fgets(buff,100,fid); pclose(fid); if (! pp) return 4; pid = atoi(buff); if (! pid) return 5; if (strEqu(signal,"pause")) nsignal = SIGSTOP; if (strEqu(signal,"resume")) nsignal = SIGCONT; if (strEqu(signal,"kill")) nsignal = SIGKILL; err = kill(pid,nsignal); return err; } /**************************************************************************/ // run a command or program as root user // sucomm: root user access command, "su" or "sudo" // command: shell command or filespec of the program to start // returns 0 if successfully started, else returns an error code int runroot(cchar *sucomm, cchar *command) { char xtcommand[500]; int err; if (strcmp(sucomm,"sudo") == 0) { snprintf(xtcommand,499,"xterm -geometry 40x3 -e sudo -S %s",command); err = system(xtcommand); if (err) err = WEXITSTATUS(err); // special BS for subprocesses v.5.8 return err; } if (strcmp(sucomm,"su") == 0) { snprintf(xtcommand,499,"xterm -geometry 40x3 -e su -c %s",command); err = system(xtcommand); if (err) err = WEXITSTATUS(err); return err; } return -1; } /**************************************************************************/ // fgets() with additional feature: trailing \n \r are removed. // optional bf flag: true if trailing blanks are to be removed. // trailing null character is assured. char * fgets_trim(char *buff, int maxcc, FILE *fid, int bf) { int cc; char *pp; pp = fgets(buff,maxcc,fid); if (! pp) return pp; cc = strlen(buff); if (bf) while (cc && buff[cc-1] > 0 && buff[cc-1] <= ' ') --cc; else while (cc && buff[cc-1] > 0 && buff[cc-1] < ' ') --cc; buff[cc] = 0; return pp; } /**************************************************************************/ // return true if both files are in the same directory // both files may be files or directories int samedirk(cchar *file1, cchar *file2) { int cc1, cc2; cchar *pp1, *pp2; if (! file1 || ! file2) return 0; pp1 = strrchr(file1,'/'); pp2 = strrchr(file2,'/'); if (! pp1 && ! pp2) return 1; if (pp1 && ! pp2) return 0; if (! pp1 && pp2) return 0; cc1 = pp1 - file1; cc2 = pp2 - file2; if (cc1 != cc2) return 0; if (strncmp(file1,file2,cc1) == 0) return 1; return 0; } /************************************************************************** Parse a pathname (filespec) and return its components. Returned strings are allocated in static memory (no zfree needed). Missing components are returned as null pointers. input ppath outputs /name1/name2/ directory /name1/name2/ with no file /name1/name2 directory /name1/name2/ if name2 a directory, otherwise directory /name1/ and file name2 /name1/name2.xxx if .xxx < 8 chars, returns file name2 and ext .xxx, otherwise returns file name2.xxx and no ext returns 0 if no error, else 1 ***************************************************************************/ int parsefile(cchar *ppath, char **pdirk, char **pfile, char **pext) { struct stat statb; static char dirk[1000], file[200], ext[8]; char *pp; int err, cc1, cc2; *pdirk = *pfile = *pext = null; cc1 = strlen(ppath); if (cc1 > 999) return 1; // ppath too long strcpy(dirk,ppath); *pdirk = dirk; err = stat(dirk,&statb); // have directory only if (! err && S_ISDIR(statb.st_mode)) return 0; pp = (char *) strrchr(dirk,'/'); if (! pp) return 1; // illegal pp++; cc2 = pp - dirk; if (cc2 < 2 || cc2 == cc1) return 0; // have /xxxx or /xxxx/ if (strlen(pp) > 199) return 1; // filename too long strcpy(file,pp); // file part *pfile = file; *pp = 0; // remove from dirk part pp = (char *) strrchr(file,'.'); if (! pp || strlen(pp) > 7) return 0; // file part, no .ext strcpy(ext,pp); // .ext part *pext = ext; *pp = 0; // remove from file part return 0; } /**************************************************************************/ // Move a source file to a destination file and delete the source file. // Equivalent to rename(), but the two files MAY be on different file systems. // Pathnames must be absolute (start with '/'). // Returns 0 if OK, +N if not. int renamez(cchar *file1, cchar *file2) // v.5.8 { char *pp1, *pp2; int err, Frename = 0; if (*file1 != '/' || *file2 != '/') return 1; // not absolute pathnames pp1 = strchr((char *) file1+1,'/'); pp2 = strchr((char *) file2+1,'/'); if (! pp1 || ! pp2) return 2; *pp1 = *pp2 = 0; if (strEqu(file1,file2)) Frename = 1; *pp1 = *pp2 = '/'; if (Frename) { err = rename(file1,file2); if (err) return errno; else return 0; } err = shell_quiet("mv -f %s %s",file1,file2); return err; } /************************************************************************** utility to measure CPU time spent in various functions or code blocks cpu_profile_init() initialize at start of test cpu_profile_enter(fnum) at entry to a function cpu_profile_exit(fnum) at exit from a function cpu_profile_report() report CPU time per function Methodology: cpu_profile_init() starts a thread that suspends and runs every 1 millisecond and updates a timer. cpu_profile_enter() and cpu_profile_exit() accumulate the time difference between entry and exit of code being measured. This may be zero because of the long interval between timer updates. Accuracy comes from statistical sampling over many seconds, so that if the time spent in a monitored function is significant, it will be accounted for. The accuracy is better than 1% as long as the measured function or code block consumes a second or more of CPU time during the measurement period. The "fnum" argument (1-99) designates the function or code block being measured. cpu_profile_report() stops the timer thread and reports time consumed per function, using the "fnum" tags in the report. The functions cpu_profile_enter() and cpu_profile_exit() subtract the timer difference and add to a counter per fnum, so the added overhead is insignificant. They are inline functions defined as follows: enter: cpu_profile_timer = cpu_profile_elapsed; exit: cpu_profile_table[fnum] += cpu_profile_elapsed - cpu_profile_timer; ***************************************************************************/ volatile double cpu_profile_table[100]; volatile double cpu_profile_timer; volatile double cpu_profile_elapsed; volatile int cpu_profile_kill = 0; void cpu_profile_init() { void * cpu_profile_timekeeper(void *); for (int ii = 0; ii < 99; ii++) cpu_profile_table[ii] = 0; cpu_profile_elapsed = 0; start_detached_thread(cpu_profile_timekeeper,null); } void cpu_profile_report() { cpu_profile_kill++; printz("elapsed: %.2f \n",cpu_profile_elapsed); for (int ii = 0; ii < 100; ii++) { double dtime = cpu_profile_table[ii]; if (dtime) printz("cpu profile func: %d time: %.2f \n",ii,dtime); } } void * cpu_profile_timekeeper(void *) { timeval time0, time1; gettimeofday(&time0,0); while (true) { gettimeofday(&time1,0); cpu_profile_elapsed = time1.tv_sec - time0.tv_sec + 0.000001 * (time1.tv_usec - time0.tv_usec); zsleep(0.001); if (cpu_profile_kill) break; } cpu_profile_kill = 0; pthread_exit(0); } /**************************************************************************/ // Returns hard page fault rate in faults/second. // First call starts a thread that runs every 2 seconds and keeps a // weighted average of hard fault rate for the last few intervals. namespace pagefaultrate_names { int ftf = 1; int samples = 0; int faultrate = 0; double time1, time2; void * threadfunc(void *); } int pagefaultrate() // v.5.8 { using namespace pagefaultrate_names; if (ftf) { ftf = 0; start_detached_thread(threadfunc,0); time1 = get_seconds(); } return faultrate; } void * pagefaultrate_names::threadfunc(void *) { using namespace pagefaultrate_names; FILE *fid; char *pp, buff[200]; double pfs1, pfs2, fps, elaps; while (true) { sleep(2); time2 = get_seconds(); elaps = time2 - time1; time1 = time2; fid = fopen("/proc/self/stat","r"); if (! fid) break; pp = fgets(buff,200,fid); fclose(fid); if (! pp) break; pp = strchr(pp,')'); // closing ')' after (short) filename if (pp) parseprocrec(pp+1,10,&pfs1,11,&pfs2,null); fps = (pfs1 + pfs2) / elaps; faultrate = 0.7 * faultrate + 0.3 * fps; } printz("pagefaultrate() failure \n"); pthread_exit(0); } /************************************************************************** strField() cchar * strField(cchar *string, cchar *delim, int Nth) Get the Nth field in input string, which contains at least N fields delimited by the character(s) in delim (e.g. blank, comma). Returns a pointer to the found field (actually a pointer to a copy of the found field, with a null terminator appended). If a delimiter is immediately followed by another delimiter, it is considered a field with zero length, and the string "" is returned. Leading blanks in a field are omitted from the returned field. A field with only blanks is returned as a single blank. The last field may be terminated by null or a delimiter. Characters within quotes (") are treated as data within a field, i.e. blanks and delimiters are not processed as such. The quotes are removed from the returned field. If there are less than N fields, a null pointer is returned. The last 100 fields are saved and recycled in a circular manner. The caller does not have to free memory. If more memory depth is needed, caller must copy the returned data elsewhere. The output string may be modified if the length is not increased. Fields within the input string must not exceed 2000 characters. Example: input string: ,a,bb, cc, ,dd"ee,ff"ggg, (first and last characters are comma) delimiter: comma Nth returned string 1: (null string) 2: a 3: bb 4: cc 5: (one blank) 6: ddee,ffggg 7: (null pointer >> no more fields) ***************************************************************************/ cchar * strField(cchar *string, cchar *delim, int Nth) { static int ftf = 1, nret = 0; static char *retf[100]; char *pf1, pf2[2000]; // 2000 limit v.5.2 cchar quote = '"'; int ii, nf, fcc = 0; static char blankstring[2], nullstring[1]; if (! string) return 0; // bad call v.5.3 if (Nth < 1) return 0; if (ftf) // overall first call { ftf = 0; for (ii = 0; ii < 100; ii++) retf[ii] = 0; strcpy(blankstring," "); *nullstring = 0; } pf1 = (char *) string - 1; // start parse nf = 0; while (nf < Nth) { pf1++; // start field nf++; fcc = 0; while (*pf1 == ' ') pf1++; // skip leading blanks while (true) { if (*pf1 == quote) { // pass chars between single quotes pf1++; // (but without the quotes) while (*pf1 && *pf1 != quote) pf2[fcc++] = *pf1++; if (*pf1 == quote) pf1++; } else if (strchr(delim,*pf1) || *pf1 == 0) break; // found delimiter or null else pf2[fcc++] = *pf1++; // pass normal character if (fcc > 1999) zappcrash("strField() too long"); // v.5.2 } if (*pf1 == 0) break; } if (nf < Nth) return 0; // no Nth field if (fcc == 0) { // empty field if (*string && pf1[-1] == ' ' && ! strchr(delim,' ')) // all blanks and non-blank delimeter, return blankstring; // return one blank if (*pf1 == 0) return 0; // no field return nullstring; // return null string } if (++nret == 100) nret = 0; // use next return slot if (retf[nret]) zfree(retf[nret]); retf[nret] = (char *) zmalloc(fcc+2); strncpy0(retf[nret],pf2,fcc+1); return retf[nret]; } cchar * strField(cchar *string, cchar delim, int Nth) // alternative with one delimiter { char delims[2] = "x"; *delims = delim; return strField(string,delims,Nth); } /************************************************************************** stat = strParms(begin, input, pname, maxcc, pval) Parse an input string with parameter names and values: "pname1=pval1 | pname2 | pname3=pval3 | pname4 ..." begin int & must be 1 to start new string, is modified input cchar * input string pname char * output parameter name maxcc int max. length for pname, including null pval double & output parameter value stat int status: 0=OK, -1=EOL, 1=parse error Each call returns the next pname and pval. A pname with no pval is assigned a value of 1 (present). Input format: pname1 | pname2=pval2 | pname3 ... null Leading blanks are ignored, and pnames may have imbedded blanks. pvals must convert to double using convSD (accepts decimal point or comma) ***/ int strParms(int &begin, cchar *input, char *pname, int maxcc, double &pval) { static int ii, beginx = 3579246; cchar *pnamex, *delim; int cc, err; if (begin == 1) { // start new string begin = ++beginx; ii = 0; } if (begin != beginx) zappcrash("strParms call error"); // thread safe, not reentrant *pname = 0; // initz. outputs to nothing pval = 0; while (input[ii] == ' ') ii++; // skip leading blanks if (input[ii] == 0) return -1; // no more data pnamex = input + ii; // next pname for (cc = 0; ; cc++) { // look for delimiter if (pnamex[cc] == '=') break; if (pnamex[cc] == '|') break; if (pnamex[cc] == 0) break; } if (cc == 0) return 1; // err: 2 delimiters if (cc >= maxcc) return 1; // err: pname too big strncpy0(pname,pnamex,cc+1); // pname >> caller strTrim(pname); // remove trailing blanks if (pnamex[cc] == 0) { // pname + null ii += cc; // position for next call pval = 1.0; // pval = 1 >> caller return 0; } if (pnamex[cc] == '|') { // pname + | ii += cc + 1; // position for next call pval = 1.0; // pval = 1 >> caller return 0; } ii += cc + 1; // pname = pval err = convSD(input + ii, pval, &delim); // parse pval (was strtod() if (err > 1) return 1; while (*delim == ' ') delim++; // skip poss. trailing blanks if (*delim && *delim != '|') return 1; // err: delimiter not | or null ii = delim - input; if (*delim) ii++; // position for next call return 0; } /**************************************************************************/ // Produce random value from hashed input string. // Output range is 0 to max-1. // Benchmark: 0.036 usec for 20 char. string 3.3 GHz Core i5 int strHash(cchar *string, int max) { uint hash = 1; uchar byte; while ((byte = *string++)) { hash *= byte; hash = hash ^ (hash >> 7); hash = hash & 0x00FFFFFF; } hash = hash % max; return hash; } /**************************************************************************/ // Copy string with specified max. length (including null terminator). // truncate if needed. null terminator is always supplied. // Returns 0 if no truncation, 1 if input string was truncated to fit. int strncpy0(char *dest, cchar *source, uint cc) { strncpy(dest,source,cc); dest[cc-1] = 0; if (strlen(source) >= cc) return 1; // truncated else return 0; } /**************************************************************************/ // Copy string with blank pad to specified length. No null is added. void strnPad(char *dest, cchar *source, int cc) { strncpy(dest,source,cc); int ii = strlen(source); for (int jj = ii; jj < cc; jj++) dest[jj] = ' '; } /**************************************************************************/ // Remove trailing blanks from a string. Returns remaining length. int strTrim(char *dest, cchar *source) { if (dest != source) strcpy(dest,source); return strTrim(dest); } int strTrim(char *dest) { int ii = strlen(dest); while (ii && (dest[ii-1] == ' ')) dest[--ii] = 0; return ii; } /**************************************************************************/ // Remove leading and trailing blanks from a string. // Returns remaining length, possibly zero. int strTrim2(char *dest, cchar *source) { cchar *pp1, *pp2; int cc; pp1 = source; pp2 = source + strlen(source) - 1; while (*pp1 == ' ') pp1++; while (*pp2 == ' ' && pp2 > pp1) pp2--; cc = pp2 - pp1 + 1; memmove(dest,pp1,cc); dest[cc] = 0; return cc; } int strTrim2(char *string) { return strTrim2(string,(cchar *) string); } /**************************************************************************/ // Remove all blanks from a string. Returns remaining length. int strCompress(char *dest, cchar *source) { if (dest != source) strcpy(dest,source); return strCompress(dest); } int strCompress(char *string) { int ii, jj; for (ii = jj = 0; string[ii]; ii++) { if (string[ii] != ' ') { string[jj] = string[ii]; jj++; } } string[jj] = 0; return jj; } /**************************************************************************/ // Concatenate multiple strings, staying within a specified overall length. // The destination string is also the first source string. // Null marks the end of the source strings (omission --> crash). // Output is truncated to fit within the specified length. // A final null is assured and is included in the length. // Returns 0 if OK, 1 if truncation was needed. int strncatv(char *dest, int maxcc, cchar *source, ...) { cchar *ps; va_list arglist; maxcc = maxcc - strlen(dest) - 1; if (maxcc < 0) return 1; va_start(arglist,source); ps = source; while (ps) { strncat(dest,ps,maxcc); maxcc = maxcc - strlen(ps); if (maxcc < 0) break; ps = va_arg(arglist,cchar *); } va_end(arglist); if (maxcc < 0) return 1; return 0; } /**************************************************************************/ // Match 1st string to N additional strings. // Return matching string number 1 to N or 0 if no match. // Supply a null argument for end of list. int strcmpv(cchar *string, ...) { int match = 0; char *stringN; va_list arglist; va_start(arglist,string); while (1) { stringN = va_arg(arglist, char *); if (stringN == null) { va_end(arglist); return 0; } match++; if (strcmp(string,stringN) == 0) { va_end(arglist); return match; } } } /**************************************************************************/ // convert string to upper case void strToUpper(char *string) { int ii; char jj; const int delta = 'A' - 'a'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'a') && (jj <= 'z')) string[ii] += delta; } void strToUpper(char *dest, cchar *source) { strcpy(dest,source); strToUpper(dest); } /**************************************************************************/ // convert string to lower case void strToLower(char *string) { int ii; char jj; const int delta = 'a' - 'A'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'A') && (jj <= 'Z')) string[ii] += delta; } void strToLower(char *dest, cchar *source) { strcpy(dest,source); strToLower(dest); } /**************************************************************************/ // Copy string strin to strout, replacing every occurrence // of the substring ssin with the substring ssout. // Returns the count of replacements, if any. // Replacement strings may be longer or shorter or have zero length. int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout) { int ccc, cc1, cc2, nfound; cchar *ppp; cc1 = strlen(ssin); cc2 = strlen(ssout); nfound = 0; while ((ppp = strstr(strin,ssin))) { nfound++; ccc = ppp - strin; strncpy(strout,strin,ccc); strout += ccc; strin += ccc; strncpy(strout,ssout,cc2); strin += cc1; strout += cc2; } strcpy(strout,strin); return nfound; } /**************************************************************************/ // Copy string strin to strout, replacing multiple substrings with replacement strings. // Multiple pairs of string arguments follow strout, a substring and a replacement string. // Last pair of string arguments must be followed by a null argument. // Returns the count of replacements, if any. // Replacement strings may be longer or shorter or have zero length. int repl_Nstrs(cchar *strin, char *strout, ...) { va_list arglist; cchar *ssin, *ssout; char ftemp[maxfcc]; int ftf, nfound; ftf = 1; nfound = 0; va_start(arglist,strout); while (true) { ssin = va_arg(arglist, char *); if (! ssin) break; ssout = va_arg(arglist, char *); if (ftf) { ftf = 0; nfound += repl_1str(strin,strout,ssin,ssout); } else { strcpy(ftemp,strout); nfound += repl_1str(ftemp,strout,ssin,ssout); } } va_end(arglist); return nfound; } /**************************************************************************/ // Copy and convert string to hex string. // Each input character 'A' >> 3 output characters "41 " void strncpyx(char *out, cchar *in, int ccin) { int ii, jj, c1, c2; char cx[] = "0123456789ABCDEF"; if (! ccin) ccin = strlen(in); for (ii = 0, jj = 0; ii < ccin; ii++, jj += 3) { c1 = (uchar) in[ii] >> 4; c2 = in[ii] & 15; out[jj] = cx[c1]; out[jj+1] = cx[c2]; out[jj+2] = ' '; } out[jj] = 0; return; } /**************************************************************************/ // Strip trailing zeros from ascii floating numbers // (e.g. 1.230000e+02 --> 1.23e+02) void StripZeros(char *pNum) { int ii, cc; int pp, k1, k2; char work[20]; cc = strlen(pNum); if (cc >= 20) return; for (ii = 0; ii < cc; ii++) { if (pNum[ii] == '.') { pp = ii; k1 = k2 = 0; for (++ii; ii < cc; ii++) { if (pNum[ii] == '0') { if (! k1) k1 = k2 = ii; else k2 = ii; continue; } if ((pNum[ii] >= '1') && (pNum[ii] <= '9')) { k1 = 0; continue; } break; } if (! k1) return; if (k1 == pp + 1) k1++; if (k2 < k1) return; strcpy(work,pNum); strcpy(work+k1,pNum+k2+1); strcpy(pNum,work); return; } } } /**************************************************************************/ // test for blank/null string // Returns status depending on input string: // 0 not a blank or null string // 1 argument string is NULL // 2 string has zero length (*string == 0) // 3 string is all blanks int blank_null(cchar *string) { if (! string) return 1; // null string if (! *string) return 2; // zero length string int cc = strlen(string); for (int ii = 0; ii < cc; ii++) if (string[ii] != ' ') return 0; // non-blank string return 3; // blank string } /**************************************************************************/ // make a copy of a string in heap memory and allocate more space // returned string is subject for zfree(); char * strdupz(cchar *string, int more) { char *pp = (char *) zmalloc(strlen(string)+1+more); strcpy(pp,string); return pp; } /**************************************************************************/ // clean \x escape sequences and replace them with the escaped character // \n >> newline \" >> doublequote \\ >> backslash etc. // see $ man ascii for the complete list int clean_escapes(char *string) { char *pp1 = string, *pp2 = string, *pp; char char1; char escapes[] = "abtnvfr"; int count = 0; while (true) { char1 = *pp1++; if (char1 == 0) { *pp2 = 0; return count; } else if (char1 == '\\') { char1 = *pp1++; pp = strchr(escapes,char1); if (pp) char1 = pp - escapes + 7; count++; } *pp2++ = char1; } } /**************************************************************************/ // Compute the graphic character count for a UTF8 character string. // Depends on UTF8 rules: // - ascii characters are positive (0x00 to 0x7F) // - 1st byte of multibyte sequence is negative (0xC0 to 0xFD) // - subsequent bytes are negative and < 0xC0 (0x80 to 0xBF) int utf8len(cchar *utf8string) { int ii, cc; char xlimit = 0xC0; for (ii = cc = 0; utf8string[ii]; ii++) { if (utf8string[ii] < 0) // multibyte character while (utf8string[ii+1] < xlimit) ii++; // skip extra bytes cc++; } return cc; } /**************************************************************************/ // Extract a UTF8 substring with a specified count of graphic characters. // utf8in input UTF8 string // utf8out output UTF8 string, which must be long enough // pos initial graphic character position to get (0 = first) // cc max. count of graphic characters to get // returns number of graphic characters extracted, <= cc // Output string is null terminated after last extracted character. int utf8substring(char *utf8out, cchar *utf8in, int pos, int cc) { int ii, jj, kk, posx, ccx; char xlimit = 0xC0; for (ii = posx = 0; posx < pos && utf8in[ii]; ii++) { if (utf8in[ii] < 0) while (utf8in[ii+1] < xlimit) ii++; posx++; } jj = ii; for (ccx = 0; ccx < cc && utf8in[jj]; jj++) { if (utf8in[jj] < 0) while (utf8in[jj+1] < xlimit) jj++; ccx++; } kk = jj - ii; strncpy(utf8out,utf8in+ii,kk); utf8out[kk] = 0; return ccx; } /**************************************************************************/ // check a string for valid utf8 encoding // returns: 0 = OK, 1 = bad string int utf8_check(cchar *string) { cchar *pp; unsigned char ch1, ch2, nch; for (pp = string; *pp; pp++) { ch1 = *pp; if (ch1 < 0x7F) continue; if (ch1 > 0xBF && ch1 < 0xE0) nch = 1; else if (ch1 < 0xF0) nch = 2; else if (ch1 < 0xF8) nch = 3; else if (ch1 < 0xFC) nch = 4; else if (ch1 < 0xFE) nch = 5; else return 1; while (nch) { pp++; ch2 = *pp; if (ch2 < 0x80 || ch2 > 0xBF) return 1; nch--; } } return 0; } /**************************************************************************/ // Find the Nth graphic character position within a UTF8 string // utf8in input UTF8 string // Nth graphic character position, zero based // returns starting character (byte) position of Nth graphic character // returns -1 if Nth is beyond the string length // v.5.5 int utf8_position(cchar *utf8in, int Nth) { int ii, posx; char xlimit = 0xC0; for (ii = posx = 0; posx < Nth && utf8in[ii]; ii++) { if (utf8in[ii] < 0) // multi-byte character while (utf8in[ii+1] && utf8in[ii+1] < xlimit) ii++; // traverse member bytes posx++; } if (utf8in[ii]) return ii; return -1; // v.5.5 } /************************************************************************** Conversion Utilities convSI(string, inum, delim) string to int convSI(string, inum, low, high, delim) string to int with range check convSD(string, dnum, delim) string to double convSD(string, dnum, low, high, delim) string to double with range check convSF(string, fnum, delim) string to float convSF(string, fnum, low, high, delim) string to float with range check convIS(inum, string, cc) int to string with returned cc convDS(fnum, digits, string, cc) double to string with specified digits of precision and returned cc string input (cchar *) or output (char *) inum input (int) or output (int &) dnum input (double) or output (double &) delim optional returned delimiter (null or cchar **) low, high input range check (int or double) cc output string length (int &) digits input digits of precision (int) to be used for output string NOTE: decimal point may be comma or period. 1000's separators must NOT be present. convIS and convDS also return the length cc of the string output. convDS accepts same formats as atof. Decimal point can be comma or period. convDS will use whatever format (f/e) gives the shortest result. Outputs like "e03" or "e+03" will be shortened to "e3". function status returned: 0 normal conversion, no invalid digits, blank/null termination 1 successful converstion, but trailing non-numeric found 2 conversion OK, but outside specified limits 3 null or blank string, converted to zero 4 conversion error, invalid data in string overlapping statuses have following precedence: 4 3 2 1 0 ***************************************************************************/ #define max10 (0x7fffffff / 10) // Convert string to integer int convSI(cchar *string, int &inum, cchar **delim) { char ch; int sign = 0, digits = 0, tnb = 0; cchar *pch = string; inum = 0; while ((ch = *pch) == ' ') pch++; // skip leading blanks if (ch == '-') sign = -1; // process leading +/- sign if (ch == '+') sign = 1; // (at most one sign character) if (sign) pch++; while ((*pch >= '0') && (*pch <= '9')) // process digits 0 - 9 { if (inum > max10) goto conv_err; // value too big inum = 10 * inum + *pch - '0'; digits++; pch++; } if (delim) *delim = pch; // terminating delimiter if (*pch && (*pch != ' ')) tnb++; // not null or blank if (! digits) // no digits found { if (tnb) return 4; // non-numeric (invalid) string else return 3; // null or blank string } if (sign == -1) inum = -inum; // negate if - sign if (! tnb) return 0; // no trailing non-numerics else return 1; // trailing non-numerics conv_err: inum = 0; return 4; } int convSI(cchar *string, int & inum, int lolim, int hilim, cchar **delim) { int stat = convSI(string,inum,delim); if (stat > 2) return stat; // invalid or null/blank if (inum < lolim) return 2; // return 2 if out of limits if (inum > hilim) return 2; // (has precedence over status 1) return stat; // limits OK, return 0 or 1 } // Convert string to double. int convSD(cchar *string, double &dnum, cchar **delim) { char ch; int ii, sign = 0, digits = 0, ndec = 0; int exp = 0, esign = 0, edigits = 0, tnb = 0; cchar *pch = string; static int first = 1; static double decimals[21], exponents[74]; if (first) // first-time called { first = 0; // pre-calculate constants for (ii = 1; ii <= 20; ii++) decimals[ii] = pow(10.0,-ii); for (ii = -36; ii <= 36; ii++) exponents[ii+37] = pow(10.0,ii); } dnum = 0.0; while ((ch = *pch) == ' ') pch++; // skip leading blanks if (ch == '-') sign = -1; // process leading +/- sign if (ch == '+') sign = 1; // (at most one sign character) if (sign) pch++; get_digits: while ((*pch >= '0') && (*pch <= '9')) // process digits 0 - 9 { dnum = 10.0 * dnum + (*pch - '0'); pch++; digits++; if (ndec) ndec++; } if ((*pch == '.') || (*pch == ',')) // process decimal point { // (allow comma or period) if (ndec) goto conv_err; ndec++; pch++; goto get_digits; } if ((*pch == 'e') || (*pch == 'E')) // process optional exponent { pch++; if (*pch == '+') esign = 1; // optional +/- sign if (*pch == '-') esign = -1; if (esign) pch++; if ((*pch < '0') || (*pch > '9')) goto conv_err; // 1st digit exp = *pch - '0'; edigits++; pch++; if ((*pch >= '0') && (*pch <= '9')) // optional 2nd digit { exp = 10 * exp + (*pch - '0'); edigits++; pch++; } if ((exp < -36) || (exp > 36)) goto conv_err; // exponent too big } if (delim) *delim = pch; // terminating delimiter if (*pch && (*pch != ' ')) tnb++; // not null or blank if (!(digits + edigits)) // no digits found { if (tnb) return 4; // non-numeric (invalid) string else return 3; // null or blank string } if (ndec > 1) dnum = dnum * decimals[ndec-1]; // compensate for decimal places if (sign == -1) dnum = - dnum; // negate if negative if (exp) { if (esign == -1) exp = -exp; // process exponent dnum = dnum * exponents[exp+37]; } if (! tnb) return 0; // no trailing non-numerics else return 1; // trailing non-numerics conv_err: dnum = 0.0; return 4; } int convSD(cchar *string, double &dnum, double lolim, double hilim, cchar **delim) { int stat = convSD(string,dnum,delim); if (stat > 2) return stat; // invalid or null/blank if (dnum < lolim) return 2; // return 2 if out of limits if (dnum > hilim) return 2; // (has precedence over status 1) return stat; // limits OK, return 0 or 1 } int convSF(cchar *string, float &fnum, cchar **delim) // v.5.2 { double dnum; int err; err = convSD(string,dnum,delim); fnum = dnum; return err; } int convSF(cchar *string, float &fnum, float lolim, float hilim, cchar **delim) // v.5.2 { double dnum, dlolim = lolim, dhilim = hilim; int err; err = convSD(string,dnum,dlolim,dhilim,delim); fnum = dnum; return err; } // Convert int to string with returned length. int convIS(int inum, char *string, int *cc) { int ccc; ccc = sprintf(string,"%d",inum); if (cc) *cc = ccc; return 0; } // Convert double to string with specified digits of precision. // Shortest length format (f/e) will be used. // Output length is returned in optional argument cc. int convDS(double dnum, int digits, char *string, int *cc) { char *pstr; sprintf(string,"%.*g",digits,dnum); pstr = strstr(string,"e+"); // 1.23e+12 > 1.23e12 if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e0"); // 1.23e02 > 1.23e2 if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e0"); if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e-0"); // 1.23e-02 > 1.23e-2 if (pstr) strcpy(pstr+2,pstr+3); pstr = strstr(string,"e-0"); if (pstr) strcpy(pstr+2,pstr+3); if (cc) *cc = strlen(string); return 0; } // format a number as "123 B" or "12.3 KB" or "1.23 MB" etc. // prec is the desired digits of precision to output. // WARNING: only the last 100 conversions remain available in memory. // Example formats for 3 digits of precision: // 12 B, 999 B, 1.23 KB, 98.7 KB, 456 KB, 2.34 MB, 45.6 GB, 12345 GB char * formatKBMB(double fnum, int prec) { #define kilo 1000 #define mega (kilo*kilo) #define giga (kilo*kilo*kilo) cchar *units; static char *output[100]; static int ftf = 1, ii; double gnum; if (ftf) { // keep last 100 conversions ftf = 0; for (ii = 0; ii < 100; ii++) output[ii] = (char *) zmalloc(20); } gnum = fabs(fnum); if (gnum > giga) { fnum = fnum / giga; units = "GB"; } else if (gnum > mega) { fnum = fnum / mega; units = "MB"; } else if (gnum > kilo) { fnum = fnum / kilo; units = "KB"; } else units = "B "; gnum = fabs(fnum); if (prec == 2 && gnum >= 99.5) prec++; // avoid e+nn formats if (prec == 3 && gnum >= 999.5) prec++; if (prec == 4 && gnum >= 9999.5) prec++; if (prec == 5 && gnum >= 99999.5) prec++; if (prec == 6 && gnum >= 999999.5) prec++; if (++ii > 99) ii = 0; snprintf(output[ii],20,"%.*g %s",prec,fnum,units); return output[ii]; } /************************************************************************** Wildcard string match Match candidate string to wildcard string containing any number of '*' or '?' wildcard characters. '*' matches any number of characters, including zero characters. '?' matches any one character. Returns 0 if match, 1 if no match. Benchmark: 0.032 usec. wild = *asdf*qwer?yxc 3.3 GHz Core i5 match = XXXasdfXXXXqwerXyxc ***************************************************************************/ int MatchWild(cchar *pWild, cchar *pString) { int ii, star; new_segment: star = 0; while (pWild[0] == '*') { star = 1; pWild++; } test_match: for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++) { if (pWild[ii] != pString[ii]) { if (! pString[ii]) return 1; if (pWild[ii] == '?') continue; if (! star) return 1; pString++; goto test_match; } } if (pWild[ii] == '*') { pString += ii; pWild += ii; goto new_segment; } if (! pString[ii]) return 0; if (ii && pWild[ii-1] == '*') return 0; if (! star) return 1; pString++; goto test_match; } /************************************************************************** Wildcard string match - ignoring case Works like MatchWild() above, but case is ignored. ***/ int MatchWildIgnoreCase(cchar *pWild, cchar *pString) // v.5.5 { int ii, star; new_segment: star = 0; while (pWild[0] == '*') { star = 1; pWild++; } test_match: for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++) { if (strncasecmp(pWild+ii,pString+ii,1) != 0) // the only code change { if (! pString[ii]) return 1; if (pWild[ii] == '?') continue; if (! star) return 1; pString++; goto test_match; } } if (pWild[ii] == '*') { pString += ii; pWild += ii; goto new_segment; } if (! pString[ii]) return 0; if (ii && pWild[ii-1] == '*') return 0; if (! star) return 1; pString++; goto test_match; } /************************************************************************** SearchWild - wildcard file search Find all files with total /pathname/filename matching a pattern, which may have any number of the wildcard characters '*' and '?' in either or both the pathname and filename. cchar * SearchWild(cchar *wfilespec, int &flag) inputs: flag = 1 to start a new search flag = 2 abort a running search *** do not modify flag within a search *** wfilespec = filespec to search with optional wildcards e.g. "/name1/na*me2/nam??e3/name4*.ext?" return: a pointer to one matching file is returned per call, or null when there are no more matching files. The search may be aborted before completion, but make a final call with flag = 2 to clean up temp file. A new search with flag = 1 will also finish the cleanup. NOT THREAD SAFE - do not use in parallel threads shell find command is used for the initial search because this is much faster than recursive use of readdir() (why?). (#) is used in place of (*) in comments below to prevent the compiler from interpreting (#/) as end of comments GNU find peculiarities: find /path/# omits "." files find /path/ includes "." files find /path/# recurses directories under /path/ find /path/#.txt does not recurse directories find /path/#/ finds all files under /path/ find /path/#/# finds files >= 1 directory level under /path/ find /path/xxx# never finds anything SearchWild uses simpler rules: '/' and '.' are treated like all other characters and match '#' and '?' no files are excluded except pure directories /path/#.txt finds all xxx.txt files under /path/ at all levels (because #.txt matches aaa.txt, /aaa/bbb.txt, etc.) Benchmark: search for /usr/share/#/README, find 457 from 101K files 1.9 secs. 3.3 GHz Core i5 SSD disk ***/ cchar * SearchWild(cchar *wpath, int &uflag) { static FILE *fid = 0; static char matchfile[maxfcc]; char searchpath[maxfcc]; char command[maxfcc]; int cc, err; char *pp; if ((uflag == 1) || (uflag == 2)) { // first call or stop flag if (fid) { pclose(fid); // if file open, close it fid = 0; } } if (uflag == 2) return 0; // kill flag, done if (uflag == 1) // first call flag { cc = strlen(wpath); if (cc == 0) return 0; if (cc > maxfcc-20) zappcrash("SearchWild: wpath > maxfcc"); pp = (char *) wpath; repl_Nstrs(pp,searchpath,"$","\\$","\"","\\\"",null); // init. search path, escape $ and " pp = strchr(searchpath,'*'); if (pp) { // not efficient but foolproof while ((*pp != '/') && (pp > searchpath)) pp--; // /aaa/bbb/cc*cc... >>> /aaa/bbb/ if (pp > searchpath) *(pp+1) = 0; } sprintf(command,"find \"%s\" -type f -or -type l",searchpath); // find files (ordinary, symlink) fid = popen(command,"r"); if (! fid) zappcrash(strerror(errno)); uflag = 763568954; // begin search } if (uflag != 763568954) zappcrash("SearchWild, uflag invalid"); while (true) { pp = fgets(matchfile,maxfcc-2,fid); // next matching file if (! pp) { pclose(fid); // no more fid = 0; return 0; } cc = strlen(matchfile); // get rid of trailing \n matchfile[cc-1] = 0; err = MatchWild(wpath,matchfile); // wildcard match? if (err) continue; // no return matchfile; // return file } } /************************************************************************** SearchWildIgnoreCase - wildcard file search - ignoring case Works like SearchWild() above, but case of file name is ignored. Actually, the trailing part of the path name is also case-insensitive, meaning that it is possible to get more matches than technically correct if directories like this are present: /AAA/BBB/.../filename /AAA/bbb/.../filename ***/ cchar * SearchWildIgnoreCase(cchar *wpath, int &uflag) // v.5.5 { static FILE *fid = 0; static char matchfile[maxfcc]; char searchpath[maxfcc]; char command[maxfcc]; int cc, err; char *pp; if ((uflag == 1) || (uflag == 2)) { // first call or stop flag if (fid) { pclose(fid); // if file open, close it fid = 0; } } if (uflag == 2) return 0; // kill flag, done if (uflag == 1) // first call flag { cc = strlen(wpath); if (cc == 0) return 0; if (cc > maxfcc-20) zappcrash("SearchWild: wpath > maxfcc"); pp = (char *) wpath; repl_Nstrs(pp,searchpath,"$","\\$","\"","\\\"",null); // init. search path, escape $ and " pp = strchr(searchpath,'*'); if (pp) { // not efficient but foolproof while ((*pp != '/') && (pp > searchpath)) pp--; // /aaa/bbb/cc*cc... >>> /aaa/bbb/ if (pp > searchpath) *(pp+1) = 0; } sprintf(command,"find \"%s\" -type f -or -type l",searchpath); // find files (ordinary, symlink) fid = popen(command,"r"); if (! fid) zappcrash(strerror(errno)); uflag = 763568954; // begin search } if (uflag != 763568954) zappcrash("SearchWild, uflag invalid"); while (true) { pp = fgets(matchfile,maxfcc-2,fid); // next matching file if (! pp) { pclose(fid); // no more fid = 0; return 0; } cc = strlen(matchfile); // get rid of trailing \n matchfile[cc-1] = 0; err = MatchWildIgnoreCase(wpath,matchfile); // wildcard match? if (err) continue; // no return matchfile; // return file } } /**************************************************************************/ // perform a binary search on sorted list of integers // return matching element or -1 if not found // Benchmark: search a list of 10 million sorted integers // 0.35 usecs. 3.3 GHz Core i5 int bsearch(int seekint, int nn, int list[]) { int ii, jj, kk, rkk; ii = nn / 2; // next element to search jj = (ii + 1) / 2; // next increment nn--; // last element rkk = 0; while (true) { kk = list[ii] - seekint; // check element if (kk > 0) { ii -= jj; // too high, go down if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up if (ii > nn) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } // Perform a binary search on sorted set of records in memory. // Return matching record number (0 based) or -1 if not found. // Benchmark: search 10 million sorted records of 20 chars. // 0.61 usecs. 3.3 GHz Core i5 int bsearch(char *seekrec, char *allrecs, int recl, int nrecs) { int ii, jj, kk, rkk; ii = nrecs / 2; // next element to search jj = (ii + 1) / 2; // next increment nrecs--; // last element rkk = 0; while (true) { kk = strcmp(allrecs+ii*recl,seekrec); // compare member rec to seek rec if (kk > 0) { ii -= jj; // too high, go down in set if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up in set if (ii > nrecs) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } // Perform a binary search on sorted set of pointers to records in memory. // Return matching record number (0 based) or -1 if not found. // The pointers are sorted in the order of the records starting at char N. // The records need not be sorted. // The string length of seekrec is compared. int bsearch(char *seekrec, char **allrecs, int N, int nrecs) { int ii, jj, kk, cc, rkk; cc = strlen(seekrec); ii = nrecs / 2; // next element to search jj = (ii + 1) / 2; // next increment nrecs--; // last element rkk = 0; while (true) { kk = strncmp(allrecs[ii]+N,seekrec,cc); // compare member rec to seek rec if (kk > 0) { ii -= jj; // too high, go down in set if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up in set if (ii > nrecs) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } /************************************************************************** heap sort functions void HeapSort(int list[], int nn) void HeapSort(float flist[], int nn) void HeapSort(double dlist[], int nn) void HeapSort(char *plist[], int nn) Sort list of nn integers, floats, doubles, or pointers to strings. Numbers are sorted in ascending order. Pointers are sorted in order of the strings they point to. The strings are not changed. Benchmarks: (3.3 GHz Core i5) 10 million integers: 1.5 secs 10 million doubles: 2.4 secs 2 million pointers to 100 character recs: 1.8 secs void HeapSort(char *plist[], int nn, compfunc) ---------------------------------------------- Sort list of nn pointers to strings. Pointers are sorted in order of the strings they point to, which is determined by the caller function compfunc. The strings are not changed. int compfunc(cchar *rec1, cchar *rec2) compare rec1 to rec2, return -1 0 +1 if rec1 < = > rec2 in sort order. void HeapSort(char *recs, int RL, int NR, compfunc) --------------------------------------------------- Sort an array of records in memory using a caller-supplied compare function. recs pointer to 1st record in array RL record length NR no. of records ***************************************************************************/ #define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp) // heapsort for array of integers static void adjust(int vv[], int n1, int n2) { int *bb, jj, kk, temp; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(int vv[], int nn) { int *bb, jj, temp; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } // heapsort for array of floats static void adjust(float vv[], int n1, int n2) { float *bb, temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(float vv[], int nn) { float *bb, temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } // heapsort for array of doubles static void adjust(double vv[], int n1, int n2) { double *bb, temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(double vv[], int nn) { double *bb, temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } // heapsort array of pointers to strings in ascending order of strings // pointers are sorted, strings are not changed. static void adjust(char *vv[], int n1, int n2) { char **bb, *temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && strcmp(bb[kk],bb[kk+1]) < 0) kk++; if (strcmp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(char *vv[], int nn) { char **bb, *temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[0], bb[jj]); adjust(vv,1,jj); } } // heapsort array of pointers to strings in user-defined order. // pointers are sorted, strings are not changed. static void adjust(char *vv[], int n1, int n2, HeapSortUcomp fcomp) { char **bb, *temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && fcomp(bb[kk],bb[kk+1]) < 0) kk++; if (fcomp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(char *vv[], int nn, HeapSortUcomp fcomp) { char **bb, *temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn,fcomp); bb = vv; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[0], bb[jj]); adjust(vv,1,jj,fcomp); } } // heapsort for array of strings or records, // using caller-supplied record compare function. // HeapSortUcomp returns [ -1 0 +1 ] for rec1 [ < = > ] rec2 // method: build array of pointers and sort these, then // use this sorted array to re-order the records at the end. static int *vv1, *vv2; static void adjust(char *recs, int RL, int n1, int n2, HeapSortUcomp fcomp) { int *bb, jj, kk, temp; char *rec1, *rec2; bb = vv1 - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { rec1 = recs + RL * bb[kk]; rec2 = recs + RL * bb[kk+1]; if (kk < n2 && fcomp(rec1,rec2) < 0) kk++; rec1 = recs + RL * bb[jj]; rec2 = recs + RL * bb[kk]; if (fcomp(rec1,rec2) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(char *recs, int RL, int NR, HeapSortUcomp fcomp) { int *bb, jj, kk, temp, flag; char *vvrec; vv1 = new int[NR]; for (jj = 0; jj < NR; jj++) vv1[jj] = jj; for (jj = NR/2; jj > 0; jj--) adjust(recs,RL,jj,NR,fcomp); bb = vv1 - 1; for (jj = NR-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(recs,RL,1,jj,fcomp); } vv2 = new int[NR]; for (jj = 0; jj < NR; jj++) vv2[vv1[jj]] = jj; vvrec = new char[RL]; flag = 1; while (flag) { flag = 0; for (jj = 0; jj < NR; jj++) { kk = vv2[jj]; if (kk == jj) continue; memmove(vvrec,recs+jj*RL,RL); memmove(recs+jj*RL,recs+kk*RL,RL); memmove(recs+kk*RL,vvrec,RL); SWAP(vv2[jj],vv2[kk]); flag = 1; } } delete vv1; delete vv2; delete vvrec; } /************************************************************************** int MemSort (char *RECS, int RL, int NR, int KEYS[][3], int NK) RECS is an array of records, to be sorted in-place. (record length = RL, record count = NR) KEYS[NK,3] is an integer array defined as follows: [N,0] starting position of Nth key field in RECS [N,1] length of Nth key field in RECS [N,2] type of sort for Nth key: 1 = char ascending 2 = char descending 3 = int*4 ascending 4 = int*4 descending 5 = float*4 ascending 6 = float*4 descending 7 = float*8 ascending (double) 8 = float*8 descending Benchmark: 2 million recs of 40 bytes with 4 sort keys: 2.5 secs (3.3 GHz Core i5). ***/ int MemSortComp(cchar *rec1, cchar *rec2); int MemSortKeys[10][3], MemSortNK; int MemSort(char *RECS, int RL, int NR, int KEYS[][3], int NK) { int ii; if (NR < 2) return 1; if (NK > 10) zappcrash("MemSort, bad NK"); if (NK < 1) zappcrash("MemSort, bad NK"); MemSortNK = NK; for (ii = 0; ii < NK; ii++) { MemSortKeys[ii][0] = KEYS[ii][0]; MemSortKeys[ii][1] = KEYS[ii][1]; MemSortKeys[ii][2] = KEYS[ii][2]; } HeapSort(RECS,RL,NR,MemSortComp); return 1; } int MemSortComp(cchar *rec1, cchar *rec2) { int ii, stat, kpos, ktype, kleng; int inum1, inum2; float rnum1, rnum2; double dnum1, dnum2; cchar *p1, *p2; for (ii = 0; ii < MemSortNK; ii++) // loop each key { kpos = MemSortKeys[ii][0]; // relative position kleng = MemSortKeys[ii][1]; // length ktype = MemSortKeys[ii][2]; // type p1 = rec1 + kpos; // absolute position p2 = rec2 + kpos; switch (ktype) { case 1: // char ascending stat = strncmp(p1,p2,kleng); // compare 2 key values if (stat) return stat; // + if rec1 > rec2, - if < break; // 2 keys are equal, check next key case 2: // char descending stat = strncmp(p1,p2,kleng); if (stat) return -stat; break; case 3: // int ascending memmove(&inum1,p1,4); memmove(&inum2,p2,4); if (inum1 > inum2) return 1; if (inum1 < inum2) return -1; break; case 4: // int descending memmove(&inum1,p1,4); memmove(&inum2,p2,4); if (inum1 > inum2) return -1; if (inum1 < inum2) return 1; break; case 5: // float ascending memmove(&rnum1,p1,4); memmove(&rnum2,p2,4); if (rnum1 > rnum2) return 1; if (rnum1 < rnum2) return -1; break; case 6: // float descending memmove(&rnum1,p1,4); memmove(&rnum2,p2,4); if (rnum1 > rnum2) return -1; if (rnum1 < rnum2) return 1; break; case 7: // double ascending memmove(&dnum1,p1,8); memmove(&dnum2,p2,8); if (dnum1 > dnum2) return 1; if (dnum1 < dnum2) return -1; break; case 8: // double descending memmove(&dnum1,p1,8); memmove(&dnum2,p2,8); if (dnum1 > dnum2) return -1; if (dnum1 < dnum2) return 1; break; default: // key type not 1-8 zappcrash("MemSort, bad KEYS sort type"); } } return 0; // records match on all keys } /************************************************************************** variable string list functions - array / list of strings pvlist * pvlist_create(int max) void pvlist_free(pvlist *pv) int pvlist_append(pvlist *pv, cchar *entry, int unique) int pvlist_prepend(pvlist *pv, cchar *entry, int unique) int pvlist_find(pvlist *pv, cchar *entry) int pvlist_remove(pvlist *pv, cchar *entry) int pvlist_remove(pvlist *pv, int Nth) int pvlist_count(pvlist *pv) int pvlist_replace(pvlist *pv, int Nth, char *entry) cchar * pvlist_get(pvlist *pv, int Nth) int pvlist_sort(pvlist *pv) These functions manage a variable length list of variable length strings. Declare such a list as: pvlist *pv; ***************************************************************************/ // Creates a pvlist with a capacity of max strings and returns a pointer. // String lengths are unlimited, but the count of strings is limited to max. // Memory is allocated for max pointers at first. Memory for the strings is // allocated and freed as the strings are added or removed. pvlist * pvlist_create(int max) { pvlist *pv; pv = (pvlist *) zmalloc(sizeof(pvlist)); pv->max = max; pv->act = 0; pv->list = (char **) zmalloc(max * sizeof(char *)); return pv; } // free memory for variable list and contained strings void pvlist_free(pvlist *pv) { int ii; for (ii = 0; ii < pv->act; ii++) zfree(pv->list[ii]); zfree(pv->list); zfree(pv); } // append new entry to end of list (optional if unique) // if list if full, first entry is removed and rest are packed down // return: N >= 0: new entry added at position N // N = -1: not unique, not added int pvlist_append(pvlist *pv, cchar *entry, int unique) { int ii; if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique if (pv->act == pv->max) pvlist_remove(pv,0); // if list full, remove 1st entry ii = pv->act; pv->list[ii] = zstrdup(entry); // add to end of list pv->act++; return ii; } // prepend new entry to list (optional if unique) // prior list entries are pushed down to make room // if list is full, last entry is removed first // return: N = 0: new entry added at position 0 // N = -1: not unique, not added int pvlist_prepend(pvlist *pv, cchar *entry, int unique) { int ii; if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique if (pv->act == pv->max) pvlist_remove(pv,pv->act-1); // if list full, remove last entry for (ii = pv->act; ii > 0; ii--) // push all list entries down pv->list[ii] = pv->list[ii-1]; pv->list[0] = zstrdup(entry); // add to start of list pv->act++; return 0; } // find list entry by name, return entry (0 based) // return -1 if not found int pvlist_find(pvlist *pv, cchar *entry) { int ii; for (ii = 0; ii < pv->act; ii++) if (strEqu(entry,pv->list[ii])) break; if (ii < pv->act) return ii; return -1; } // remove an entry by name and repack list // return (former) entry or -1 if not found int pvlist_remove(pvlist *pv, cchar *entry) { int ii; ii = pvlist_find(pv,entry); if (ii < 0) return -1; pvlist_remove(pv,ii); return ii; } // remove an entry by number and repack list // returns -1 if entry is beyond list end int pvlist_remove(pvlist *pv, int ii) { if (ii < 0 || ii >= pv->act) return -1; zfree(pv->list[ii]); for (++ii; ii < pv->act; ii++) { // pre-increment v.5.6 if (! pv->act) printz("meaningless reference %d",ii); // stop g++ optimization bug /// pv->list[ii-1] = pv->list[ii]; } pv->act--; return 0; } // return entry count int pvlist_count(pvlist *pv) { return pv->act; } // replace Nth entry with new one int pvlist_replace(pvlist * pv, int ii, cchar *entry) { if (ii < 0 || ii >= pv->act) return -1; zfree(pv->list[ii]); pv->list[ii] = zstrdup(entry); return 0; } // return Nth entry or null char * pvlist_get(pvlist *pv, int Nth) { if (Nth >= pv->act) return 0; return pv->list[Nth]; } // sort list in ascending order int pvlist_sort(pvlist *pv) { HeapSort(pv->list,pv->act); return 0; } /**************************************************************************/ // Random number generators with explicit context // and improved randomness over a small series. // Benchmark: lrandz 0.012 usec drandz 0.014 usec 3.3 GHz Core i5 int lrandz(int64 *seed) // returns 0 to 0x7fffffff { *seed = *seed ^ (*seed << 17); *seed = *seed ^ (*seed << 20); return nrand48((unsigned int16 *) seed); } int lrandz() // implicit seed, repeatable sequence { static int64 seed = 12345678; return lrandz(&seed); } double drandz(int64 *seed) // returns 0.0 to 0.99999... { *seed = *seed ^ (*seed << 17); *seed = *seed ^ (*seed << 20); return erand48((unsigned int16 *) seed); } double drandz() // implicit seed, repeatable sequence { static int64 seed = 23459876; return drandz(&seed); } /************************************************************************** spline1: define a curve using a set of data points (x and y values) spline2: for a given x-value, return a y-value fitting the curve For spline1, the no. of curve-defining points must be < 100. For spline2, the given x-value must be within the range defined in spline1. The algorithm was taken from the book "Numerical Recipes" (Cambridge University Press) and converted from Fortran to C++. ***/ namespace splinedata { int nn; float px1[100], py1[100], py2[100]; } void spline1(int dnn, float *dx1, float *dy1) { using namespace splinedata; float sig, p, u[100]; int ii; nn = dnn; if (nn > 100) zappcrash("spline1(), > 100 data points"); for (ii = 0; ii < nn; ii++) { px1[ii] = dx1[ii]; py1[ii] = dy1[ii]; if (ii && px1[ii] <= px1[ii-1]) zappcrash("spline1(), x-value not increasing"); } py2[0] = u[0] = 0; for (ii = 1; ii < nn-1; ii++) { sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]); p = sig * py2[ii-1] + 2; py2[ii] = (sig - 1) / p; u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1]) / (px1[ii] - px1[ii-1])) / (px1[ii+1] - px1[ii-1]) - sig * u[ii-1]) / p; } py2[nn-1] = 0; for (ii = nn-2; ii >= 0; ii--) py2[ii] = py2[ii] * py2[ii+1] + u[ii]; return; } float spline2(float x) { using namespace splinedata; int kk, klo = 0, khi = nn-1; float h, a, b, y; while (khi - klo > 1) { kk = (khi + klo) / 2; if (px1[kk] > x) khi = kk; else klo = kk; } h = px1[khi] - px1[klo]; a = (px1[khi] - x) / h; b = (x - px1[klo]) / h; y = a * py1[klo] + b * py1[khi] + ((a*a*a - a) * py2[klo] + (b*b*b - b) * py2[khi]) * (h*h) / 6; return y; } /**************************************************************************/ // Add text strings to a FIFO queue, retrieve text strings. // v.5.7 // Can be used by one or two threads. // thread 1: open queue, get strings, close queue. // thread 2: put strings into queue. // initialize Qtext queue, empty status void Qtext_open(Qtext *qtext, int cap) { int cc; qtext->qcap = cap; qtext->qnewest = -1; qtext->qoldest = -1; qtext->qdone = 0; cc = cap * sizeof(char *); qtext->qtext = (char **) zmalloc(cc); memset(qtext->qtext,0,cc); return; } // add new text string to Qtext queue // if queue full, sleep until space is available void Qtext_put(Qtext *qtext, cchar *format, ...) { int qnext; va_list arglist; char message[200]; va_start(arglist,format); vsnprintf(message,199,format,arglist); va_end(arglist); qnext = qtext->qnewest + 1; if (qnext == qtext->qcap) qnext = 0; while (qtext->qtext[qnext]) zsleep(0.1); qtext->qtext[qnext] = zstrdup(message); qtext->qnewest = qnext; return; } // remove oldest text string from Qtext queue // if queue empty, return a null string char * Qtext_get(Qtext *qtext) { int qnext; char *text; if (qtext->qcap == 0) return 0; qnext = qtext->qoldest + 1; if (qnext == qtext->qcap) qnext = 0; text = qtext->qtext[qnext]; if (! text) return 0; qtext->qtext[qnext] = 0; qtext->qoldest = qnext; return text; } // free() any leftover strings void Qtext_close(Qtext *qtext) { for (int ii = 0; ii < qtext->qcap; ii++) if (qtext->qtext[ii]) zfree(qtext->qtext[ii]); qtext->qcap = 0; return; } /************************************************************************** Initialize application files according to following conventions: // new version + binary executable is at: /prefix/bin/appname // = PREFIX/bin/appname + other application directories are derived as follows: /prefix/share/appname/data/ desktop, parameters ... /prefix/share/doc/appname/ README, changelog, userguide-xx.html ... /prefix/share/appname/icons/ icon files: filename.png /prefix/share/appname/locales/ translate-xx.po ... (original) /home/user/.appname/ some installation files are copied here /home/user/.appname/logfile log file with error messages /home/user/.appname/locales translate-xx.po ... (user modified) zprefix install location normally /usr, has subtrees /bin /share /doc zdatadir installed data files /prefix/share/appname/data/ zdocdir documentation files /prefix/share/doc/appname/ zicondir icons /prefix/share/appname/icons/ zlocalesdir translation files /prefix/share/appname/locales/ zuserdir local app files /home//.appname /home//.appname/locales If it does not already exist, an application directory for the current user is created at /home/username/.appname (following common Linux convention). If this directory was created for the first time, copy specified files (following the 1st argument) from the install directory into the newly created user-specific directory. The assumption is that all initial data files for the application (e.g. parameters) will be in the install data directory, and these are copied to the user directory where the user or application can modify them. If the running program is not connected to a terminal device, stdout and stderr are redirected to the log file at /home/user/.appname/logfile char * get_zprefix() returns install top directory (has /bin and /share under it) char * get_zuserdir() returns /home/user/.appname (or /root/.appname) char * get_zdatadir() returns directory where application data files reside char * get_zdocdir() returns directory for application documentation files char * get_zicondir() returns directory for application icons char * get_zlocalesdir() returns directory for translation files ***/ cchar * get_zprefix() { return zfuncs::zprefix; } // /usr or /home/ cchar * get_zuserdir() { return zfuncs::zuserdir; } // /home/user/.appname cchar * get_zdatadir() { return zfuncs::zdatadir; } // parameters, icons cchar * get_zdocdir() { return zfuncs::zdocdir; } // documentation files cchar * get_zicondir() { return zfuncs::zicondir; } // icon files cchar * get_zlocalesdir() { return zfuncs::zlocalesdir; } // translation files int zinitapp(cchar *appname, ...) { using namespace zfuncs; char work[200]; char logfile[200], oldlog[200]; cchar *appfile; int cc, secs, err; time_t Tnow; char *chTnow; struct stat statdat; va_list arglist; FILE *fid; TRACE catch_signals(); // catch segfault, do backtrace strcpy(zappname,appname); // save app name v.5.6 #ifndef PREFIX // install location #define PREFIX "/usr" #endif strncpy0(work,PREFIX,199); // /usr or /home/ strcpy(zprefix,work); // /prefix strncatv(zdatadir,199,work,"/share/",zappname,"/data",null); // /prefix/share/appname/data strncatv(zicondir,199,work,"/share/",zappname,"/icons",null); // /prefix/share/appname/icons strncatv(zlocalesdir,199,work,"/share/",zappname,"/locales",null); // /prefix/share/appname/locales strncatv(zdocdir,199,work,"/share/doc/",zappname,null); // /prefix/share/doc/appname #ifdef DOCDIR strncpy0(zdocdir,DOCDIR,199); // flexible DOCDIR location #endif snprintf(zuserdir,199,"%s/.%s",getenv("HOME"),zappname); // /home//.appname/ cc = strlen(zuserdir); // stop humongous username v.5.3 if (cc > 160) zappcrash("too big: %s",zuserdir); err = stat(zuserdir,&statdat); // does it exist already? if (err) { err = mkdir(zuserdir,0750); // no, create and initialize if (err) zappcrash("cannot create %s",zuserdir); va_start(arglist,appname); // copy req. application files while (true) { // from /prefix/share/appname/data/* appfile = va_arg(arglist, cchar *); // to /home/user/.appname/* if (! appfile) break; err = shell_ack("cp %s/%s %s",zdatadir,appfile,zuserdir); } va_end(arglist); } tid_main = pthread_self(); // thread ID of main() process Tnow = time(0); chTnow = ctime(&Tnow); chTnow[19] = 0; if (! isatty(fileno(stdin))) { // not attached to a terminal v.5.7 snprintf(logfile,199,"%s/logfile",zuserdir); // /home//logfile v.5.5 snprintf(oldlog,199,"%s/logfile.old",zuserdir); err = stat(logfile,&statdat); if (! err) { secs = Tnow - statdat.st_mtime; // if log file age > 1 hour v.5.0 if (secs > 3600) rename(logfile,oldlog); // rename to *.old } fid = freopen(logfile,"a",stdout); // redirect output to log file fid = freopen(logfile,"a",stderr); if (! fid) printz("*** cannot redirect stdout and stderr \n"); } printz("\n =========== start %s %s \n",zappname,chTnow); fflush(0); // v.5.2 return 1; } // Find a locale-dependent installation file or user file. // file type: doc, data, locale, user, userlocale // file name: README, changelog, userguide.html, parameters, translate.po ... // Returns complete file name, e.g. /usr/share/appname/locales/translate-xx.po // Output filespec should be 200 bytes (limit for all installation files). // Returns 0 if OK, +N if not found. int locale_filespec(cchar *filetype, cchar *filename, char *filespec) // v.5.5 { using namespace zfuncs; char *pp, fname[20], fext[8]; char lc_RC[8]; // -lc or -lc_RC int cc, err; struct stat statb; filespec[0] = '/'; strcat(filespec,filetype); // leave /type as default if (strEqu(filetype,"doc")) strcpy(filespec,zdocdir); // /usr/share/doc/appname if (strEqu(filetype,"data")) strcpy(filespec,zdatadir); // /usr/share/appname/data if (strEqu(filetype,"locale")) strcpy(filespec,zlocalesdir); // /usr/share/appname/locales if (strEqu(filetype,"user")) strcpy(filespec,zuserdir); // /home//.appname if (strEqu(filetype,"userlocale")) { strcpy(filespec,zuserdir); // /home//.appname/locales strcat(filespec,"/locales"); } strncpy0(fname,filename,20); pp = strchr(fname,'.'); if (pp) { strcpy(fext,pp); // file type .fext *pp = 0; } else *fext = 0; // no type lc_RC[0] = '-'; strncpy0(lc_RC+1,zlang,6); // locale with region code: -lc_RC tryextras: cc = strlen(filespec); filespec[cc] = '/'; // /directories.../ strcpy(filespec+cc+1,fname); // /directories.../fname cc = strlen(filespec); // | pp = filespec + cc; // pp strcpy(pp,lc_RC); // /directories.../fname-lc_RC.fext strcat(pp,fext); err = stat(filespec,&statb); if (! err) return 0; strcpy(pp+3,fext); // /directories.../fname-lc.fext err = stat(filespec,&statb); if (! err) return 0; strcpy(pp,"-en"); // /directories.../fname-en.fext strcat(pp,fext); err = stat(filespec,&statb); if (! err) return 0; strcpy(pp,fext); // /directories.../fname.fext err = stat(filespec,&statb); if (! err) return 0; if (strEqu(filetype,"doc")) { // these files may be placed in strcpy(filespec,zdocdir); // /usr/share/doc/appname/extras strcat(filespec,"/extras"); // due to Linux chaos filetype = ""; goto tryextras; // try again using /extras } printz("file not found: %s %s \n",filetype,filename); return 1; } /**************************************************************************/ // Display help file in a separate process so application is not blocked. // help file: /zdatadir/userguide-lc_RC.html (or) *-lc.html (or) *-en.html // context: optional arg. show file starting at internal link = context // look for user guide file in /usr/share/data/appname/ [ extras/ ] void showz_userguide(cchar *context) // v.5.5 { char filespec[200], url[200]; int err; err = locale_filespec("data","userguide.html",filespec); if (err) { zmessageACK(null,null,ZTX("user guide not found")); return; } snprintf(url,199,"file://%s",filespec); if (context && *context) // specific topic wanted strncatv(url,199,"#",context,null); // file://.../userguide-xx.html#context showz_html(url); return; } /**************************************************************************/ // display application log file in a popup window // The log file is /home//.appname/logfile void showz_logfile() // log file v.5.2 { using namespace zfuncs; char buff[200]; fflush(0); // v.5.2 snprintf(buff,199,"cat %s/logfile",zuserdir); popup_command(buff,800,600); return; } // find and show a text file in /usr/share/doc/appname/ // or /usr/share/appname/data // the text file may also be a compressed .gz file // type is "doc" or "data" void showz_textfile(const char *type, const char *file) // v.5.5 { char filex[40], filespec[200], command[200]; int err; strncpy0(filex,file,36); // look for gzip file first v.5.7 strcat(filex,".gz"); err = locale_filespec(type,filex,filespec); if (! err) { snprintf(command,200,"zcat %s",filespec); popup_command(command,600,400); return; } strncpy0(filex,file,36); // look for uncompressed file err = locale_filespec(type,filex,filespec); if (! err) { snprintf(command,200,"cat %s",filespec); popup_command(command,600,400); return; } zmessageACK(0,0,"file not found: %s %s",type,file); return; } // show a local or remote html file using the user's preferred browser // to show a local file starting at an internal live link location: // url = "file://directory/.../filename#livelink void showz_html(cchar *url) { static char prog[20]; static int ftf = 1, err; if (ftf) { ftf = 0; *prog = 0; err = system("which firefox"); // use xdg-open only as last resort if (! err) strcpy(prog,"firefox"); // v.5.2 else { err = system("which chromium-browser"); if (! err) strcpy(prog,"chromium-browser"); else { err = system("which xdg-open"); if (! err) strcpy(prog,"xdg-open"); } } } if (! *prog) { zmessLogACK(null,"no firefox or chromium, cannot show document"); return; } shell_ack("%s %s &",prog,url); return; } /**************************************************************************/ // Creates a desktop icon / launcher and a system menu entry. // The menu name is taken from the input command, without options. // A command like "mycom -optA -optB" would generate a menu name of "mycom". // The categories should be separated by semicolons and conform to LSB categories. // The generic name is free text to describe the application, e.g. "Image Editor". // If the target system is not LSB compliant this function will not work. void zmake_menu_launcher(cchar *command, cchar *categories, cchar *genericname) { using namespace zfuncs; char appname[20], dtdir[200], dtfile[200], work[200]; cchar *xdgcomm = "xdg-user-dir DESKTOP"; cchar *pp; FILE *fid; pp = strField(command,' ',1); if (! pp) pp = "?"; strncpy0(appname,pp,20); fid = popen(xdgcomm,"r"); // get desktop directory for user locale if (! fid) { // v.5.8 zmessageACK(0,0,"%s \n %s",xdgcomm,strerror(errno)); return; } int nn = fscanf(fid,"%s",dtdir); pclose(fid); if (nn != 1) { zmessageACK(0,0,"xdg-user-dir DESKTOP failed"); return; } snprintf(dtfile,200,"%s/%s.desktop",dtdir,appname); fid = fopen(dtfile,"w"); if (! fid) { zmessageACK(0,0,"%s \n %s",dtfile,strerror(errno)); return; } fputs("[Desktop Entry]\n",fid); // [Desktop Entry] snprintf(work,199,"Name=%s\n",appname); // Name=appname fputs(work,fid); snprintf(work,199,"Categories=%s\n",categories); // Categories=Cat1;Cat2; ... fputs(work,fid); snprintf(work,199,"GenericName=%s\n",genericname); // GenericName=generic app name fputs(work,fid); fputs("Type=Application\n",fid); // Type=Application fputs("Terminal=false\n",fid); // Terminal=false snprintf(work,199,"Exec=%s/bin/%s\n",zprefix,command); // Exec=/usr/bin/appname -options fputs(work,fid); snprintf(work,199,"Icon=%s/%s.png\n",zicondir,appname); // /usr/share/appname/icons/appname.png fputs(work,fid); fclose(fid); shell_ack("chmod 0750 %s",dtfile); // make executable shell_ack("xdg-desktop-menu install --novendor %s",dtfile); // add menu entry return; } /************************************************************************** Translation Functions Translation files are standard .po files as used in the Gnu gettext system. However the .po files are used directly, and there is no need to merge and compile them into a binary format (.mo files). A translation file is one of: /translate-lc.po or *-lc_RC.po where "lc" is a standard language code and "lc_RC" a language and region code. The file may also be compressed with the file type .po.gz Translation files contain two record types: msgid "english text" msgstr "translation text" The text strings may continue on multiple lines, each such segment enclosed in quotes. The strings may contain C-format codes (%s %d etc.) and may contain escaped special characters (\n \" etc.). A text string may have a context part "context::string", where "context" is any short string, "::" is a separator, and "string" is the string to translate or the translation of a string. This is to handle the case where a single English string may need multiple translations, depending on context. The English string may be present multiple times in a .po file, each one marked with a different context and having a different translation. The context part is optional in the translation strings and is not displayed in the GUI. Initialize translations: int ZTXinit(cchar *lang) lang is "lc" or "lc_RC" or null (current locale will be used) Initializes the running application for the translation of text message strings for non-English locales. It reads a translation file which matches the English text strings in the source program to translated text strings for the user's locale. This file is uncompressed if necessary and copied into the user's files at /locales/translate-lc.po. This is done only if the file at is newer than the copy at . The local file is read and processed into a translation table for use by ZTX() and ZTX_translation_start(). lang: 2-character language code 'lc' ("de" "fr" "es" etc.) or 5-character language and region code 'lc_RC' ("de_AT" etc.) or null to use the current locale Status returned: 0 = OK, 1 = unable to process translation files. Translate a text string: cchar *translation = ZTX(cchar *english) english: text string which may have printf formats (%d %s ...) translation: the returned equivalent translation If the user language is English or if no translation is found, the input string is returned, else the translated string. example: program code: printf(ZTX("answer: %d %s \n next line"), 123, "qwerty"); A German .po file (translate-de.po) would have the following: msgid "" "answer: %d %s \n" " next line" msgstr "" "Antwort: %d %s \n" " nächste Zeile" ***************************************************************************/ namespace ZTXnames { FILE *fidr, *fidw; char buff[ZTXmaxcc], *ppq1, *ppq2; char *porec, *wporec; char Etext[ZTXmaxcc], Ttext[ZTXmaxcc]; // .po text: "line 1 %s \n" "line 2" char **etext, **ttext; // arrays, english and translations char **estring, **tstring; // merged, un-quoted, un-escaped int Ntext = 0; // array counts void ZTXgettext(char *text); char *ZTXmergetext(cchar *text); } // read and process .po file at application startup // prepare english strings and translations for quick access void ZTXinit(cchar *lang) // initialize translations { using namespace zfuncs; using namespace ZTXnames; int ii, err, Flocal = 0, Finstall = 0; time_t localdt = 0, installdt = 0; char localpo[200], installpo[200], ulocalesdir[200]; char *pp, poname[20], ponamexx[20]; struct stat statb; TRACE if (Ntext) { // free prior translation for (ii = 0; ii < Ntext; ii++) { zfree(etext[ii]); zfree(ttext[ii]); zfree(estring[ii]); zfree(tstring[ii]); } zfree(etext); zfree(ttext); zfree(estring); zfree(tstring); Ntext = 0; } etext = (char **) zmalloc(ZTXmaxent * sizeof(char *)); // english text and translations ttext = (char **) zmalloc(ZTXmaxent * sizeof(char *)); // (segmented, quoted, escaped) estring = (char **) zmalloc(ZTXmaxent * sizeof(char *)); // english strings and translations tstring = (char **) zmalloc(ZTXmaxent * sizeof(char *)); // (merged, un-quoted, un-escaped) if (lang && *lang) strncpy0(zlang,lang,6); // use language from caller else { // help Linux chaos pp = getenv("LANG"); // use $LANG if defined if (! pp) pp = getenv("LANGUAGE"); // use $LANGUAGE if defined if (! pp) pp = setlocale(LC_MESSAGES,""); // use locale if defined if (pp) strncpy0(zlang,pp,6); // "lc_RC" language/region code else strcpy(zlang,"en"); // use English } if (*zlang < 'a') strcpy(zlang,"en"); // use English if garbage printz("language: %s \n",zlang); if (strnEqu(zlang,"en",2)) return; // English, do nothing err = locale_filespec("userlocale","translate.po",localpo); if (! err) { Flocal = 1; // .po file was found in user area stat(localpo,&statb); localdt = statb.st_mtime; } err = locale_filespec("locale","translate.po",installpo); if (err) err = locale_filespec("locale","translate.po.gz",installpo); if (! err) { // install .po file was found Finstall = 1; stat(installpo,&statb); installdt = statb.st_mtime; } if (! Finstall && ! Flocal) { // no local or installed .po found printz("*** no translate-%s.po file found \n",zlang); return; } if (Finstall && Flocal) { // both local and install .po present if (installdt > localdt) { // local .po is stale, replace it printz("installed .po file is newer than local .po \n"); // v.5.5 Flocal = 0; } } if (Flocal) printz("using local .po file: %s \n",localpo); else { printz("using installed .po file: %s \n",installpo); snprintf(ulocalesdir,200,"%s/locales",zuserdir); // /home//.appname/locales mkdir(ulocalesdir,0750); // bugfix v.5.6 pp = strrchr(installpo,'/'); // extract filename from full path strcpy(poname,pp+1); pp = strstr(poname,".gz"); // uncompress/copy installed .po file if (pp) { // to local .po file v.5.5 *pp = 0; err = shell_ack("gunzip -c %s > %s/%s",installpo,ulocalesdir,poname); } else err = shell_ack("cp %s %s/%s",installpo,ulocalesdir,poname); if (err) return; pp = strstr(poname,"-en.po"); // if translate-en was used, if (pp) { // rename to the target locale code strcpy(ponamexx,poname); strcpy(poname+(pp+1-poname),zlang); // translate-en.po >> translate-xx.po strcat(poname,".po"); shell_ack("mv -f %s/%s %s/%s",ulocalesdir,ponamexx,ulocalesdir,poname); } sprintf(localpo,"%s/%s",ulocalesdir,poname); // final uncompressed local .po file printz("new local .po file: %s \n",localpo); } fidr = fopen(localpo,"r"); // open .po file if (! fidr) { printz("*** cannot open .po file: %s \n",localpo); return; } porec = 0; // no .po record yet *Etext = *Ttext = 0; // no text yet while (true) { if (! porec) porec = fgets_trim(buff,ZTXmaxcc,fidr); // get next .po record if (! porec) break; // EOF if (blank_null(porec)) { // blank record porec = 0; continue; } if (*porec == '#') { // comment porec = 0; continue; } if (strnEqu(porec,"msgid",5)) // start new english string { if (*Etext) { // two in a row printz("no translation: %s \n",Etext); *Etext = 0; } if (*Ttext) { printz("orphan translation: %s \n",Ttext); *Ttext = 0; } porec += 5; // get segmented text string ZTXgettext(Etext); // "segment1 %s \n" "segment2" ... } else if (strnEqu(porec,"msgstr",6)) // start new translation { porec += 6; // get segmented string ZTXgettext(Ttext); if (! *Etext) { printz("orphan translation: %s \n",Ttext); *Ttext = 0; continue; } if (strlen(Ttext) < 3) // translation is "" (quotes included) printz("no translation: %s \n",Etext); // leave as "" v.5.6 } else { printz("unrecognized .po record: %s \n",porec); porec = 0; continue; } if (*Etext && *Ttext) // have an english/translation pair { etext[Ntext] = zstrdup(Etext); // add to translation tables ttext[Ntext] = zstrdup(Ttext); *Etext = *Ttext = 0; Ntext++; if (Ntext == ZTXmaxent) // cannot continue zappcrash("more than %d translations",ZTXmaxent); } } fclose(fidr); printz(".po file has %d entries \n",Ntext); for (ii = 0; ii < Ntext; ii++) { pp = ZTXmergetext(etext[ii]); // merge segmented text strings estring[ii] = zstrdup(pp); pp = ZTXmergetext(ttext[ii]); tstring[ii] = zstrdup(pp); } return; } // private function // read and combine multiple 'msgid' or 'msgstr' quoted strings // output is one string with one or more quoted segments: // "text line 1 %s \n" "text line 2" ... // each segment comes from a different line in the input .po file void ZTXnames::ZTXgettext(char *pstring) { using namespace ZTXnames; int cc, scc = 0; while (true) // join multiple quoted strings { while (*porec && *porec != '"') porec++; // find opening string quote if (! *porec) { porec = fgets_trim(buff,ZTXmaxcc,fidr); // get next .po record if (! porec) return; if (strnEqu(porec,"msgid",5)) return; // end of this string if (strnEqu(porec,"msgstr",6)) return; } ppq1 = porec; // opening quote ppq2 = ppq1 + 1; while ((*ppq2 && *ppq2 != '"') || // find closing (non-escaped) quote (*ppq2 == '"' && *(ppq2-1) == '\\')) ppq2++; if (! *ppq2) return; cc = ppq2 - ppq1 + 1; // min. case is "" if (cc + 1 + scc >= ZTXmaxcc) printz("*** string is too long %s \n",pstring); else { strncpy0(pstring+scc,ppq1,cc+1); // accum. substrings, minus quotes scc += cc; } porec = ppq2 + 1; } return; } // private function // convert quoted string segments into binary form that // matches the compiled string in the source program // (remove quotes, merge strings, un-escape \n \" etc.) char * ZTXnames::ZTXmergetext(cchar *dirtystring) { static char cleanstring[ZTXmaxcc]; int ii, jj; strncpy0(cleanstring,dirtystring,ZTXmaxcc); clean_escapes(cleanstring); for (ii = jj = 0; cleanstring[ii]; ii++) if (cleanstring[ii] != '"') cleanstring[jj++] = cleanstring[ii]; cleanstring[jj] = 0; return cleanstring; } // Translate the input english string or return the input string. // Look for "context::string" and return "string" only if context found. // This function may need a few microseconds if thousands of strings must be searched. cchar * ZTX(cchar *english) { using namespace ZTXnames; cchar *pp, *pp2; int ii; if (! english) return "null"; for (ii = 0; ii < Ntext; ii++) // find english / translation pair if (strEqu(english,estring[ii])) break; if (ii < Ntext) pp = tstring[ii]; // translation else pp = english; // missing english in .po file if (strlen(pp) == 0) pp = english; // translation is "" v.5.6 for (pp2 = pp; *pp2 && pp2 < pp+30; pp2++) // remove context if present if (*pp2 == ':' && *(pp2+1) == ':') return pp2+2; return pp; } // Find all untranslated strings and return them one per call. // Set ftf = 1 for first call, will be returned = 0. // Returns null after last untranslated string. cchar * ZTX_missing(int &ftf) // v.5.6 { using namespace ZTXnames; int ii; static int next; if (ftf) ftf = next = 0; for (ii = next; ii < Ntext; ii++) if (strlen(tstring[ii]) == 0) break; // translation is "" v.5.6 next = ii + 1; if (ii < Ntext) return estring[ii]; // return english return 0; // EOL } /************************************************************************** GTK utility functions **************************************************************************/ // Iterate main loop every "skip" calls. // If called within the main() thread, does a GTK main loop to process menu events, etc. // You must do this periodically within long-running main() thread tasks if you wish to // keep menus, buttons, output windows, etc. alive and working. The skip argument will // cause the function to do nothing for skip calls, then perform the normal function. // This allows it to be imbedded in loops with little execution time penalty. // If skip = 1000, zmainloop() will do nothing for 1000 calls, execute normally, etc. // If called from a thread, zmainloop() does nothing. void zmainloop(int skip) { static int xskip = 0; if (skip) { if (++xskip < skip) return; xskip = 0; } if (! pthread_equal(pthread_self(),zfuncs::tid_main)) return; // thread caller, do nothing v.5.0 while (gtk_events_pending()) // gdk_flush() removed v.5.2 gtk_main_iteration_do(0); // use gtk_main_iteration_do v.5.2 return; } /**************************************************************************/ // crash if current execution is not the main() thread void zthreadcrash() { if (pthread_equal(pthread_self(),zfuncs::tid_main)) return; zappcrash("forbidden function called from thread"); return; } /**************************************************************************/ // write message to GTK text view window // line: +N existing lines from top (replace) // -N existing lines from bottom (replace) // 0 next line (add new line at bottom) // scroll logic assumes only one \n per message void wprintx(GtkWidget *mLog, int line, cchar *message, cchar *font) { GtkTextMark *endMark; GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; int nlines, scroll = 0; if (! mLog) { // if no GUI use STDOUT printz("%s",message); return; } zthreadcrash(); // thread usage not allowed textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! textBuff) return; // v.5.5 endMark = gtk_text_buffer_get_mark(textBuff,"wpxend"); // get my end mark if (! endMark) { gtk_text_buffer_get_end_iter(textBuff,&iter1); // new buffer, set my end mark endMark = gtk_text_buffer_create_mark(textBuff,"wpxend",&iter1,0); } nlines = gtk_text_buffer_get_line_count(textBuff); // lines now in buffer if (line == 0) scroll++; // auto scroll is on if (line < 0) { line = nlines + line + 1; // last lines: -1, -2 ... if (line < 1) line = 1; // above top, use line 1 } if (line > nlines) line = 0; // below bottom, treat as append if (line == 0) gtk_text_buffer_get_end_iter(textBuff,&iter1); // append new line if (line > 0) { gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1); // old line start if (line < nlines) gtk_text_buffer_get_iter_at_line(textBuff,&iter2,line); // old line end if (line == nlines) // or buffer end gtk_text_buffer_get_end_iter(textBuff,&iter2); gtk_text_buffer_delete(textBuff,&iter1,&iter2); // delete old line } if (font) { // insert new line with caller font fontag = gtk_text_buffer_create_tag(textBuff,0,"font",font,0); // fontag is textBuff specific gtk_text_buffer_insert_with_tags(textBuff,&iter1,message,-1,fontag,null); } else // insert new line with default font gtk_text_buffer_insert(textBuff,&iter1,message,-1); if (scroll) // scroll line into view gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(mLog),endMark,0,0,1,1); zmainloop(); return; } void wprintf(GtkWidget *mLog, int line, cchar *format, ... ) // "printf" version { va_list arglist; char message[1000]; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); wprintx(mLog,line,message); return; } void wprintf(GtkWidget *mLog, cchar *format, ... ) // "printf", scrolling output { va_list arglist; char message[1000]; va_start(arglist,format); vsnprintf(message,999,format,arglist); // stop overflow, remove warning va_end(arglist); wprintx(mLog,0,message); return; } /**************************************************************************/ // scroll a text view window to put a given line on screen // 1st line = 1. for last line use line = 0. void wscroll(GtkWidget *mLog, int line) { GtkTextBuffer *textbuff; GtkTextIter iter; GtkTextMark *mark; if (! mLog) return; zthreadcrash(); // thread usage not allowed textbuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! textbuff) return; // v.5.6 if (line <= 0) line = gtk_text_buffer_get_line_count(textbuff); line = line - 1; gtk_text_buffer_get_iter_at_line(textbuff,&iter,line); mark = gtk_text_buffer_create_mark(textbuff,0,&iter,0); // bugfix, gtk_text_view_scroll_to_iter() gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(mLog),mark,0,0,1,1); // fails with no error return; } /**************************************************************************/ // clear a text view window and get a new buffer (a kind of defrag) void wclear(GtkWidget *mLog) { GtkTextBuffer *buff; if (! mLog) return; zthreadcrash(); // thread usage not allowed buff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! buff) return; // v.5.6 gtk_text_buffer_set_text(buff,"",-1); return; } // clear a text view window from designated line to end of buffer void wclear(GtkWidget *mLog, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; if (! mLog) return; zthreadcrash(); // thread usage not allowed textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (! textBuff) return; // v.5.6 gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1); // iter at line start gtk_text_buffer_get_end_iter(textBuff,&iter2); gtk_text_buffer_delete(textBuff,&iter1,&iter2); // delete existing line return; } /**************************************************************************/ // Read from a text widget, one line at a time. // Set ftf = 1 for first call (will be returned = 0). // The next line of text is returned, or null if no more text. // A final \n character is removed if present. // Only one line is remembered, so it must be copied before the next call. char * wscanf(GtkWidget *mLog, int & ftf) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; static char *precs = 0, *prec1, *pret; static int cc; if (! mLog) return 0; zthreadcrash(); // thread usage not allowed if (ftf) { // get all window text ftf = 0; if (precs) g_free(precs); // free prior memory if there textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); // get all text if (! textBuff) return 0; // v.5.6 gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); precs = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); prec1 = precs; // 1st record } if (! precs || (*prec1 == 0)) // no more records { if (precs) g_free(precs); precs = 0; return 0; } cc = 0; while ((prec1[cc] != 0) && (prec1[cc] != '\n')) cc++; // scan for terminator pret = prec1; prec1 = prec1 + cc; // next record if (*prec1 == '\n') prec1++; pret[cc] = 0; // replace \n with 0 return pret; } /**************************************************************************/ // dump text window into file // return: 0: OK +N: error int wfiledump_maxcc = 0; int wfiledump(GtkWidget *mLog, char *filespec) { FILE *fid; char *prec; int ftf, err, cc; if (! mLog) return 0; fid = fopen(filespec,"w"); // open file if (! fid) { zmessageACK(null,null,ZTX("cannot open file %s"),filespec); return 1; } wfiledump_maxcc = 0; ftf = 1; while (true) { prec = wscanf(mLog,ftf); // get text line if (! prec) break; fprintf(fid,"%s\n",prec); // output with \n cc = strlen(prec); if (cc > wfiledump_maxcc) wfiledump_maxcc = cc; } err = fclose(fid); // close file if (err) { zmessageACK(null,null,"file close error"); return 2; } else return 0; } /**************************************************************************/ // save text window to file, via file chooser dialog void wfilesave(GtkWidget *mLog) { int err; char *file; if (! mLog) return; file = zgetfile(ZTX("save screen to file"),"save","screen-save.txt"); if (! file) return; err = wfiledump(mLog,file); if (err) zmessageACK(null,null,"save screen failed (%d)",err); zfree(file); return; } /**************************************************************************/ // print text window to default printer // use landscape mode if max. print line > A4 width void wprintp(GtkWidget *mLog) { int pid, err; char tempfile[50]; if (! mLog) return; pid = getpid(); snprintf(tempfile,49,"/tmp/wprintp-%d",pid); err = wfiledump(mLog,tempfile); if (err) return; if (wfiledump_maxcc < 97) err = shell_ack("lp -o %s -o %s -o %s -o %s -o %s -o %s %s", "cpi=14","lpi=8","page-left=50","page-top=50", "page-right=40","page-bottom=40",tempfile); else err = shell_ack("lp -o %s -o %s -o %s -o %s -o %s -o %s -o %s %s", "landscape","cpi=14","lpi=8","page-left=50","page-top=50", "page-right=40","page-bottom=40",tempfile); return; } /**************************************************************************/ // Set a function to be called when a GTK text view widget is mouse-clicked. // Function returns the clicked line number and position, both zero based. // A wrapped line is still one logical line. // Note: cannot be used on text windows that are edited. // The called function looks like this: // void clickfunc(GtkWidget *widget, int &line, int &pos) void textwidget_set_clickfunc(GtkWidget *widget, clickfunc_t clickfunc) { int textwidget_mousefunc(GtkWidget *widget, GdkEventButton *event, clickfunc_t clickfunc); gtk_widget_add_events(widget,GDK_ALL_EVENTS_MASK); // connect events v.5.2 G_SIGNAL(widget,"event",textwidget_mousefunc,clickfunc); return; } int textwidget_mousefunc(GtkWidget *widget, GdkEventButton *event, clickfunc_t clickfunc) { static GdkCursor *arrowcursor = 0; GdkWindow *gdkwin; GtkTextIter iter1; int mpx, mpy, tbx, tby, line, pos; #define TEXT GTK_TEXT_WINDOW_TEXT #define VIEW GTK_TEXT_VIEW #define ARROW GDK_TOP_LEFT_ARROW if (! arrowcursor) arrowcursor = gdk_cursor_new(ARROW); gdkwin = gtk_text_view_get_window(VIEW(widget),TEXT); if (gdkwin) gdk_window_set_cursor(gdkwin,arrowcursor); // why must this be repeated? v.5.5 if (event->type != GDK_BUTTON_RELEASE) return 0; // v.5.7 mpx = int(event->x); // mouse click position mpy = int(event->y); gtk_text_view_window_to_buffer_coords(VIEW(widget),TEXT,mpx,mpy,&tbx,&tby); gtk_text_view_get_iter_at_location(VIEW(widget),&iter1,tbx,tby); line = gtk_text_iter_get_line(&iter1); // clicked line pos = gtk_text_iter_get_line_offset(&iter1); // clicked position clickfunc(widget,line,pos); // call user function return 0; // v.5.6 } // get a given line of text from a GTK text view widget // returned text is subject for zfree() char * textwidget_get_line(GtkWidget *widget, int line, int hilite) { GtkTextBuffer *textbuffer; GtkTextIter iter1, iter2; char *text, *ztext; int cc; textbuffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (! textbuffer) return 0; // v.5.6 gtk_text_buffer_get_iter_at_line(textbuffer,&iter1,line); iter2 = iter1; gtk_text_iter_forward_line(&iter2); if (hilite) // highlight selected line v.5.6 gtk_text_buffer_select_range(textbuffer,&iter1,&iter2); text = gtk_text_buffer_get_text(textbuffer,&iter1,&iter2,0); if (! text) return 0; ztext = zstrdup(text); zfree(text); cc = strlen(ztext); while (cc && ztext[cc-1] < ' ') cc--; ztext[cc] = 0; if (cc == 0) { zfree(ztext); return 0; } return ztext; } // get the clicked word within the line // words are defined by line starts and ends, and the given delimiter // returns word and delimiter (&end) char * textwidget_get_word(char *line, int pos, cchar *dlims, char &end) { char *pp1, *pp2, *ztext; int cc; if (! line) return 0; pos = utf8_position(line,pos); // graphic position to byte position if (pos < 0) return 0; // v.5.5 pp1 = line + pos; if (! *pp1 || strchr(dlims,*pp1)) return 0; // reject edge position or delimiter while (pp1 > line && ! strchr(dlims,pp1[-1])) pp1--; // find start of word pp2 = pp1; while (pp2[1] && ! strchr(dlims,pp2[1])) pp2++; // find following delimiter or EOL end = pp2[1]; // return delimiter while (*pp1 == ' ') pp1++; // no leading or trailing blanks while (*pp2 == ' ') pp2--; cc = pp2 - pp1 + 1; if (cc < 1) return 0; // all blanks? ztext = (char *) zmalloc(cc+1); strncpy0(ztext,pp1,cc+1); return ztext; } /**************************************************************************/ // get the screen and mouse device for a given widget // v.5.8 // returns 0 on success, +N on error int get_mouse_device(GtkWidget *widget, GdkScreen **screen, GdkDevice **mouse) { GdkWindow *window; GdkDisplay *display; // X workstation (KB, mouse, screen) GdkDeviceManager *manager; // knows screen / mouse associations zthreadcrash(); // thread usage not allowed window = gtk_widget_get_window(widget); if (! window) return 1; display = gdk_window_get_display(window); if (! display) return 2; *screen = gdk_window_get_screen(window); if (! *screen) return 3; manager = gdk_display_get_device_manager(display); if (! manager) return 4; *mouse = gdk_device_manager_get_client_pointer(manager); if (! *mouse) return 5; return 0; } /************************************************************************** simplified GTK menu bar, tool bar, status bar functions These functions simplify the creation of GTK menus and toolbars. The functionality is limited but adequate for most purposes. mbar = create_menubar(vbox) create menubar mitem = add_menubar_item(mbar, label, func) add menu item to menubar msub = add_submenu_item(mitem, label, func, tip) add submenu item to menu or submenu tbar = create_toolbar(vbox, iconsize) create toolbar add_toolbar_button(tbar, label, tip, icon, func) add button to toolbar stbar = create_stbar(vbox) create status bar stbar_message(stbar, message) display message in status bar These functions to the following: * create a menu bar and add to existing window verticle packing box * add menu item to menu bar * add submenu item to menu bar item or submenu item * create a toolbar and add to existing window * add button to toolbar, using stock icon or custom icon * create a status bar and add to existing window * display a message in the status bar argument definitions: vbox GtkWidget * a verticle packing box (in a window) mbar GtkWidget * reference for menu bar popup GtkWidget * reference for popup menu mitem GtkWidget * reference for menu item (in a menu bar) msub GtkWidget * reference for submenu item (in a menu) label cchar * menu or toolbar name or label tbar GtkWidget * reference for toolbar tip cchar * tool button tool tip (popup text via mouse-over) icon cchar * stock icon name or custom icon file name (see below) func see below menu or tool button response function arg cchar * argument to response function stbar int reference for status bar message cchar * message to display in status bar The icon argument for the function add_toolbar_button() has two forms. For a GTK stock item referenced with a macro like GTK_STOCK_OPEN, use the corresponding text name, like "gtk-open". For a custom icon, use the icon's file name like "my-icon.png". The file is expected to be in get_zdatadir()/icons. The icon file may be any size, and is resized to 32x32 for use on the toolbar. If the file is not found, the stock icon "gtk-missing-image" is used (".png" and ".jpg" files both work). For a button with no icon (text label only), use 0 or null for the icon argument. For a menu separator, use the menu name "separator". For a toolbar separator, use the label "separator". For a title menu (no response function), set the response function to null. The response function for both menus and toolbar buttons looks like this: void func(GtkWidget *, cchar *) The following macro is also supplied to simplify the coding of response functions: G_SIGNAL(window,event,func,arg) which expands to: g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg) ***************************************************************************/ // create menu bar and add to vertical packing box GtkWidget * create_menubar(GtkWidget *vbox) // icon size removed { GtkWidget *wmbar; wmbar = gtk_menu_bar_new(); gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0); return wmbar; } // add menu item to menu bar, with optional response function GtkWidget * add_menubar_item(GtkWidget *wmbar, cchar *mname, cbFunc func) { GtkWidget *wmitem; wmitem = gtk_menu_item_new_with_label(mname); gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem); if (func) G_SIGNAL(wmitem,"activate",func,mname); return wmitem; } // add submenu item to menu item, with optional response function GtkWidget * add_submenu_item(GtkWidget *wmitem, cchar *mlab, cbFunc func, cchar *mtip) { GtkWidget *wmsub, *wmsubitem; wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem)); // add submenu if not already if (wmsub == null) { wmsub = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(wmitem),wmsub); } if (strEqu(mlab,"separator")) wmsubitem = gtk_separator_menu_item_new(); else wmsubitem = gtk_menu_item_new_with_label(mlab); // add menu item with label only gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem); // append submenu item to submenu if (func) G_SIGNAL(wmsubitem,"activate",func,mlab); // connect optional response function if (mtip) g_object_set(G_OBJECT(wmsubitem),"tooltip-text",mtip,null); // add optional popup menu tip v.5.2 return wmsubitem; } /**************************************************************************/ // create toolbar and add to vertical packing box int tbIconSize = 24; // valid during toolbar construction GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize) { using namespace zfuncs; GtkWidget *wtbar; wtbar = gtk_toolbar_new(); // gtk_toolbar_set_style(GTK_TOOLBAR(wtbar),GTK_TOOLBAR_BOTH); // v.5.8 gtk_box_pack_start(GTK_BOX(vbox),wtbar,0,0,0); tbIconSize = iconsize; return wtbar; } // add toolbar button with label and icon ("iconfile.png") and tool tip // at least one of label and icon should be present GtkWidget * add_toolbar_button(GtkWidget *wtbar, cchar *blab, cchar *btip, cchar *icon, cbFunc func) { using namespace zfuncs; GtkToolItem *tbutton; GError *gerror = 0; GdkPixbuf *pixbuf; GtkWidget *wicon = 0; char iconpath[200]; if (blab && strEqu(blab,"separator")) { tbutton = gtk_separator_tool_item_new(); gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1); return (GtkWidget *) tbutton; } if (icon && *icon) { // get icon pixbuf *iconpath = 0; strncatv(iconpath,199,zicondir,"/",icon,null); pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,tbIconSize,tbIconSize,1,&gerror); if (pixbuf) wicon = gtk_image_new_from_pixbuf(pixbuf); } tbutton = gtk_tool_button_new(wicon,blab); // v.5.8 if (! wicon) gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(tbutton),"gtk-missing-image"); if (btip) gtk_tool_item_set_tooltip_text(tbutton,btip); gtk_tool_item_set_homogeneous(tbutton,0); gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1); if (func) G_SIGNAL(tbutton,"clicked",func,blab); return (GtkWidget *) tbutton; } /**************************************************************************/ // create a status bar and add to the start of a packing box GtkWidget * create_stbar(GtkWidget *pbox) { GtkWidget *stbar; static PangoFontDescription *fontdesc; stbar = gtk_statusbar_new(); fontdesc = pango_font_description_from_string("Monospace 9"); gtk_widget_override_font(stbar,fontdesc); gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0); gtk_widget_show(stbar); return stbar; } // display message in status bar int stbar_message(GtkWidget *wstbar, cchar *message) { static int ctx = -1; if (ctx == -1) ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all"); gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx); gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message); return 0; } /************************************************************************** Popup Menu popup = create_popmenu() create a popup menu mitem = add_popmenu_item(popup, label, func, arg, tip) add menu item to popup menu popup_menu(GtkWidget *widget, popup) popup the menu at mouse position GtkWidget *popup, *mitem cchar *label, *arg, *tip void func(GtkWidget *, cchar *arg) Call 'create_popmenu' and then 'add_popmenu_item' for each item in the menu. 'label' is the menu name, 'func' the response function, 'arg' an argument for 'func', and 'tip' is a tool-tip. 'arg' and 'tip' may be null. A call to 'popup_menu' will show all menu entries at the mouse position. Clicking an entry will call the respective response function. Hovering on the entry will show the tool-tip. The response function looks like this: void func(GtkWidget *, cchar *) ***/ // create a popup menu GtkWidget * create_popmenu() { int popmenu_event(GtkWidget *, GdkEvent *); GtkWidget *popmenu; popmenu = gtk_menu_new(); gtk_widget_add_events(popmenu,GDK_BUTTON_PRESS_MASK); // v.5.5 G_SIGNAL(popmenu,"button-press-event",popmenu_event,0); // no event passed in Ubuntu 13.10 v.5.7 return popmenu; } // handle mouse button event in a popup menu int popmenu_event(GtkWidget *popmenu, GdkEvent *event) // not called in Ubuntu 13.10 v.5.7 { if (((GdkEventButton *) event)->button != 1) // if not left mouse, kill menu gtk_menu_popdown(GTK_MENU(popmenu)); return 0; } // add a menu item to a popup menu GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, cbFunc func, cchar *arg, cchar *mtip) { GtkWidget *wmitem; wmitem = gtk_menu_item_new_with_label(mname); if (mtip) g_object_set(G_OBJECT(wmitem),"tooltip-text",mtip,null); // add optional popup menu tip v.5.5 gtk_menu_shell_append(GTK_MENU_SHELL(popmenu),wmitem); if (func) { if (arg) G_SIGNAL(wmitem,"activate",func,arg); // call func with arg v.5.3 else G_SIGNAL(wmitem,"activate",func,mname); // call func with menu name } return wmitem; } // pop-up the menu at current mouse position void popup_menu(GtkWidget *widget, GtkWidget *popmenu) { void popup_moveloc(GtkMenu *, int *, int *, int *, void *); // v.5.8 gtk_menu_popup(GTK_MENU(popmenu),0,0,popup_moveloc,widget,1,GDK_CURRENT_TIME); gtk_widget_show_all(popmenu); return; } // move popup menu to the right of the mouse to avoid automatically // v.5.8 // selecting the first entry when the mouse button is released void popup_moveloc(GtkMenu *menu, int *mx, int *my, int *push, void *widget) { GdkScreen *screen; GdkDevice *mouse; int err; err = get_mouse_device((GtkWidget *) widget,&screen,&mouse); if (err) return; gdk_device_get_position(mouse,&screen,mx,my); *mx += 20; *my += 10; *push = 0; return; } /************************************************************************** Customizable Graphic Popup Menu void gmenuz(GtkWidget *parent, cchar *configfile, callbackfunc) Open a popup window with a customizable graphic menu. parent parent window or null configfile menu configuration file, will be created if missing callbackfunc callback function to receive clicked menu entry: typedef void callbackfunc(cchar *menu) This function allows an application to offer a customizable menu which a user can populate with frequently used (menu) functions, arranged as desired. A menu entry selected by the user is passed to the application for execution. The initial popup window is blank. Right click an empty space on the popup window to define a new menu entry. Right click an existing entry to modify it. Use the resulting dialog to define or change the menu entry. menu text optional text appearing in the popup window menu func text that is passed to the application callback function menu icon optional menu icon: /directory.../filename.png icon size rendered icon size in the popup window, 24x24 to 64x64 pixels close checkbox: option to close the popup window when menu is used Left drag a menu entry to move it somewhere else on the popup window. The popup window can be resized to fit the contained menu entries. Left click a menu entry to select the menu. The callback function will be called to execute the menu function. If "close" was checked, the popup window will close. All menu settings are saved in the supplied configuration file whenever the popup window is closed, if any changes were made since it was opened. Icon files are copied into the same directory as the configuration file and these copies are used. The icon selected when the menu entry is created can disappear without consequence. If the [x] window kill button is pressed, the window is closed and the calling program is informed by passing "quit" to the callback function. layout of menu configuration file: popup xpos ypos width height popup window position and size posn xpos ypos ww hh menu position in popup window menu menu text menu text on popup window func funcname argument for user callback function icon /.../file.png optional menu icon file size NN optional icon size (default 24) kill optional flag, kill window when used menu menu text next menu entry ... ***************************************************************************/ namespace gmenuznames { #define maxME 200 // max. menu entries #define maxText 1000 // max. text size, all menu fields #define menuFont "sans 9" // menu font #define iconSize 24 // menu icon size (default) typedef void callbackfunc(cchar *menu); // user callback function callbackfunc *gmenuzcallback; char *menuconfigfile = 0; // configuration file from user GtkWidget *mWin, *layout; // popup and drawing windows GtkWidget *pWin; // parent window int winposx=100, winposy=100, winww=400, winhh=300; // initial popup WRT parent window struct menuent { // menu entry on popup window int xpos, ypos, ww, hh; // layout position, extent char *menu; // text on window on null char *func; // func name for user callback char *icon; // icon file or null GdkPixbuf *pixbuf; // icon pixbuf or null int size; // icon size or zero int kill; // kill popup window when menu used }; menuent menus[200]; // menu entries int NME = 0; // entry count zdialog *zdedit = 0; // active edit dialog int mpx, mpy; // mouse click/drag position int me; // current menu entry int Fchanged = 0; // flag, menu edited or resized int Fquit = 0; // popup is being closed int Fbusy = 0; // popup is active void wpaint(GtkWidget *, cairo_t *); // window repaint - draw event void resize(); // window resize event void quit(); // kill window and exit void update_configfile(); // update menu configuration file void mouse_event(GtkWidget *, GdkEventButton *, void *); // mouse event function void KB_event(GtkWidget *, GdkEventKey *, void *); // KB event function void draw_text(cairo_t *, char *, int px, int py, int &ww, int &hh); // draw text and return pixel extent void edit_menu(); // dialog to create/edit menu entry int edit_menu_event(zdialog *zd, cchar *event); // dialog event function } // user callable function to build the menu from user's configuration file void gmenuz(GtkWidget *parent, cchar *title, cchar *ufile, gmenuznames::callbackfunc ufunc) { using namespace gmenuznames; FILE *fid; int nn, xx, yy, ww, hh, size; int pposx, pposy; int xpos, ypos; char *pp, buff[maxText]; GdkPixbuf *pixbuf; GError *gerror; if (Fbusy) return; // don't allow multiple popups pWin = parent; // get parent window if (menuconfigfile) zfree(menuconfigfile); menuconfigfile = zstrdup(ufile); // get menu configuration file gmenuzcallback = ufunc; // get user callback function NME = 0; fid = fopen(menuconfigfile,"r"); // read window geometry if (fid) { nn = fscanf(fid," popup %d %d %d %d",&xx,&yy,&ww,&hh); // get popup window position and size if (nn == 4 && ww > 50 && ww < 1000 && hh > 50 && hh < 1000) { winposx = xx; // OK to use winposy = yy; winww = ww; winhh = hh; } while (true) { pp = fgets_trim(buff,maxText-1,fid,1); // read next menu entry if (! pp) break; if (strnEqu(pp,"posn ",5)) { // position in popup window if (NME == maxME) { zmessageACK(mWin,0,"exceeded %d menu entries",maxME); break; } me = NME; // new entry NME++; // entry count memset(&menus[me],0,sizeof(menuent)); // clear all menu data nn = sscanf(pp+5," %d %d ",&xpos,&ypos); // position in popup window if (nn != 2) xpos = ypos = 100; if (xpos > 1000) xpos = 1000; if (ypos > 1000) ypos = 1000; menus[me].xpos = xpos; menus[me].ypos = ypos; } if (strnEqu(pp,"menu ",5)) { // menu text if (strlen(pp+5) > 0) menus[me].menu = zstrdup(pp+5); // get menu text else menus[me].menu = 0; } if (strnEqu(pp,"func ",5)) { // function name if (strlen(pp+5)) menus[me].func = zstrdup(pp+5); else menus[me].func = 0; } if (strnEqu(pp,"icon ",5)) { // menu icon file if (strlen(pp+5)) { menus[me].icon = zstrdup(pp+5); gerror = 0; pixbuf = gdk_pixbuf_new_from_file(pp+5,&gerror); if (! pixbuf && gerror) printz("%s \n",gerror->message); menus[me].pixbuf = pixbuf; } else menus[me].pixbuf = 0; } if (strnEqu(pp,"size ",5)) { size = atoi(pp+5); if (size < 24) size = 24; if (size > 64) size = 64; menus[me].size = size; } if (strnEqu(pp,"kill",4)) // kill window flag menus[me].kill = 1; } fclose(fid); } Fchanged = 0; // no changes yet Fquit = 0; // not being closed Fbusy = 1; mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create popup window for menu entries if (! pWin) pposx = pposy = 0; // no parent window else { gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (NW corner) gtk_window_set_transient_for(GTK_WINDOW(mWin),GTK_WINDOW(pWin)); // popup window belongs to parent } if (title) gtk_window_set_title(GTK_WINDOW(mWin),title); // v.5.8 winposx += pposx; // popup position relative to parent winposy += pposy; gtk_window_set_default_size(GTK_WINDOW(mWin),winww,winhh); // set size and position gtk_window_move(GTK_WINDOW(mWin),winposx,winposy); layout = gtk_layout_new(0,0); // create drawing window gtk_container_add(GTK_CONTAINER(mWin),layout); // add to popup window G_SIGNAL(mWin,"destroy",quit,0); // connect signals to windows G_SIGNAL(mWin,"delete_event",quit,0); G_SIGNAL(mWin,"notify",resize,0); G_SIGNAL(mWin,"key-release-event",KB_event,0); // connect KB key release event G_SIGNAL(layout,"draw",wpaint,0); gtk_widget_add_events(layout,GDK_BUTTON_PRESS_MASK); // connect mouse events gtk_widget_add_events(layout,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(layout,GDK_BUTTON_MOTION_MASK); gtk_widget_add_events(layout,GDK_POINTER_MOTION_MASK); G_SIGNAL(layout,"button-press-event",mouse_event,0); G_SIGNAL(layout,"button-release-event",mouse_event,0); G_SIGNAL(layout,"motion-notify-event",mouse_event,0); gtk_widget_show_all(mWin); // show all widgets return; } // paint window when created, exposed, resized void gmenuznames::wpaint(GtkWidget *, cairo_t *cr) { using namespace gmenuznames; GdkPixbuf *pixbuf; char *text, *text2; int xpos, ypos, ww, hh, size, yadd; for (int me = 0; me < NME; me++) // loop all menu entries { xpos = menus[me].xpos; // window position ypos = menus[me].ypos; text = menus[me].menu; // menu text pixbuf = menus[me].pixbuf; // icon size = menus[me].size; // size if (pixbuf) { // draw icon at window position gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos); cairo_paint(cr); if (! size) size = iconSize; // use default if not specified } else size = 0; yadd = 0; if (pixbuf) yadd = size + 2; // extra space under icon if (text) { text2 = (char *) zmalloc(strlen(text)+2); // replace "\n" with newline repl_1str(text,text2,"\\n","\n"); draw_text(cr,text2,xpos,ypos+yadd,ww,hh); // returns size of text written zfree(text2); } else ww = hh = 0; if (ww < size) ww = size; // menu entry enclosing rectangle hh += yadd; menus[me].ww = ww; menus[me].hh = hh; } return; } // resize event - save current window size void gmenuznames::resize() { using namespace gmenuznames; int xx, yy, ww, hh; if (Fquit) return; // ignore bogus call when killed gtk_window_get_position(GTK_WINDOW(mWin),&xx,&yy); gtk_window_get_size(GTK_WINDOW(mWin),&ww,&hh); if (xx == winposx && yy == winposy && ww == winww && hh == winhh) return; // no change winposx = xx; winposy = yy; winww = ww; winhh = hh; Fchanged = 1; // mark window changed return; } // [x] kill window // Save current menu status for next session. void gmenuznames::quit() { using namespace gmenuznames; Fquit = 1; Fbusy = 0; if (Fchanged) update_configfile(); gtk_widget_destroy(mWin); gmenuzcallback("quit"); // inform host program v.5.4 return; } // menu changed, save all menu data to menu config. file void gmenuznames::update_configfile() { using namespace gmenuznames; char *pp, *pxbfile; int pposx, pposy; FILE *fid; GError *gerror; if (pWin) gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (may have moved) else pposx = pposy = 0; winposx -= pposx; // popup position relative to parent winposy -= pposy; fid = fopen(menuconfigfile,"w"); // open for write if (! fid) { zmessageACK(mWin,0," %s \n %s",menuconfigfile,strerror(errno)); // diagnose permissions error v.5.7 return; } fprintf(fid,"popup %d %d %d %d \n",winposx,winposy,winww,winhh); for (me = 0; me < NME; me++) // write all menu entries to file { if (! menus[me].menu && ! menus[me].pixbuf) { // no text and no icon printz("gmenuz: skip empty menu entry \n"); continue; } fprintf(fid,"\n"); // blank line separator fprintf(fid,"posn %d %d \n",menus[me].xpos, menus[me].ypos); // menu position in window if (menus[me].menu) // menu text fprintf(fid,"menu %s \n",menus[me].menu); if (menus[me].func) // menu function (text) fprintf(fid,"func %s \n",menus[me].func); if (menus[me].pixbuf) { // pixbuf image for menu icon pxbfile = strdupz(menuconfigfile,20); pp = pxbfile + strlen(pxbfile); // create a local PNG file for pixbuf snprintf(pp,20,"-pixbuf-%03d.png",me); gerror = 0; gdk_pixbuf_save(menus[me].pixbuf,pxbfile,"png",&gerror,null); // write pixbuf to file if (gerror) printz("%s \n %s \n",menus[me].menu,gerror->message); else fprintf(fid,"icon %s \n",pxbfile); // pixbuf file name in menu file zfree(pxbfile); } if (menus[me].size) // icon size fprintf(fid,"size %d \n",menus[me].size); if (menus[me].kill) fprintf(fid,"kill \n"); // kill window flag } fclose(fid); Fchanged = 0; return; } // mouse event function - capture buttons and drag movements void gmenuznames::mouse_event(GtkWidget *, GdkEventButton *event, void *) { using namespace gmenuznames; static int bdtime = 0, butime = 0; static int Lmouse = 0, Rmouse = 0, Fdrag = 0; static int elapsed, mpx0 = 0, mpy0 = 0; int Fclick, dx, dy, xpos, ypos; mpx = int(event->x); // mouse position in window mpy = int(event->y); if (event->type == GDK_BUTTON_PRESS) { Lmouse = Rmouse = Fdrag = 0; if (event->button == 1) Lmouse++; // left or right mouse button if (event->button == 3) Rmouse++; bdtime = event->time; for (me = 0; me < NME; me++) // look for clicked menu entry { if (mpx < menus[me].xpos) continue; if (mpy < menus[me].ypos) continue; if (mpx > menus[me].xpos + menus[me].ww) continue; if (mpy > menus[me].ypos + menus[me].hh) continue; break; } if (me < NME) { // menu item clicked (selected) mpx0 = mpx; // set new drag origin mpy0 = mpy; } else me = -1; // indicate empty space clicked } if (event->type == GDK_BUTTON_RELEASE) { Fclick = 0; butime = event->time; elapsed = butime - bdtime; // button down time, milliseconds if (elapsed < 500 && ! Fdrag) Fclick = 1; // mouse clicked if (me >= 0 && Fclick && Lmouse) { // menu entry was left-clicked if (menus[me].func) gmenuzcallback(menus[me].func); // caller user function(func) if (menus[me].kill) quit(); // close menu after app launch } else if (Fclick && Rmouse) // menu entry or empty space right-clicked edit_menu(); // edit menu else if (me >= 0 && Fdrag) { // menu entry drag ended xpos = menus[me].xpos; // align to 16-pixel raster ypos = menus[me].ypos; xpos = 16 * ((xpos + 8) / 16) + 4; // min. 4 pixels from edge ypos = 16 * ((ypos + 8) / 16) + 4; menus[me].xpos = xpos; menus[me].ypos = ypos; gtk_widget_queue_draw(layout); // repaint window Fdrag = 0; Fchanged = 1; // mark menu revised } Lmouse = Rmouse = 0; // mouse click action completed } if (event->type == GDK_MOTION_NOTIFY) // mouse movement { if (me >= 0 && Lmouse) { // menu drag underway dx = mpx - mpx0; dy = mpy - mpy0; if (Fdrag || (abs(dx) + abs(dy) > 15)) { // ignore small drags v.5.5 Fdrag++; mpx0 = mpx; // set new drag origin mpy0 = mpy; menus[me].xpos += dx; // add motion to image position menus[me].ypos += dy; gtk_widget_queue_draw(layout); // repaint window } } } return; } // KB release event function - send KB F1 key to main app void gmenuznames::KB_event(GtkWidget *, GdkEventKey *kbevent, void *) { int KBkey = kbevent->keyval; if (KBkey == GDK_KEY_F1) KBstate(kbevent,0); // v.5.6 return; } // draw text into layout and return pixel dimensions of enclosing rectangle void gmenuznames::draw_text(cairo_t *cr, char *text, int x, int y, int &w, int &h) { using namespace gmenuznames; static PangoFontDescription *pfont = 0; static PangoLayout *playout = 0; if (! pfont) { pfont = pango_font_description_from_string(menuFont); // first call, get font sizing poop playout = gtk_widget_create_pango_layout(layout,0); pango_layout_set_font_description(playout,pfont); } pango_layout_set_text(playout,text,-1); // compute layout pango_layout_get_pixel_size(playout,&w,&h); // pixel width and height of layout cairo_move_to(cr,x,y); // draw layout with text cairo_set_source_rgb(cr,0,0,0); pango_cairo_show_layout(cr,playout); return; } // dialog to create a new menu entry from user inputs void gmenuznames::edit_menu() { using namespace gmenuznames; if (me < 0) { // new menu entry if (NME == maxME) { zmessageACK(mWin,0,"capacity limit exceeded"); return; } me = NME; memset(&menus[me],0,sizeof(menuent)); // clear all data } if (! zdedit) // create dialog if not already { zdedit = zdialog_new("edit menu entry",mWin,"apply","delete","cancel",null); zdialog_add_widget(zdedit,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zdedit,"vbox","vb1","hb1",0,"homog"); zdialog_add_widget(zdedit,"vbox","vb2","hb1",0,"homog|expand"); // menu text [_______________] zdialog_add_widget(zdedit,"label","lab11","vb1","menu text"); // menu func [_______________] zdialog_add_widget(zdedit,"label","lab12","vb1","menu func"); // menu icon [_______________] [browse] zdialog_add_widget(zdedit,"label","lab13","vb1","menu icon"); // icon size [___|+-] close window [x] zdialog_add_widget(zdedit,"label","lab14","vb1","icon size"); // zdialog_add_widget(zdedit,"entry","text","vb2",0,"scc=40"); // [apply] [delete] [cancel] zdialog_add_widget(zdedit,"entry","func","vb2",0,"scc=40"); zdialog_add_widget(zdedit,"hbox","hb2","vb2",0,"expand"); zdialog_add_widget(zdedit,"entry","icon","hb2",0,"expand"); zdialog_add_widget(zdedit,"button","browse","hb2","browse","space=5"); zdialog_add_widget(zdedit,"hbox","hb3","vb2"); zdialog_add_widget(zdedit,"spin","size","hb3","24|64|1|24"); zdialog_add_widget(zdedit,"check","kill","hb3","close window","space=30"); zdialog_run(zdedit,edit_menu_event); } if (menus[me].menu) // stuff menu text into dialog zdialog_stuff(zdedit,"text",menus[me].menu); else zdialog_stuff(zdedit,"text",""); if (menus[me].func) // stuff menu function zdialog_stuff(zdedit,"func",menus[me].func); else zdialog_stuff(zdedit,"func",""); if (menus[me].icon) // stuff icon file zdialog_stuff(zdedit,"icon",menus[me].icon); else zdialog_stuff(zdedit,"icon",""); if (menus[me].size) // stuff icon size zdialog_stuff(zdedit,"size",menus[me].size); if (menus[me].kill) // stuff window kill flag zdialog_stuff(zdedit,"kill",1); else zdialog_stuff(zdedit,"kill",0); if (me == NME) { // new menu entry menus[me].xpos = mpx; // initial position from mouse menus[me].ypos = mpy; } return; } // menu entry dialog event function int gmenuznames::edit_menu_event(zdialog *zd, cchar *event) { using namespace gmenuznames; char text[maxText]; char *iconfile; int size; GdkPixbuf *pixbuf; GError *gerror; if (strEqu(event,"browse")) { // browse for icon file iconfile = zgetfile("select icon","file",0); if (iconfile) zdialog_stuff(zd,"icon",iconfile); } if (zd->zstat) // dialog complete { if (zd->zstat == 2) { // [delete] - delete menu entry if (me < 0 || me >= NME) return 0; for (int me2 = me; me2 < NME-1; me2++) // remove menu entry and close hole menus[me2] = menus[me2+1]; NME--; Fchanged = 1; // mark menu revised gtk_widget_queue_draw(layout); // repaint window } if (zd->zstat != 1) { // not [apply] - kill dialog zdialog_free(zdedit); return 0; } // [apply] - update menu from dialog data zdialog_fetch(zd,"text",text,maxText); if (*text) menus[me].menu = zstrdup(text); // menu text, optional else menus[me].menu = 0; zdialog_fetch(zd,"func",text,maxText); // menu function name strTrim2(text); if (*text) menus[me].func = zstrdup(text); else menus[me].func = 0; zdialog_fetch(zd,"icon",text,maxText); // menu icon file, optional strTrim2(text); if (*text) { zdialog_fetch(zd,"size",size); // icon size gerror = 0; pixbuf = gdk_pixbuf_new_from_file_at_size(text,size,size,&gerror); if (! pixbuf) { if (gerror) zmessageACK(mWin,0,gerror->message); // bad icon file zd->zstat = 0; // keep dialog open return 0; // do nothing } menus[me].icon = zstrdup(text); menus[me].pixbuf = pixbuf; menus[me].size = size; } else { menus[me].icon = 0; menus[me].pixbuf = 0; menus[me].size = 0; } zdialog_fetch(zd,"kill",menus[me].kill); // popup window kill flag if (me == NME) NME++; // if new menu entry, incr. count Fchanged = 1; // mark menu revised zdialog_free(zdedit); // destroy dialog gtk_widget_queue_draw(layout); // repaint window } return 0; } /************************************************************************** Vertical Menu / Toolbar Build a custom vertical menu and/or toolbar in a vertical packing box vbm = Vmenu_new(GtkWidget *vbox) create base menu Vmenu_add(vbm, name, icon, desc, func, arg) add menu item or toolbar button Vmenu *vbm cchar *name, *icon, *desc, *arg void func(GtkWidget *, cchar *name) Create a vertical menu / toolbar in a vertical packing box. Added items can have a menu name, icon, description, response function, and function argument. 'name' and 'icon' can be null but not both. 'icon' is a filespec for a .png file containing the icon. 'desc' is optional and is used as a tool tip if the mouse is hovered over the displayed 'name/icon'. When 'name/icon' is clicked, 'func' is called with 'arg'. If 'arg' is null, 'name' is used instead. To create a menu entry that is a popup menu with multiple entries, do as follows: popup = create_popmenu(); add_popup_menu_item(popup ...); // see create_popmenu() add_popup_menu_item(popup ...); ... Vmenu_add(vbm, name, icon, desc, create_popmenu, (cchar *) popup); i.e. use create_popmenu() as the response function and use the previously created menu 'popup' as the argument (cast to cchar *). ***/ namespace Vmenunames { #define menufont1 "sans 10" // menu font #define menufont2 "sans bold 10" // menu font (selected) #define fontheight 18 // menu text height in layout #define margin 5 // margins for menu text #define BGCOLOR 59000,59000,59000 // = cairo 0.9,0.9,0.9 GdkRGBA GDKgray; PangoFontDescription *pfont1, *pfont2; PangoAttrList *pattrlist; PangoAttribute *pbackground; void wpaint(GtkWidget *, cairo_t *, Vmenu *); // window repaint - draw event void mouse_event(GtkWidget *, GdkEventButton *, Vmenu *); // mouse event function void paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite); // paint menu entry, opt. highlight } Vmenu *Vmenu_new(GtkWidget *vbox) // revised v.5.7 { using namespace Vmenunames; int cc; cc = sizeof(Vmenu); Vmenu *vbm = (Vmenu *) zmalloc(cc); memset(vbm,0,cc); vbm->vbox = vbox; vbm->layout = gtk_layout_new(0,0); vbm->mcount = 0; gtk_box_pack_start(GTK_BOX(vbox),vbm->layout,1,1,0); vbm->xmax = vbm->ymax = 10; // layout size v.5.8 GDKgray.red = GDKgray.green = GDKgray.blue = 0.6; gtk_widget_override_background_color(vbm->layout, (GtkStateFlags) 0, &GDKgray); pattrlist = pango_attr_list_new(); pbackground = pango_attr_background_new(BGCOLOR); pango_attr_list_change(pattrlist,pbackground); pfont1 = pango_font_description_from_string(menufont1); pfont2 = pango_font_description_from_string(menufont2); gtk_widget_add_events(vbm->layout,GDK_BUTTON_PRESS_MASK); gtk_widget_add_events(vbm->layout,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(vbm->layout,GDK_POINTER_MOTION_MASK); gtk_widget_add_events(vbm->layout,GDK_LEAVE_NOTIFY_MASK); G_SIGNAL(vbm->layout,"button-press-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"button-release-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"motion-notify-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"leave-notify-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"draw",wpaint,vbm); return vbm; } void Vmenu_add(Vmenu *vbm, cchar *name, cchar *icon, int iconww, int iconhh, cchar *desc, cbFunc func, cchar *arg) { using namespace Vmenunames; int me, cc1, cc2, xpos, ww, hh; char iconpath[200], *mdesc, *name__; cchar *blanks = " "; // 20 blanks GdkPixbuf *pixbuf; GError *gerror = 0; PangoLayout *playout; PangoFontDescription *pfont; if (! name && ! icon) return; me = vbm->mcount++; // track no. menu entries if (name) vbm->menu[me].name = zstrdup(name); // create new menu entry from caller data if (icon) { vbm->menu[me].icon = zstrdup(icon); vbm->menu[me].iconww = iconww; // v.5.8 vbm->menu[me].iconhh = iconhh; } if (desc) { // pad description with blanks for looks cc1 = strlen(desc); // v.5.6 mdesc = (char *) zmalloc(cc1+3); mdesc[0] = ' '; strcpy(mdesc+1,desc); strcpy(mdesc+cc1+1," "); vbm->menu[me].desc = mdesc; } vbm->menu[me].func = func; vbm->menu[me].arg = name; // argument is menu name or arg if avail. if (arg) vbm->menu[me].arg = arg; vbm->menu[me].pixbuf = 0; if (icon) { // if icon is named, get pixbuf *iconpath = 0; strncatv(iconpath,199,zfuncs::zicondir,"/",icon,null); pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,iconww,iconhh,1,&gerror); if (pixbuf) vbm->menu[me].pixbuf = pixbuf; } if (me == 0) vbm->ymax = margin; // first menu, top position vbm->menu[me].iconx = 0; vbm->menu[me].icony = 0; vbm->menu[me].namex = 0; vbm->menu[me].namey = 0; if (icon) { vbm->menu[me].iconx = margin; // ______ vbm->menu[me].icony = vbm->ymax; // | | if (name) { // | icon | menu name vbm->menu[me].namex = margin + iconww + margin; // |______| vbm->menu[me].namey = vbm->ymax + (iconhh - fontheight) / 2 + 2; } vbm->menu[me].ylo = vbm->ymax; vbm->ymax += iconhh + iconhh / 8; // position for next menu entry vbm->menu[me].yhi = vbm->ymax; if (margin + iconww > vbm->xmax) vbm->xmax = margin + iconww; // keep track of max. layout width } else if (name) { vbm->menu[me].namex = margin; // menu name vbm->menu[me].namey = vbm->ymax; vbm->menu[me].ylo = vbm->ymax; vbm->ymax += fontheight; vbm->menu[me].yhi = vbm->ymax; } vbm->menu[me].playout1 = gtk_widget_create_pango_layout(vbm->layout,0); vbm->menu[me].playout2 = gtk_widget_create_pango_layout(vbm->layout,0); if (name) { xpos = vbm->menu[me].namex; cc1 = strlen(name); // prepare menu name with trailing blanks cc2 = 0.3 * cc1 + 3; // so normal name longer than bold name if (cc2 > 20) cc2 = 20; // otherwise bold not 100% overwritten name__ = strdupz(name,cc2); strncpy0(name__+cc1,blanks,cc2); playout = vbm->menu[me].playout1; // normal font pfont = pfont1; pango_layout_set_attributes(playout,pattrlist); pango_layout_set_font_description(playout,pfont); pango_layout_set_text(playout,name__,-1); // compute layout pango_layout_get_pixel_size(playout,&ww,&hh); // pixel width and height of layout playout = vbm->menu[me].playout2; // bold font pfont = pfont2; pango_layout_set_attributes(playout,pattrlist); pango_layout_set_font_description(playout,pfont); pango_layout_set_text(playout,name,-1); // compute layout pango_layout_get_pixel_size(playout,&ww,&hh); // pixel width and height of layout if (xpos + ww > vbm->xmax) vbm->xmax = xpos + ww; // keep track of max. layout width } gtk_widget_set_size_request(vbm->layout,vbm->xmax+margin,0); // add right margin to layout width return; } // paint window when created, exposed, resized void Vmenunames::wpaint(GtkWidget *widget, cairo_t *cr, Vmenu *vbm) { using namespace Vmenunames; cairo_set_source_rgb(cr,0.9,0.9,0.9); // white background cairo_paint(cr); for (int me = 0; me < vbm->mcount; me++) // paint all menu entries paint_menu(cr,vbm,me,0); return; } // draw menu text into layout void Vmenunames::paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite) { using namespace Vmenunames; GdkPixbuf *pixbuf; PangoLayout *playout; int xpos, ypos; int iconww, iconhh; cchar *name; pixbuf = vbm->menu[me].pixbuf; // icon if (pixbuf) { // draw menu icon at menu position xpos = vbm->menu[me].iconx; ypos = vbm->menu[me].icony; iconww = vbm->menu[me].iconww; iconhh = vbm->menu[me].iconhh; if (! hilite) { // erase box around icon cairo_set_source_rgb(cr,0.9,0.9,0.9); cairo_rectangle(cr,xpos-1,ypos-1,iconww+2,iconhh+2); cairo_fill(cr); } gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos); // draw icon cairo_paint(cr); if (hilite) { cairo_set_source_rgb(cr,0.5,0.5,0.5); // draw box around icon cairo_set_line_width(cr,1); cairo_set_line_join(cr,CAIRO_LINE_JOIN_ROUND); cairo_rectangle(cr,xpos,ypos,iconww,iconhh); cairo_stroke(cr); } } name = vbm->menu[me].name; // menu text if (name) { // draw menu text at menu position xpos = vbm->menu[me].namex; ypos = vbm->menu[me].namey; cairo_move_to(cr,xpos,ypos); // draw layout with text cairo_set_source_rgb(cr,0,0,0); if (hilite) playout = vbm->menu[me].playout2; else playout = vbm->menu[me].playout1; pango_cairo_show_layout(cr,playout); } return; } // mouse event function - capture buttons and drag movements void Vmenunames::mouse_event(GtkWidget *widget, GdkEventButton *event, Vmenu *vbm) { using namespace Vmenunames; GdkDevice *mouse; GdkWindow *gdkwin; cchar *desc; int me, mpy, ylo, yhi; static int me0 = -1; cairo_t *cr; gdkwin = gtk_layout_get_bin_window(GTK_LAYOUT(widget)); cr = gdk_cairo_create(gdkwin); mpy = int(event->y); // mouse y-position mouse = event->device; if (event->type == GDK_MOTION_NOTIFY) // mouse inside layout { for (me = 0; me < vbm->mcount; me++) { // find menu where mouse is ylo = vbm->menu[me].ylo; yhi = vbm->menu[me].yhi; if (mpy < ylo) continue; if (mpy < yhi) break; } if (me != me0 && me0 >= 0) { // unhilight prior paint_menu(cr,vbm,me0,0); desc = vbm->menu[me0].desc; // remove prior tool tip if (desc) poptext_mouse(0,mouse,0,0,0,0); me0 = -1; } if (me == me0) goto MEreturn; // same as before if (me == vbm->mcount) goto MEreturn; // no new menu match paint_menu(cr,vbm,me,1); // hilight menu entry at mouse desc = vbm->menu[me].desc; // show tool tip if (desc) poptext_mouse(desc,mouse,30,20,0,5); me0 = me; // remember last match goto MEreturn; } if (me0 >= 0) // mouse left layout { paint_menu(cr,vbm,me0,0); // unhilight prior desc = vbm->menu[me0].desc; // remove prior tool tip if (desc) poptext_mouse(0,mouse,0,0,0,0); me0 = -1; } if (event->type == GDK_BUTTON_RELEASE) { if (event->button != 1) goto MEreturn; // refresh menu entries for (me = 0; me < vbm->mcount; me++) { // look for clicked menu entry ylo = vbm->menu[me].ylo; yhi = vbm->menu[me].yhi; if (mpy < ylo) continue; if (mpy < yhi) break; } if (me == vbm->mcount) goto MEreturn; // no menu match paint_menu(cr,vbm,me,0); // unhighlight menu if (vbm->menu[me].func) (vbm->menu[me].func)(widget,vbm->menu[me].arg); // caller function(widget,arg) } MEreturn: cairo_destroy(cr); return; } /************************************************************************** simplified GTK dialog functions create a dialog with response buttons (OK, cancel ...) add widgets to dialog (button, text entry, combo box ...) put data into widgets (text or numeric data) run the dialog, get user inputs (button press, text entry, checkbox selection ...) run caller event function when widgets change from user inputs run caller event function when dialog is completed or canceled get dialog completion status (OK, cancel, destroyed ...) get data from dialog widgets (the user inputs) destroy the dialog and free memory ***************************************************************************/ // private functions for widget events and dialog completion int zdialog_widget_event(GtkWidget *, zdialog *zd); int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd); // create a new zdialog dialog // The title and parent arguments may be null. // optional arguments: up to zdmaxbutts button labels followed by null // returned dialog status: +N = button N (1 to zdmaxbutts) // <0 = [x] button or other GTK destroy action // completion buttons are also events like other widgets // all dialogs run parallel, use zdialog_wait() if needed // The status returned by zdialog_wait() corresponds to the button // (1-10) used to end the dialog. Status < 0 indicates the [x] button // was used or the dialog was killed by the program itself. zdialog * zdialog_new(cchar *title, GtkWidget *parent, ...) // parent added { using namespace zfuncs; zdialog *zd; GtkWidget *topwin, *hbox, *vbox, *butt, *hsep; cchar *bulab[zdmaxbutts]; int cc, ii, nbu; va_list arglist; TRACE zthreadcrash(); // thread usage not allowed if (! monofont) { // v.5.7 monofont = pango_font_description_from_string("Monospace 9"); widgetfont = pango_font_description_from_string("Sans Bold 9"); } va_start(arglist,parent); for (nbu = 0; nbu < zdmaxbutts; nbu++) // get completion buttons { bulab[nbu] = va_arg(arglist, cchar *); if (! bulab[nbu]) break; } va_end(arglist); if (! title) title = " "; // v.5.0 topwin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // use window, not dialog v.5.5 gtk_window_set_title(GTK_WINDOW(topwin),title); vbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(topwin),vbox); // add to main window gtk_box_set_spacing(GTK_BOX(vbox),2); cc = sizeof(zdialog); // allocate zdialog zd = (zdialog *) zmalloc(cc); zdialog_count++; if (parent) gtk_window_set_transient_for(GTK_WINDOW(topwin),GTK_WINDOW(parent)); if (nbu) // there are some completion buttons { hbox = gtk_box_new(HORIZONTAL,2); // add hbox for buttons at bottom gtk_box_pack_end(GTK_BOX(vbox),hbox,0,0,2); hsep = gtk_separator_new(HORIZONTAL); // add separator line gtk_box_pack_end(GTK_BOX(vbox),hsep,0,0,2); for (ii = nbu-1; ii >= 0; ii--) { // add buttons to hbox butt = gtk_button_new_with_label(bulab[ii]); // reverse order nbu-1...0 gtk_widget_override_font(butt,widgetfont); // smaller font v.5.7 gtk_box_pack_end(GTK_BOX(hbox),butt,0,0,2); G_SIGNAL(butt,"clicked",zdialog_widget_event,zd); // connect to event function zd->compbutt[ii] = butt; // save button widgets } } zd->compbutt[nbu] = 0; // mark EOL v.5.8 GtkWidget *space = gtk_box_new(VERTICAL,0); // make space before 1st widget v.5.7 gtk_box_pack_start(GTK_BOX(vbox),space,0,0,1); zd->parent = parent; // parent window or null zd->sentinel1 = zdsentinel | (lrandz() & 0x0000FFFF); // validity sentinels v.5.8 zd->sentinel2 = zd->sentinel1; // fixed part + random part zd->eventCB = 0; // no user event callback function zd->zstat = 0; // no zdialog status zd->disabled = 1; // widget signals disabled zd->saveposn = 0; // position not saved zd->widget[0].type = "dialog"; // set up 1st widget = dialog zd->widget[0].name = "dialog"; zd->widget[0].pname = 0; zd->widget[0].data = zstrdup(title); zd->widget[0].cblist = 0; zd->widget[0].widget = topwin; zd->widget[1].type = 0; // eof - no contained widgets yet return zd; } // add widget to existing zdialog // // Arguments after parent are optional and default to 0. // zd zdialog *, created with zdialog_new() // type string, one of the above widget types // name string, widget name, used to stuff or fetch widget data // parent string, parent name: "dialog" or a previously added container widget // data string, initial data for widget (label name, entry string, spin value, etc.) // options string, optional args in the form "scc=nn|homog|expand|space=nn|wrap" // scc width for text entry widget, characters // homog for hbox or vbox to make even space allocation for contained widgets // expand widget should expand with dialog box expansion // space extra space between this widget and neighbors, pixels // wrap allow text to wrap at right margin // // data can be a string ("initial widget data") or a number in string form ("12.345") // data for togbutt / check / radio: use "0" or "1" for OFF or ON // data for spin / hscale / vscale: use format "min|max|step|value" (default: 0 | 100 | 1 | 50) // data for colorbutt: use "rrr|ggg|bbb" with values 0-255 for each RGB color. // This format is written to initialize the control, and this format is read back // after the user selects a color. // // Multiple radio buttons with same parent are a group: pressing one turns the others OFF. int zdialog_add_widget ( zdialog *zd, cchar *type, cchar *name, cchar *pname, // mandatory args cchar *data, int scc, int homog, int expand, int space, int wrap) // optional args (default = 0) { using namespace zfuncs; GtkWidget *widget = 0, *pwidget = 0; GtkWidget *entry, *vbox; GtkTextBuffer *editBuff = 0; GdkPixbuf *pixbuf = 0; GdkRGBA gdkrgba; GError *gerror = 0; cchar *pp, *ptype = 0; char vdata[30], iconpath[200]; double min, max, step, val; double f256 = 1.0 / 256.0; int iiw, iip, kk, err; if (! zdialog_valid(zd)) zappcrash("zdialog invalid"); for (iiw = 1; zd->widget[iiw].type; iiw++); // find next avail. slot if (iiw > zdmaxwidgets-2) zappcrash("too many widgets: %d",iiw); zd->widget[iiw].type = zstrdup(type); // initz. widget struct zd->widget[iiw].name = zstrdup(name); // all strings in nonvolatile mem zd->widget[iiw].pname = zstrdup(pname); zd->widget[iiw].data = 0; zd->widget[iiw].cblist = 0; zd->widget[iiw].scc = scc; zd->widget[iiw].homog = homog; zd->widget[iiw].expand = expand; zd->widget[iiw].space = space; zd->widget[iiw].wrap = wrap; zd->widget[iiw].widget = 0; zd->widget[iiw].stopKB = 0; zd->widget[iiw+1].type = 0; // set new EOF marker if (strcmpv(type,"dialog","hbox","vbox","hsep","vsep","frame","scrwin", "label","link","entry","edit","text","button","togbutt", "check","combo","comboE","radio","spin","hscale", "vscale","colorbutt","image",null) == 0) { printz("zdialog, bad widget type: %s \n",type); return 0; } for (iip = iiw-1; iip >= 0; iip--) // find parent (container) widget if (strEqu(pname,zd->widget[iip].name)) break; if (iip < 0) zappcrash("zdialog, no parent for widget: %s",name); pwidget = zd->widget[iip].widget; // parent widget, type ptype = zd->widget[iip].type; if (strcmpv(ptype,"dialog","hbox","vbox","frame","scrwin",null) == 0) zappcrash("zdialog, bad widget parent type: %s",ptype); if (strEqu(type,"hbox")) widget = gtk_box_new(HORIZONTAL,space); // expandable container boxes v.5.4 if (strEqu(type,"vbox")) widget = gtk_box_new(VERTICAL,space); if (strstr("hbox vbox",type)) gtk_box_set_homogeneous(GTK_BOX(widget),homog); if (strEqu(type,"hsep")) widget = gtk_separator_new(HORIZONTAL); // horiz. & vert. separators if (strEqu(type,"vsep")) widget = gtk_separator_new(VERTICAL); if (strEqu(type,"frame")) { // frame around contained widgets widget = gtk_frame_new(data); gtk_frame_set_shadow_type(GTK_FRAME(widget),GTK_SHADOW_IN); data = 0; // v.5.3 data not further used } if (strEqu(type,"scrwin")) { // scrolled window container widget = gtk_scrolled_window_new(0,0); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget), GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC); data = 0; // v.5.3 data not further used } if (strEqu(type,"label")) { // label (static text) widget = gtk_label_new(data); data = 0; // v.5.3 data not further used } if (strEqu(type,"link")) { // label is clickable if (strEqu(name,"nolabel")) widget = gtk_link_button_new(data); // link is also label else // v.5.6 widget = gtk_link_button_new_with_label(data,name); // label >> link G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); data = 0; // v.5.3 data not further used } if (strEqu(type,"entry")) { // 1-line text entry widget = gtk_entry_new(); if (data) gtk_entry_set_text(GTK_ENTRY(widget),data); if (scc) gtk_entry_set_width_chars(GTK_ENTRY(widget),scc); gtk_widget_override_font(widget,monofont); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); } if (strEqu(type,"edit")) { // multiline edit box widget = gtk_text_view_new(); editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (data) gtk_text_buffer_set_text(editBuff,data,-1); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1); if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); gtk_widget_override_font(widget,monofont); G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd); // buffer signals, not widget } if (strEqu(type,"text")) { // text output (not editable) widget = gtk_text_view_new(); // added v.5.5 editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (data) gtk_text_buffer_set_text(editBuff,data,-1); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0); if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); gtk_widget_override_font(widget,monofont); } if (strEqu(type,"button")) { // button widget = gtk_button_new_with_label(data); G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); data = 0; // v.5.3 data not further used } if (strEqu(type,"togbutt")) { // toggle button widget = gtk_toggle_button_new_with_label(data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // v.5.3 default data } if (strEqu(type,"check")) { // checkbox if (data) widget = gtk_check_button_new_with_label(data); else widget = gtk_check_button_new(); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // v.5.3 default data } if (strEqu(type,"combo")) { // combo box widget = gtk_combo_box_text_new(); zd->widget[iiw].cblist = pvlist_create(zdcbmax); // for drop-down list if (! blank_null(data)) { pvlist_append(zd->widget[iiw].cblist,data); // add data to drop-down list gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widget),data); gtk_combo_box_set_active(GTK_COMBO_BOX(widget),0); } gtk_widget_override_font(widget,monofont); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); } if (strEqu(type,"comboE")) { // combo box with entry box widget = gtk_combo_box_text_new_with_entry(); zd->widget[iiw].cblist = pvlist_create(zdcbmax); // for drop-down list if (! blank_null(data)) { entry = gtk_bin_get_child(GTK_BIN(widget)); gtk_entry_set_text(GTK_ENTRY(entry),data); // entry = initial data pvlist_append(zd->widget[iiw].cblist,data); // add data to drop-down list gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widget),data); } gtk_widget_override_font(widget,monofont); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); } if (strEqu(type,"radio")) { // radio button for (kk = iip+1; kk <= iiw; kk++) if (strEqu(zd->widget[kk].pname,pname) && // find first radio button strEqu(zd->widget[kk].type,"radio")) break; // with same container if (kk == iiw) widget = gtk_radio_button_new_with_label(null,data); // this one is first else widget = gtk_radio_button_new_with_label_from_widget // not first, add to group (GTK_RADIO_BUTTON(zd->widget[kk].widget),data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // v.5.3 default data } if (strcmpv(type,"spin","hscale","vscale",null)) { // spin button or sliding scale if (! data) zappcrash("zdialog_add_widget(): data missing"); // v.5.8 pp = strField(data,'|',1); err = convSD(pp,min); pp = strField(data,'|',2); err += convSD(pp,max); pp = strField(data,'|',3); err += convSD(pp,step); pp = strField(data,'|',4); err += convSD(pp,val); if (err) { min = 0; max = 100; step = 1; val = 50; } if (*type == 's') { widget = gtk_spin_button_new_with_range(min,max,step); gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val); } if (*type == 'h') { widget = gtk_scale_new_with_range(HORIZONTAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); } if (*type == 'v') { widget = gtk_scale_new_with_range(VERTICAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); } gtk_widget_set_double_buffered(widget,0); // GTK "sticky slider" bug work-around v.5.8 G_SIGNAL(widget,"value-changed",zdialog_widget_event,zd); sprintf(vdata,"%g",val); data = vdata; } if (strEqu(type,"colorbutt")) { // color edit button if (! data) data = "0|0|0"; // data format: "nnn|nnn|nnn" = RGB pp = strField(data,'|',1); gdkrgba.red = f256 * atoi(pp); // RGB values are 0-1 v.5.8 pp = strField(data,'|',2); gdkrgba.green = f256 * atoi(pp); pp = strField(data,'|',3); gdkrgba.blue = f256 * atoi(pp); gdkrgba.alpha = 1.0; // v.5.8 widget = gtk_color_button_new_with_rgba(&gdkrgba); G_SIGNAL(widget,"color-set",zdialog_widget_event,zd); } if (strEqu(type,"image")) { // image widget v.5.7 snprintf(iconpath,200,"%s/%s",get_zicondir(),data); data = 0; // data not further used pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,24,24,1,&gerror); if (pixbuf) { widget = gtk_image_new_from_pixbuf(pixbuf); g_object_unref(pixbuf); } else widget = gtk_image_new_from_icon_name("missing",GTK_ICON_SIZE_BUTTON); } // all widget types come here if (strstr("label button togbutt check radio spin",type)) // smaller font v.5.7 gtk_widget_override_font(widget,widgetfont); zd->widget[iiw].widget = widget; // set widget in zdialog if (strcmpv(type,"entry","edit","comboE",null) > 0) // for data entry widgets, stop zd->widget[iiw].stopKB = 1; // propagation of KB events if (strEqu(ptype,"hbox") || strEqu(ptype,"vbox")) // add to hbox/vbox gtk_box_pack_start(GTK_BOX(pwidget),widget,expand,expand,space); if (strEqu(ptype,"frame")) // add to frame gtk_container_add(GTK_CONTAINER(pwidget),widget); if (strEqu(ptype,"scrwin")) // add to scroll window gtk_container_add(GTK_CONTAINER(pwidget),widget); if (strEqu(ptype,"dialog")) { // add to dialog box vbox = gtk_bin_get_child(GTK_BIN(pwidget)); // dialog is a gtkwindow v.5.5 gtk_box_pack_start(GTK_BOX(vbox),widget,expand,expand,space); } if (data) zd->widget[iiw].data = zstrdup(data); // use heap memory return 0; } // add widget to existing zdialog - alternative form (clearer and easier code) // options: "scc=nn | homog | expand | space=nn | wrap" (all optional, any order) int zdialog_add_widget(zdialog *zd, cchar *type, cchar *name, cchar *parent, cchar *data, cchar *options) { int stat, scc = 0, homog = 0, expand = 0, space = 0, wrap = 0, begin = 1; char pname[8]; double pval; while (true) { stat = strParms(begin,options,pname,8,pval); if (stat == -1) break; if (stat == 1) zappcrash("bad zdialog options: %s",options); if (strEqu(pname,"scc")) scc = (int(pval)); else if (strEqu(pname,"homog")) homog = 1; else if (strEqu(pname,"expand")) expand = 1; else if (strEqu(pname,"space")) space = (int(pval)); else if (strEqu(pname,"wrap")) wrap = 1; else zappcrash("bad zdialog options: %s",options); } stat = zdialog_add_widget(zd,type,name,parent,data,scc,homog,expand,space,wrap); return stat; } // return 1/0 if zdialog is valid/invalid int zdialog_valid(zdialog *zd) // v.5.8 { int ok; TRACE if (! zd) { printz("zdialog is null \n"); return 0; } ok = 1; if ((zd->sentinel1 & 0xFFFF0000) != zdsentinel) ok = 0; if (zd->sentinel2 != zd->sentinel1) ok = 0; if (! ok) { printz("zdialog sentinel invalid \n"); return 0; } return 1; } // get GTK widget from zdialog and widget name GtkWidget * zdialog_widget(zdialog *zd, cchar *name) { if (! zdialog_valid(zd)) return 0; for (int ii = 0; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) return zd->widget[ii].widget; return 0; } // set a common group for a set of radio buttons // (GTK, this does not work) int zdialog_set_group(zdialog *zd, cchar *radio1, ...) { va_list arglist; cchar *radio2; GtkWidget *gwidget, *widget; GSList *glist; gwidget = zdialog_widget(zd,radio1); glist = gtk_radio_button_get_group(GTK_RADIO_BUTTON(gwidget)); if (! glist) zappcrash("no radio button group"); va_start(arglist,radio1); while (true) { radio2 = va_arg(arglist,cchar *); if (! radio2) break; widget = zdialog_widget(zd,radio2); gtk_radio_button_set_group(GTK_RADIO_BUTTON(widget),glist); } va_end(arglist); return 0; } // resize dialog to a size greater than initial size // (as determined by the included widgets) int zdialog_resize(zdialog *zd, int width, int height) { if (! zdialog_valid(zd)) zappcrash("zdialog invalid"); GtkWidget *window = zd->widget[0].widget; gtk_window_set_default_size(GTK_WINDOW(window),width,height); return 1; } // put data into a zdialog widget // private function int zdialog_put_data(zdialog *zd, cchar *name, cchar *data) { GtkWidget *widget, *entry; GtkTextBuffer *textBuff; GdkRGBA gdkrgba; int iiw, nn, kk; cchar *type, *pp; char *wdata; double val, f256 = 1.0 / 256.0; TRACE zthreadcrash(); // thread usage not allowed if (! zdialog_valid(zd)) return 0; if (! name || ! *name) { printz("*** zdialog_put_data(%s), widget name invalid \n",name); return 0; } for (iiw = 1; zd->widget[iiw].type; iiw++) // find widget if (strEqu(zd->widget[iiw].name,name)) break; if (! zd->widget[iiw].type) return 0; // log message removed v.5.8 type = zd->widget[iiw].type; widget = zd->widget[iiw].widget; wdata = zd->widget[iiw].data; if (wdata) zfree(wdata); // free prior data memory zd->widget[iiw].data = 0; if (data) { wdata = zstrdup(data); // set new data for widget zd->widget[iiw].data = wdata; if (utf8_check(wdata)) printz("*** zdialog: bad UTF8 encoding %s \n",wdata); } zd->disabled++; // disable for widget stuffing if (strEqu(type,"label")) gtk_label_set_text(GTK_LABEL(widget),data); if (strEqu(type,"link")) gtk_label_set_text(GTK_LABEL(widget),data); if (strEqu(type,"entry")) gtk_entry_set_text(GTK_ENTRY(widget),data); if (strEqu(type,"button")) // change button label gtk_button_set_label(GTK_BUTTON(widget),data); if (strEqu(type,"edit")) { textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,data,-1); } if (strEqu(type,"text")) { // text output v.5.5 textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,data,-1); } if (strcmpv(type,"togbutt","check","radio",null)) { if (! data) kk = nn = 0; else kk = convSI(data,nn); if (kk != 0) nn = 0; // data not integer, force zero if (nn <= 0) nn = 0; else nn = 1; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),nn); // set gtk widget value } if (strEqu(type,"spin")) { kk = convSD(data,val); if (kk != 0) val = 0.0; gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val); } if (strEqu(type,"colorbutt")) { // color button data is nnn|nnn|nnn pp = strField(data,'|',1); if (pp) gdkrgba.red = f256 * atoi(pp); // RGB range is 0-1 v.5.8 pp = strField(data,'|',2); if (pp) gdkrgba.green = f256 * atoi(pp); pp = strField(data,'|',3); if (pp) gdkrgba.blue = f256 * atoi(pp); gdkrgba.alpha = 1.0; gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba); // v.5.8 } if (strcmpv(type,"hscale","vscale",null)) { kk = convSD(data,val); if (kk != 0) val = 0.0; gtk_range_set_value(GTK_RANGE(widget),val); } if (strEqu(type,"combo")) { if (! blank_null(data)) { kk = pvlist_prepend(zd->widget[iiw].cblist,data,1); // add to drop-down list if (kk == 0) // (only if unique) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(widget),data); kk = pvlist_find(zd->widget[iiw].cblist,data); gtk_combo_box_set_active(GTK_COMBO_BOX(widget),kk); // make the active entry } else gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1); // make no active entry } if (strEqu(type,"comboE")) { entry = gtk_bin_get_child(GTK_BIN(widget)); if (! blank_null(data)) { kk = pvlist_prepend(zd->widget[iiw].cblist,data,1); // add to drop-down list if (kk == 0) // (only if unique) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(widget),data); gtk_entry_set_text(GTK_ENTRY(entry),data); // stuff entry box with new data } else gtk_entry_set_text(GTK_ENTRY(entry),""); // stuff entry box with nothing } zd->disabled--; // re-enable dialog return iiw; } // get data from a dialog widget based on its name // private function cchar * zdialog_get_data(zdialog *zd, cchar *name) { TRACE if (! zdialog_valid(zd)) return 0; for (int ii = 1; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) return zd->widget[ii].data; return 0; } // set new limits for a numeric data entry widget (spin, hscale, vscale) int zdialog_set_limits(zdialog *zd, cchar *name, double min, double max) { GtkWidget *widget; cchar *type; int iiw; if (! zdialog_valid(zd)) return 0; for (iiw = 1; zd->widget[iiw].type; iiw++) if (strEqu(name,zd->widget[iiw].name)) break; if (! zd->widget[iiw].type) { printz("*** zdialog_stuff_limits, %s not found \n",name); return 0; } widget = zd->widget[iiw].widget; type = zd->widget[iiw].type; if (*type == 's') gtk_spin_button_set_range(GTK_SPIN_BUTTON(widget),min,max); if (*type == 'h' || *type == 'v') gtk_range_set_range(GTK_RANGE(widget),min,max); return 1; } // run the dialog and send events to the event function // // evfunc: int func(zdialog *zd, cchar *event) // If present, eventFunc is called when a dialog widget is changed or the dialog // is completed. If a widget was changed, event is the widget name. // Get the new widget data with zdialog_fetch(). // If a completion button was pressed, event is "zstat" and zd->zstat will be // the button number 1-N. // If the dialog was destroyed, event is "zstat" and zd->zstat is negative. // // posn: optional dialog box position: // "mouse" = position at mouse // "desktop" = center on desktop // "parent" = center on parent window // "nn/nn" = position NW corner at relative x/y position in parent window, // where nn/nn is a percent 0-100 of the parent window x/y dimensions. // "save" = save last user-set position and use this whenever the dialog is repeated, // also across sessions. // // KBstate: extern void KBstate(GdkEventKey *event, int state) // This function must be supplied by the caller of zdialog. // It is called when Ctrl|Shift|Alt|F1 is pressed or released. int zdialog_run(zdialog *zd, zdialog_event evfunc, cchar *posn) { int zdialog_KBpress(GtkWidget *, GdkEventKey *event, zdialog *zd); int zdialog_KBrelease(GtkWidget *, GdkEventKey *event, zdialog *zd); int zdialog_focus_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd); int ii; GtkWidget *widget, *dialog, *entry; TRACE if (! zdialog_valid(zd)) zappcrash("zdialog invalid"); dialog = zd->widget[0].widget; if (posn) zdialog_set_position(zd,posn); // put dialog at remembered position for (ii = 1; zd->widget[ii].type; ii++) // *** stop auto-selection { // (GTK "feature") if (strEqu(zd->widget[ii].type,"entry")) { widget = zd->widget[ii].widget; if (gtk_editable_get_editable(GTK_EDITABLE(widget))) gtk_editable_set_position(GTK_EDITABLE(widget),-1); break; } if (strEqu(zd->widget[ii].type,"comboE")) { // also combo edit box widget = zd->widget[ii].widget; entry = gtk_bin_get_child(GTK_BIN(widget)); gtk_editable_set_position(GTK_EDITABLE(entry),-1); break; } } if (evfunc) zd->eventCB = (void *) evfunc; // link to dialog event callback dialog = zd->widget[0].widget; gtk_widget_show_all(dialog); // activate dialog gtk_window_present(GTK_WINDOW(dialog)); if (! zd->parent) // if no parent, force to top gtk_window_set_keep_above(GTK_WINDOW(dialog),1); G_SIGNAL(dialog,"focus-in-event",zdialog_focus_event_signal,zd); // connect focus event function v.5.0 G_SIGNAL(dialog,"key-press-event",zdialog_KBpress,zd); // connect key press event function G_SIGNAL(dialog,"key-release-event",zdialog_KBrelease,zd); // connect key release event function G_SIGNAL(dialog,"delete-event",zdialog_delete_event,zd); // connect delete event function v.5.5 zd->zstat = 0; // dialog not complete v.5.7 zd->disabled = 0; // enable widget events zfuncs::zdialog_busy++; // count open zdialogs return 0; // return now, dialog is non-modal } // zdialog event handler - called for dialog events. // Updates data in zdialog, calls user callback function (if present). // private function int zdialog_widget_event(GtkWidget *widget, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; GtkTextIter iter1, iter2; GdkRGBA gdkrgba; GtkWidget *entry; static GtkWidget *lastwidget = 0; int ii, nn; cchar *wname, *type, *wdata, *event; char sdata[20]; double dval; static int cbadded = 0; TRACE if (! zdialog_valid(zd)) return 0; if (zd->disabled) return 1; // re-entry from zdialog_put_data() zd->disabled = 1; // or user event function v.5.8 for (ii = 0; ii < zdmaxbutts; ii++) { // check completion buttons if (zd->compbutt[ii] == null) break; // EOL v.5.8 if (zd->compbutt[ii] != widget) continue; zd->zstat = ii+1; // zdialog status = button no. event = "zstat"; goto call_evfunc; // call zdialog event function v.5.7 } for (ii = 1; zd->widget[ii].type; ii++) // find widget in zdialog if (zd->widget[ii].widget == widget) goto found_widget; for (ii = 1; zd->widget[ii].type; ii++) { // failed, test if buffer if (strEqu(zd->widget[ii].type,"edit")) { // of text view widget textView = GTK_TEXT_VIEW(zd->widget[ii].widget); textBuff = gtk_text_view_get_buffer(textView); if (widget == (GtkWidget *) textBuff) goto found_widget; } } printz("*** zdialog event ignored \n"); // not found, ignore event zd->disabled = 0; return 1; found_widget: wname = zd->widget[ii].name; type = zd->widget[ii].type; wdata = 0; if (strEqu(type,"button")) wdata = "clicked"; if (strEqu(type,"entry")) wdata = gtk_entry_get_text(GTK_ENTRY(widget)); if (strEqu(type,"edit")) { gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); } if (strcmpv(type,"radio","check","togbutt",null)) { nn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)); if (nn == 0) wdata = "0"; else wdata = "1"; } if (strEqu(type,"combo")) wdata = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widget)); if (strEqu(type,"comboE")) { if (widget == lastwidget && cbadded) { pvlist_remove(zd->widget[ii].cblist,0); // detect multiple edits (keystrokes) gtk_combo_box_text_remove(GTK_COMBO_BOX_TEXT(widget),0); // amd replace with new entry } entry = gtk_bin_get_child(GTK_BIN(widget)); wdata = gtk_entry_get_text(GTK_ENTRY(entry)); cbadded = 0; if (! blank_null(wdata)) { nn = pvlist_prepend(zd->widget[ii].cblist,wdata,1); // add entry to drop-down list if (nn == 0) { // (only if unique) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(widget),wdata); cbadded = 1; } } } if (strEqu(type,"spin")) { dval = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget)); sprintf(sdata,"%g",dval); wdata = sdata; } if (strEqu(type,"colorbutt")) // color button v.5.8 { gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba); sprintf(sdata,"%.0f|%.0f|%.0f",gdkrgba.red*255,gdkrgba.green*255,gdkrgba.blue*255); wdata = sdata; } if (strcmpv(type,"hscale","vscale",null)) { dval = gtk_range_get_value(GTK_RANGE(widget)); sprintf(sdata,"%g",dval); wdata = sdata; } // all widgets come here if (zd->widget[ii].data) zfree(zd->widget[ii].data); // clear prior data zd->widget[ii].data = 0; if (wdata) zd->widget[ii].data = zstrdup(wdata); // set new data lastwidget = widget; // remember last widget updated event = wname; // event = widget name call_evfunc: // call zdialog event function v.5.7 if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function evfunc(zd,event); } zd->stopKB = zd->widget[ii].stopKB; // stop KB propagation if req. v.5.1 zd->disabled = 0; // re-enable widgets return 1; } // zdialog response handler for "focus-in-event" signal // v.5.0 // private function zdialog *zdialog_focus_zd; // v.5.8 current zdialog int zdialog_focus_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd) { TRACE if (! zdialog_valid(zd)) return 0; if (zd->zstat) return 0; // already complete zdialog_focus_zd = zd; zdialog_send_event(zd,"focus"); // notify dialog event function return 0; // must be 0 } // zdialog response handler for keyboard events // key symbols can be found at /usr/include/gtk-3.0/gdk/gdkkeysyms.h // main app must provide: extern void KBstate(GdkEventKey *event, int state) // private function int KB_Ctrl = 0; // track state of KB Ctrl int KB_Shift = 0; // and Shift and Alt keys int KB_Alt = 0; int zdialog_KBpress(GtkWidget *, GdkEventKey *kbevent, zdialog *zd) { TRACE KB_Ctrl = KB_Shift = KB_Alt = 0; if (kbevent->state & GDK_CONTROL_MASK) KB_Ctrl = 1; // v.5.6 if (kbevent->state & GDK_SHIFT_MASK) KB_Shift = 1; if (kbevent->state & GDK_MOD1_MASK) KB_Alt = 1; if (KB_Ctrl || KB_Alt) { // if Ctrl or Alt pressed, v.5.6 KBstate(kbevent,1); // send next key to main app return 1; } return 0; // send to dialog widget } int zdialog_KBrelease(GtkWidget *widget, GdkEventKey *kbevent, zdialog *zd) { void zdialog_pastefunc(GtkClipboard *, cchar *, void *); int KBkey = kbevent->keyval; GtkClipboard *clipboard = 0; TRACE KB_Ctrl = KB_Shift = KB_Alt = 0; if (kbevent->state & GDK_CONTROL_MASK) KB_Ctrl = 1; // v.5.6 if (kbevent->state & GDK_SHIFT_MASK) KB_Shift = 1; if (kbevent->state & GDK_MOD1_MASK) KB_Alt = 1; if (KB_Ctrl && KBkey == GDK_KEY_v) // Ctrl+v = paste clipboard v.5.8 { clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_clipboard_request_text(clipboard,zdialog_pastefunc,widget); return 1; } if (KB_Ctrl || KB_Alt) { // if Ctrl or Alt pressed, KBstate(kbevent,0); // send next key to main app v.5.6 return 1; } if (KBkey == GDK_KEY_F1) { // send F1 (help) to main app v.5.6 KBstate(kbevent,0); return 1; } if (KBkey == GDK_KEY_F11) { // F11 (toggle full screen) to main app KBstate(kbevent,0); // v.5.6 return 1; } if (KBkey == GDK_KEY_Escape) { // ESC key is cancel dialog v.5.6 zdialog_destroy(zd); zdialog_send_response(zd,-1); return 1; } if (KBkey == GDK_KEY_Return) { // Return (Enter) key v.5.8 zdialog_send_event(zd,"enter"); return 1; } if (zd->stopKB) return 1; // ignore KB events v.5.1 return 0; // send KB input to dialog widget } // process Ctrl+v (paste text) events for a zdialog widget // v.5.8 // private function void zdialog_pastefunc(GtkClipboard *clipboard, cchar *cliptext, void *arg) { GtkWindow *window = (GtkWindow *) arg; GtkWidget *widget = gtk_window_get_focus(window); GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; zdialog *zd = zdialog_focus_zd; int ii, cc = 0; cchar *wname; char text[1000]; TRACE if (! widget) return; // widget for pasted text if (! cliptext || ! *cliptext) return; // clipboard text pasted if (! zdialog_valid(zd)) return; if (zd->zstat) return; for (ii = 1; zd->widget[ii].type; ii++) // find widget in zdialog if (zd->widget[ii].widget == widget) goto found_widget; for (ii = 1; zd->widget[ii].type; ii++) { // failed, test if buffer if (strEqu(zd->widget[ii].type,"edit")) { // of text view widget textView = GTK_TEXT_VIEW(zd->widget[ii].widget); textBuff = gtk_text_view_get_buffer(textView); if (widget == (GtkWidget *) textBuff) goto found_widget; } } return; // not found found_widget: wname = zd->widget[ii].name; zdialog_fetch(zd,wname,text,999); // current text in widget cc = strlen(text); if (cc > 995) return; strncpy(text+cc,cliptext,999-cc); // add clipboard text text[999] = 0; zdialog_stuff(zd,wname,text); return; } // private function called when zdialog is completed. // called when dialog is canceled via [x] button or destroyed by GTK (zstat < 0). int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function TRACE if (! zdialog_valid(zd)) return 1; if (zd->zstat) return 1; // already complete zd->zstat = -1; // set zdialog cancel status v.5.5 if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function evfunc(zd,"zstat"); // (should do zdialog_free()) } else zdialog_destroy(zd); // v.5.8 return 0; } // Send the event "name" to an active zdialog. // The response function eventFunc() will be called with this event. int zdialog_send_event(zdialog *zd, cchar *event) { zdialog_event * evfunc = 0; // dialog event callback function TRACE if (! zdialog_valid(zd)) return 0; evfunc = (zdialog_event *) zd->eventCB; if (! evfunc) return 0; evfunc(zd,event); // call dialog event function return 1; } // Complete an active dialog and assign a status. // Equivalent to the user pressing a dialog completion button. // The dialog completion function is called if defined, // and zdialog_wait() is unblocked. // returns: 0 = no active dialog or completion function, 1 = OK int zdialog_send_response(zdialog *zd, int zstat) { zdialog_event *evfunc = 0; // dialog event callback function TRACE if (! zdialog_valid(zd)) return 0; zd->zstat = zstat; // set status evfunc = (zdialog_event *) zd->eventCB; if (! evfunc) return 0; evfunc(zd,"zstat"); return 1; } // show or hide a zdialog window // returns 1 if successful, 0 if zd does not exist. int zdialog_show(zdialog *zd, int show) { static GtkWidget *widget, *pwidget = 0; static int posx, posy; TRACE widget = zdialog_widget(zd,"dialog"); if (! widget) return 0; if (show) { // show window if (widget == pwidget) { // restore prior position v.5.4 gtk_window_move(GTK_WINDOW(widget),posx,posy); pwidget = 0; } gtk_widget_show_all(widget); // GTK bug?? window can no longer //// } // receive focus events else { // hide window pwidget = widget; gtk_window_get_position(GTK_WINDOW(widget),&posx,&posy); // save position v.5.4 gtk_widget_hide(widget); } return 1; } // Destroy the zdialog - must be done by zdialog_run() caller // (else dialog continues active even after completion button). // Data in widgets remains valid until zdialog_free() is called. int zdialog_destroy(zdialog *zd) { using namespace zfuncs; TRACE if (! zdialog_valid(zd)) return 0; if (zd->zstat < 0) { // destroyed by [x] button or GTK if (zd->widget[0].widget) zdialog_busy--; // bugfix v.5.8 zd->widget[0].widget = 0; // assume GTK dialog is gone } if (zd->widget[0].widget) { // multiple destroys OK if (zd->saveposn) zdialog_save_position(zd); // save position for next use v.5.6 gtk_widget_destroy(zd->widget[0].widget); // destroy GTK dialog zd->widget[0].widget = 0; zdialog_busy--; } if (! zd->zstat) zd->zstat = -1; // status = destroyed if (zd->parent) gtk_window_present(GTK_WINDOW(zd->parent)); // focus on parent window v.5.7 return 1; } // free zdialog memory (will destroy first, if not already) // zd is set to null int zdialog_free(zdialog *&zd) // reference { using namespace zfuncs; TRACE if (! zdialog_valid(zd)) return 0; zdialog_save_inputs(zd); // save user inputs for next use v.5.3 zdialog_destroy(zd); // destroy GTK dialog if there zd->sentinel1 = zd->sentinel2 = 0; // mark sentinels invalid v.5.8 zfree(zd->widget[0].data); // bugfix memory leak for (int ii = 1; zd->widget[ii].type; ii++) // loop through widgets { if (strcmpv(zd->widget[ii].type,"combo","comboE",null)) // free combo list pvlist_free(zd->widget[ii].cblist); zfree((char *) zd->widget[ii].type); // free strings zfree((char *) zd->widget[ii].name); zfree((char *) zd->widget[ii].pname); if (zd->widget[ii].data) zfree(zd->widget[ii].data); // free data } zfree(zd); // free zdialog memory zd = 0; // clear pointer zdialog_count--; return 1; } // Wait for a dialog to complete or be destroyed. This is a zmainloop() loop. // The returned status is the button 1-N used to complete the dialog, or negative // if the dialog was destroyed with [x] or otherwise by GTK. If the status was 1-N and // the dialog will be kept active, set zd->zstat = 0 to restore the active state. int zdialog_wait(zdialog *zd) { TRACE while (true) { zmainloop(); if (! zdialog_valid(zd)) return -1; if (zd->zstat) return zd->zstat; zsleep(0.01); } } // put cursor at named widget int zdialog_goto(zdialog *zd, cchar *name) { GtkWidget *widget; if (! zdialog_valid(zd)) return 0; widget = zdialog_widget(zd, name); if (! widget) return 0; gtk_editable_select_region(GTK_EDITABLE(widget),0,-1); // focus on widget gtk_widget_grab_focus(widget); return 1; } // set cursor for zdialog (e.g. a busy cursor) void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor) { GtkWidget *dialog; GdkWindow *window; if (! zdialog_valid(zd)) return; dialog = zd->widget[0].widget; if (! dialog) return; window = gtk_widget_get_window(dialog); gdk_window_set_cursor(window,cursor); return; } // insert data into a zdialog widget int zdialog_stuff(zdialog *zd, cchar *name, cchar *data) // stuff a string { if (data) zdialog_put_data(zd, name, data); else zdialog_put_data(zd, name, ""); // null > "" v.5.2 return 1; } int zdialog_stuff(zdialog *zd, cchar *name, int idata) // stuff an integer { char string[16]; sprintf(string,"%d",idata); zdialog_put_data(zd,name,string); return 1; } int zdialog_stuff(zdialog *zd, cchar *name, double ddata) // stuff a double { char string[32]; snprintf(string,31,"%g",ddata); // outputs decimal point or comma zdialog_put_data(zd,name,string); // (per locale) return 1; } // get data from a zdialog widget int zdialog_fetch(zdialog *zd, cchar *name, char *data, int maxcc) // fetch string data { cchar *zdata; zdata = zdialog_get_data(zd,name); if (! zdata) { *data = 0; return 0; } return strncpy0(data,zdata,maxcc); // 0 = OK, 1 = truncation } int zdialog_fetch(zdialog *zd, cchar *name, int &idata) // fetch an integer { cchar *zdata; zdata = zdialog_get_data(zd,name); if (! zdata) { idata = 0; return 0; } idata = atoi(zdata); return 1; } int zdialog_fetch(zdialog *zd, cchar *name, double &ddata) // fetch a double { int stat; cchar *zdata; zdata = zdialog_get_data(zd,name); if (! zdata) { ddata = 0; return 0; } stat = convSD(zdata,ddata); // period or comma decimal point OK if (stat < 4) return 1; return 0; } int zdialog_fetch(zdialog *zd, cchar *name, float &fdata) // fetch a float { int stat; cchar *zdata; double ddata; zdata = zdialog_get_data(zd,name); if (! zdata) { fdata = 0; return 0; } stat = convSD(zdata,ddata); // period or comma decimal point OK fdata = ddata; if (stat < 4) return 1; return 0; } /************************************************************************** Widget type Combo Box with Entry -------------------------------- stat = zdialog_cb_app(zd, name, data) append to combo box list stat = zdialog_cb_prep(zd, name, data) prepend to combo box list data = zdialog_cb_get(zd, name, Nth) get combo box list item data = zdialog_cb_delete(zd, name, data) delete entry matching "data" data = zdialog_cb_clear(zd, name) clear all entries zdialog_cb_popup(zd, name) open the combo box entries These functions map and track a combo box drop-down list, by maintaining a parallel list in memory. The function zdialog-stuff, when called for a comboE widget, automatically prepends the stuffed data to the drop-down list. The private function zdialog_event(), when processing user input to the edit box of a comboE widget, adds the updated entry to the drop-down list. The drop-down list is kept free of redundant and blank entries. ***************************************************************************/ // append new item to combo box list without changing entry box int zdialog_cb_app(zdialog *zd, cchar *name, cchar *data) { int ii, nn; TRACE if (! zdialog_valid(zd)) zappcrash("zdialog invalid"); if (blank_null(data)) return 0; // find widget for (ii = 1; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_append(zd->widget[ii].cblist,data,1); // append unique if (nn >= 0) gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),data); return 1; } // prepend new item to combo box list without changing entry box int zdialog_cb_prep(zdialog *zd, cchar *name, cchar *data) { int ii, nn; TRACE if (! zdialog_valid(zd)) zappcrash("zdialog invalid"); if (blank_null(data)) return 0; // find widget for (ii = 1; zd->widget[ii].type; ii++) if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_prepend(zd->widget[ii].cblist,data,1); // append unique if (nn == 0) gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),data); return 1; } // get combo box drop-down list entry // Nth = 0 = first list entry (not comboE edit box) char * zdialog_cb_get(zdialog *zd, cchar *name, int Nth) { int ii; TRACE if (! zdialog_valid(zd)) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box return pvlist_get(zd->widget[ii].cblist,Nth); } // delete entry by name from combo drop down list int zdialog_cb_delete(zdialog *zd, cchar *name, cchar *data) { int ii, nn; TRACE if (! zdialog_valid(zd)) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_find(zd->widget[ii].cblist,data); // find entry by name if (nn < 0) return -1; pvlist_remove(zd->widget[ii].cblist,nn); // remove from memory list gtk_combo_box_text_remove(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),nn); // and from widget gtk_combo_box_set_active(GTK_COMBO_BOX(zd->widget[ii].widget),-1); // set no active entry return 0; } // delete all entries from combo drop down list int zdialog_cb_clear(zdialog *zd, cchar *name) { int ii, jj, nn; GtkWidget *widget, *entry; TRACE if (! zdialog_valid(zd)) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box nn = pvlist_count(zd->widget[ii].cblist); // entry count for (jj = nn-1; jj >= 0; jj--) { pvlist_remove(zd->widget[ii].cblist,jj); // remove from memory list gtk_combo_box_text_remove(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget),jj); // remove from widget } widget = zd->widget[ii].widget; gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1); // set no active entry if (strEqu(zd->widget[ii].type,"comboE")) { // stuff entry box with nothing entry = gtk_bin_get_child(GTK_BIN(widget)); gtk_entry_set_text(GTK_ENTRY(entry),""); } return 0; } // make a combo box drop down to show all entries int zdialog_cb_popup(zdialog *zd, cchar *name) { int ii; TRACE if (! zdialog_valid(zd)) return 0; for (ii = 1; zd->widget[ii].type; ii++) // find widget if (strEqu(zd->widget[ii].name,name)) break; if (! zd->widget[ii].type) return 0; // not found if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0; // not combo box gtk_combo_box_popup(GTK_COMBO_BOX(zd->widget[ii].widget)); return 0; } /**************************************************************************/ // functions to save and recall zdialog window positions namespace zdposn_names { struct zdposn_t { float xpos, ypos; // window position WRT parent or desktop char wintitle[64]; // window title (ID) } zdposn[200]; // space for 200 windows int Nzdposn; // no. in use int Nzdpmax = 200; // table size } // Load zdialog positions table from its file (application startup) // or save zdialog positions table to its file (application exit). // Action is "load" or "save". Number of table entries is returned. int zdialog_positions(cchar *action) { using namespace zfuncs; using namespace zdposn_names; char posfile[200], buff[100], wintitle[64], *pp; float xpos, ypos; int nn, ii; FILE *fid; TRACE snprintf(posfile,199,"%s/zdialog_positions",zuserdir); // /home//.appname/zdialog_positions if (strEqu(action,"load")) // load dialog positions table from file { fid = fopen(posfile,"r"); if (! fid) { Nzdposn = 0; return 0; } for (nn = 0; nn < Nzdpmax; nn++) { pp = fgets(buff,100,fid); if (! pp) break; if (strlen(pp) < 64) continue; strncpy0(wintitle,buff,64); strTrim(wintitle); if (strlen(wintitle) < 3) continue; ii = sscanf(buff + 64," %f %f ",&xpos,&ypos); if (ii != 2) continue; strcpy(zdposn[nn].wintitle,wintitle); zdposn[nn].xpos = xpos; zdposn[nn].ypos = ypos; } fclose(fid); Nzdposn = nn; return Nzdposn; } if (strEqu(action,"save")) // save dialog positions table to file { fid = fopen(posfile,"w"); if (! fid) { printz("*** cannot write zdialog_positions file \n"); return 0; } for (nn = 0; nn < Nzdposn; nn++) fprintf(fid,"%-64s %0.1f %0.1f \n",zdposn[nn].wintitle, zdposn[nn].xpos, zdposn[nn].ypos); fclose(fid); return Nzdposn; } printz("*** zdialog_positions bad action: %s \n",action); return 0; } // Set the initial or new zdialog window position from "posn". // Called by zdialog_run(). // null: window manager decides // "mouse" put dialog at mouse position // "desktop" center dialog in desktop window // "parent" center dialog in parent window // "save" use the same position last set by the user // "nn/nn" put NW corner of dialog in parent window at % size // (e.g. "50/50" puts NW corner at center of parent) void zdialog_set_position(zdialog *zd, cchar *posn) { using namespace zdposn_names; int ii, ppx, ppy, zdpx, zdpy, pww, phh; float xpos, ypos; char wintitle[64], *pp; GtkWidget *parent, *dialog; TRACE parent = zd->parent; dialog = zd->widget[0].widget; if (strEqu(posn,"mouse")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE); return; } if (strEqu(posn,"desktop")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER); return; } if (strEqu(posn,"parent")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER_ON_PARENT); return; } if (! parent) { // no parent window ppx = ppy = 0; // use desktop pww = gdk_screen_width(); phh = gdk_screen_height(); } else { gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy); // parent window NW corner gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh); // parent window size } if (strEqu(posn,"save")) // use last saved window position { zd->saveposn = 1; // set flag for zdialog_free() pp = (char *) gtk_window_get_title(GTK_WINDOW(dialog)); // get window title, used as ID if (! pp) return; if (strlen(pp) < 2) return; strncpy0(wintitle,pp,64); // window title, < 64 chars. for (ii = 0; ii < Nzdposn; ii++) // search table for title if (strEqu(wintitle,zdposn[ii].wintitle)) break; if (ii == Nzdposn) return; // not found - zdialog_free() will add zdpx = ppx + 0.01 * zdposn[ii].xpos * pww; // position for dialog window zdpy = ppy + 0.01 * zdposn[ii].ypos * phh; gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy); return; } else // "nn/nn" // position from caller { ii = sscanf(posn,"%f/%f",&xpos,&ypos); // parse "nn/nn" if (ii != 2) return; zdpx = ppx + 0.01 * xpos * pww; // position for dialog window zdpy = ppy + 0.01 * ypos * phh; gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy); return; } } // If the dialog window position is "save" then save // its position WRT parent or desktop for next use. // called by zdialog_destroy(). void zdialog_save_position(zdialog *zd) { using namespace zdposn_names; int ii, ppx, ppy, pww, phh, zdpx, zdpy; float xpos, ypos; char wintitle[64], *pp; GtkWidget *parent, *dialog; TRACE dialog = zd->widget[0].widget; if (! dialog) return; // destroyed gtk_window_get_position(GTK_WINDOW(dialog),&zdpx,&zdpy); // dialog window NW corner if (! zdpx && ! zdpy) return; // (0,0) ignore v.5.7 parent = zd->parent; // parent window if (! parent) { // no parent window ppx = ppy = 0; // use desktop pww = gdk_screen_width(); phh = gdk_screen_height(); } else { gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy); // parent window NW corner gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh); // parent window size } xpos = 100.0 * (zdpx - ppx) / pww; // dialog window relative position ypos = 100.0 * (zdpy - ppy) / phh; // (as percent of parent size) pp = (char *) gtk_window_get_title(GTK_WINDOW(dialog)); if (! pp) return; if (strlen(pp) < 2) return; strncpy0(wintitle,pp,64); // window title, < 64 chars. for (ii = 0; ii < Nzdposn; ii++) // search table for window if (strEqu(wintitle,zdposn[ii].wintitle)) break; if (ii == Nzdposn) { // not found if (ii == Nzdpmax) return; // table full strcpy(zdposn[ii].wintitle,wintitle); // add window to table Nzdposn++; } zdposn[ii].xpos = xpos; // save window position zdposn[ii].ypos = ypos; return; } /**************************************************************************/ // Functions to save and restore zdialog user inputs // within an app session or across app sessions. namespace zdinputs_names { struct zdinputs_t { char *zdtitle; // zdialog title int Nw; // no. of widgets char **wname; // list of widget names char **wdata; // list of widget data } zdinputs[200]; // space for 200 dialogs int Nzd = 0; // no. zdialogs in use int Nzdmax = 200; // max. zdialogs int Nwmax = zdmaxwidgets; // max. widgets in a zdialog int ccmax1 = 99; // max. widget name length int ccmax2 = 199; // max. widget data length } // Load zdialog input fields from its file (app startup) // or save zdialog input fields to its file (app shutdown). // Action is "load" or "save". // Number of zdialogs is returned. int zdialog_inputs(cchar *action) // v.5.3 { using namespace zfuncs; using namespace zdinputs_names; char zdinputsfile[200], buff[200]; char zdtitle[100], wname[100][100], wdata[100][200]; char *pp, *pp1, *pp2, wdata2[200]; FILE *fid; int Nw, ii, jj, cc, cc1, cc2; TRACE snprintf(zdinputsfile,199,"%s/zdialog_inputs",zuserdir); // /home//.appname/zdialog_inputs if (strEqu(action,"load")) // load dialog input fields from its file { Nzd = 0; fid = fopen(zdinputsfile,"r"); // no file if (! fid) return 0; while (true) { pp = fgets_trim(buff,200,fid,1); // read next zdialog title record if (! pp) break; if (! strnEqu(pp,"zdialog == ",11)) continue; strncpy0(zdtitle,pp+11,ccmax1); // save new zdialog title pp = fgets_trim(buff,200,fid,1); // read widget count if (! pp) break; Nw = atoi(pp); if (Nw < 1 || Nw > Nwmax) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); continue; } for (ii = 0; ii < Nw; ii++) // read widget data recs { pp = fgets_trim(buff,200,fid,1); if (! pp) break; pp1 = pp; pp2 = strstr(pp1," =="); if (! pp2) break; // widget has no data cc1 = pp2 - pp1; pp1[cc1] = 0; pp2 += 3; if (*pp2 == ' ') pp2++; cc2 = strlen(pp2); if (cc1 < 1 || cc1 > ccmax1) break; if (cc2 < 1) pp2 = (char *) ""; if (cc2 > ccmax2) break; strcpy(wname[ii],pp1); // save widget name and data strcpy(wdata2,pp2); repl_1str(wdata2,wdata[ii],"\\n","\n"); // replace "\n" with newline chars. } if (ii < Nw) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); continue; } if (Nzd == Nzdmax) { printz("*** zdialog_inputs() too many dialogs \n"); break; } zdinputs[Nzd].zdtitle = zstrdup(zdtitle); // save acculumated zdialog data zdinputs[Nzd].Nw = Nw; cc = Nw * sizeof(char *); zdinputs[Nzd].wname = (char **) zmalloc(cc); zdinputs[Nzd].wdata = (char **) zmalloc(cc); for (ii = 0; ii < Nw; ii++) { zdinputs[Nzd].wname[ii] = zstrdup(wname[ii]); zdinputs[Nzd].wdata[ii] = zstrdup(wdata[ii]); } Nzd++; } fclose(fid); return Nzd; } if (strEqu(action,"save")) // save dialog input fields to its file { fid = fopen(zdinputsfile,"w"); if (! fid) { printz("*** zdialog_inputs() cannot write file \n"); return 0; } for (ii = 0; ii < Nzd; ii++) { fprintf(fid,"zdialog == %s \n",zdinputs[ii].zdtitle); // zdialog == zdialog title Nw = zdinputs[ii].Nw; fprintf(fid,"%d \n",Nw); // widget count for (jj = 0; jj < Nw; jj++) { pp1 = zdinputs[ii].wname[jj]; // widget name == widget data pp2 = zdinputs[ii].wdata[jj]; repl_1str(pp2,wdata2,"\n","\\n"); // replace newline chars. with "\n" fprintf(fid,"%s == %s \n",pp1,wdata2); } fprintf(fid,"\n"); } fclose(fid); return Nzd; } printz("*** zdialog_inputs bad action: %s \n",action); return 0; } // Save dialog user input fields when a dialog is finished. // Called automatically by zdialog_free(). int zdialog_save_inputs(zdialog *zd) // v.5.3 { using namespace zdinputs_names; char zdtitle[100], wname[100], wdata[200], *type; int ii, jj, Nw, cc; TRACE if (! zdialog_valid(zd)) return 0; if (! zd->saveinputs) return 0; // zdialog does not use this service strncpy0(zdtitle,zd->widget[0].data,ccmax1); // zdialog title is widget[0].data for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs table if (strEqu(zdtitle,zdinputs[ii].zdtitle)) break; if (ii < Nzd) { // found zfree(zdinputs[ii].zdtitle); // delete obsolete zdinputs data for (jj = 0; jj < zdinputs[ii].Nw; jj++) { zfree(zdinputs[ii].wname[jj]); zfree(zdinputs[ii].wdata[jj]); } zfree(zdinputs[ii].wname); zfree(zdinputs[ii].wdata); Nzd--; // decr. zdialog count for (ii = ii; ii < Nzd; ii++) // pack down the rest zdinputs[ii] = zdinputs[ii+1]; } if (Nzd == Nzdmax) { printz("*** zdialog_save_inputs, too many zdialogs \n"); return 0; } ii = Nzd; // next zdinputs table entry for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) // count zdialog widgets { type = (char *) zd->widget[jj].type; if (strstr("dialog hbox vbox hsep vsep frame " // skip non-input widgets "scrwin label link button",type)) continue; Nw++; } if (! Nw) return 0; // no input widgets if (Nw > Nwmax) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); return 0; } zdinputs[ii].zdtitle = zstrdup(zdtitle); // set zdialog title cc = Nw * sizeof(char *); // allocate pointers for widgets zdinputs[ii].wname = (char **) zmalloc(cc); zdinputs[ii].wdata = (char **) zmalloc(cc); for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) // add widget names and data { type = (char *) zd->widget[jj].type; if (strstr("dialog hbox vbox hsep vsep frame " "scrwin label link button",type)) continue; strncpy0(wname,zd->widget[jj].name,ccmax1); if (zd->widget[jj].data) strncpy0(wdata,zd->widget[jj].data,ccmax2); else strcpy(wdata,""); zdinputs[ii].wname[Nw] = zstrdup(wname); zdinputs[ii].wdata[Nw] = zstrdup(wdata); Nw++; } zdinputs[ii].Nw = Nw; // set widget count Nzd++; // add zdialog to end of zdinputs return 1; } // Restore user input fields from prior use of the same dialog. // Call this if wanted after zdialog is built and before it is run. // Override old user inputs with zdialog_stuff() where needed. int zdialog_restore_inputs(zdialog *zd) // v.5.3 { using namespace zdinputs_names; char *zdtitle, *wname, *wdata; int ii, jj; TRACE zd->saveinputs = 1; // flag, save data at zdialog_free() zdtitle = (char *) zd->widget[0].data; // zdialog title for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs if (strEqu(zdtitle,zdinputs[ii].zdtitle)) break; if (ii == Nzd) return 0; // not found for (jj = 0; jj < zdinputs[ii].Nw; jj++) { // stuff all saved widget data wname = zdinputs[ii].wname[jj]; wdata = zdinputs[ii].wdata[jj]; zdialog_put_data(zd,wname,wdata); } return 1; } /**************************************************************************/ // Output text to a popup window. // Window is left on screen until user destroys it with [x] button or // caller closes it with "close" action. // action: open: create window with title = text and size = ww, hh // write: write text to next line in window // top: scroll back to line 1 after writing last line // close: close window now // close N: close window after N seconds // The parent argument is optional and will cause the popup window to center // on the parent window. The GtkTextView window is returned for possible // use with textwidget_set_clickfunc() etc. GtkWidget * write_popup_text(cchar *action, cchar *text, int ww, int hh, GtkWidget *parent) { int popup_text_timeout(GtkWidget **mWin); static GtkWidget *mWin = 0, *mVbox, *mScroll; static GtkWidget *mLog = 0; static PangoFontDescription *monofont = 0; int secs; zthreadcrash(); // thread usage not allowed if (! monofont) monofont = pango_font_description_from_string("monospace 9"); if (strEqu(action,"open")) { if (mWin) gtk_widget_destroy(mWin); // only one at a time if (! ww) ww = 400; if (! hh) hh = 300; mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create main window gtk_window_set_title(GTK_WINDOW(mWin),text); gtk_window_set_default_size(GTK_WINDOW(mWin),ww,hh); if (parent) { // parent added gtk_window_set_transient_for(GTK_WINDOW(mWin),GTK_WINDOW(parent)); gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER_ON_PARENT); } else gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_MOUSE); mVbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(mWin),mVbox); // add to main window mScroll = gtk_scrolled_window_new(0,0); // scrolled window gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0); // add to main window mVbox gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(mScroll), GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC); mLog = gtk_text_view_new(); // text edit window gtk_container_add(GTK_CONTAINER(mScroll),mLog); // add to scrolled window gtk_widget_override_font(mLog,monofont); // set small monospaced font G_SIGNAL(mWin,"destroy",write_popup_text,"destroypop"); // connect window destroy event gtk_widget_show_all(mWin); // show window gtk_window_present(GTK_WINDOW(mWin)); if (! parent) gtk_window_set_keep_above(GTK_WINDOW(mWin),1); zfuncs::open_popup_windows++; } if (strEqu(action,"write")) // add text to window if (mWin) wprintf(mLog," %s\n",text); if (strEqu(action,"top")) // scroll to top line if (mWin) wscroll(mLog,1); if (strnEqu(action,"close",5)) { // close window secs = atoi(action+5); if (secs < 1) { if (mWin) gtk_widget_destroy(mWin); // immediate mWin = 0; } else // after N seconds g_timeout_add_seconds(secs,(GSourceFunc) popup_text_timeout,&mWin); } if (text && strEqu(text,"destroypop")) { // "destroy" signal from [x] zfuncs::open_popup_windows--; mWin = 0; } zmainloop(); return mLog; } // private function for timeout int popup_text_timeout(GtkWidget **mWin) // v.5.6 { if (*mWin) gtk_widget_destroy(*mWin); return 0; } /**************************************************************************/ // execute a command and show the output in a scrolling popup window int popup_command(cchar *command, int ww, int hh, GtkWidget *parent) // use write_popup_text() { char *buff; int err, contx = 0; write_popup_text("open",command,ww,hh,parent); // bugfix while ((buff = command_output(contx,command))) { write_popup_text("write",buff); zfree(buff); } write_popup_text("top",0); // back to top of window err = command_status(contx); return err; } /**************************************************************************/ // display message box and wait for user acknowledgement // may be called from a thread (uses xterm message). void zmessageACK(GtkWidget *parent, cchar *title, cchar *format, ... ) // with title v.5.5 { va_list arglist; char message[1000]; zdialog *zd; GtkWidget *widget; TRACE if (! pthread_equal(pthread_self(),zfuncs::tid_main)) { // from a thread, no GTK allowed zpopup_message(format); return; } va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); zd = zdialog_new(title,parent,"OK",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 gtk_window_present(GTK_WINDOW(widget)); // should not be needed (GTK bug) v.5.5 zdialog_run(zd,0,"mouse"); // v.5.5 zdialog_wait(zd); zdialog_free(zd); return; } /**************************************************************************/ // log error message to STDOUT as well as message box and user OK void zmessLogACK(GtkWidget *parent, cchar *format, ...) { va_list arglist; char message[1000]; TRACE va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); printz("%s \n",message); zmessageACK(parent,0,message); return; } /**************************************************************************/ // display message box and wait for user Yes or No response // returns 1 or 0 int zmessageYN(GtkWidget *parent, cchar *format, ... ) { va_list arglist; char message[400]; zdialog *zd; int zstat; GtkWidget *widget; TRACE zthreadcrash(); // thread usage not allowed va_start(arglist,format); vsnprintf(message,400,format,arglist); va_end(arglist); zd = zdialog_new("message",parent,ZTX("Yes"),ZTX("No"),null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat == 1) return 1; return 0; } /**************************************************************************/ // display message until timeout (can be forever) or user cancel // or caller kills it with zdialog_free() zdialog * zmessage_post(GtkWidget *parent, int seconds, cchar *format, ... ) { int zmessage_post_timeout(zdialog *zd); int zmessage_post_event(zdialog *zd, cchar *event); va_list arglist; char message[400]; static zdialog *zd; TRACE zthreadcrash(); // thread usage not allowed va_start(arglist,format); vsnprintf(message,400,format,arglist); va_end(arglist); zd = zdialog_new("message",parent,ZTX("cancel"),null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_run(zd,zmessage_post_event); if (seconds) g_timeout_add_seconds(seconds,(GSourceFunc) zmessage_post_timeout,zd); zmainloop(); // v.5.2 return zd; } int zmessage_post_timeout(zdialog *zd) { TRACE if (! zdialog_valid(zd)) return 0; if (zd->zstat == 0) zdialog_free(zd); return 0; } int zmessage_post_event(zdialog *zd, cchar *event) { TRACE if (! zdialog_valid(zd)) return 0; if (zd->zstat) zdialog_free(zd); return 0; } /**************************************************************************/ // get text input from a popup dialog // returned text is subject for zfree() // null is returned if user presses [cancel] button. char * zdialog_text(GtkWidget *parent, cchar *title, cchar *inittext) { zdialog *zd; int zstat; char *text; GtkWidget *widget; TRACE zthreadcrash(); // thread usage not allowed zd = zdialog_new(title,parent,"OK",ZTX("cancel"),null); zdialog_add_widget(zd,"frame","fred","dialog"); zdialog_add_widget(zd,"edit","edit","fred"); if (inittext) zdialog_stuff(zd,"edit",inittext); zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); if (zstat == 1) text = (char *) zdialog_get_data(zd,"edit"); else text = 0; if (text) text = zstrdup(text); zdialog_free(zd); return text; } /**************************************************************************/ // Display a dialog with a message and 1-5 choice buttons. // Returns choice 1-N corresponding to button selected. // Parent window may be NULL. // List of buttons must be NULL terminated. int zdialog_choose(cchar *title, GtkWidget *parent, cchar *message, ...) { zdialog *zd; va_list arglist; int ii, zstat, Nbutts; cchar *butts[6]; GtkWidget *widget; TRACE zthreadcrash(); // thread usage not allowed va_start(arglist,message); for (ii = 0; ii < 5; ii++) { butts[ii] = va_arg(arglist,cchar *); if (! butts[ii]) break; } Nbutts = ii; if (! Nbutts) zappcrash("zdialog_choose(), no buttons"); repeat: zd = zdialog_new(title,parent,butts[0],butts[1],butts[2],butts[3],butts[4],null); zdialog_add_widget(zd,"hbox","hbmess","dialog","space=3"); zdialog_add_widget(zd,"label","labmess","hbmess",message,"space=5"); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat < 1) goto repeat; return zstat; } /**************************************************************************/ // functions to show popup text windows namespace poptext { char *ptext = 0; GtkWidget *pwidget = 0; char *pcurrent = 0; GdkRGBA pblack = { 0.2, 0.2, 0.2, 1 }; // v.5.6 GdkRGBA pwhite = { 0.9, 0.9, 0.9, 1 }; #define GSFNORMAL GTK_STATE_FLAG_NORMAL } // timer function to show popup window after a given time int poptext_show(char *current) { using namespace poptext; if (current != pcurrent) return 0; if (pwidget) gtk_widget_show_all(pwidget); return 0; } // timer function to kill popup window after a given time int poptext_kill(char *current) { using namespace poptext; if (current != pcurrent) return 0; if (pwidget) gtk_widget_destroy(pwidget); if (ptext) zfree(ptext); pwidget = 0; ptext = 0; return 0; } // kill popup window unconditionally // v.5.8 int poptext_killnow() { using namespace poptext; if (pwidget) gtk_widget_destroy(pwidget); if (ptext) zfree(ptext); pwidget = 0; ptext = 0; return 0; } // Show a popup text message at current mouse position + offsets. // v.5.6 // Any prior popup will be killed and replaced. // If text == null, kill without replacement. // secs1 is time to delay before showing the popup. // secs2 is time to kill the popup after it is shown. // This function returns immediately. // Get mouse via GdkEvent->device. void poptext_mouse(cchar *text, GdkDevice *mouse, int dx, int dy, float secs1, float secs2) { using namespace poptext; GtkWidget *label; int cc, mx, my; int millisec1, millisec2; zthreadcrash(); // thread usage not allowed if (pwidget) gtk_widget_destroy(pwidget); // kill prior popup if (ptext) zfree(ptext); pwidget = 0; ptext = 0; pcurrent++; // make current != pcurrent if (! text) return; cc = strlen(text); // construct popup window ptext = (char *) zmalloc(cc+1); // with caller's text strcpy(ptext,text); pwidget = gtk_window_new(GTK_WINDOW_POPUP); label = gtk_label_new(ptext); gtk_container_add(GTK_CONTAINER(pwidget),label); gtk_widget_override_background_color(pwidget,GSFNORMAL,&pblack); // window not label v.5.6 gtk_widget_override_color(label,GSFNORMAL,&pwhite); gdk_device_get_position(mouse,0,&mx,&my); // position popup window gtk_window_move(GTK_WINDOW(pwidget),mx+dx,my+dy); if (secs1 > 0) { // delayed popup display millisec1 = secs1 * 1000; g_timeout_add(millisec1,(GSourceFunc) poptext_show,pcurrent); } else gtk_widget_show_all(pwidget); // immediate display if (secs2 > 0) { // popup kill timer millisec2 = (secs1 + secs2) * 1000; g_timeout_add(millisec2,(GSourceFunc) poptext_kill,pcurrent); } return; } // Show a popup text message at the given window position. // v.5.6 // Any prior popup will be killed and replaced. // If text == null, kill without replacement. // secs1 is time to delay before showing the popup. // secs2 is time to kill the popup after it is shown (-1 = never). // This function returns immediately. void poptext_window(cchar *text, GtkWindow *pwin, int mx, int my, float secs1, float secs2) { using namespace poptext; GtkWidget *label; int cc, pmx, pmy; int millisec1, millisec2; zthreadcrash(); // thread usage not allowed if (pwidget) gtk_widget_destroy(pwidget); // kill prior popup if (ptext) zfree(ptext); pwidget = 0; ptext = 0; pcurrent++; // make current != pcurrent if (! text) return; cc = strlen(text); // construct popup window ptext = (char *) zmalloc(cc+1); // with caller's text strcpy(ptext,text); pwidget = gtk_window_new(GTK_WINDOW_POPUP); label = gtk_label_new(ptext); gtk_container_add(GTK_CONTAINER(pwidget),label); gtk_widget_override_background_color(pwidget,GSFNORMAL,&pblack); // window not label v.5.6 gtk_widget_override_color(label,GSFNORMAL,&pwhite); if (pwin) { // position relative to pwin gtk_window_get_position(pwin,&pmx,&pmy); mx += pmx; my += pmy; } gtk_window_move(GTK_WINDOW(pwidget),mx,my); if (secs1 > 0) { // delayed popup display millisec1 = secs1 * 1000; g_timeout_add(millisec1,(GSourceFunc) poptext_show,pcurrent); } else gtk_widget_show_all(pwidget); // immediate display if (secs2 > 0) { // popup kill timer millisec2 = (secs1 + secs2) * 1000; g_timeout_add(millisec2,(GSourceFunc) poptext_kill,pcurrent); } return; } // show an image file in a new popup window // returns 0 if OK, +N otherwise int popup_image(cchar *imagefile, GtkWindow *parent, int size) // v.5.8 { void popup_image_paint(GtkWidget *, cairo_t *, cchar *); GtkWidget *window, *drawarea; window = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create output window gtk_window_set_default_size(GTK_WINDOW(window),size,size); drawarea = gtk_drawing_area_new(); // add drawing window gtk_container_add(GTK_CONTAINER(window),drawarea); G_SIGNAL(drawarea,"draw",popup_image_paint,imagefile); // connect paint function if (parent) gtk_window_set_transient_for(GTK_WINDOW(window),parent); gtk_window_set_position(GTK_WINDOW(window),GTK_WIN_POS_MOUSE); gtk_widget_show_all(window); return 0; } void popup_image_paint(GtkWidget *drawarea, cairo_t *cr, cchar *imagefile) { GdkPixbuf *pixbuf; GError *gerror = 0; GtkWidget *window; int ww, hh; cchar *pp; ww = gtk_widget_get_allocated_width(drawarea); // drawing window size hh = gtk_widget_get_allocated_height(drawarea); pixbuf = gdk_pixbuf_new_from_file_at_size(imagefile,ww,hh,&gerror); // load image file into pixbuf if (! pixbuf) { printz("file: %s \n %s \n",imagefile,gerror->message); return; } ww = gdk_pixbuf_get_width(pixbuf); hh = gdk_pixbuf_get_height(pixbuf); window = gtk_widget_get_parent(drawarea); gtk_window_set_default_size(GTK_WINDOW(window),ww,hh); gtk_window_resize(GTK_WINDOW(window),ww,hh); pp = strrchr(imagefile,'/'); gtk_window_set_title(GTK_WINDOW(window),pp+1); gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0); // paint window cairo_paint(cr); g_object_unref(pixbuf); return; } // move the mouse pointer to given position in given window int move_pointer(GtkWidget *widget, int px, int py) // v.5.6 { GdkScreen *screen; GdkDevice *mouse; GdkWindow *window; int err, rpx, rpy; err = get_mouse_device(widget,&screen,&mouse); if (err) return 0; window = gtk_widget_get_window(widget); if (! window) return 0; gdk_window_get_root_coords(window,px,py,&rpx,&rpy); gdk_device_warp(mouse,screen,rpx,rpy); return 1; } /************************************************************************** File chooser dialog for one or more files Action: "file" select an existing file "files" select multiple existing files "save" select an existing or new file "folder" select existing folder "folders" select multiple existing folders "create folder" select existing or new folder buttx: "hidden" add button to toggle display of hidden files "quality" add button to set JPG file save quality optional, default = null Returns a list of filespecs terminated with null. Memory for returned list and returned files are subjects for zfree(); ***************************************************************************/ // version for 1 file only: file, save, folder, create folder // returns one filespec or null char * zgetfile(cchar *title, cchar *action, cchar *initfile, cchar *buttx) { if (! strcmpv(action,"file","save","folder","create folder",null)) zappcrash("zgetfile() call error: %s",action); char **flist = zgetfiles(title,action,initfile,buttx); if (! flist) return 0; char *file = *flist; zfree(flist); return file; } // version for 1 or more files // returns a list of filespecs (char **) terminated with null char ** zgetfiles(cchar *title, cchar *action, cchar *initfile, cchar *buttx) { using namespace zfuncs; void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget); // private functions int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event); void zgetfile_newfolder(GtkFileChooser *dialog, void *); GtkFileChooserAction fcact = GTK_FILE_CHOOSER_ACTION_OPEN; GtkWidget *dialog; GtkWidget *pvwidget = gtk_image_new(); GSList *gslist = 0; cchar *button1 = 0, *buttxx = 0; char *pdir, *pfile; int ii, err, NF, setfname = 0; int fcstat, bcode = 0, qnum, hide = 0; char *qual, *file1, *file2, **flist = 0; struct stat fstat; zthreadcrash(); // thread usage not allowed if (strEqu(action,"file")) { fcact = GTK_FILE_CHOOSER_ACTION_OPEN; button1 = ZTX("choose file"); } else if (strEqu(action,"files")) { fcact = GTK_FILE_CHOOSER_ACTION_OPEN; button1 = ZTX("choose files"); } else if (strEqu(action,"save")) { fcact = GTK_FILE_CHOOSER_ACTION_SAVE; button1 = ZTX("save"); setfname = 1; } else if (strEqu(action,"folder")) { fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; button1 = ZTX("choose folder"); } else if (strEqu(action,"folders")) { fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; button1 = ZTX("choose folders"); } else if (strEqu(action,"create folder")) { fcact = GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER; button1 = ZTX("create folder"); setfname = 1; } else zappcrash("zgetfiles() call error: %s",action); if (buttx) { if (strnEqu(buttx,"hidden",6)) { // generate text for translation buttxx = ZTX("hidden"); bcode = 103; } if (strEqu(buttx,"quality")) { buttxx = ZTX("quality"); bcode = 104; } } dialog = gtk_file_chooser_dialog_new(title, null, fcact, // create file selection dialog button1, GTK_RESPONSE_ACCEPT, ZTX("cancel"), GTK_RESPONSE_CANCEL, buttxx, bcode, null); gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog),pvwidget); G_SIGNAL(dialog,"update-preview",zgetfile_preview,pvwidget); // create preview for selected file G_SIGNAL(dialog,"key-release-event",zgetfile_KBkey,0); // respond to F1 help key gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE); // put dialog at mouse position gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),0); // default: no show hidden if (strEqu(action,"save")) // overwrite confirmation gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),1); if (strEqu(action,"files") || strEqu(action,"folders")) gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog),1); // select multiple files or folders v.5.8 if (initfile) { // pre-select filespec err = stat(initfile,&fstat); if (err) { pdir = zstrdup(initfile); // non-existent file pfile = strrchr(pdir,'/'); if (pfile && pfile > pdir) { *pfile++ = 0; // set folder name gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),pdir); } if (setfname) // set new file name gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pfile); zfree(pdir); } else if (S_ISREG(fstat.st_mode)) // select given file gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile); else if (S_ISDIR(fstat.st_mode)) // select given folder gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),initfile); } gtk_widget_show_all(dialog); while (true) { fcstat = gtk_dialog_run(GTK_DIALOG(dialog)); // run dialog, get status button if (fcstat == 103) { // show/hide hidden files hide = gtk_file_chooser_get_show_hidden(GTK_FILE_CHOOSER(dialog)); hide = 1 - hide; gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),hide); } else if (fcstat == 104) { // get JPG quality parameter while (true) { qual = zdialog_text(null,ZTX("JPG quality 0-100"),JPGquality); if (! qual) break; // cancel = no change err = convSI(qual,qnum,0,100); zfree(qual); if (err) continue; // enforce 0-100 snprintf(JPGquality,4,"%d",qnum); break; } } else break; // some other button } if (fcstat == GTK_RESPONSE_ACCEPT) { gslist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); if (! gslist) goto fcreturn; NF = g_slist_length(gslist); // no. selected files if (! NF) goto fcreturn; flist = (char **) zmalloc((NF+1)*sizeof(char *)); // allocate returned list for (ii = 0; ii < NF; ii++) { // process selected files file1 = (char *) g_slist_nth_data(gslist,ii); if (strlen(file1) >= maxfcc) file1 = (char *) "ridiculously long filespec"; file2 = zstrdup(file1); // re-allocate memory g_free(file1); flist[ii] = file2; } flist[ii] = 0; // EOL marker } fcreturn: if (gslist) g_slist_free(gslist); gtk_widget_destroy(dialog); return flist; } // zgetfile private function - get preview images for image files void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget) { using namespace zfuncs; GdkPixbuf *thumbnail; char *filename; filename = gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(dialog)); if (! filename) { gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0); return; } thumbnail = get_thumbnail(filename,192); // use 192x192 pixels g_free(filename); if (thumbnail) { gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail); gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1); g_object_unref(thumbnail); } else gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0); return; } // zgetfile private function - send F1 key (help) to main app int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event) { int KBkey = event->keyval; if (KBkey != GDK_KEY_F1) return 0; // v.5.6 KBstate(event,0); return 1; } /************************************************************************** print_image_file(GtkWidget *parent, cchar *imagefile) Print an image file using the printer, paper, orientation, margins, and scale set by the user. HPLIP problem: Setting paper size was made less flexible. GtkPrintSettings paper size must agree with the one in the current printer setup. This can only be set in the printer setup dialog, not in the application. Also the print size (width, height) comes from the chosen paper size and cannot be changed in the application. Print margins can be changed to effect printing a smaller or shifted image on a larger paper size. ***************************************************************************/ namespace print_image { #define MM GTK_UNIT_MM #define INCH GTK_UNIT_INCH #define PRINTOP GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG #define PORTRAIT GTK_PAGE_ORIENTATION_PORTRAIT #define LANDSCAPE GTK_PAGE_ORIENTATION_LANDSCAPE #define QUALITY GTK_PRINT_QUALITY_HIGH GtkWidget *parent = 0; GtkPageSetup *priorpagesetup = 0; GtkPageSetup *pagesetup; GtkPrintSettings *printsettings = 0; GtkPrintOperation *printop; GtkPageOrientation orientation = PORTRAIT; GdkPixbuf *pixbuf; cchar *printer = 0; int landscape = 0; // true if landscape double width = 21.0, height = 29.7; // paper size, CM (default A4 portrait) double margins[4] = { 0.5, 0.5, 0.5, 0.5 }; // margins, CM (default 0.5) v.5.5 double imagescale = 100; // image print scale, percent double pwidth, pheight; // printed image size int paper_setup(); int margins_setup(); int margins_dialog_event(zdialog *zd, cchar *event); void get_printed_image_size(); void print_page(GtkPrintOperation *, GtkPrintContext *, int page); } // user callable function to set paper, margins, scale, and then print void print_image_file(GtkWidget *pwin, cchar *imagefile) // overhauled v.5.2 { using namespace print_image; GtkPrintOperationResult printstat; GError *gerror = 0; int err; parent = pwin; // save parent window pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror); // read image file if (! pixbuf) { zmessageACK(null,0,gerror->message); return; } err = paper_setup(); // select paper if (err) return; // v.5.5 err = margins_setup(); // set margins and scale if (err) return; // v.5.5 printop = gtk_print_operation_new(); // print operation gtk_print_operation_set_default_page_setup(printop,pagesetup); gtk_print_operation_set_print_settings(printop,printsettings); gtk_print_operation_set_n_pages(printop,1); g_signal_connect(printop,"draw-page",G_CALLBACK(print_page),0); // start print printstat = gtk_print_operation_run(printop,PRINTOP,0,0); if (printstat == GTK_PRINT_OPERATION_RESULT_ERROR) { gtk_print_operation_get_error(printop,&gerror); zmessageACK(null,0,gerror->message); } g_object_unref(printop); return; } // draw the graphics for the print page // rescale with cairo void print_image::print_page(GtkPrintOperation *printop, GtkPrintContext *printcontext, int page) { using namespace print_image; cairo_t *cairocontext; double iww, ihh, pww, phh, scale; pww = gtk_print_context_get_width(printcontext); // print context size, pixels phh = gtk_print_context_get_height(printcontext); iww = gdk_pixbuf_get_width(pixbuf); // original image size ihh = gdk_pixbuf_get_height(pixbuf); scale = pww / iww; // rescale to fit page if (phh / ihh < scale) scale = phh / ihh; cairocontext = gtk_print_context_get_cairo_context(printcontext); // use cairo to rescale cairo_translate(cairocontext,0,0); cairo_scale(cairocontext,scale,scale); gdk_cairo_set_source_pixbuf(cairocontext,pixbuf,0,0); cairo_paint(cairocontext); return; } // Do a print paper format selection, after which the page width, height // and orientation are available to the caller. Units are CM. // (paper width and height are reversed for landscape orientation) int print_image::paper_setup() { using namespace zfuncs; using namespace print_image; char printsettingsfile[200], pagesetupfile[200]; snprintf(printsettingsfile,200,"%s/printsettings",zuserdir); snprintf(pagesetupfile,200,"%s/pagesetup",zuserdir); if (! printsettings) { // start with prior print settings printsettings = gtk_print_settings_new_from_file(printsettingsfile,0); if (! printsettings) printsettings = gtk_print_settings_new(); } if (! priorpagesetup) { // start with prior page setup priorpagesetup = gtk_page_setup_new_from_file(pagesetupfile,0); if (! priorpagesetup) priorpagesetup = gtk_page_setup_new(); } pagesetup = gtk_print_run_page_setup_dialog // select printer, paper, orientation (GTK_WINDOW(parent),priorpagesetup,printsettings); // user cancel cannot be detected v.5.5 g_object_unref(priorpagesetup); // save for next call priorpagesetup = pagesetup; orientation = gtk_print_settings_get_orientation(printsettings); // save orientation if (orientation == LANDSCAPE) landscape = 1; else landscape = 0; gtk_print_settings_set_quality(printsettings,QUALITY); // set high quality 300 dpi gtk_print_settings_set_resolution(printsettings,300); gtk_print_settings_to_file(printsettings,printsettingsfile,0); // save print settings to file gtk_page_setup_to_file(pagesetup,pagesetupfile,0); // save print settings to file return 0; } // Optionally set the print margins and print scale. // If canceled the margins are zero (or printer-dependent minimum) // and the scale is 100% (fitting the paper and margins). int print_image::margins_setup() { using namespace print_image; zdialog *zd; int zstat; char imagesize[100]; #define formatsize ZTX("printed image width: %.1f height: %.1f cm") /*** top bottom left right margins [___] [___] [___] [___] scale [80|-+] percent printed image width: xx.x height: xx.x cm ***/ zd = zdialog_new(ZTX("margins"),parent,ZTX("done"),ZTX("cancel"),null); zdialog_add_widget(zd,"hbox","hbmlab","dialog"); zdialog_add_widget(zd,"vbox","vbmarg","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbtop","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbbottom","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbleft","hbmlab",0,"space=5"); zdialog_add_widget(zd,"vbox","vbright","hbmlab",0,"space=5"); zdialog_add_widget(zd,"label","space","vbmarg"," "); zdialog_add_widget(zd,"label","labtop","vbtop",ZTX("top")); zdialog_add_widget(zd,"label","labbot","vbbottom",ZTX("bottom")); zdialog_add_widget(zd,"label","lableft","vbleft",ZTX("left")); zdialog_add_widget(zd,"label","labright","vbright",ZTX("right")); zdialog_add_widget(zd,"label","labmarg","vbmarg",ZTX("margins"),"space=5"); zdialog_add_widget(zd,"spin","mtop","vbtop","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mbottom","vbbottom","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mleft","vbleft","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mright","vbright","0|3|0.1|0"); zdialog_add_widget(zd,"hbox","hbscale","dialog",0,"space=8"); zdialog_add_widget(zd,"label","labscale","hbscale",ZTX("image scale"),"space=6"); zdialog_add_widget(zd,"spin","scale","hbscale","5|100|0.1|100"); zdialog_add_widget(zd,"label","labpct","hbscale",ZTX("percent"),"space=5"); zdialog_add_widget(zd,"hbox","hbsize","dialog"); zdialog_add_widget(zd,"label","labsize","hbsize","xxx","space=6"); zdialog_stuff(zd,"mtop",margins[0]); // stuff prior page margins zdialog_stuff(zd,"mbottom",margins[1]); zdialog_stuff(zd,"mleft",margins[2]); zdialog_stuff(zd,"mright",margins[3]); zdialog_stuff(zd,"scale",imagescale); // stuff prior image scale gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % get_printed_image_size(); snprintf(imagesize,100,formatsize,pwidth,pheight); // show print size in dialog zdialog_stuff(zd,"labsize",imagesize); zdialog_run(zd,margins_dialog_event); // run dialog zstat = zdialog_wait(zd); // wait for completion zdialog_free(zd); // kill dialog if (zstat == 1) return 0; return 1; } // dialog event function // save user margin and scale changes // recompute print image size int print_image::margins_dialog_event(zdialog *zd, cchar *event) { using namespace print_image; char imagesize[100]; zdialog_fetch(zd,"mtop",margins[0]); // save print margins zdialog_fetch(zd,"mbottom",margins[1]); zdialog_fetch(zd,"mleft",margins[2]); zdialog_fetch(zd,"mright",margins[3]); zdialog_fetch(zd,"scale",imagescale); // save image print scale gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % get_printed_image_size(); snprintf(imagesize,100,formatsize,pwidth,pheight); // show print size in dialog zdialog_stuff(zd,"labsize",imagesize); return 1; } // compute printed image size based on paper size, // orientation, margins, and scale (percent) void print_image::get_printed_image_size() { using namespace print_image; double iww, ihh, pww, phh, scale; pww = 0.1 * gtk_page_setup_get_paper_width(pagesetup,MM); // get paper size phh = 0.1 * gtk_page_setup_get_paper_height(pagesetup,MM); // (mm to cm units) pww = pww - margins[2] - margins[3]; // reduce for margins phh = phh - margins[1] - margins[2]; pww = pww / 2.54 * 300; // convert to dots @ 300 dpi phh = phh / 2.54 * 300; iww = gdk_pixbuf_get_width(pixbuf); // original image size, pixels ihh = gdk_pixbuf_get_height(pixbuf); scale = pww / iww; // rescale image to fit page if (phh / ihh < scale) scale = phh / ihh; scale = scale * 0.01 * imagescale; // adjust for user scale setting pwidth = iww * scale / 300 * 2.54; // dots to cm pheight = ihh * scale / 300 * 2.54; return; } /**************************************************************************/ // connect a user callback function to a window drag-drop event void drag_drop_connect(GtkWidget *window, drag_drop_func *ufunc) { int drag_drop_connect2(GtkWidget *, void *, int, int, void *, int, int, void *); char string[] = "STRING"; GtkTargetEntry file_drop_target = { string, 0, 0 }; gtk_drag_dest_set(window, GTK_DEST_DEFAULT_ALL, &file_drop_target, 1, GDK_ACTION_COPY); G_SIGNAL(window, "drag-data-received", drag_drop_connect2, ufunc); gtk_drag_dest_add_uri_targets(window); // accept URI (file) drop return; } // private function // get dropped file, clean escapes, pass to user function // passed filespec is subject for zfree() int drag_drop_connect2(GtkWidget *, void *, int mpx, int mpy, void *sdata, int, int, void *ufunc) { char * drag_drop_unescape(cchar *escaped_string); drag_drop_func *ufunc2; char *text, *text2, *file, *file2; int cc; text = (char *) gtk_selection_data_get_data((GtkSelectionData *) sdata); ufunc2 = (drag_drop_func *) ufunc; if (strstr(text,"file://")) // text is a filespec { file = zstrdup(text+7); // get rid of junk added by GTK cc = strlen(file); while (file[cc-1] < ' ') cc--; file[cc] = 0; file2 = drag_drop_unescape(file); // clean %xx escapes from Nautilus zfree(file); ufunc2(mpx,mpy,file2); // pass file to user function } else { text2 = zstrdup(text); ufunc2(mpx,mpy,text2); } return 1; } // private function // Clean %xx escapes from strange Nautilus drag-drop file names char * drag_drop_unescape(cchar *inp) { int drag_drop_convhex(char ch); char inch, *out, *outp; int nib1, nib2; out = (char *) zmalloc(strlen(inp)+1); outp = out; while ((inch = *inp++)) { if (inch == '%') { nib1 = drag_drop_convhex(*inp++); nib2 = drag_drop_convhex(*inp++); *outp++ = nib1 << 4 | nib2; } else *outp++ = inch; } *outp = 0; return out; } // private function - convert character 0-F to number 0-15 int drag_drop_convhex(char ch) { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; return ch; } /************************************************************************** Miscellaneous GDK/GTK functions ***************************************************************************/ // Get thumbnail image for given image file. // Returned thumbnail belongs to caller: g_object_unref() is necessary. GdkPixbuf * get_thumbnail(char *fpath, int size) // v.5.0 { using namespace zfuncs; GdkPixbuf *thumbpxb; GError *gerror = 0; int err; char *bpath; struct stat statf; zthreadcrash(); // thread usage not allowed err = stat(fpath,&statf); // fpath status info if (err) return 0; if (S_ISDIR(statf.st_mode)) { // if directory, return folder image bpath = (char *) zmalloc(500); *bpath = 0; // v.5.8 strncatv(bpath,499,zicondir,"/folder.png",null); thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror); zfree(bpath); return thumbpxb; } thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,size,size,&gerror); return thumbpxb; // return pixbuf to caller } // make a cursor from a graphic file in application's icon directory // (see initz_appfiles()). GdkCursor * zmakecursor(cchar *iconfile) { using namespace zfuncs; GError *gerror = 0; GdkPixbuf *pixbuf; GdkDisplay *display; GdkCursor *cursor = 0; char iconpath[200]; zthreadcrash(); // thread usage not allowed display = gdk_display_get_default(); *iconpath = 0; strncatv(iconpath,199,zicondir,"/",iconfile,null); pixbuf = gdk_pixbuf_new_from_file(iconpath,&gerror); if (pixbuf && display) cursor = gdk_cursor_new_from_pixbuf(display,pixbuf,0,0); else printz("*** %s \n",gerror->message); return cursor; } /************************************************************************** GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf, float angle, int acolor) Rotate a pixbuf through an arbitrary angle (degrees). The returned image has the same size as the original, but the pixbuf envelope is increased to accomodate the rotated original (e.g. a 100x100 pixbuf rotated 45 deg. needs a 142x142 pixbuf). Pixels added around the rotated image have all RGB values = acolor. Angle is in degrees. Positive direction is clockwise. Pixbuf must have 8 bits per channel and 3 or 4 channels. Loss of resolution is about 1/2 pixel. Speed is about 28 million pixels/sec. on 3.3 GHz CPU. (e.g. a 10 megapix image needs about 0.36 seconds) NULL is returned if the function fails for one of the following: - pixbuf not 8 bits/channel or < 3 channels - unable to create output pixbuf (lack of memory?) Algorithm: create output pixbuf big enough for rotated input pixbuf compute coefficients for affine transform loop all output pixels (px2,py2) get corresp. input pixel (px1,py1) using affine transform if outside of pixmap output pixel = black continue for 4 input pixels based at (px0,py0) = (int(px1),int(py1)) compute overlap (0 to 1) with (px1,py1) sum RGB values * overlap output aggregate RGB to pixel (px2,py2) Benchmark: rotate 7 megapixel image 10 degrees 0.31 secs. 3.3 GHz Core i5 ***/ GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf1, float angle, int acolor) { typedef unsigned char *pixel; // 3 RGB values, 0-255 each GdkPixbuf *pixbuf2; GdkColorspace color; int nch, nbits, alpha; int ww1, hh1, rs1, ww2, hh2, rs2; int px2, py2, px0, py0; pixel ppix1, ppix2, pix0, pix1, pix2, pix3; float px1, py1; float f0, f1, f2, f3, red, green, blue, tran = 0; float a, b, d, e, ww15, hh15, ww25, hh25; float PI = 3.141593; zthreadcrash(); // thread usage not allowed nch = gdk_pixbuf_get_n_channels(pixbuf1); nbits = gdk_pixbuf_get_bits_per_sample(pixbuf1); if (nch < 3) return 0; // must have 3+ channels (colors) if (nbits != 8) return 0; // must be 8 bits per channel color = gdk_pixbuf_get_colorspace(pixbuf1); // get input pixbuf1 attributes alpha = gdk_pixbuf_get_has_alpha(pixbuf1); ww1 = gdk_pixbuf_get_width(pixbuf1); hh1 = gdk_pixbuf_get_height(pixbuf1); rs1 = gdk_pixbuf_get_rowstride(pixbuf1); while (angle < -180) angle += 360; // normalize, -180 to +180 while (angle > 180) angle -= 360; angle = angle * PI / 180; // radians, -PI to +PI if (fabsf(angle) < 0.001) { // bugfix 0.01 >> 0.001 pixbuf2 = gdk_pixbuf_copy(pixbuf1); // angle is zero within my precision return pixbuf2; } ww2 = int(ww1*fabsf(cosf(angle)) + hh1*fabsf(sinf(angle))); // rectangle containing rotated image hh2 = int(ww1*fabsf(sinf(angle)) + hh1*fabsf(cosf(angle))); pixbuf2 = gdk_pixbuf_new(color,alpha,nbits,ww2,hh2); // create output pixbuf2 if (! pixbuf2) return 0; rs2 = gdk_pixbuf_get_rowstride(pixbuf2); ppix1 = gdk_pixbuf_get_pixels(pixbuf1); // input pixel array ppix2 = gdk_pixbuf_get_pixels(pixbuf2); // output pixel array ww15 = 0.5 * ww1; hh15 = 0.5 * hh1; ww25 = 0.5 * ww2; hh25 = 0.5 * hh2; a = cosf(angle); // affine transform coefficients b = sinf(angle); d = - sinf(angle); e = cosf(angle); for (py2 = 0; py2 < hh2; py2++) // loop through output pixels for (px2 = 0; px2 < ww2; px2++) { px1 = a * (px2 - ww25) + b * (py2 - hh25) + ww15; // (px1,py1) = corresponding py1 = d * (px2 - ww25) + e * (py2 - hh25) + hh15; // point within input pixels px0 = int(px1); // pixel containing (px1,py1) py0 = int(py1); if (px1 < 0 || px0 >= ww1-1 || py1 < 0 || py0 >= hh1-1) { // if outside input pixel array pix2 = ppix2 + py2 * rs2 + px2 * nch; // output is acolor pix2[0] = pix2[1] = pix2[2] = acolor; continue; } pix0 = ppix1 + py0 * rs1 + px0 * nch; // 4 input pixels based at (px0,py0) pix1 = pix0 + rs1; pix2 = pix0 + nch; pix3 = pix0 + rs1 + nch; f0 = (px0+1 - px1) * (py0+1 - py1); // overlap of (px1,py1) f1 = (px0+1 - px1) * (py1 - py0); // in each of the 4 pixels f2 = (px1 - px0) * (py0+1 - py1); f3 = (px1 - px0) * (py1 - py0); red = f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0]; // sum the weighted inputs green = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1]; blue = f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2]; if (alpha) tran = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3]; // 4th color = alpha if (red == acolor && green == acolor && blue == acolor) { // avoid acolor in image if (blue == 0) blue = 1; else blue--; } pix2 = ppix2 + py2 * rs2 + px2 * nch; // output pixel pix2[0] = int(red); pix2[1] = int(green); pix2[2] = int(blue); if (alpha) pix2[3] = int(tran); // bugfix } return pixbuf2; } /************************************************************************** parameter management functions int initParmList(int maxparms) int loadParms() int loadParms(char *filename) int saveParms() int saveParms(char *filename) int setParm(cchar *parmname, double parmval) double getParm(cchar *parmname) char * getParm(int Nth) int listParms(GtkWidget *textWin) int editParms(GtkWidget *textWin, int addp) initParms call before any other functions to initialize empty list loadParms load parameters from a file - file selection dialog loadParms load parameters from a file - filename argument saveParms save parameters to a file - file selection dialog saveParms save parameters to a file - filename argument setParm sets the value of a parameter (created if not already defined) getParm returns the value of a parameter by name (NAN if non-existent) getParm returns the name of the Nth parameter (0 based), or null listParms list all parameter names and values in text window editParms edit the parameters with a GUI loadParms and saveParms use zgetfile() selection dialog loadParms returns the count of parameters loaded saveParms returns the count saved editParms() starts a dialog which shows all parameters and allows them to be edited. The dialog has buttons to allow loading parameters from a file or saving them to a file. If the argument textWin is non-zero, a button is added which lists all parameters to the window. If the argument addp is non-zero, a button is added to allow new parameters to be defined. ***************************************************************************/ struct t_parmlist { // parameter list in memory int max; // max parameter count int count; // actual parameter count char **name; // pointer to names (list of char *) double *value; // pointer to values (list of double) } parmlist; int parmlistvalid = 0; // flag char zparmfile[200]; // last used parm file // initialize parameter list - must be called first int initParmlist(int max) { using namespace zfuncs; int err; if (! parmlistvalid) { // start with default parms file err = locale_filespec("user","parameters",zparmfile); if (err) return 0; } if (parmlistvalid) { // delete old parms delete [] parmlist.name; delete [] parmlist.value; } parmlist.max = max; parmlist.count = 0; char **names = new char*[max]; // allocate max pointers for names double *values = new double[max]; // allocate max doubles for values parmlist.name = names; parmlist.value = values; parmlistvalid = 1; return 0; } // Load user parameters if the file exists, else initialize the // user parameters file from default application parameters. int initz_userParms() { if (! parmlistvalid) zappcrash("parmlistvalid = 0"); int np = loadParms("parameters"); if (! np) { saveParms("parameters"); zmessageACK(0,0,ZTX("Initial parameters file created. \n" "Inspect and revise if necessary.")); } return np; } // load parameters from a file, with file selection dialog int loadParms() { char *pfile; int np; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); pfile = zgetfile(ZTX("load parameters from a file"),"file",zparmfile,"hidden"); if (! pfile) return 0; np = loadParms(pfile); zfree(pfile); return np; } // load parameters from a file // returns no. parameters loaded int loadParms(cchar *pfile) { FILE *fid; int Nth, np1, np2 = 0, err; char buff[100], *fgs, *pp; cchar *pname, *pvalue; double dvalue; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); if (! pfile) pfile = zparmfile; if (*pfile != '/') { // if parm file name only, pp = (char *) strrchr(zparmfile,'/'); // make complete absolute path if (pp) strcpy(pp+1,pfile); // in same directory as prior pfile = zparmfile; } fid = fopen(pfile,"r"); if (! fid) return 0; // bad file strncpy0(zparmfile,pfile,200); // set current parm file while (true) // read file { fgs = fgets_trim(buff,99,fid,1); if (! fgs) break; // EOF pp = strchr(buff,'#'); // eliminate comments if (pp) *pp = 0; Nth = 1; // parse parm name, value pname = strField(buff,' ',Nth++); if (! pname) continue; pvalue = strField(buff,' ',Nth); if (! pvalue) continue; err = convSD(pvalue,dvalue); if (err) continue; np1 = setParm(pname,dvalue); // set the parameter if (! np1) continue; np2++; } fclose(fid); // close file return np2; // return parameter count } // save parameters to a file, with file selection dialog int saveParms() { char *pfile; int np; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); pfile = zgetfile(ZTX("save parameters to a file"),"save",zparmfile,"hidden"); if (! pfile) return 0; np = saveParms(pfile); zfree(pfile); return np; } // save parameters to a file int saveParms(cchar *pfile) { FILE *fid; int np; char *pp; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); if (*pfile != '/') { // if parm file name only, pp = (char *) strrchr(zparmfile,'/'); // make complete absolute path if (pp) strcpy(pp+1,pfile); // in same directory as prior pfile = zparmfile; } fid = fopen(pfile,"w"); if (! fid) { zmessageACK(null,null,ZTX("cannot open file %s"),pfile); return 0; } strncpy0(zparmfile,pfile,200); for (np = 0; np < parmlist.count; np++) fprintf(fid," \"%s\" %.12g \n",parmlist.name[np],parmlist.value[np]); fclose(fid); return np; } // create a new paramater or change value of existing parameter int setParm(cchar *parmname, double parmval) { int ii, cc; char *ppname; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); for (ii = 0; ii < parmlist.count; ii++) if (strEqu(parmlist.name[ii],parmname)) break; if (ii == parmlist.max) return 0; if (ii == parmlist.count) { parmlist.count++; cc = strlen(parmname); ppname = new char[cc+1]; strTrim(ppname,parmname); parmlist.name[ii] = ppname; } parmlist.value[ii] = parmval; return parmlist.count; } // get parameter value from parameter name double getParm(cchar *parmname) { if (! parmlistvalid) zappcrash("parmlistvalid = 0"); for (int ii = 0; ii < parmlist.count; ii++) { if (strNeq(parmlist.name[ii],parmname)) continue; return parmlist.value[ii]; } return NAN; } // get Nth parameter name (zero-based) char * getParm(int Nth) { if (! parmlistvalid) zappcrash("parmlistvalid = 0"); if (Nth >= parmlist.count) return null; return parmlist.name[Nth]; } // list parameters in supplied text entry window int listParms(GtkWidget *textWin) { int ii; cchar *pname; double pvalue; for (ii = 0; ii < parmlist.count; ii++) { pname = getParm(ii); pvalue = getParm(pname); wprintf(textWin," %s %.12g \n",pname,pvalue); } return parmlist.count; } // edit parameters with a GUI // textWin != null enables button to list parameters in window // addp != 0 enables button to add new parameters // return: 0 if cancel, else parameter count int editParms(GtkWidget *textWin, int addp) { GtkWidget *peDialog, *peLabel[100], *peEdit[100], *peHbox[100]; GtkWidget *vbox; char ptemp[20], *pname; cchar *pchval; double pvalue; int ii, err, iie = -1, zstat, floaded = 0; int bcancel=1, bapply=2, bload=3, bsave=4, blist=5, baddp=6; if (! parmlistvalid) zappcrash("parmlistvalid = 0"); zthreadcrash(); // thread usage not allowed build_dialog: // build parameter edit dialog if (parmlist.count > 100) zappcrash("more than 100 parameters"); if (textWin && addp) peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, // non-modal ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); else if (textWin) peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); else if (addp) peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); else peDialog = gtk_dialog_new_with_buttons (ZTX("edit parameters"), null, (GtkDialogFlags) 0, ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null); gtk_window_set_position(GTK_WINDOW(peDialog),GTK_WIN_POS_MOUSE); for (ii = 0; ii < parmlist.count; ii++) // labels and edit boxes side by side { // (parm names and parm values) peLabel[ii] = gtk_label_new(parmlist.name[ii]); gtk_misc_set_alignment(GTK_MISC(peLabel[ii]),1,0.5); gtk_label_set_width_chars(GTK_LABEL(peLabel[ii]),30); peEdit[ii] = gtk_entry_new(); gtk_entry_set_width_chars(GTK_ENTRY(peEdit[ii]),12); sprintf(ptemp,"%.12g",parmlist.value[ii]); gtk_entry_set_text(GTK_ENTRY(peEdit[ii]),ptemp); peHbox[ii] = gtk_box_new(HORIZONTAL,0); gtk_box_pack_start(GTK_BOX(peHbox[ii]),peLabel[ii],0,0,5); gtk_box_pack_start(GTK_BOX(peHbox[ii]),peEdit[ii],0,0,5); vbox = gtk_dialog_get_content_area(GTK_DIALOG(peDialog)); gtk_box_pack_start(GTK_BOX(vbox),peHbox[ii],1,1,2); } run_dialog: // display dialog and get inputs if (iie > -1) { gtk_editable_select_region(GTK_EDITABLE(peEdit[iie]),0,-1); // focus on new or bad parameter gtk_widget_grab_focus(peEdit[iie]); iie = -1; } gtk_widget_show_all(peDialog); zstat = gtk_dialog_run(GTK_DIALOG(peDialog)); if (zstat <= bcancel) // kill, cancel { if (floaded) { zstat = zmessageYN(null,ZTX("apply?")); // if file loaded, clarify if (! zstat) { gtk_widget_destroy(peDialog); return 0; } zstat = bapply; } } if (zstat == bload) // load from file { loadParms(); gtk_widget_destroy(peDialog); floaded = 1; goto build_dialog; } for (ii = 0; ii < parmlist.count; ii++) // capture inputs and check if OK { pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii])); err = convSD(pchval,pvalue); if (err && iie < 0) iie = ii; // remember 1st error } if (iie >= 0) goto run_dialog; // re-get bad input if (zstat == bapply) // apply new values { for (ii = 0; ii < parmlist.count; ii++) // capture inputs and save them { pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii])); err = convSD(pchval,parmlist.value[ii]); } gtk_widget_destroy(peDialog); // done return parmlist.count; } if (zstat == bsave) // save to file { for (ii = 0; ii < parmlist.count; ii++) // apply new values { pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii])); err = convSD(pchval,parmlist.value[ii]); } saveParms(); floaded = 0; goto run_dialog; } if (zstat == blist) // list parameters { listParms(textWin); goto run_dialog; } if (zstat == baddp) // add parameter { // main dialog must be non-modal pname = zdialog_text(null,ZTX("add parameter"),ZTX("(new parm name)")); if (! pname) goto run_dialog; setParm(pname,0.0); zfree(pname); floaded = 1; iie = parmlist.count - 1; // focus on new parm gtk_widget_destroy(peDialog); goto build_dialog; } gtk_widget_destroy(peDialog); // unknown status return 0; } /************************************************************************** xstring class (dynamic length string) xstring(int cc = 0) default constructor xstring(cchar * ) string constructor xstring(const xstring &) copy constructor ~xstring() destructor operator cchar * () const { return xpp; } conversion operator (cchar *) xstring operator= (const xstring &) operator = xstring operator= (cchar *) operator = friend xstring operator+ (const xstring &, const xstring &) operator + (catenate) friend xstring operator+ (const xstring &, cchar *) operator + friend xstring operator+ (cchar *, const xstring &) operator + void insert(int pos, cchar *string, int cc = 0) insert substring at pos (expand) void overlay(int pos, cchar *string, int cc = 0) replace substring (possibly expand) static void getStats(int & tcount2, int & tmem2) get statistics void validate() const verify integrity int getcc() const { return xcc; } return string length ***************************************************************************/ #define wmiv 1648734981 int xstring::tcount = 0; // initz. static members int xstring::tmem = 0; xstring::xstring(int cc) // new xstring(cc) { wmi = wmiv; xmem = (cc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; // allocate if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory xcc = 0; // string cc = 0 *xpp = 0; // string = null } xstring::xstring(cchar *string) // new xstring("initial string") { wmi = wmiv; xcc = 0; if (string) xcc = strlen(string); // string length xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; // allocate if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory *xpp = 0; if (xcc) strcpy(xpp,string); // copy string } xstring::xstring(const xstring & xstr) // new xstring2(xstring1) { wmi = wmiv; xmem = xstr.xmem; // allocate same length xcc = xstr.xcc; xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory strcpy(xpp,xstr.xpp); // copy string } xstring::~xstring() // delete xstring { validate(); delete[] xpp; // release allocated memory xpp = 0; tcount--; // decr. object count tmem -= xmem; // decr. allocated memory if (tcount < 0) zappcrash("xstring count < 0",null); if (tmem < 0) zappcrash("xstring memory < 0",null); if (tcount == 0 && tmem > 0) zappcrash("xstring memory leak",null); } xstring xstring::operator= (const xstring & xstr) // xstring2 = xstring1 { validate(); xstr.validate(); if (this == &xstr) return *this; xcc = xstr.xcc; if (xmem < xcc+1) { delete[] xpp; // expand memory if needed tmem -= xmem; xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tmem += xmem; } strcpy(xpp,xstr.xpp); // copy string return *this; } xstring xstring::operator= (cchar *str) // xstring = "some string" { validate(); xcc = 0; *xpp = 0; if (str) xcc = strlen(str); if (xmem < xcc+1) { delete[] xpp; // expand memory if needed tmem -= xmem; xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tmem += xmem; } if (xcc) strcpy(xpp,str); // copy string return *this; } xstring operator+ (const xstring & x1, const xstring & x2) // xstring1 + xstring2 { x1.validate(); x2.validate(); xstring temp(x1.xcc + x2.xcc); // build temp xstring strcpy(temp.xpp,x1.xpp); // with both input strings strcpy(temp.xpp + x1.xcc, x2.xpp); temp.xcc = x1.xcc + x2.xcc; temp.validate(); return temp; } xstring operator+ (const xstring & x1, cchar *s2) // xstring + "some string" { x1.validate(); int cc2 = 0; if (s2) cc2 = strlen(s2); xstring temp(x1.xcc + cc2); // build temp xstring strcpy(temp.xpp,x1.xpp); // with both input strings if (s2) strcpy(temp.xpp + x1.xcc, s2); temp.xcc = x1.xcc + cc2; temp.validate(); return temp; } xstring operator+ (cchar *s1, const xstring & x2) // "some string" + xstring { x2.validate(); int cc1 = 0; if (s1) cc1 = strlen(s1); xstring temp(cc1 + x2.xcc); // build temp xstring if (s1) strcpy(temp.xpp,s1); // with both input strings strcpy(temp.xpp + cc1, x2.xpp); temp.xcc = cc1 + x2.xcc; temp.validate(); return temp; } void xstring::insert(int pos, cchar *string, int cc) // insert cc chars from string at pos { // pad if pos > xcc or cc > string validate(); int scc = strlen(string); if (! cc) cc = scc; int pad = pos - xcc; if (pad < 0) pad = 0; if (xmem < xcc + cc + pad + 1) // allocate more memory if needed { int newmem = xcc + cc + pad; newmem = (newmem & 0x7ffffff8) + 8; // mod 8 length char * xpp2 = new char[newmem]; if (! xpp2) zappcrash("xstring NEW failure",null); strcpy(xpp2,xpp); // copy to new space delete[] xpp; xpp = xpp2; tmem += newmem - xmem; xmem = newmem; } if (pad) memset(xpp+xcc,' ',pad); // add blanks up to pos for (int ii = xcc + pad; ii >= pos; ii--) // make hole for inserted string *(xpp+ii+cc) = *(xpp+ii); if (cc > scc) memset(xpp+pos+scc,' ',cc-scc); // blank pad if cc > string if (cc < scc) scc = cc; strncpy(xpp+pos,string,scc); // insert string, without null xcc += cc + pad; // set new length xpp[xcc] = 0; validate(); } void xstring::overlay(int pos, cchar *string, int cc) // overlay substring { validate(); int scc = strlen(string); if (! cc) cc = scc; if (xmem < pos + cc + 1) // allocate more memory if needed { int newmem = pos + cc; newmem = (newmem & 0x7ffffff8) + 8; // mod 8 length char * xpp2 = new char[newmem]; if (! xpp2) zappcrash("xstring NEW failure",null); strcpy(xpp2,xpp); // copy to new space delete[] xpp; xpp = xpp2; tmem += newmem - xmem; xmem = newmem; } if (pos > xcc) memset(xpp+xcc,' ',pos-xcc); // add blanks up to pos if (cc > scc) memset(xpp+pos+scc,' ',cc-scc); // blank pad if cc > string if (cc < scc) scc = cc; strncpy(xpp+pos,string,scc); // insert string, without null if (pos + cc > xcc) xcc = pos + cc; // set new length xpp[xcc] = 0; validate(); } void xstring::getStats(int & tcount2, int & tmem2) // get statistics { tcount2 = tcount; tmem2 = tmem; } void xstring::validate() const // validate integrity { if (wmi != wmiv) zappcrash("xstring bad wmi",null); if (xmem < xcc+1) zappcrash("xstring xmem < xcc+1",null); if (xcc != (int) strlen(xpp)) zappcrash("xstring xcc != strlen(xpp)",null); } /************************************************************************** Vxstring class (array or vector of xstring) Vxstring(int = 0); constructor ~Vxstring(); destructor Vxstring(const Vxstring &); copy constructor Vxstring operator= (const Vxstring &); operator = xstring & operator[] (int); operator [] const xstring & operator[] (int) const; operator [] (const) int search(cchar *string); find element in unsorted Vxstring int bsearch(cchar *string); find element in sorted Vxstring int sort(int nkeys, int keys[][3]); sort elements by designated subfields int sort(int pos = 0, int cc = 0); sort elements by 1 subfield (cc 0 = all) int getCount() const { return nd; } get current count Sort with keys: keys[N][0] = key position (0 based) of key N keys[N][1] = key length keys[N][2] = sort type: 1/2 = ascending/descending, 3/4 = same, ignoring case ***************************************************************************/ Vxstring::Vxstring(int ii) // constructor { pdata = 0; nd = ii; if (nd) pdata = new xstring[nd]; if (nd && !pdata) zappcrash("Vxstring NEW fail",null); } Vxstring::~Vxstring() // destructor { if (nd) delete[] pdata; pdata = 0; nd = 0; } Vxstring::Vxstring(const Vxstring & pold) // copy constructor { pdata = 0; nd = pold.nd; // set size if (nd) pdata = new xstring[nd]; // allocate memory if (nd && !pdata) zappcrash("Vxstring NEW fail"); for (int ii = 0; ii < nd; ii++) pdata[ii] = pold[ii]; // copy defined elements } Vxstring Vxstring::operator= (const Vxstring & vdstr) // operator = { if (nd) delete[] pdata; // delete old memory pdata = 0; nd = vdstr.nd; if (nd) pdata = new xstring[nd]; // allocate new memory if (nd && !pdata) zappcrash("Vxstring NEW fail",null); for (int ii = 0; ii < nd; ii++) pdata[ii] = vdstr.pdata[ii]; // copy elements return *this; } xstring & Vxstring::operator[] (int ii) // operator [] { static xstring xnull(0); if (ii < nd) return pdata[ii]; // return reference zappcrash("Vxstring index invalid %d %d",nd,ii,null); return xnull; } const xstring & Vxstring::operator[] (int ii) const // operator [] { static xstring xnull(0); if (ii < nd) return pdata[ii]; // return reference zappcrash("Vxstring index invalid %d %d",nd,ii,null); return xnull; } int Vxstring::search(cchar *string) // find element in unsorted Vxstring { for (int ii = 0; ii < nd; ii++) if (strEqu(pdata[ii],string)) return ii; return -1; } int Vxstring::bsearch(cchar *string) // find element in sorted Vxstring { // (binary search) int nn, ii, jj, kk, rkk; nn = nd; if (! nn) return 0; // empty list ii = nn / 2; // next element to search jj = (ii + 1) / 2; // next increment nn--; // last element rkk = 0; while (1) { kk = strcmp(pdata[ii],string); // check element if (kk > 0) { ii -= jj; // too high, go down if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up if (ii > nn) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } static int VDsortKeys[10][3], VDsortNK; int Vxstring::sort(int NK, int keys[][3]) // sort elements by subfields { // key[ii][0] = position int NR, RL, ii; // [1] = length HeapSortUcomp VDsortComp; // [2] = 1/2 = ascending/desc. // = 3/4 = + ignore case NR = nd; if (NR < 2) return 1; RL = sizeof(xstring); if (NK < 1) zappcrash("Vxstring::sort, bad NK",null); if (NK > 10) zappcrash("Vxstring::sort, bad NK",null); VDsortNK = NK; for (ii = 0; ii < NK; ii++) { VDsortKeys[ii][0] = keys[ii][0]; VDsortKeys[ii][1] = keys[ii][1]; VDsortKeys[ii][2] = keys[ii][2]; } HeapSort((char *) pdata,RL,NR,VDsortComp); return 1; } int VDsortComp(cchar *r1, cchar *r2) { xstring *d1, *d2; cchar *p1, *p2; int ii, stat, kpos, ktype, kleng; d1 = (xstring *) r1; d2 = (xstring *) r2; p1 = *d1; p2 = *d2; for (ii = 0; ii < VDsortNK; ii++) // compare each key { kpos = VDsortKeys[ii][0]; kleng = VDsortKeys[ii][1]; ktype = VDsortKeys[ii][2]; if (ktype == 1) { stat = strncmp(p1+kpos,p2+kpos,kleng); if (stat) return stat; continue; } else if (ktype == 2) { stat = strncmp(p1+kpos,p2+kpos,kleng); if (stat) return -stat; continue; } else if (ktype == 3) { stat = strncasecmp(p1+kpos,p2+kpos,kleng); if (stat) return stat; continue; } else if (ktype == 4) { stat = strncasecmp(p1+kpos,p2+kpos,kleng); if (stat) return -stat; continue; } zappcrash("Vxstring::sort, bad KEYS sort type",null); } return 0; } int Vxstring::sort(int pos, int cc) // sort elements ascending { int key[3]; if (! cc) cc = 999999; key[0] = pos; key[1] = cc; key[2] = 1; sort(1,&key); return 1; } /************************************************************************** Hash Table class HashTab(int cc, int cap); constructor ~HashTab(); destructor int Add(cchar *string); add a new string int Del(cchar *string); delete a string int Find(cchar *string); find a string int GetCount() { return count; } get string count int GetNext(int & first, char *string); get first/next string int Dump(); dump hash table to std. output constructor: cc = string length of table entries, cap = table capacity cap should be set 30% higher than needed to reduce collisions and improve speed Benchmark: 0.056 usec. to find 19 char string in a table of 100,000 which is 80% full. 3.3 GHz Core i5 ***************************************************************************/ // static members (robust for tables up to 60% full) int HashTab::trys1 = 100; // Add() tries int HashTab::trys2 = 200; // Find() tries HashTab::HashTab(int _cc, int _cap) // constructor { cc = 4 * (_cc + 4) / 4; // + 1 + mod 4 length cap = _cap; int len = cc * cap; table = new char [len]; if (! table) zappcrash("HashTab() new %d fail",len,null); memset(table,0,len); } HashTab::~HashTab() // destructor { delete [] table; table = 0; } // Add a new string to table int HashTab::Add(cchar *string) { int pos, fpos, trys; pos = strHash(string,cap); // get random position pos = pos * cc; for (trys = 0, fpos = -1; trys < trys1; trys++, pos += cc) // find next free slot at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) // empty slot: string not found { if (fpos != -1) pos = fpos; // use prior deleted slot if there strncpy(table+pos,string,cc); // insert new string table[pos+cc-1] = 0; // insure null terminator return (pos/cc); // return rel. table entry } if (table[pos] == -1) // deleted slot { if (fpos == -1) fpos = pos; // remember 1st one found continue; } if (strEqu(string,table+pos)) return -2; // string already present } return -3; // table full (trys1 exceeded) } // Delete a string from table int HashTab::Del(cchar *string) { int pos, trys; pos = strHash(string,cap); // get random position pos = pos * cc; for (trys = 0; trys < trys2; trys++, pos += cc) // search for string at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) return -1; // empty slot, string not found if (strEqu(string,table+pos)) // string found { table[pos] = -1; // delete table entry return (pos/cc); // return rel. table entry } } zappcrash("HashTab::Del() bug",null); // exceed trys2, must not happen return 0; // (table too full to function) } // Find a table entry. int HashTab::Find(cchar *string) { int pos, trys; pos = strHash(string,cap); // get random position pos = pos * cc; for (trys = 0; trys < trys2; trys++, pos += cc) // search for string at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) return -1; // empty slot, string not found if (strEqu(string,table+pos)) return (pos/cc); // string found, return rel. entry } zappcrash("HashTab::Find() bug",null); // cannot happen return 0; } // return first or next table entry int HashTab::GetNext(int & ftf, char *string) { static int pos; if (ftf) // initial call { pos = 0; ftf = 0; } while (pos < (cap * cc)) { if ((table[pos] == 0) || (table[pos] == -1)) // empty or deleted slot { pos += cc; continue; } strcpy(string,table+pos); // return string pos += cc; return 1; } return -4; // EOF } int HashTab::Dump() { int ii, pos; for (ii = 0; ii < cap; ii++) { pos = ii * cc; if (table[pos] && table[pos] != -1) printz("%d, %s \n", ii, table + pos); if (table[pos] == -1) printz("%d, deleted \n", pos); } return 1; } /************************************************************************** class for queue of dynamic strings Queue(int cap); create queue with capacity ~Queue(); destroy queue int getCount(); get current entry count int push(const xstring *entry, double secs); add new entry with max. wait time xstring *pop1(); get 1st entry (oldest) xstring *popN(); get Nth entry (newest) constructor: cap is queue capacity push: secs is max. time to wait if queue is full. This makes sense if the queue is being pop'd from another thread. Use zero otherwise. Execution time: 0.48 microsecs per push + pop on queue with 100 slots kept full. (2.67 GHz Intel Core i7) ***************************************************************************/ Queue::Queue(int cap) // constructor { int err; err = mutex_init(&qmutex, 0); // create mutex = queue lock if (err) zappcrash("Queue(), mutex init fail",null); qcap = cap; // queue capacity ent1 = entN = qcount = 0; // state = empty vd = new Vxstring(qcap); // create vector of xstring's if (! vd) zappcrash("Queue(), NEW fail %d",cap,null); strcpy(wmi,"queue"); return; } Queue::~Queue() // destructor { if (strNeq(wmi,"queue")) zappcrash("~Queue wmi fail",null); wmi[0] = 0; mutex_destroy(&qmutex); // destroy mutex qcount = qcap = ent1 = entN = -1; delete vd; vd = 0; return; } void Queue::lock() // lock queue (private) { int err; err = mutex_lock(&qmutex); // reserve mutex or suspend if (err) zappcrash("Queue mutex lock fail",null); return; } void Queue::unlock() // unlock queue (private) { int err; err = mutex_unlock(&qmutex); // release mutex if (err) zappcrash("Queue mutex unlock fail",null); return; } int Queue::getCount() // get current entry count { if (strNeq(wmi,"queue")) zappcrash("Queue getCount wmi fail",null); return qcount; } int Queue::push(const xstring *newEnt, double wait) // add entry to queue, with max. wait { double elaps = 0.0; int count; if (strNeq(wmi,"queue")) zappcrash("Queue::push wmi fail",null); lock(); // lock queue while (qcount == qcap) { // queue full unlock(); // unlock queue if (elaps >= wait) return -1; // too long, return -1 status usleep(1000); // sleep in 1 millisec. steps elaps += 0.001; // until queue not full lock(); // lock queue } (* vd)[entN] = *newEnt; // copy new entry into queue entN++; // incr. end pointer if (entN == qcap) entN = 0; qcount++; // incr. queue count count = qcount; unlock(); // unlock queue return count; // return curr. queue count } xstring *Queue::pop1() // get 1st (oldest) entry and remove { xstring *entry; if (strNeq(wmi,"queue")) zappcrash("Queue::pop1 wmi fail",null); lock(); // lock queue if (qcount == 0) entry = 0; // queue empty else { entry = &(* vd)[ent1]; // get first entry ent1++; // index pointer to next if (ent1 == qcap) ent1 = 0; qcount--; // decr. queue count } unlock(); // unlock queue return entry; } xstring *Queue::popN() // get last (newest) entry and remove { xstring *entry; if (strNeq(wmi,"queue")) zappcrash("Queue::popN wmi fail",null); lock(); // lock queue if (qcount == 0) entry = 0; // queue empty else { if (entN == 0) entN = qcap; // index pointer to prior entN--; qcount--; // decr. queue count entry = &(* vd)[entN]; // get last entry } unlock(); // unlock queue return entry; } /************************************************************************** Tree class, tree-structured data storage without limits Store any amount of data at any depth within a tree-structure with named nodes. Data can be found using an ordered list of node names or node numbers. Tree(cchar *name); create Tree ~Tree(); destroy Tree int put(void *data, int dd, char *nodes[], int nn); put data by node names[] int put(void *data, int dd, int nodes[], int nn); put data by node numbers[] int get(void *data, int dd, char *nodes[], int nn); get data by node names[] int get(void *data, int dd, int nodes[], int nn); get data by node numbers[] A Tree can also be thought of as an N-dimensional array with the cells or nodes having both names and numbers. Data can be stored and retrieved with a list of node names or numbers. The nodes are created as needed. Nodes are sparse: those with no data do not exist. Node numbers are created when data is stored by node numbers. Node numbers are also added when data is stored by node names: the numbers are assigned sequentially from zero at each level in the tree. nodes array of node names or numbers nn no. of nodes used for a put() or get() call dd data length to put, or the max. data length to get (i.e. the space available) put() returns 1 if successful and crashes with a message if not (out of memory) get() returns the length of the data retrieved (<= dd) or 0 if not found there is no assumption that the data is character data and no null is appended data returned has the same length as the data stored (if dd arg is big enough) example: char *snodes[10]; // up to 10 node names (max tree depth) int knodes[10]; // up to 10 node numbers char mydata[20]; // data length up to 20 Tree *mytree = new Tree("myname"); // create Tree snodes[0] = "name1"; snodes[1] = "name2"; mytree->put("string1",8,snodes,2); // put "string1" at ["name1","name2"] snodes[1] = "name3"; mytree->put("string22",9,snodes,2); // put "string22" at ["name1","name3"] snodes[1] = "name2"; mytree->get(mydata,20,snodes,2); // get data at ["name1","name2"] ("string1") knodes[0] = 0; knodes[1] = 0; mytree->get(mydata,20,knodes,2); // get data at [0,0] ("string1") knodes[1] = 1; mytree->get(mydata,20,knodes,2); // get data at [0,1] ("string22") When data was stored at ["name1","name2"] these node names were created along with the corresponding node numbers [0,0]. When data was stored at ["name1","name3"] a new node "name3" was created under the existing node "name1", and assigned the numbers [0,1]. Benchmark Execution times: 2.67 GHz Intel Core i7 Tree with 1 million nodes and average depth of 8 levels (peak 15 levels) put() all data by node names: 2.1 secs get() all data by node names: 1.5 secs put() all data by node numbers: 2.0 secs get() all data by node numbers: 0.72 secs Internal code conventions: - caller level is node 0, next level is node 1, etc. - node names and numbers in calls to get() and put() refer to next levels - number of levels = 1+nn, where nn is max. in calls to put(...nodes[], nn) ***************************************************************************/ #define wmid 1374602859 // integrity check key // constructor Tree::Tree(cchar *name) { wmi = wmid; tname = 0; tmem = 0; tdata = 0; nsub = 0; psub = 0; if (name) { int cc = strlen(name); tname = new char[cc+1]; if (! tname) zappcrash("Tree, no memory",null); strcpy(tname,name); } } // destructor Tree::~Tree() { if (wmi != wmid) zappcrash("not a Tree",null); if (tname) delete [] tname; tname = 0; if (tmem) zfree(tdata); tmem = 0; tdata = 0; for (int ii = 0; ii < nsub; ii++) delete psub[ii]; if (psub) zfree(psub); nsub = 0; psub = 0; } // put data by node names[] int Tree::put(void *data, int dd, char *nodes[], int nn) { Tree *tnode; if (wmi != wmid) zappcrash("not a Tree",null); tnode = make(nodes,nn); if (tnode->tdata) zfree(tnode->tdata); tnode->tdata = new char[dd]; if (! tnode->tdata) zappcrash("Tree, no memory",null); tnode->tmem = dd; memmove(tnode->tdata,data,dd); return 1; } // put data by node numbers[] int Tree::put(void *data, int dd, int nodes[], int nn) { Tree *tnode; if (wmi != wmid) zappcrash("not a Tree",null); tnode = make(nodes,nn); if (tnode->tdata) zfree(tnode->tdata); tnode->tdata = new char[dd]; if (! tnode->tdata) zappcrash("Tree, no memory",null); tnode->tmem = dd; memmove(tnode->tdata,data,dd); return 1; } // get data by node names[] int Tree::get(void *data, int dd, char *nodes[], int nn) { Tree *tnode = find(nodes,nn); if (! tnode) return 0; if (! tnode->tmem) return 0; if (dd > tnode->tmem) dd = tnode->tmem; memmove(data,tnode->tdata,dd); return dd; } // get data by node numbers[] int Tree::get(void *data, int dd, int nodes[], int nn) { Tree *tnode = find(nodes,nn); if (! tnode) return 0; if (! tnode->tmem) return 0; if (dd > tnode->tmem) dd = tnode->tmem; memmove(data,tnode->tdata,dd); return dd; } // find a given node by names[] Tree * Tree::find(char *nodes[], int nn) { int ii; for (ii = 0; ii < nsub; ii++) if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break; if (ii == nsub) return 0; if (nn == 1) return psub[ii]; return psub[ii]->find(&nodes[1],nn-1); } // find a given node by numbers[] Tree * Tree::find(int nodes[], int nn) { int ii = nodes[0]; if (ii >= nsub) return 0; if (! psub[ii]) return 0; if (nn == 1) return psub[ii]; return psub[ii]->find(&nodes[1],nn-1); } // find or create a given node by names[] Tree * Tree::make(char *nodes[], int nn) { int ii; Tree **psub2; for (ii = 0; ii < nsub; ii++) if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break; if (ii == nsub) { psub2 = new Tree * [nsub+1]; if (! psub2) zappcrash("Tree, no memory",null); for (ii = 0; ii < nsub; ii++) psub2[ii] = psub[ii]; delete [] psub; psub = psub2; nsub++; psub[ii] = new Tree(nodes[0]); if (! psub[ii]) zappcrash("Tree, no memory",null); } if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } // find or create a given node by numbers[] Tree * Tree::make(int nodes[], int nn) { Tree **psub2; int ii, jj; ii = nodes[0]; if ((ii < nsub) && psub[ii]) { if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } if (ii >= nsub) { psub2 = new Tree * [ii+1]; if (psub2 == null) zappcrash("Tree, no memory",null); for (jj = 0; jj < nsub; jj++) psub2[jj] = psub[jj]; for (jj = nsub; jj < ii; jj++) psub2[jj] = 0; delete [] psub; psub = psub2; nsub = ii + 1; } psub[ii] = new Tree("noname"); if (! psub[ii]) zappcrash("Tree, no memory",null); if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } // dump tree data to stdout (call with level 0) void Tree::dump(int level) { cchar *name; if (! tname) name = "noname"; else name = tname; printz("%*s level: %d name: %s subs: %d mem: %d \n", level*2,"",level,name,nsub,tmem); for (int ii = 0; ii < nsub; ii++) if (psub[ii]) psub[ii]->dump(level+1); } // get node counts and total data per level // level 0 + nn more levels, as given in calls to put(...nodes[],nn) // caller must initialize counters to zero void Tree::stats(int nn[], int nd[]) { nn[0] += 1; nd[0] += tmem; for (int ii = 0; ii < nsub; ii++) if (psub[ii]) psub[ii]->stats(&nn[1],&nd[1]); } dkopp-6.5/Makefile0000644000175000017500000000406512343020444012602 0ustar micomico# dkopp makefile PROGRAM = dkopp VERSION = 6.5 SOURCE = $(PROGRAM)-$(VERSION).cc # defaults for parameters that may be pre-defined CXXFLAGS ?= -O2 -Wall -ggdb LDFLAGS ?= -rdynamic -lpthread PREFIX ?= /usr # target install directories BINDIR = $(PREFIX)/bin SHAREDIR = $(PREFIX)/share/$(PROGRAM) DATADIR = $(SHAREDIR)/data ICONDIR = $(SHAREDIR)/icons DOCDIR = $(PREFIX)/share/doc/$(PROGRAM) MANDIR = $(PREFIX)/share/man/man1 MENUFILE = $(PREFIX)/share/applications/$(PROGRAM).desktop CFLAGS = $(CXXFLAGS) -c `pkg-config --cflags gtk+-3.0` LIBS = `pkg-config --libs gtk+-3.0` -lpthread $(PROGRAM): $(PROGRAM).o zfuncs.o $(CXX) $(LDFLAGS) $(PROGRAM).o zfuncs.o $(LIBS) -o $(PROGRAM) $(PROGRAM).o: $(SOURCE) $(CXX) $(CFLAGS) -o $(PROGRAM).o $(SOURCE) zfuncs.o: zfuncs.cc zfuncs.h $(CXX) $(CFLAGS) zfuncs.cc \ -D PREFIX=\"$(PREFIX)\" -D DOCDIR=\"$(DOCDIR)\" install: $(PROGRAM) rm -f $(DESTDIR)$(BINDIR)/$(PROGRAM) rm -f -R $(DESTDIR)$(SHAREDIR) rm -f -R $(DESTDIR)$(DOCDIR) rm -f $(DESTDIR)$(MANDIR)/$(PROGRAM).1.gz xdg-desktop-menu uninstall $(DESTDIR)$(MENUFILE) rm -f $(DESTDIR)$(MENUFILE) mkdir -p $(DESTDIR)$(BINDIR) mkdir -p $(DESTDIR)$(DATADIR) mkdir -p $(DESTDIR)$(ICONDIR) mkdir -p $(DESTDIR)$(DOCDIR) mkdir -p $(DESTDIR)$(MANDIR) mkdir -p $(DESTDIR)$(PREFIX)/share/applications cp -f $(PROGRAM) $(DESTDIR)$(BINDIR) cp -f -R data/* $(DESTDIR)$(DATADIR) cp -f -R icons/* $(DESTDIR)$(ICONDIR) cp -f -R doc/* $(DESTDIR)$(DOCDIR) gzip -f -9 $(DESTDIR)$(DOCDIR)/changelog # man page cp -f doc/$(PROGRAM).man $(PROGRAM).1 gzip -f -9 $(PROGRAM).1 cp $(PROGRAM).1.gz $(DESTDIR)$(MANDIR) rm -f $(PROGRAM).1.gz # menu (desktop) file cp -f desktop $(DESTDIR)$(MENUFILE) chmod +x $(DESTDIR)$(MENUFILE) xdg-desktop-menu install --novendor $(DESTDIR)$(MENUFILE) uninstall: rm -f $(DESTDIR)$(BINDIR)/$(PROGRAM) rm -f -R $(DESTDIR)$(SHAREDIR) rm -f -R $(DESTDIR)$(DOCDIR) rm -f $(DESTDIR)$(MANDIR)/$(PROGRAM).1.gz xdg-desktop-menu uninstall $(DESTDIR)$(MENUFILE) rm -f $(DESTDIR)$(MENUFILE) clean: rm -f $(PROGRAM) rm -f *.o