pax_global_header00006660000000000000000000000064141226253110014507gustar00rootroot0000000000000052 comment=af8907b2026c37c1d268a89700fe5d25a37d76c5 tirex-0.7.0/000077500000000000000000000000001412262531100126465ustar00rootroot00000000000000tirex-0.7.0/BUGS000066400000000000000000000006771412262531100133430ustar00rootroot00000000000000 Tirex Bugs ========== * There are config options metatile_rows and metatile_columns and defaults for those settings in the Perl code as $Tirex::METATILE_ROWS and $Tirex::METATILE_COLUMNS. Some Tirex code looks into the config, some uses the variables above and some has hardcoded dependencies on metatile size. Nobody has ever tested Tirex with sizes other than 8x8. Especially non-quadratic metatiles will probably not work at all. tirex-0.7.0/LICENSE000066400000000000000000000431031412262531100136540ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. tirex-0.7.0/Makefile000066400000000000000000000137441412262531100143170ustar00rootroot00000000000000INSTALLOPTS=-g root -o root build: Makefile.perl cd backend-mapnik; $(MAKE) $(MFLAGS) $(MAKE) -f Makefile.perl Makefile.perl: Makefile.PL perl Makefile.PL PREFIX=/usr DESTDIR=$(DESTDIR) FIRST_MAKEFILE=Makefile.perl rm -f Makefile.perl.old install-all: install install-example-map install-munin install-nagios install-example-map: install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/usr/share/tirex install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/usr/share/tirex/example-map install -m 644 ${INSTALLOPTS} example-map/example.xml $(DESTDIR)/usr/share/tirex/example-map install -m 644 ${INSTALLOPTS} example-map/ocean.* $(DESTDIR)/usr/share/tirex/example-map install -m 644 ${INSTALLOPTS} example-map/README $(DESTDIR)/usr/share/tirex/example-map install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer/mapnik install -m 644 ${INSTALLOPTS} example-map/mapnik-example.conf $(DESTDIR)/etc/tirex/renderer/mapnik/example.conf install-munin: install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/usr/share/munin/plugins install -m 755 ${INSTALLOPTS} munin/* $(DESTDIR)/usr/share/munin/plugins install-nagios: install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/usr/lib/nagios/plugins install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/nagios/nrpe.d install -m 755 ${INSTALLOPTS} nagios/tirex* $(DESTDIR)/usr/lib/nagios/plugins install -m 644 ${INSTALLOPTS} nagios/cfg/*.cfg $(DESTDIR)/etc/nagios/nrpe.d install: build install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/usr/bin/ for program in bin/*; do \ install -m 755 ${INSTALLOPTS} $$program $(DESTDIR)/usr/bin/; \ done install -m 755 ${INSTALLOPTS} backends/test $(DESTDIR)/usr/libexec/tirex-backend-test install -m 755 ${INSTALLOPTS} backends/wms $(DESTDIR)/usr/libexec/tirex-backend-wms install -m 755 ${INSTALLOPTS} backends/tms $(DESTDIR)/usr/libexec/tirex-backend-tms install -m 755 ${INSTALLOPTS} backends/mapserver $(DESTDIR)/usr/libexec/tirex-backend-mapserver install -m 755 ${INSTALLOPTS} backends/openseamap $(DESTDIR)/usr/libexec/tirex-backend-openseamap install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex install -m 644 ${INSTALLOPTS} etc/tirex.conf.dist $(DESTDIR)/etc/tirex/tirex.conf install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer/test install -m 644 ${INSTALLOPTS} etc/renderer/test.conf.dist $(DESTDIR)/etc/tirex/renderer/test.conf install -m 644 ${INSTALLOPTS} etc/renderer/test/checkerboard.conf.dist $(DESTDIR)/etc/tirex/renderer/test/checkerboard.conf install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer/wms install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer/tms install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer/openseamap install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer/mapserver install -m 644 ${INSTALLOPTS} etc/renderer/wms.conf.dist $(DESTDIR)/etc/tirex/renderer/wms.conf install -m 644 ${INSTALLOPTS} etc/renderer/tms.conf.dist $(DESTDIR)/etc/tirex/renderer/tms.conf install -m 644 ${INSTALLOPTS} etc/renderer/openseamap.conf.dist $(DESTDIR)/etc/tirex/renderer/openseamap.conf install -m 644 ${INSTALLOPTS} etc/renderer/mapserver.conf.dist $(DESTDIR)/etc/tirex/renderer/mapserver.conf install -m 644 ${INSTALLOPTS} etc/renderer/wms/demowms.conf.dist $(DESTDIR)/etc/tirex/renderer/wms/demowms.conf install -m 644 ${INSTALLOPTS} etc/renderer/tms/demotms.conf.dist $(DESTDIR)/etc/tirex/renderer/tms/demotms.conf install -m 644 ${INSTALLOPTS} etc/renderer/openseamap/openseamap.conf.dist $(DESTDIR)/etc/tirex/renderer/openseamap/openseamap.conf install -m 644 ${INSTALLOPTS} etc/renderer/mapserver/msdemo.conf.dist $(DESTDIR)/etc/tirex/renderer/mapserver/msdemo.conf install -m 644 ${INSTALLOPTS} etc/renderer/mapserver/msdemo.map $(DESTDIR)/etc/tirex/renderer/mapserver/msdemo.map install -m 644 ${INSTALLOPTS} etc/renderer/mapserver/fonts.lst $(DESTDIR)/etc/tirex/renderer/mapserver/fonts.lst install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/etc/tirex/renderer/mapnik install -m 644 ${INSTALLOPTS} etc/renderer/mapnik.conf.dist $(DESTDIR)/etc/tirex/renderer/mapnik.conf install -m 755 ${INSTALLOPTS} -d $(DESTDIR)/usr/share/man/man1/ for program in bin/*; do \ if grep -q "=head" $$program; then \ pod2man $$program > $(DESTDIR)/usr/share/man/man1/`basename $$program`.1; \ fi; \ done cd backend-mapnik; $(MAKE) DESTDIR=$(DESTDIR) "INSTALLOPTS=${INSTALLOPTS}" install $(MAKE) -f Makefile.perl install clean: Makefile.perl $(MAKE) -f Makefile.perl clean cd backend-mapnik; $(MAKE) DESTDIR=$(DESTDIR) clean rm -f Makefile.perl rm -f Makefile.perl.old rm -f build-stamp rm -f configure-stamp rm -rf blib deb: debuild -I -us -uc deb-clean: debuild clean check: podchecker bin/* find lib -type f -name \*.pm | sort | xargs podchecker htmldoc: rm -fr htmldoc mkdir -p htmldoc for pod in `find bin -type f | grep -v '\.'`; do \ mkdir -p htmldoc/`dirname $$pod` ;\ pod2html --css=foo.css --htmldir=htmldoc --podpath=lib:bin:doc --infile=$$pod --outfile=htmldoc/$$pod.html; \ done for pod in `find lib -name \*.pm`; do \ mkdir -p htmldoc/`dirname $$pod` ;\ pod2html --htmldir=htmldoc --podpath=lib:bin:doc --infile=$$pod --outfile=htmldoc/$${pod%.pm}.html; \ done tirex-0.7.0/Makefile.PL000066400000000000000000000014271412262531100146240ustar00rootroot00000000000000use ExtUtils::MakeMaker; unless (eval { require Test::More; 1 }) { print STDERR "*** Test::More is not installed, you will not be able to run the tests\n"; } unless ($] >= 5.006001) { print STDERR "*** This Perl version ($]) is not supported\n"; } # See lib/ExtUtils/MakeMaker.pm for details of how to influence # the contents of the Makefile that is written. WriteMakefile( 'NAME' => 'Tirex', 'AUTHOR' => 'Jochen Topf', 'LICENSE' => 'GPL', 'INSTALLDIRS' => 'vendor', 'FIRST_MAKEFILE' => 'Makefile.perl', 'VERSION_FROM' => 'lib/Tirex.pm', # finds $VERSION 'ABSTRACT' => "Modules for the Tirex tile server", 'PREREQ_PM' => { 'IPC::ShareLite' => 0, 'JSON' => 0 }, test => { TESTS => join(' ', glob('t/*/*.t')) }, ); tirex-0.7.0/README.md000066400000000000000000000037551412262531100141370ustar00rootroot00000000000000# Tirex Tile Rendering System Tirex is a bunch of tools that let you run a tile server. A tile server is a web server that hands out pre-rendered map raster images to clients. The web page for Tirex is at http://wiki.openstreetmap.org/wiki/Tirex . See there for more information. ## PREREQUISITES You'll need the following Perl modules to run Tirex: * IPC::ShareLite (Debian/Ubuntu: libipc-sharelite-perl) * JSON (Debian/Ubuntu: libjson-perl) * GD (Debian/Ubuntu: libgd-gd2-perl) * LWP (Debian/Ubuntu: libwww-perl) You'll need a C++ compiler and build tools to compile the Mapnik backend. ## BUILDING To build Tirex run make in the main directory. This will compile the mapnik backend and create the man pages for the Perl modules. Call 'make clean' to cleanup after a 'make'. ## INSTALLING To install Tirex call make install as root user. This will install the main parts of Tirex including the tirex-master, tirex-backend-manager and the Mapnik backend. This will not install the example map, or the munin or nagios plugins. To install those, call make install-example-map make install-munin make install-nagios respectively. You can also install everything with make install-all ## DEBIAN/UBUNTU To create Debian/Ubuntu packages you need the package 'devscripts' installed. Call make deb to create the packages. The following packages will be created in the parent directory: tirex-core tirex-backend-mapnik tirex-backend-wms tirex-backend-mapserver tirex-example-map tirex-munin-plugin tirex-nagios-plugin tirex-syncd Call 'make deb-clean' to cleanup after a 'make deb'. ## TESTS Call 'prove' in the main directory to run Perl unit tests. You need Test::More (Debian/Ubuntu: libtest-simple-perl) and Test::Harness (Debian/Ubuntu: libtest-harness-perl) installed. There are some other tests in the 'test' directory. See the description at the beginning of the scripts for information on how to use them. tirex-0.7.0/backend-mapnik/000077500000000000000000000000001412262531100155125ustar00rootroot00000000000000tirex-0.7.0/backend-mapnik/Makefile000066400000000000000000000012171412262531100171530ustar00rootroot00000000000000INSTALLOPTS=-g root -o root CFLAGS += -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 CXXFLAGS = `mapnik-config --cflags` $(CFLAGS) CXXFLAGS += -Wall -Wextra -pedantic -Wredundant-decls -Wdisabled-optimization -Wctor-dtor-privacy -Wnon-virtual-dtor -Woverloaded-virtual -Wsign-promo -Wold-style-cast LDFLAGS= `mapnik-config --libs --ldflags --dep-libs` backend-mapnik: renderd.o metatilehandler.o networklistener.o networkmessage.o networkrequest.o networkresponse.o debuggable.o requesthandler.o $(CXX) -o $@ $^ $(LDFLAGS) clean: rm -f backend-mapnik *.o install: install -m 755 ${INSTALLOPTS} backend-mapnik $(DESTDIR)/usr/libexec/tirex-backend-mapnik tirex-0.7.0/backend-mapnik/debuggable.cc000066400000000000000000000003041412262531100200770ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include "debuggable.h" bool Debuggable::msDebugLogging = false; tirex-0.7.0/backend-mapnik/debuggable.h000066400000000000000000000024411412262531100177450ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * Debuggable * * Superclass for classes that may log debug info */ #ifndef debuggable_included #define debuggable_included #include #include #include #include #include #include class Debuggable { protected: void debug(const char *fmt, ...) const { if (!msDebugLogging) return; va_list ap; va_start(ap, fmt); vsyslog(LOG_DEBUG, fmt, ap); va_end(ap); } void info(const char *fmt, ...) const { va_list ap; va_start(ap, fmt); vsyslog(LOG_INFO, fmt, ap); va_end(ap); } void notice(const char *fmt, ...) const { va_list ap; va_start(ap, fmt); vsyslog(LOG_NOTICE, fmt, ap); va_end(ap); } void warning(const char *fmt, ...) const { va_list ap; va_start(ap, fmt); vsyslog(LOG_WARNING, fmt, ap); va_end(ap); } void error(const char *fmt, ...) const { va_list ap; va_start(ap, fmt); vsyslog(LOG_ERR, fmt, ap); va_end(ap); } public: static bool msDebugLogging; }; #endif tirex-0.7.0/backend-mapnik/metatilehandler.cc000066400000000000000000000256231412262531100211730ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include "metatilehandler.h" #include "renderrequest.h" #include "renderresponse.h" #include "sys/time.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #if MAPNIK_VERSION >= 300000 # include # include #endif #define MERCATOR_WIDTH 40075016.685578488 #define MERCATOR_OFFSET 20037508.342789244 MetatileHandler::MetatileHandler(const std::string& tiledir, const std::map& stylefiles, unsigned int tilesize, double scalefactor, int buffersize, unsigned int mtrowcol, const std::string& imagetype) : mTileWidth(tilesize), mTileHeight(tilesize), mMetaTileRows(mtrowcol), mMetaTileColumns(mtrowcol), mImageType(imagetype), mBufferSize(buffersize), mScaleFactor(scalefactor), mTileDir(tiledir) { for (unsigned int i=0; i<=MAXZOOM; i++) { mPerZoomMap[i]=NULL; } for (auto itr = stylefiles.begin(); itr != stylefiles.end(); itr++) { if (itr->first == "") { load_map(mMap, itr->second); debug("load %s without zoom restrictions", itr->second.c_str()); } else if (itr->first.at(0) != '.') { throw std::invalid_argument("malformed mapfile config postfix '" + itr->first + "'"); } else { char *endptr; long int num = strtol(itr->first.c_str()+1, &endptr, 10); if (*endptr || (num<0) || (num>MAXZOOM)) { throw std::invalid_argument("malformed mapfile config postfix '" + itr->first + "'"); } mPerZoomMap[num] = new mapnik::Map; debug("load %s for zoom %d", itr->second.c_str(), num); load_map(*(mPerZoomMap[num]), itr->second); } } fourpow[0] = 1; twopow[0] = 1; for (unsigned int i = 1; i < MAXZOOM; i++) { fourpow[i] = 4 * fourpow[i-1]; twopow[i] = 2 * twopow[i-1]; } } MetatileHandler::~MetatileHandler() { } const NetworkResponse *MetatileHandler::handleRequest(const NetworkRequest *request) { debug(">> MetatileHandler::handleRequest"); timeval start, end; gettimeofday(&start, NULL); int x = request->getParam("x", -1); int y = request->getParam("y", -1); int z = request->getParam("z", -1); if (x % mMetaTileColumns) { error("given value for 'x' (%d) is not divisible by %d", x, mMetaTileColumns); return NetworkResponse::makeErrorResponse(request, "invalid value for x"); } if (y % mMetaTileRows) { error("given value for 'y' (%d) is not divisible by %d", y, mMetaTileRows); return NetworkResponse::makeErrorResponse(request, "invalid value for y"); } unsigned int mtc = mMetaTileColumns; if (mtc > fourpow[z]) mtc = fourpow[z]; unsigned int mtr = mMetaTileRows; if (mtr > fourpow[z]) mtr = fourpow[z]; RenderRequest rr; // compute render extent in epsg:3857 which the database is likely to use. // note that if the database should use something different, this will be // taken care of later. rr.west = x * MERCATOR_WIDTH / twopow[z] - MERCATOR_OFFSET; rr.east = (x + mtc) * MERCATOR_WIDTH / twopow[z] - MERCATOR_OFFSET; rr.north = (twopow[z] - y) * MERCATOR_WIDTH / twopow[z] - MERCATOR_OFFSET; rr.south = (twopow[z] - y - mtr) * MERCATOR_WIDTH / twopow[z] - MERCATOR_OFFSET; rr.scale_factor = mScaleFactor; rr.buffer_size = mBufferSize; rr.zoom = z; // we specify the bbox in epsg:3857, and we also want our image returned // in this projection. rr.bbox_srs = 3857; rr.srs = 3857; rr.width = mTileWidth * mtc; rr.height = mTileHeight * mtr; std::string map = request->getParam("map", "default"); updateStatus("rendering z=%d x=%d y=%d map=%s", z, x, y, map.c_str()); const RenderResponse *rrs = render(&rr); updateStatus("idle"); NetworkResponse *resp; if (!rrs) { return NetworkResponse::makeErrorResponse(request, "renderer internal error"); } else { meta_layout m; int numtiles = mMetaTileRows * mMetaTileColumns; entry *offsets = static_cast(malloc(sizeof(entry) * numtiles)); std::vector rawpng(numtiles); memset(&m, 0, sizeof(m)); memset(offsets, 0, numtiles * sizeof(entry)); // it seems that mod_tile expects us to always put the theoretical // number of tiles in this meta tile, not the real number (in standard // setup, only zoom levels 3+ will have 64 tiles, 0-2 have less) // m.count = mtr * mtc; m.count = mMetaTileRows * mMetaTileColumns; memcpy(m.magic, "META", 4); m.x = x; m.y = y; m.z = z; size_t offset = sizeof(m) + numtiles * sizeof(entry); int index = 0; char metafilename[PATH_MAX]; xyz_to_meta(metafilename, PATH_MAX, mTileDir.c_str(), x, y, z); if (!mkdirp(mTileDir.c_str(), x, y, z)) { free(offsets); return NetworkResponse::makeErrorResponse(request, "renderer internal error"); } char tmpfilename[PATH_MAX]; snprintf(tmpfilename, PATH_MAX, "%s.%d.tmp", metafilename, getpid()); std::ofstream outfile(tmpfilename, std::ios::out | std::ios::binary | std::ios::trunc); outfile.write(reinterpret_cast(&m), sizeof(m)); for (unsigned int col = 0; col < mMetaTileColumns; col++) { for (unsigned int row = 0; row < mMetaTileRows; row++) { if ((col < mtc) && (row < mtr)) { #if MAPNIK_VERSION >= 300000 mapnik::image_view> vw1(col * mTileWidth, row * mTileHeight, mTileWidth, mTileHeight, *(rrs->image)); struct mapnik::image_view_any view(vw1); #else mapnik::image_view view(col * mTileWidth, row * mTileHeight, mTileWidth, mTileHeight, rrs->image->data()); #endif rawpng[index] = mapnik::save_to_string(view, mImageType); offsets[index].offset = offset; offset += offsets[index].size = rawpng[index].length(); } else { offsets[index].offset = 0; offsets[index].size = 0; } index++; } } outfile.write(reinterpret_cast(offsets), numtiles * sizeof(entry)); for (int i=0; i < index; i++) { outfile.write(rawpng[i].data(), rawpng[i].size()); } outfile.close(); delete rrs; rename(tmpfilename, metafilename); debug("created %s", metafilename); resp = new NetworkResponse(request); resp->setParam("map", map); resp->setParam("result", "ok"); resp->setParam("x", x); resp->setParam("y", y); resp->setParam("z", z); resp->setParam("metatile", metafilename); gettimeofday(&end, NULL); char buffer[20]; snprintf(buffer, 20, "%ld", (end.tv_sec-start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000); resp->setParam("render_time", buffer); free(offsets); } debug("<< MetatileHandler::handleRequest"); return resp; } void MetatileHandler::xyz_to_meta(char *path, size_t len, const char *tile_dir, int x, int y, int z) const { unsigned char i, hash[5]; for (i=0; i<5; i++) { hash[i] = ((x & 0x0f) << 4) | (y & 0x0f); x >>= 4; y >>= 4; } snprintf(path, len, "%s/%d/%u/%u/%u/%u/%u.meta", tile_dir, z, hash[4], hash[3], hash[2], hash[1], hash[0]); return; } bool MetatileHandler::mkdirp(const char *tile_dir, int x, int y, int z) const { unsigned char i, hash[5]; char path[PATH_MAX]; for (i=0; i<5; i++) { hash[i] = ((x & 0x0f) << 4) | (y & 0x0f); x >>= 4; y >>= 4; } snprintf(path, PATH_MAX, "%s/%d/%u/%u/%u/%u", tile_dir, z, hash[4], hash[3], hash[2], hash[1]); try { boost::filesystem::create_directories(path); } catch(std::exception const& ex) { error("cannot create directory %s: %s", path, ex.what()); return false; } return true; } const RenderResponse *MetatileHandler::render(const RenderRequest *rr) { debug(">> MetatileHandler::render"); char init[255]; mapnik::Map *map = mPerZoomMap[rr->zoom] ? mPerZoomMap[rr->zoom] : &mMap; sprintf(init, "+init=epsg:%d", rr->srs); // commented out - rely on proper SRS specification in map.xml // mMap.set_srs(init); double west = rr->west; double south = rr->south; double east = rr->east; double north = rr->north; if (rr->srs != rr->bbox_srs) { mapnik::projection p1(init); sprintf(init, "+init=epsg:%d", rr->bbox_srs); mapnik::projection p0(init); mapnik::proj_transform pt(p0, p1); double z = 0.0; pt.forward(west, south, z); z = 0.0; pt.forward(east, north, z); debug("rendering format %s for %f,%f - %f,%f in SRS %d (projected from %f,%f - %f,%f in SRS %d) to %dx%dpx", mImageType.c_str(), west, south, east, north, rr->srs, rr->west, rr->south, rr->east, rr->north, rr->bbox_srs, rr->width, rr->height); } else { debug("rendering format %s for area %f,%f - %f,%f in SRS %d to %dx%d px", mImageType.c_str(), west, south, east, north, rr->srs, rr->width, rr->height); } mapnik::box2d bbox(west, south, east, north); map->resize(rr->width, rr->height); map->zoom_to_box(bbox); if (rr->buffer_size > -1) { map->set_buffer_size(rr->buffer_size); } else if (map->buffer_size() < 128) { map->set_buffer_size(128); } debug("width: %d, height:%d", rr->width, rr->height); RenderResponse *resp = new RenderResponse(); resp->image = new mapnik::image_32(rr->width, rr->height); mapnik::agg_renderer renderer(*map, *(resp->image), rr->scale_factor, 0u, 0u); try { renderer.apply(); } catch (mapnik::datasource_exception const& dex) { delete resp; resp = NULL; error("Mapnik datasource exception: %s", dex.what()); } catch (std::exception const& ex) { delete resp; resp = NULL; error("Mapnik config error: %s", ex.what()); } debug("<< MetatileHandler::render"); return resp; } tirex-0.7.0/backend-mapnik/metatilehandler.h000066400000000000000000000036051412262531100210310ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * MetatileHandler * * This class is responsible for analysing a "metatile" request received from * the network, calling the proper rendering functions to fulfil the request, * preparing the render result, and returning an answer to the client. */ #ifndef metatilehandler_included #define metatilehandler_included #include #include #include "requesthandler.h" #include "networkrequest.h" #include "networkresponse.h" #include "renderrequest.h" #include "renderresponse.h" #define MAXZOOM 25 struct entry { int offset; int size; }; struct meta_layout { char magic[4]; int count; // METATILE ^ 2 int x, y, z; // lowest x,y of this metatile, plus z // entry index[]; // count entries }; class MetatileHandler : public RequestHandler { public: MetatileHandler(const std::string& tiledir, const std::map& stylefiles, unsigned int tilesize, double scalefactor, int buffersize, unsigned int mtrowcol, const std::string & imagetype); ~MetatileHandler(); const NetworkResponse *handleRequest(const NetworkRequest *request); void xyz_to_meta(char *path, size_t len, const char *tile_dir, int x, int y, int z) const; bool mkdirp(const char *tile_dir, int x, int y, int z) const; const std::string getRequestType() const { return "metatile_request"; } private: int64_t fourpow[MAXZOOM]; int64_t twopow[MAXZOOM]; const RenderResponse *render(const RenderRequest *rr); unsigned int mTileWidth; unsigned int mTileHeight; unsigned int mMetaTileRows; unsigned int mMetaTileColumns; std::string mImageType; int mBufferSize; double mScaleFactor; std::string mTileDir; mapnik::Map mMap; mapnik::Map *mPerZoomMap[MAXZOOM+1]; }; #endif tirex-0.7.0/backend-mapnik/mortal.h000066400000000000000000000012421412262531100171600ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * Mortal * * Superclass for classes that may terminate the program. */ #ifndef mortal_included #define mortal_included #include #include #include #include #include class Mortal { protected: void die(const char *fmt, ...) { char *cpy = static_cast(malloc(strlen(fmt) + 256)); sprintf(cpy, "%s\n", fmt); va_list ap; va_start(ap, fmt); vfprintf(stderr, cpy, ap); va_end(ap); exit(1); } }; #endif tirex-0.7.0/backend-mapnik/networklistener.cc000066400000000000000000000143421412262531100212640ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include #include #include #include #include #include #include #include #include #include #include #include "networklistener.h" #include "networkrequest.h" #include "networkresponse.h" // stuff for handling the hangup signal properly extern "C" { static volatile sig_atomic_t gHangupOccurred; static void hangup_signal_handler(int /*param*/) { gHangupOccurred = 1; } static void install_sighup_handler(bool with_restart) { struct sigaction action; sigemptyset(&action.sa_mask); action.sa_handler = hangup_signal_handler; action.sa_flags = with_restart ? SA_RESTART : 0; sigaction(SIGHUP, &action, NULL); } static void ignore_sigpipe() { struct sigaction action; sigemptyset(&action.sa_mask); action.sa_handler = SIG_IGN; action.sa_flags = 0; sigaction(SIGPIPE, &action, NULL); } } NetworkListener::NetworkListener(int port, int sockfd, int parentfd, std::map *handlers, int maxreq) : mpRequestHandlers(handlers), mSocket(-1), mParent(parentfd), mMaxRequests(maxreq) { mRequestCount = 0; socklen_t length; sockaddr_in server; if (sockfd >= 0) { mSocket = sockfd; debug("using existing socket %d", sockfd); } else { mSocket = socket(AF_INET, SOCK_DGRAM, 0); if (mSocket < 0) die ("cannot open socket: %s", strerror(errno)); int one = 1; setsockopt(mSocket, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); length = sizeof(server); bzero(&server, length); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr("127.0.0.1"); server.sin_port = htons(port); if (bind(mSocket, reinterpret_cast(&server), length) < 0) die("cannot bind to port %d: %s", port, strerror(errno)); debug("bound to port %d", port); } } NetworkListener::~NetworkListener() { } void NetworkListener::run() { sockaddr_in client; socklen_t fromlen = sizeof(sockaddr_in); char buf[MAX_DGRAM]; // install SIGHUP signal handler. use sigaction to avoid restarting after signal. gHangupOccurred = 0; int errcnt = 0; fd_set rfds; FD_ZERO(&rfds); time_t last_alive_sent = 0; install_sighup_handler(false); ignore_sigpipe(); while (!gHangupOccurred) { timeval to = { 5, 0 }; FD_SET(mSocket, &rfds); // do not select for writability of mParent, since it is // always writable. int n = select(mSocket + 1, &rfds, NULL, NULL, &to); // send alive message to parent. if (mParent > -1) { time_t now = time(NULL); if (now >= last_alive_sent + 5) { // we really are not interested in the write() result since // the parent is going to kill us anyway if it does not recieve // an alive message. The following construction gets rid of // the compiler warning about not using the return value. if (write(mParent, static_cast("alive"), 5)) {}; last_alive_sent = now; } } if (n <= 0) { if (n < 0 && errno != EINTR) { error("error while reading data: %s", strerror(errno)); if (errcnt++ > 10) { error("too many errors - exiting"); break; } } continue; } n = recvfrom(mSocket, buf, MAX_DGRAM, MSG_DONTWAIT, reinterpret_cast(&client), &fromlen); if (n < 0) { if (errno != EWOULDBLOCK && errno != EAGAIN && errno != EINTR) { error("error while reading data: %s", strerror(errno)); if (errcnt++ > 10) { error("too many errors - exiting"); break; } } } else { errcnt = 0; NetworkRequest *req = new NetworkRequest(); std::string strbuf(buf, n); debug("read: %s", strbuf.c_str()); const NetworkResponse *resp; if (!req->parse(strbuf)) { error("error parsing request"); resp = NetworkResponse::makeErrorResponse(NULL, "cannot parse request"); } else { std::map::const_iterator h = mpRequestHandlers->find(req->getParam("map", "")); install_sighup_handler(true); if (h != mpRequestHandlers->end()) { if (!(resp = h->second->handleRequest(req))) { error("handler returned null"); resp = NetworkResponse::makeErrorResponse(req, "Handler for map '%s' encountered an error", req->getParam("map", "").c_str()); } } else { error("no handler found for map style '%s'", req->getParam("map", "").c_str()); resp = NetworkResponse::makeErrorResponse(req, "map style '%s' is not known", req->getParam("map", "").c_str()); } install_sighup_handler(false); } std::string responseString; resp->build(responseString); debug("sending: %s", responseString.c_str()); n = sendto(mSocket, responseString.data(), responseString.length(), 0, reinterpret_cast(&client), fromlen); if (n < 0) { error("error in sendto"); } delete resp; delete req; if (mMaxRequests > -1 && ++mRequestCount > mMaxRequests) { error("maxrequests reached, terminating"); exit(1); } } } } tirex-0.7.0/backend-mapnik/networklistener.h000066400000000000000000000016551412262531100211310ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * NetworkListener * * Class that handles the main network loop, waiting for input on the * specified UDP socket, then calling the appropriate request handler * for the type of request received. */ #ifndef networklistener_included #define networklistener_included #include #include #include "requesthandler.h" #include "mortal.h" #include "debuggable.h" #define MAX_DGRAM 0xffff class NetworkListener : public Mortal, public Debuggable { public: NetworkListener(int port, int sockfd, int parentfd, std::map *handlers, int maxreq); ~NetworkListener(); void run(); private: std::map *mpRequestHandlers; int mSocket; int mParent; int mMaxRequests; int mRequestCount; }; #endif tirex-0.7.0/backend-mapnik/networkmessage.cc000066400000000000000000000036371412262531100210700ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include #include #include "networkmessage.h" NetworkMessage::NetworkMessage() { } NetworkMessage::~NetworkMessage() { } const std::string NetworkMessage::getParam(const std::string &key, const std::string &def) const { std::map::const_iterator i = mParams.find(key); return (i == mParams.end() ? def : i->second); } int NetworkMessage::getParam(const std::string &key, int def) const { std::map::const_iterator i = mParams.find(key); return (i == mParams.end() ? def : atoi(i->second.c_str())); } void NetworkMessage::setParam(const std::string &key, const std::string &value) { mParams[key] = value; } void NetworkMessage::setParam(const std::string &key, int value) { char buffer[32]; snprintf(buffer, 31, "%d", value); buffer[31]=0; mParams[key] = buffer; } bool NetworkMessage::parse(const std::string &buffer) { debug(">> NetworkMessage::parse"); char *dup = strdup(buffer.c_str()); char *token; token = strtok(dup, "\r\n"); while (token) { char *eq = strchr(token, '='); if (eq) { *eq++ = 0; mParams[token] = eq; } else { // invalid line } token = strtok(NULL, "\r\n"); } free(dup); debug("<< NetworkMessage::parse"); return true; } bool NetworkMessage::build(std::string &buffer) const { debug(">> NetworkMessage::build"); buffer.clear(); for (std::map::const_iterator i = mParams.begin(); i != mParams.end(); i++) { buffer.append(i->first); buffer.append("="); buffer.append(i->second); buffer.append("\n"); } debug("<< NetworkMessage::build"); return true; } tirex-0.7.0/backend-mapnik/networkmessage.h000066400000000000000000000024641412262531100207270ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * NetworkMessage * * Superclass for messages of the Tirex network protocol. Protocol * messages consist of a series of lines, ending with LF or CRLF, and * each line contains a plain-text key, followed by an equal sign, and * a plain-text value. Keys must not contain equal signs; lines without * equal signs are ignored; the order does not matter; and neither keys * nor values may contain CR or LF. * * Example message: * * request=render * type=metatile * map=default * x=16 * y=24 * z=5 */ #ifndef networkmessage_included #define networkmessage_included #include "debuggable.h" #include #include class NetworkMessage : public Debuggable { private: std::map mParams; public: NetworkMessage(); ~NetworkMessage(); bool parse(const std::string &buffer); bool build(std::string &buffer) const; const std::string getParam(const std::string &key, const std::string &def) const; int getParam(const std::string &key, int def) const; void setParam(const std::string &key, const std::string &value); void setParam(const std::string &key, int value); }; #endif tirex-0.7.0/backend-mapnik/networkrequest.cc000066400000000000000000000005741412262531100211310ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include "networkrequest.h" NetworkRequest::NetworkRequest() : mDefaultType("metatile_render_request") { } NetworkRequest::~NetworkRequest() { } const std::string NetworkRequest::getType() const { return getParam("type", mDefaultType); } tirex-0.7.0/backend-mapnik/networkrequest.h000066400000000000000000000010541412262531100207650ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * NetworkRequest * * A request as read from the network interface. * * @see NetworkMessage for details. */ #ifndef networkrequest_included #define networkrequest_included #include "networkmessage.h" class NetworkRequest : public NetworkMessage { private: std::string mDefaultType; public: const std::string getType() const; NetworkRequest(); ~NetworkRequest(); }; #endif tirex-0.7.0/backend-mapnik/networkresponse.cc000066400000000000000000000017451412262531100213000ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include "networkresponse.h" #include #include #include #include #include NetworkResponse::NetworkResponse(const NetworkRequest *request) { std::string id = request->getParam("id", ""); if (id.length()) setParam("id", id); std::string type = request->getType(); setParam("type", type); } NetworkResponse::NetworkResponse() { } NetworkResponse::~NetworkResponse() { } const NetworkResponse *NetworkResponse::makeErrorResponse(const NetworkRequest *request, const char *fmt, ...) { char buffer[0xffff]; va_list ap; va_start(ap, fmt); vsnprintf(buffer, sizeof(buffer), fmt, ap); va_end(ap); NetworkResponse *rv = request ? new NetworkResponse(request) : new NetworkResponse(); rv->setParam("errmsg", buffer); rv->setParam("result", "error"); return rv; } tirex-0.7.0/backend-mapnik/networkresponse.h000066400000000000000000000012321412262531100211310ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * NetworkResponse * * A response as written to the network interface. * * @see NetworkMessage for details. */ #ifndef networkresponse_included #define networkresponse_included #include "networkmessage.h" #include "networkrequest.h" class NetworkResponse : public NetworkMessage { private: public: static const NetworkResponse *makeErrorResponse(const NetworkRequest *request, const char *fmt, ...); NetworkResponse(const NetworkRequest *request); NetworkResponse(); ~NetworkResponse(); }; #endif tirex-0.7.0/backend-mapnik/renderd.cc000066400000000000000000000171011412262531100174440ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include "renderd.h" #include #include #include #include #include #include #include "networklistener.h" bool RenderDaemon::loadFonts(const boost::filesystem::path &dir, bool recurse) { if (!boost::filesystem::exists(dir)) return false; boost::filesystem::directory_iterator end_itr; for (boost::filesystem::directory_iterator itr(dir); itr != end_itr; ++itr) { if (boost::filesystem::is_directory(*itr) && recurse) { if (!loadFonts(*itr, true)) return false; } else { #if (BOOST_FILESYSTEM_VERSION == 3) mapnik::freetype_engine::register_font(itr->path().string()); #else // v2 mapnik::freetype_engine::register_font(itr->string()); #endif } } return true; } bool RenderDaemon::loadMapnikWrapper(const char *configfile) { // create mapnik instances bool rv = false; FILE *f = fopen(configfile, "r"); if (!f) { warning("cannot open '%s'", configfile); return rv; } char linebuf[255]; std::string tiledir; std::map mapfiles; std::string stylename; unsigned int tilesize = 256; unsigned int mtrowcol = 8; double scalefactor = 1.0; int buffersize = -1; std::string imagetype = "png256"; int lineno = 0; while (char *line = fgets(linebuf, sizeof(linebuf), f)) { lineno++; while (isspace(*line)) line++; if (*line == '#') continue; if (!*line) continue; char *eq = strchr(line, '='); if (eq) { char *last = eq-1; // trim space before equal sign while (last > line && isspace(*last)) *last-- = 0; *eq++ = 0; // trim space after equal sign while (isspace(*eq)) eq++; // trim space at end of line last = eq + strlen(eq) - 1; while (last > eq && isspace(*last)) *last-- = 0; if (!strcmp(line, "tiledir")) { tiledir.assign(eq); } else if (!strncmp(line, "mapfile", 7)) { mapfiles.insert(std::pair(line+7, eq)); } else if (!strcmp(line, "scalefactor")) { scalefactor = atof(eq); } else if (!strcmp(line, "buffersize")) { buffersize = atoi(eq); } else if (!strcmp(line, "tilesize")) { tilesize = atoi(eq); } else if (!strcmp(line, "metarowscols")) { mtrowcol = atoi(eq); } else if (!strcmp(line, "maxrequests")) { mMaxRequests = atoi(eq); } else if (!strcmp(line, "name")) { stylename.assign(eq); } else if (!strcmp(line, "imagetype")) { imagetype.assign(eq); } else if (!strcmp(line, "minz")) { // no error } else if (!strcmp(line, "maxz")) { // no error } else { warning("parse error on line %d of config file %s", lineno, configfile); } } else { warning("parse error on line %d of config file %s", lineno, configfile); } } fclose(f); if (mapfiles.empty()) { warning("cannot add %s: missing mapfile option", configfile); return rv; } if (tiledir.empty()) { warning("cannot add %s: missing tiledir option", configfile); return rv; } if (access(tiledir.c_str(), W_OK) == -1) { warning("cannot add %s: tile directory '%s' not accessible", configfile, tiledir.c_str()); return rv; } if (stylename.empty()) { warning("cannot add %s: missing name option", configfile); return rv; } try { mHandlerMap[stylename] = new MetatileHandler(tiledir, mapfiles, tilesize, scalefactor, buffersize, mtrowcol, imagetype); mHandlerMap[stylename]->setStatusReceiver(this); debug("added style '%s' from map %s", stylename.c_str(), configfile); rv = true; } catch (std::exception const& ex) { warning("cannot add %s", configfile); warning("%s", ex.what()); } return rv; } RenderDaemon::RenderDaemon(int argc, char **argv) : mArgc(argc), mArgv(argv), mProgramName(argc ? argv[0] : "") { setStatus("initializing"); mMaxRequests = -1; char *tmp = getenv("TIREX_BACKEND_DEBUG"); Debuggable::msDebugLogging = tmp ? true : false; std::string strfac; tmp = getenv("TIREX_BACKEND_SYSLOG_FACILITY"); if (tmp) strfac = tmp; int fac = LOG_DAEMON; if (strfac.empty()) fac = LOG_DAEMON; else if (strfac == "local0") fac = LOG_LOCAL0; else if (strfac == "local1") fac = LOG_LOCAL1; else if (strfac == "local2") fac = LOG_LOCAL2; else if (strfac == "local3") fac = LOG_LOCAL3; else if (strfac == "local4") fac = LOG_LOCAL4; else if (strfac == "local5") fac = LOG_LOCAL4; else if (strfac == "local6") fac = LOG_LOCAL6; else if (strfac == "user") fac = LOG_USER; else if (strfac == "daemon") fac = LOG_DAEMON; else { die("Cannot use log facility '%s' - only local0-local7, user, daemon are allowed.", strfac.c_str()); } openlog("tirex-backend-mapnik", Debuggable::msDebugLogging ? LOG_PERROR|LOG_PID : LOG_PID, fac); info("Renderer started (name=%s)", getenv("TIREX_BACKEND_NAME")); tmp = getenv("TIREX_BACKEND_SOCKET_FILENO"); mSocketFd = tmp ? atoi(tmp) : -1; tmp = getenv("TIREX_BACKEND_PIPE_FILENO"); mParentFd = tmp ? atoi(tmp) : -1; tmp = getenv("TIREX_BACKEND_PORT"); mPort = tmp ? atoi(tmp) : 9320; tmp = getenv("TIREX_BACKEND_CFG_plugindir"); #if MAPNIK_VERSION >= 200200 if (tmp) mapnik::datasource_cache::instance().register_datasources(tmp); #else if (tmp) mapnik::datasource_cache::instance()->register_datasources(tmp); #endif tmp = getenv("TIREX_BACKEND_CFG_fontdir_recurse"); bool fr = tmp ? atoi(tmp) : false; tmp = getenv("TIREX_BACKEND_CFG_fontdir"); if (tmp) loadFonts(tmp, fr); tmp = getenv("TIREX_BACKEND_MAP_CONFIGS"); if (tmp) { char *dup = strdup(tmp); char *tkn = strtok(dup, " "); while (tkn) { loadMapnikWrapper(tkn); tkn = strtok(NULL, " "); } } if (mHandlerMap.empty()) die("Cannot load any Mapnik styles"); } RenderDaemon::~RenderDaemon() { } void RenderDaemon::run() { NetworkListener listener(mPort, mSocketFd, mParentFd, &mHandlerMap, mMaxRequests); setStatus("idle"); listener.run(); } void RenderDaemon::setStatus(const char *status) { #ifdef __linux__ char **p = mArgv; for (int i=1;i #include #include class RenderDaemon : public Mortal, public Debuggable, public StatusReceiver { private: bool loadFonts(const boost::filesystem::path &dir, bool recurse); bool loadMapnikWrapper(const char *file); int mPort; int mSocketFd; int mParentFd; std::map mHandlerMap; int mArgc; int mMaxRequests; char **mArgv; std::string mProgramName; protected: void setStatus(const char *status); public: void run(); RenderDaemon(int argc, char **argv); ~RenderDaemon(); }; #endif tirex-0.7.0/backend-mapnik/renderrequest.h000066400000000000000000000012451412262531100205550ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * RenderRequest * * Class that encapsulates a render request. Any metatile information * must already have been resolved into plain coordinates. */ #ifndef renderrequest_included #define renderrequest_included class RenderRequest { public: unsigned int width; unsigned int height; double east; double west; double north; double south; double scale_factor; int buffer_size; unsigned int srs; unsigned int bbox_srs; unsigned int zoom; }; #endif tirex-0.7.0/backend-mapnik/renderresponse.h000066400000000000000000000014011412262531100207150ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * RenderResponse * * Class that encapsulates a render response - usually a plain image. */ #ifndef renderresponse_included #define renderresponse_included #include #if MAPNIK_VERSION >= 300000 #define image_data_32 image_rgba8 #define image_32 image_rgba8 #include #include #else #include #endif class RenderResponse { public: #if MAPNIK_VERSION >= 800 mapnik::image_32 *image; #else mapnik::Image32 *image; #endif RenderResponse() { image = NULL; } ~RenderResponse() { if (image) delete image; } }; #endif tirex-0.7.0/backend-mapnik/requesthandler.cc000066400000000000000000000010271412262531100210470ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ #include "requesthandler.h" #include #include RequestHandler::RequestHandler() : mpStatusReceiver(NULL) { } void RequestHandler::updateStatus(const char *fmt, ...) const { char buffer[256]; va_list args; va_start(args, fmt); vsnprintf(buffer, 255, fmt, args); buffer[255]=0; va_end(args); if (mpStatusReceiver) mpStatusReceiver->setStatus(buffer); } tirex-0.7.0/backend-mapnik/requesthandler.h000066400000000000000000000015761412262531100207220ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * RequestHandler * * Superclass for classes that handle network requests. */ #ifndef requesthandler_included #define requesthandler_included #include #include #include "networkrequest.h" #include "networkresponse.h" #include "debuggable.h" #include "statusreceiver.h" class RequestHandler : public Debuggable { private: StatusReceiver *mpStatusReceiver; protected: void updateStatus(const char *fmt, ...) const; public: RequestHandler(); virtual ~RequestHandler() { } void setStatusReceiver(StatusReceiver *sr) { mpStatusReceiver = sr; } virtual const std::string getRequestType() const = 0; virtual const NetworkResponse *handleRequest(const NetworkRequest *request) = 0; }; #endif tirex-0.7.0/backend-mapnik/statusreceiver.h000066400000000000000000000006761412262531100207440ustar00rootroot00000000000000/* * Tirex Tile Rendering System * * Mapnik rendering backend * * Originally written by Jochen Topf & Frederik Ramm. * */ /** * StatusReceiver * * Superclass ("interface") for classes that receive status chnage * information. */ #ifndef statusreceiver_included #define statusreceiver_included class StatusReceiver { public: virtual ~StatusReceiver() { } virtual void setStatus(const char *status) = 0; }; #endif tirex-0.7.0/backend-mapnik/test/000077500000000000000000000000001412262531100164715ustar00rootroot00000000000000tirex-0.7.0/backend-mapnik/test/test.sh000066400000000000000000000037671412262531100200210ustar00rootroot00000000000000#!/bin/sh if [ -f ne_110m_admin_0_countries.shp ] then : else wget -O admin.zip http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip unzip -o admin.zip rm -f admin.zip rm -f ne_110m_admin_0_countries.README.html rm -f ne_110m_admin_0_countries.VERSION.txt fi TILEDIR=/tmp/tile$$.dir mkdir $TILEDIR cat > test.conf < test.xml << EOF countries EOF fi FONTDIR=/tmp/font$$.dir mkdir $FONTDIR export TIREX_BACKEND_CFG_plugindir=/usr/lib/mapnik/input export TIREX_BACKEND_CFG_fontdir=$FONTDIR export TIREX_BACKEND_CFG_fontdir_recurse=0 export TIREX_BACKEND_CFG_MAP_CONFIGS=test.conf export TIREX_BACKEND_PORT=9330 export TIREX_BACKEND_SYSLOG_FACILITY=local0 export TIREX_BACKEND_MAP_CONFIGS=test.conf export TIREX_BACKEND_DEBUG=1 export TIREX_BACKEND_PIPE_FILENO=1 export TIREX_BACKEND_ALIVE_TIMEOUT=10 echo starting backend... ../backend-mapnik > /dev/null 2>&1 & PID=$! sleep 2 echo sending query... echo "id=1 map=test prio=1 type=metatile_render_request x=0 y=0 z=3" | nc -w 5 -u localhost $TIREX_BACKEND_PORT > nc.out.$$ MT=`grep metatile= nc.out.$$ |cut -d= -f2` RS=`grep result= nc.out.$$ |cut -d= -f2` rm -f nc.out.$$ rmdir $FONTDIR kill $PID if [ "$RS" = "ok" ] then if [ -f $MT ] then echo looks ok - check $MT with viewmeta.pl exit 0 fi fi echo something is wrong tirex-0.7.0/backends/000077500000000000000000000000001412262531100144205ustar00rootroot00000000000000tirex-0.7.0/backends/mapserver000077500000000000000000000027221412262531100163550ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # backends/mapserver # # heavily based on WMS Backend by Jochen Topf # #----------------------------------------------------------------------------- # See Tirex::Backend::Mapserver for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2012 Sven Geggus # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Backend::Mapserver; #----------------------------------------------------------------------------- my $backend = Tirex::Backend::Mapserver->new(); $backend->main(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/backends/openseamap000077500000000000000000000026511412262531100165020ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # backends/openseamap # #----------------------------------------------------------------------------- # See Tirex::Backend::Test for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2019 Frederik Ramm # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Backend::OpenSeaMap; #----------------------------------------------------------------------------- my $backend = Tirex::Backend::OpenSeaMap->new(); $backend->main(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/backends/test000077500000000000000000000027301412262531100153270ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # backends/test # #----------------------------------------------------------------------------- # See Tirex::Backend::Test for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Backend::Test; #----------------------------------------------------------------------------- my $backend = Tirex::Backend::Test->new(); $backend->main(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/backends/tms000077500000000000000000000030371412262531100151540ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # backends/tms # #----------------------------------------------------------------------------- # See Tirex::Backend::TMS for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2020 Frederik Ramm # # This backend code has been developed in the course of a project # funded by the German Federal Office for Radiation Protection (BfS). # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Backend::TMS; #----------------------------------------------------------------------------- my $backend = Tirex::Backend::TMS->new(); $backend->main(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/backends/wms000077500000000000000000000027241412262531100151610ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # backends/wms # #----------------------------------------------------------------------------- # See Tirex::Backend::WMS for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Backend::WMS; #----------------------------------------------------------------------------- my $backend = Tirex::Backend::WMS->new(); $backend->main(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/bin/000077500000000000000000000000001412262531100134165ustar00rootroot00000000000000tirex-0.7.0/bin/tirex-backend-manager000077500000000000000000000423071412262531100175020ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-backend-manager # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Fcntl; use Getopt::Long qw( :config gnu_getopt ); use IO::Pipe; use IO::Select; use IO::Socket; use POSIX qw(sys_wait_h setsid); use Pod::Usage qw(); use Socket; use Sys::Syslog; use Tirex; use Tirex::Renderer; use Tirex::Map; #----------------------------------------------------------------------------- die("refusing to run as root\n") if ($< == 0); #----------------------------------------------------------------------------- # Reading command line and config #----------------------------------------------------------------------------- my @argv = @ARGV; my %opts = (); GetOptions( \%opts, 'help|h', 'debug|d', 'foreground|f', 'config|c=s' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-backend-manager - tirex backend manager\n", -exitval => 0 ); } $Tirex::DEBUG = 1 if ($opts{'debug'}); $Tirex::FOREGROUND = 1 if ($opts{'foreground'}); # debug implies foreground $Tirex::FOREGROUND =1 if ($opts{'debug'}); my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- # Initialize logging #----------------------------------------------------------------------------- openlog('tirex-backend-manager', $Tirex::DEBUG ? 'pid|perror' : 'pid', Tirex::Config::get('backend_manager_syslog_facility', $Tirex::BACKEND_MANAGER_SYSLOG_FACILITY, qr{^(daemon|syslog|user|local[0-7])$})); syslog('info', 'tirex-backend-manager started with cmd line options: %s', join(' ', @argv)); Tirex::Config::dump_to_syslog(); #----------------------------------------------------------------------------- # Daemonize unless in debug mode or foreground #----------------------------------------------------------------------------- if (!$Tirex::FOREGROUND) { chdir('/') or die("Cannot chdir to /: $!"); open(STDIN, '<', '/dev/null') or die("Cannot read from /dev/null: $!"); open(STDOUT, '>', '/dev/null') or die("Cannot write to /dev/null: $!"); defined(my $pid = fork) or die("Cannot fork: $!"); exit(0) if ($pid); setsid() or die("Cannot start a new session: $!"); open(STDERR, '>&STDOUT') or die("Cannot dup stdout: $!"); } #----------------------------------------------------------------------------- # Write pid to pidfile #----------------------------------------------------------------------------- my $pidfile = Tirex::Config::get('backend_manager_pidfile', $Tirex::BACKEND_MANAGER_PIDFILE); if (open(my $pidfh, '>', $pidfile)) { print $pidfh "$$\n"; close($pidfh); } else { syslog('err', "Can't open pidfile '$pidfile' for writing: $!\n"); # keep going, we'd rather go on without a pidfile than stopping the show } #----------------------------------------------------------------------------- # Set signal handler #----------------------------------------------------------------------------- my $received_sighup = 0; my $received_sigterm = 0; $SIG{'HUP'} = \&sighup_handler; $SIG{'TERM'} = \&sigterm_handler; $SIG{'INT'} = \&sigterm_handler; #----------------------------------------------------------------------------- # workers must check in every so often; note they will not check # in during tile rendering so allow ample time. my $ALIVE_TIMEOUT = Tirex::Config::get('backend_manager_alive_timeout', $Tirex::BACKEND_MANAGER_ALIVE_TIMEOUT) * 60; # minutes -> seconds # a child that has been sent a signal must react this quickly my $HANGUP_TIMEOUT = 15; my $TERM_TIMEOUT = 5; # timeout when waiting for alive messages from workers my $SELECT_TIMEOUT = 10; #----------------------------------------------------------------------------- # Main loop #----------------------------------------------------------------------------- { my $sockets = {}; while (1) { my $workers = {}; Tirex::Renderer->read_config_dir($config_dir); syslog('info', 'Found config for renderers: %s', join(' ', map { $_->get_name(); } Tirex::Renderer->all())); $sockets = open_sockets($sockets); $received_sighup = 0; $SIG{'HUP'} = \&sighup_handler; while (! $received_sighup) { start_workers($workers, $sockets); check_for_alive_messages($workers); cleanup_dead_workers($workers); kill_old_workers($workers); if ($received_sigterm) { syslog('info', 'TERM/INT received, forwarding to children: %s', join(' ', keys %$workers)); foreach my $pid (keys %$workers) { kill('HUP', $pid); } unlink($pidfile); # ignore return code exit(0); } sleep 1; } syslog('info', 'HUP received, forwarding to children: %s', join(' ', keys %$workers)); foreach my $pid (keys %$workers) { kill('HUP', $pid); } Tirex::Renderer->clear(); Tirex::Map->clear(); } } #----------------------------------------------------------------------------- # Open sockets for all renderers or re-use old ones after a SIGHUP #----------------------------------------------------------------------------- sub open_sockets { my $old_sockets = shift; my $new_sockets = {}; # go through all renderers and open sockets if they are not already open foreach my $renderer (Tirex::Renderer->all()) { my $port = $renderer->get_port(); if ($old_sockets->{$port}) { syslog('debug', 're-using socket for port %d', $port); $new_sockets->{$port} = $old_sockets->{$port}; delete $old_sockets->{$port}; } else { my $socket = IO::Socket::INET->new( LocalAddr => 'localhost', LocalPort => $port, Proto => 'udp', ReuseAddr => 1, ); if ($socket) { syslog('debug', "opened port %d for renderer '%s'", $port, $renderer->get_name()); $socket->fcntl(Fcntl::F_SETFD, 0); # unset close-on-exec $new_sockets->{$port} = $socket; } else { syslog('err', "could not open socket on port %d for renderer '%s', renderer disabled", $port, $renderer->get_name()); $renderer->disable(); } } } # close all sockets that are not needed by any renderer foreach my $port (keys %$old_sockets) { syslog('info', 'closing socket for port %d', $port); $old_sockets->{$port}->close(); } return $new_sockets; } #----------------------------------------------------------------------------- # Start workers if there are less than configured #----------------------------------------------------------------------------- sub start_workers { my $workers = shift; my $sockets = shift; foreach my $renderer (Tirex::Renderer->enabled()) { my $socket = $sockets->{$renderer->get_port()}; next unless ($socket); while ($renderer->num_workers() < $renderer->get_procs()) { my $pipe = create_pipe(); my $pid = fork(); if ($pid == 0) # child { # remove hash with workers because it contains references to open pipes and sockets # that should be closed in the child $workers = undef; $pipe->writer(); execute_renderer($renderer, $pipe->fileno(), $socket->fileno()); # if we are here the execute failed syslog('err', "Cannot execute renderer %s (%s)", $renderer->get_name(), $renderer->get_path()); exit($Tirex::EXIT_CODE_DISABLE); } elsif ($pid > 0) # parent { $pipe->reader(); syslog('info', 'renderer %s started with pid %d', $renderer->get_name(), $pid); $workers->{$pid} = { pid => $pid, last_seen_alive => time(), handle => $pipe, renderer => $renderer, }; $renderer->add_worker($pid); } else { syslog('crit', 'error in fork(): %s', $!); } } } } #----------------------------------------------------------------------------- # Create pipe for alive message from worker child to parent #----------------------------------------------------------------------------- sub create_pipe { my $reader = IO::Pipe::End->new(); my $writer = IO::Pipe::End->new(); my $pipe = IO::Pipe->new($reader, $writer); $reader->fcntl(F_SETFD, 0); # unset close-on-exec $writer->fcntl(F_SETFD, 0); # unset close-on-exec $reader->blocking(0); $writer->blocking(0); return $pipe; } #----------------------------------------------------------------------------- # Check for alive messages from workers # will return after $SELECT_TIMEOUT seconds, when a message arrived or when # we caught a signal #----------------------------------------------------------------------------- sub check_for_alive_messages { my $workers = shift; my $select = IO::Select->new(); foreach my $worker (values %$workers) { $select->add([$worker->{'handle'}, $worker]); } foreach my $handle_wrapper ($select->can_read($SELECT_TIMEOUT)) { my ($handle, $worker) = @$handle_wrapper; my $buf; $handle->read($buf, 9999); if (length($buf) > 0) { $worker->{'last_seen_alive'} = time(); } } } #----------------------------------------------------------------------------- # Cleanup dead workers #----------------------------------------------------------------------------- sub cleanup_dead_workers { my $workers = shift; while ((my $pid = waitpid(-1, WNOHANG)) > 0) { my $exit_code = $? >> 8; my $signal = $? & 127; syslog('warning', 'child %d terminated (exit_code=%d, signal=%d)', $pid, $exit_code, $signal); # this will happen if a worker child dies which we don't know about # because we got a SIGHUP in between next unless ($workers->{$pid}); # if the return code of the child is something other than $EXIT_CODE_RESTART, the renderer is disabled # this does not happen if the worker child was killed because of a timeout #if ($exit_code != $Tirex::EXIT_CODE_RESTART && ! defined $workers->{$pid}->{'hungup'}) #{ # my $renderer = $workers->{$pid}->{'renderer'}; # if ($renderer->get_debug()) # { # syslog('debug', 'renderer %s not disabled because we are in debug mode', $renderer->get_name()); # } # else # { # $renderer->disable(); # syslog('err', 'disabled renderer %s', $renderer->get_name()); # } #} $workers->{$pid}->{'handle'}->close(); $workers->{$pid}->{'renderer'}->remove_worker($pid); delete $workers->{$pid}; } } #----------------------------------------------------------------------------- # Kill workers that haven't send an alive message for a while #----------------------------------------------------------------------------- sub kill_old_workers { my $workers = shift; my $now = time(); foreach my $worker (values %$workers) { if ($worker->{'last_seen_alive'} < $now - $ALIVE_TIMEOUT) { if (! defined($worker->{'killed'}) && defined($worker->{'terminated'}) && ($worker->{'terminated'} < $now - $TERM_TIMEOUT)) { syslog('info', "sending KILL to worker '%s' with pid %d (due to timeout)", $worker->{'renderer'}->get_name(), $worker->{'pid'}); kill('KILL', $worker->{'pid'}); $worker->{'killed'} = $now; } elsif (! defined($worker->{'terminated'}) && defined($worker->{'hungup'}) && ($worker->{'hungup'} < $now - $HANGUP_TIMEOUT)) { syslog('info', "sending TERM to worker '%s' with pid %d (due to timeout)", $worker->{'renderer'}->get_name(), $worker->{'pid'}); kill('TERM', $worker->{'pid'}); $worker->{'terminated'} = $now; } elsif (! defined($worker->{'hungup'})) { syslog('info', "sending HUP to worker '%s' with pid %d (due to timeout)", $worker->{'renderer'}->get_name(), $worker->{'pid'}); kill('HUP', $worker->{'pid'}); $worker->{'hungup'} = $now; } } } } #----------------------------------------------------------------------------- # Execute the worker child with the right environment #----------------------------------------------------------------------------- sub execute_renderer { my $renderer = shift; my $pipe_fileno = shift; my $socket_fileno = shift; $ENV{'TIREX_BACKEND_NAME'} = $renderer->get_name(); $ENV{'TIREX_BACKEND_PORT'} = $renderer->get_port(); $ENV{'TIREX_BACKEND_SYSLOG_FACILITY'} = $renderer->get_syslog_facility(); $ENV{'TIREX_BACKEND_MAP_CONFIGS'} = join(' ', map { $_->get_filename() } $renderer->get_maps()); $ENV{'TIREX_BACKEND_ALIVE_TIMEOUT'} = $ALIVE_TIMEOUT - 20; # give the child 20 seconds less than what the parent uses as timeout to be on the safe side $ENV{'TIREX_BACKEND_PIPE_FILENO'} = $pipe_fileno; $ENV{'TIREX_BACKEND_SOCKET_FILENO'} = $socket_fileno; $ENV{'TIREX_BACKEND_DEBUG'} = 1 if ($Tirex::DEBUG || $renderer->get_debug()); my $cfg = $renderer->get_config(); foreach my $key (keys %$cfg) { $ENV{"TIREX_BACKEND_CFG_$key"} = $cfg->{$key}; } exec($renderer->get_path()); } #----------------------------------------------------------------------------- # Signal handlers #----------------------------------------------------------------------------- sub sighup_handler { $received_sighup = 1; $SIG{'HUP'} = 'IGNORE'; } sub sigterm_handler { $received_sigterm = 1; } __END__ =head1 NAME tirex-backend-manager - manages Tirex rendering backend workers =head1 SYNOPSIS tirex-backend-manager [OPTIONS] =head1 OPTIONS =over 4 =item B<--help> Display help message. =item B<-d>, B<--debug> Run in debug mode, and pass on the debug flag to workers =item B<-f>, B<--foreground> Run in foreground. E.g. when started from systemd service =item B<--config=DIR> Use the DIR config directory instead of /etc/tirex. =back =head1 DESCRIPTION The backend manager starts and monitors as many backend workers as given in the configuration. The backend processes do the actual rendering of tiles. The backend manager expects each worker process to send an "alive" message in regular intervals, and will kill the process if it does not do so in time. The backend manager does not handle render requests in any way; these are read directly from a local UDP socket by the individual backend processes. If the backend manager receives a HUP signal, it will relay this signal to all backends, causing them to exit after completing their current request. It will reload the renderer and map configuration and re-start all workers with the new configuration. =head1 FILES =over 4 =item F The configuration file. =item F Configuration files for renderers =item F Map configurations =back =head1 DIAGNOSTICS The backend manager logs to the I syslog facility unless configured otherwise. In debug mode, logging is also copied to stderr. =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ------------------------------------------------------------------ tirex-0.7.0/bin/tirex-batch000077500000000000000000000333611412262531100155640ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-batch # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use File::stat; use Getopt::Long qw( :config gnu_getopt ); use IO::Socket; use JSON; use Pod::Usage qw(); use Socket; use Time::HiRes; use Tirex; use Tirex::Status; use Tirex::Renderer; use Tirex::Map; #----------------------------------------------------------------------------- # Reading command line and config #----------------------------------------------------------------------------- my %opts = (); GetOptions( \%opts, 'help|h', 'debug|d', 'config|c=s', 'quit|q', 'num|n=i', 'prio|p=i', 'expire|e=s', 'filter|f=s', 'remove', 'count-only' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-batch - send rendering requests to tirex master\n", -exitval => 0 ); } $Tirex::DEBUG = 1 if ($opts{'debug'}); my $prio = $opts{'prio'} || 99; # default batch prio is 99 my $num = $opts{'num'} || 999_999_999; # huge queue size as default max size my @filters; @filters = split(qr{\s*;\s*}, $opts{'filter'}) if (defined $opts{'filter'}); foreach my $filter (@filters) { if ($filter !~ qr{^(exists|not-exists|older\(([^)]+)\)|newer\(([^)]+)\)|multi\(2,[01]\))$}) { print STDERR "unknown filter: $filter\n"; exit(2); } } if ($Tirex::DEBUG) { print STDERR "Using prio: $prio\n"; print STDERR "Using expire: $opts{'expire'}\n" if (exists $opts{'expire'}); print STDERR "Using filters: ", join('; ', @filters) ,"\n"; } my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); Tirex::Renderer->read_config_dir($config_dir); #----------------------------------------------------------------------------- my $master_socket_name = Tirex::Config::get('socket_dir', $Tirex::SOCKET_DIR) . '/master.sock'; my $socket = IO::Socket::UNIX->new( Type => SOCK_DGRAM, # Local => '', Peer => $master_socket_name, ) or die("Cannot open connection to master: $!\n"); my $status; if ($opts{'num'}) { $status = eval { Tirex::Status->new(); }; die("Can't connect to shared memory. Is the tirex-master running?\n") if ($@); } if ($opts{'quit'} && !$opts{'num'}) { die("--quit can only be used in conjunction with --num"); } #----------------------------------------------------------------------------- my $mx = Tirex::Config::get('metatile_columns') || $Tirex::METATILE_COLUMNS; my $my = Tirex::Config::get('metatile_rows') || $Tirex::METATILE_ROWS; #----------------------------------------------------------------------------- my $count = 0; # if there are still command line args, use those as init string if (scalar(@ARGV) > 0) { $count += handle_init(join(' ', @ARGV)); } # else read init strings from STDIN else { while () { chomp; $count += handle_init($_); } } print "metatiles: $count\n" if ($opts{'count-only'}); exit(0); #----------------------------------------------------------------------------- # get queue size of given priority from master #----------------------------------------------------------------------------- sub queue_size { my $prio = shift; if (defined $status) { my $s = $status->read(); if (defined $s) { my $queues = JSON::from_json($s)->{'queue'}->{'prioqueues'}; foreach my $q (@$queues) { return $q->{'size'} if ($q->{'prio'} == $prio); } return 0; } else { die("can't read status\n"); } } else { die("can't get status\n"); } } #----------------------------------------------------------------------------- # handle one init string, ie. decode and send job requests to server #----------------------------------------------------------------------------- sub handle_init { my $init = shift; my $count_metatiles = 0; # if this looks like a metatile path name, decode it if ($init =~ qr{^\.?/([^/]+)/(.*)$}) { my $metatile = Tirex::Metatile->new_from_filename_and_map($2, $1); $init = $metatile->to_s(); } my $range = eval { Tirex::Metatiles::Range->new(init => $init); }; if ($@) { print STDERR "Error parsing init string: $@"; exit(2); } print STDERR "Range: ", $range->to_s(), "\n" if ($Tirex::DEBUG); while (1) { my $queue_size = 0; if (!$opts{'count-only'} && $opts{'num'}) { # wait for queue to have some space while (($queue_size = queue_size($prio)) >= $opts{'num'}) { if ($opts{'quit'}) { print STDERR " queue size $queue_size >= max queue size $opts{'num'}; terminating (--quit set)\n" if ($Tirex::DEBUG); exit(0); } print STDERR " queue size $queue_size >= max queue size $opts{'num'}. waiting...\n" if ($Tirex::DEBUG); sleep(1); } print STDERR " queue size $queue_size, can send up to ", $opts{'num'}-$queue_size ," jobs\n" if ($Tirex::DEBUG); } # send as many jobs as will fit into queue METATILE: while ($queue_size <= $num) { my $metatile = $range->next(); # if there are no more jobs, we are done return $count_metatiles unless (defined $metatile); print STDERR "Considering ", $metatile->to_s(), "\n" if ($Tirex::DEBUG); foreach my $filter (@filters) { if ($filter eq 'exists') { next METATILE unless ($metatile->exists()); } elsif ($filter eq 'not-exists') { next METATILE if ($metatile->exists()); } elsif ($filter =~ qr{^older\(([0-9]+)\)$}) { next METATILE unless ($metatile->older($1)); } # seconds since epoch elsif ($filter =~ qr{^older\(([^)]+)\)$}) { next METATILE unless ($metatile->older(get_mtime($1))); } # filename elsif ($filter =~ qr{^newer\(([0-9]+)\)$}) { next METATILE unless ($metatile->newer($1)); } # seconds since epoch elsif ($filter =~ qr{^newer\(([^)]+)\)$}) { next METATILE unless ($metatile->newer(get_mtime($1))); } # filename elsif ($filter =~ qr{^multi\(([0-9]+),([0-9]+)\)$}) { next METATILE if (($metatile->get_x()/$mx + $metatile->get_y()/$my) % $1 != $2); } } $count_metatiles++; if (!$opts{'count-only'}) { $queue_size++; my %jobparams = ( metatile => $metatile, prio => $prio ); if (defined $opts{'expire'}) { if ($opts{'expire'} =~ /^\+/) { $jobparams{'expire'} = time() + $opts{'expire'}; } else { $jobparams{'expire'} = $opts{'expire'}; } } my $job = Tirex::Job->new(%jobparams); my $request = $job->to_msg( id => undef, type => $opts{'remove'} ? 'metatile_remove_request' : 'metatile_enqueue_request' ); print STDERR " sending: ", $request->to_s(), "\n" if ($Tirex::DEBUG); my $ret = $request->send($socket); if (! defined $ret) { print STDERR "Can't send request. Is the master server running?\n"; exit(1); } Time::HiRes::usleep(1000); # don't send more than 1000 requests/s to not overwhelm the UDP receive buffer or the master } } sleep(1) unless ($opts{'count-only'}); } } sub get_mtime { my $filename = shift; my $st = File::stat::stat($filename); if (! $st) { print "Can't stat $filename: $!\n"; exit(2); } return $st->mtime; } __END__ =head1 NAME tirex-batch - send rendering requests to tirex master =head1 SYNOPSIS tirex-batch [OPTIONS] [INIT] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-d>, B<--debug> Run in debug mode. You'll see the actual messages sent and received. =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =item B<-n>, B<--num=NUM> Try to keep the number of jobs in the queue below this number (Only checked once per second). Disable with NUM=0. =item B<-q>, B<--quit> Quit if the number of jobs in the queue is higher than the number given with -n. Without -q, tirex-batch would wait, and continue to fill the queue once it has gone below the threshold. =item B<-p>, B<--prio=PRIO> Priority for requests. =item B<-e>, B<--expire=TIME> Expire time (seconds since epoch) for jobs. If it starts with '+', number of seconds added to current time. =item B<-f>, B<--filter> Add filters to metatile selection, see section FILTERS. =item B<--remove> Send remove request instead of rendering request. Jobs will be removed from the queue. =item B<--count-only> Only count how many metatiles would be rendered, do not actually send the requests. This will take the filters into account, so it will possibly check the disk for thousands of files! =back =head1 DESCRIPTION INIT is a string describing a range of tiles. If no INIT string is given on the command line, tirex-batch reads init strings from STDIN, one per line. Several different ways of describing the tiles are allowed: Single metatile: map=foo x=4 y=7 z=15 (coordinates will be rounded to metatile numbers) Multiple metatiles: map=foo x=0-32 y=16-32 z=15 Using longitude and latitude ranges (8 to 9 degrees longitude, 48 to 49 degrees latitude) map=foo lon=8,9 lat=48,49 z=15-17 Using a bounding box (8 to 9 degrees longitude, 48 to 49 degrees latitude) map=foo bbox=8,48,9,49 z=15-17 Multiple maps are allowed, too: map=foo,bar You can use a z range (z=10-15). This does not work together with x and y ranges, but works with lon/lat ranges. Ranges of x, y, and z numbers are written as "MIN,MAX" or "MIN-MAX". Ranges of lon and lat are written as "MIN,MAX" (lon and lat can be negative, so range with "-" is problematic). You can also just give a pathname of a metatile file as INIT string. It has to start with './' or '/'. The first directory component must be the name of the map. =head1 FILTERS FILTER is a ;-separated list of filter options. Metatiles not matching the filter are skipped. Filter Options: =over 8 =item B Matches if the meta tile exists on disk. =item B Matches if the meta tile does not exist on disk. =item B Matches if the meta tile's last modification time is before the given Unix time stamp (seconds since the epoch, 1970-01-01 00:00:00). Also matches if the meta tile does not exist on disk. If you want to match only files older than the given date which do actually exist, add the I filter. =item B Instead of the time in seconds since the epoch you can also enter a filename here. The mtime (last modified) of this file will be used. tirex-batch will exit with return code 2 if the file does not exist. =item B Matches if the meta tile's last modification time is after the given Unix time stamp (seconds since the epoch, 1970-01-01 00:00:00). Also matches if the meta tile does not exist on disk. If you want to match only files newer than the given date which do actually exist, add the I filter. =item B Instead of the time in seconds since the epoch you can also enter a filename here. The mtime (last modified) of this file will be used. tirex-batch will exit with return code 2 if the file does not exist. =item B A magic filter that divides all meta tiles up in I classes, and matches only if the current meta tile is in class I of these. Hence the allowed range for I is always 0..I-1. Currently only I=2 is supported. This filter can be used to distribute rendering requests among different tile servers (which may or may not then use F to share resulting tiles). =back =head1 FILES =over 8 =item F The configuration file. =back =head1 DIAGNOSTICS Returns 0 on success, 1 if there was a problem sending the request and 2 if there was a problem parsing the command line or init string. =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ---------------------------------------------------------------------------- tirex-0.7.0/bin/tirex-check-config000077500000000000000000000251171412262531100170230ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-check-config # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Getopt::Long qw( :config gnu_getopt ); use Pod::Usage qw(); use Tirex; use Tirex::Renderer; use Tirex::Map; #----------------------------------------------------------------------------- # Reading command line #----------------------------------------------------------------------------- my %opts = (); GetOptions( \%opts, 'help|h', 'config|c=s' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-check-config - check Tirex config files\n", -exitval => 0 ); } #----------------------------------------------------------------------------- my $warn = 0; my $err = 0; #----------------------------------------------------------------------------- my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; if (! -d $config_dir) { print "X Not a directory: $config_dir\n"; exit(2); } print " Reading config directory: $config_dir\n"; my @extra_files_in_config_dir = sort grep { $_ !~ /^(renderer|tirex\.conf)$/ } map { $_ =~ s{^.*/}{}; $_ } glob("$config_dir/*"); if (scalar(@extra_files_in_config_dir)) { print "\n Found files in config directory that Tirex doesn't know about and will not read:\n"; foreach my $filename (@extra_files_in_config_dir) { print "! $filename [unknown]\n"; } } my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; if (! -f $config_file) { print "X Main config file doesn't exist: $config_file\n"; exit(2); } print "\n Reading config file: $config_file\n"; Tirex::Config::init($config_file); my %known_config_options = ( 'metatile_columns' => [ $Tirex::METATILE_COLUMNS, \&valid_metatile_size], 'metatile_rows' => [ $Tirex::METATILE_ROWS, \&valid_metatile_size], 'stats_dir' => [ $Tirex::STATS_DIR, \&valid_file], 'socket_dir' => [ $Tirex::SOCKET_DIR, \&valid_file], 'backend_manager_alive_timeout' => [ $Tirex::BACKEND_MANAGER_ALIVE_TIMEOUT, \&valid_positive_int], 'backend_manager_pidfile' => [ $Tirex::BACKEND_MANAGER_PIDFILE, \&valid_file], 'backend_manager_syslog_facility' => [ $Tirex::BACKEND_MANAGER_SYSLOG_FACILITY, \&valid_syslog_facility], 'bucket' => [ undef, \&valid_bucket], 'master_logfile' => [ $Tirex::MASTER_LOGFILE, \&valid_file], 'master_pidfile' => [ $Tirex::MASTER_PIDFILE, \&valid_file], 'master_syslog_facility' => [ $Tirex::MASTER_SYSLOG_FACILITY, \&valid_syslog_facility], 'master_rendering_timeout' => [ $Tirex::MASTER_RENDERING_TIMEOUT, \&valid_positive_int], 'modtile_socket_name' => [ $Tirex::MODTILE_SOCK, \&valid_file], 'sync_to_host' => [ undef, \&valid_domain_name], 'syncd_pidfile' => [ $Tirex::SYNCD_PIDFILE , \&valid_file], 'syncd_udp_port' => [ $Tirex::SYNCD_UDP_PORT, \&valid_port], ); print "\n Config options in config file:\n"; foreach my $conf (sort keys %$Tirex::Config::confhash) { my $value = $Tirex::Config::confhash->{$conf}; next if ($conf eq 'bucket'); if (! defined $known_config_options{$conf}) { print "! $conf=$value [unknown config option]\n"; } else { my ($default, $valid) = @{$known_config_options{$conf}}; if (my ($indicator, $text) = &$valid($value)) { print "$indicator $conf=$value [$text]\n"; $err = 1 if ($indicator eq 'X'); $warn = 1 if ($indicator eq '!'); } else { print " $conf=$value\n"; } } } if ($Tirex::Config::confhash->{'bucket'}) { my $minprio = 999_999_999; my $maxproc = 999; my $maxload = 999; print "\n Buckets in config file:\n"; foreach my $bucket (sort { $a->{'minprio'} <=> $b->{'minprio'} } @{$Tirex::Config::confhash->{'bucket'}}) { $minprio = $bucket->{'minprio'} if ($bucket->{'minprio'} < $minprio); printf(" name=%s minprio=%d maxproc=%d maxload=%d\n", $bucket->{'name'}, $bucket->{'minprio'}, $bucket->{'maxproc'}, $bucket->{'maxload'}); if ($bucket->{'name'} !~ /^[a-z0-9_]+$/) { print "X Illegal bucket name: ", $bucket->{'name'}, "\n"; $err = 1; } if ($bucket->{'minprio'} !~ /^[0-9]+$/) { print "X Illegal minprio: ", $bucket->{'minprio'}, "\n"; $err = 1; } if ($bucket->{'maxproc'} !~ /^[1-9]?[0-9]$/) { print "X Illegal maxproc: ", $bucket->{'maxproc'}, "\n"; $err = 1; } if ($bucket->{'maxload'} !~ /^[1-9]?[0-9]$/) { print "X Illegal maxload: ", $bucket->{'maxload'}, "\n"; $err = 1; } if ($bucket->{'maxproc'} > $maxproc) { print "! maxproc for this bucket larger then previous\n"; $warn = 1; } if ($bucket->{'maxload'} > $maxload) { print "! maxload for this bucket larger then previous\n"; $warn = 1; } $maxproc = $bucket->{'maxproc'}; $maxload = $bucket->{'maxload'}; } if ($minprio != 1) { print "X Smallest minprio is not 1 but ", $minprio, "\n"; $err = 1; } } else { print "\nX No bucket configuration in config file!\n"; $err = 1; } print "\n Config options not in config file:\n"; foreach my $conf (sort keys %known_config_options) { next if (defined $Tirex::Config::confhash->{$conf}); my ($default, $valid) = @{$known_config_options{$conf}}; if (defined $default) { print " $conf=$default [default]\n"; } else { $warn = 1; print "! $conf [missing option without default]\n"; } } print "\n Renderer config:\n"; Tirex::Renderer->read_config_dir($config_dir); my @renderer = Tirex::Renderer->all(); my %renderer_dir = map { $_ => 1 } glob("$config_dir/renderer/*"); foreach my $renderer (@renderer) { my $name = $renderer->get_name(); print " Renderer name=$name\n"; if (! -d "$config_dir/renderer/$name") { print "X Missing directory $config_dir/renderer/$name\n"; } delete $renderer_dir{"$config_dir/renderer/$name"}; delete $renderer_dir{"$config_dir/renderer/$name.conf"}; } foreach my $file (sort keys %renderer_dir) { printf("! Found extra file '%s', will be ignored\n", $file); } print "\nAt this point this check program should read and check the\nrenderer and map configs but currently doesn't. Patched welcome.\n"; #----------------------------------------------------------------------------- exit($err ? 2 : $warn ? 1 : 0); #----------------------------------------------------------------------------- sub valid_port { my $port = shift; return if ($port =~ /^[0-9]+$/ && $port >= 1025 && $port <= 65535); return('X', 'must be integer between 1025 and 65535'); } sub valid_syslog_facility { my $facility = shift; return if ($facility =~ /(daemon|user|local[0-7])/); return('X', 'must be one of these: daemon, user, local[0-7]'); } sub valid_file { my $filename = shift; return('X', 'must be absolute path name') if ($filename !~ qr{^/}); (my $dirname = $filename) =~ s{/[^/]*$}{}; return('X', 'directory does not exist') if (! -d $dirname); return; } sub valid_domain_name { my $name = shift; return('X', 'must be valid domain/host name') if ($name !~ /^(([A-Za-z0-9]+)\.)+([A-Za-z0-9]+)$/); return; } sub valid_string { return; } sub valid_positive_int { my $val = shift; return('X', 'must be positive integer') if ($val !~ /^[0-9]+$/); return; } sub valid_metatile_size { my $val = shift; return('X', 'must be positive integer') if ($val !~ /^[0-9]+$/); return('!', 'values other than 8 are experimental and might not work') if ($val ne '8'); return; } sub valid_bucket { return; } #----------------------------------------------------------------------------- __END__ =head1 NAME tirex-check-config - check Tirex config files =head1 SYNOPSIS tirex-check-config [OPTIONS] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =back =head1 DESCRIPTION Checks the Tirex config files and outputs errors and warnings. All lines beginning with 'X' are errors, all warnings begin with '!'. Note that this program is intended to help the admin. It can't detect all errors. Currently only the main config file is checked. Checking of renderer and map config should be added. Patches welcome! =head1 FILES =over 8 =item F The configuration file. =back =head1 DIAGNOSTICS tirex-check-config returns =over 8 =item 0 if the config looks ok =item 1 if warnings were found =item 2 if errors were found =back =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ---------------------------------------------------------------------------- tirex-0.7.0/bin/tirex-master000077500000000000000000000570061412262531100160000ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-master # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Fcntl; use Getopt::Long qw( :config gnu_getopt ); use IO::Select; use IO::Socket; use POSIX 'setsid'; use Pod::Usage qw(); use Socket; use Sys::Syslog; use Data::Dumper; use Tirex; use Tirex::Queue; use Tirex::Manager; use Tirex::Source; use Tirex::Status; use Tirex::Renderer; use Tirex::Map; #----------------------------------------------------------------------------- die("refusing to run as root\n") if ($< == 0); #----------------------------------------------------------------------------- # Reading command line and config #----------------------------------------------------------------------------- my @argv = @ARGV; my %opts = (); GetOptions( \%opts, 'help|h', 'debug|d', 'foreground|f', 'config|c=s' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-master - tirex master daemon\n", -exitval => 0 ); } $Tirex::DEBUG = 1 if ($opts{'debug'}); $Tirex::FOREGROUND = 1 if ($opts{'foreground'}); # debug implies foreground $Tirex::FOREGROUND =1 if ($opts{'debug'}); my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- # Initialize logging #----------------------------------------------------------------------------- openlog('tirex-master', $Tirex::DEBUG ? 'pid|perror' : 'pid', Tirex::Config::get('master_syslog_facility', $Tirex::MASTER_SYSLOG_FACILITY, qr{^(daemon|syslog|user|local[0-7])$})); syslog('info', 'tirex-master started with cmd line options: %s', join(' ', @argv)); Tirex::Config::dump_to_syslog(); read_renderer_and_map_config(); #----------------------------------------------------------------------------- # Prepare sockets #----------------------------------------------------------------------------- my $want_read = IO::Select->new(); my $want_write = IO::Select->new(); # Set up the master socket. This is where we receive render requests using # the Tirex protocol. # This is a UNIX domain datagram socket. my $master_socket_name = Tirex::Config::get('socket_dir', $Tirex::SOCKET_DIR) . '/master.sock'; unlink($master_socket_name); # remove if left over from last run my $master_socket = IO::Socket::UNIX->new( Type => SOCK_DGRAM, Local => $master_socket_name, ) or die("Cannot open master UNIX domain socket: $!\n"); chmod(0777, $master_socket_name); # make sure everybody can write to socket $want_read->add([$master_socket, undef]); syslog('info', 'Listening for commands on socket %s', $master_socket_name); # Set up the mod_tile socket. This is where mod_tile requests come in. # This is a stream socket. my $modtile_socket_name = Tirex::Config::get('modtile_socket_name', $Tirex::MODTILE_SOCK); my $modtile_listen_count = int(Tirex::Config::get('modtile_listen_count', 10)); $modtile_listen_count = 10 if($modtile_listen_count < 1 || $modtile_listen_count > 128); unlink($modtile_socket_name); # ignore return code, opening the socket will fail if something was wrong here my $modtile_socket = IO::Socket::UNIX->new( Type => SOCK_STREAM, Listen => $modtile_listen_count, Local => $modtile_socket_name, ) or die("Cannot open modtile socket: $!\n"); $modtile_socket->blocking(0); chmod($Tirex::MODTILE_PERM, $modtile_socket_name) or die("can't chmod socket '$modtile_socket_name': $!"); $want_read->add([$modtile_socket, undef]); syslog('info', 'Listening for mod_tile connections on %s (UNIX)', $modtile_socket_name); my $to_syncd; if (Tirex::Config::get('sync_to_host')) { my $syncd_udp_port = Tirex::Config::get('syncd_udp_port', $Tirex::SYNCD_UDP_PORT, qr{^[1-9][0-9]{1,4}$}); $to_syncd = Socket::pack_sockaddr_in( $syncd_udp_port, Socket::inet_aton('localhost') ); } #----------------------------------------------------------------------------- # Daemonize unless in debug mode or foreground #----------------------------------------------------------------------------- if (!$Tirex::FOREGROUND) { chdir('/') or die("Cannot chdir to /: $!"); open(STDIN, '<', '/dev/null') or die("Cannot read from /dev/null: $!"); open(STDOUT, '>', '/dev/null') or die("Cannot write to /dev/null: $!"); defined(my $pid = fork) or die("Cannot fork: $!"); exit(0) if ($pid); setsid() or die("Cannot start a new session: $!"); open(STDERR, '>&STDOUT') or die("Cannot dup stdout: $!"); } #----------------------------------------------------------------------------- # Write pid to pidfile #----------------------------------------------------------------------------- my $pidfile = Tirex::Config::get('master_pidfile', $Tirex::MASTER_PIDFILE); if (open(my $pidfh, '>', $pidfile)) { print $pidfh "$$\n"; close($pidfh); } else { syslog('err', "Can't open pidfile '$pidfile' for writing: $!\n"); # keep going, we'd rather go on without a pidfile than stopping the show } #----------------------------------------------------------------------------- # Initialize status, queue, and rendering manager #----------------------------------------------------------------------------- my $status = Tirex::Status->new(master => 1); my $queue = Tirex::Queue->new(); my $rendering_manager = Tirex::Manager->new( queue => $queue ); foreach my $bucket_config (@{Tirex::Config::get('bucket')}) { $rendering_manager->add_bucket(%$bucket_config); } # Set up the backend return socket. This is where the rendering backend notifies # us when a request was completed. # This is a datagram socket. my $backend_return_socket = $rendering_manager->get_socket(); $want_read->add([$backend_return_socket, undef]); syslog('info', 'Listening for backend responses'); #----------------------------------------------------------------------------- # Set signal handler #----------------------------------------------------------------------------- $SIG{'TERM'} = \&sigterm_handler; $SIG{'INT'} = \&sigint_handler; $SIG{'HUP'} = \&sighup_handler; # run main loop in eval() since Perl socket operations are notorious for # calling croak() when unhappy; if they do, we want to know what happened. my $started = time(); eval { main_loop() }; my $err = $@; if ($err) { syslog('crit', $err); die($err); } #----------------------------------------------------------------------------- # The big loop #----------------------------------------------------------------------------- sub main_loop { my $select_timeout = 1; my $accept_timeout = 10; my $read_timeout = 10; my $write_timeout = 10; my $last_status_update = 0; while (1) { my $now = time(); # keep status in shared memory updated once per second if ($last_status_update < $now) { $status->update(started => $started, queue => $queue->status(), rm => $rendering_manager->status(), renderers => Tirex::Renderer->status(), maps => Tirex::Map->status()); $last_status_update = $now; } # clean out closed handles foreach my $handle ($want_read->handles()) { my ($socket, $source) = @$handle; my $fcn = $socket->fcntl(F_GETFD, 0); if (!defined($fcn)) { syslog('warning', "dropping closed fh %d from want_read (internal state: %s)", $socket->fileno(), $socket->opened() ? "opened" : "closed"); $want_read->remove($handle); } } foreach my $handle ($want_write->handles()) { my ($socket, $source) = @$handle; my $fcn = $socket->fcntl(F_GETFD, 0); if (!defined($fcn)) { syslog('warning', "dropping closed fh %d from want_write (internal state: %s)", $socket->fileno(), $socket->opened() ? "opened" : "closed"); $want_write->remove($handle); } } my ($readable, $writable, $dummy) = IO::Select::select($want_read, $want_write, undef, $select_timeout); # first process all readable handles. # each handle is an array reference, the first element of which is # the socket, and we use the second element to point to a Source object # where appropriate. foreach my $handle (@$readable) { my ($sock, $source) = @$handle; if ($sock == $master_socket) { my $source = Tirex::Source::Command->new( socket => $master_socket ); $source->readable($sock); syslog('debug', "got msg: ". join(' ', map { "$_=$source->{$_}" } sort(keys %$source))) if ($Tirex::DEBUG); my $msg_type = $source->get_msg_type(); if ($msg_type eq 'metatile_enqueue_request') { my $job = $source->make_job(); if ($job) { if (my $already_rendering_job = $rendering_manager->requests_by_metatile($job)) { $already_rendering_job->add_notify($source) if (defined $source->{id}); } else { $queue->add($job); } } } elsif ($msg_type eq 'metatile_remove_request') { my $job = $source->make_job(); $queue->remove($job); } elsif ($msg_type eq 'ping') { syslog('info', 'got ping request'); $source->reply({ type => $msg_type, result => 'ok' }); } elsif ($msg_type eq 'reset_max_queue_size') { syslog('info', 'got reset_max_queue_size message'); $queue->reset_maxsize(); $source->reply({ type => $msg_type, result => 'ok' }); } elsif ($msg_type eq 'quit') { syslog('info', 'got quit message, shutting down now'); $source->reply({ type => 'quit', result => 'ok' }); cleanup(); exit(0); } elsif ($msg_type eq 'debug') { syslog('info', 'got debug message, activating debug mode'); $source->reply({ type => 'debug', result => 'ok' }); $Tirex::DEBUG=1; } elsif ($msg_type eq 'nodebug') { syslog('info', 'got nodebug message, deactivating debug mode'); $source->reply({ type => 'nodebug', result => 'ok' }); $Tirex::DEBUG=0; } elsif ($msg_type eq 'stop_rendering_bucket') { syslog('info', 'got stop_rendering_bucket message (bucket=%s)', $source->{'bucket'} || ''); my $res = defined($source->{'bucket'}) ? $rendering_manager->set_active_flag_on_bucket(0, $source->{'bucket'}) : undef; $source->reply({ type => $msg_type, result => $res ? 'ok' : 'error_bucket_not_found' }); } elsif ($msg_type eq 'continue_rendering_bucket') { syslog('info', 'got continue_rendering_bucket message (bucket=%s)', $source->{'bucket'} || ''); my $res = defined($source->{'bucket'}) ? $rendering_manager->set_active_flag_on_bucket(1, $source->{'bucket'}) : undef; $source->reply({ type => $msg_type, result => $res ? 'ok' : 'error_bucket_not_found' }); } elsif ($msg_type eq 'reload_config') { syslog('info', 'got reload_config message'); read_renderer_and_map_config(); $queue->remove_jobs_for_unknown_maps(); $source->reply({ type => 'reload_config', result => 'ok' }); } elsif ($msg_type eq 'shutdown') { syslog('info', 'got shutdown message, shutting down cleanly'); $source->reply({ type => 'shutdown', result => 'error_not_implemented' }); #XXX } else { syslog('warning', "ignoring unknown message type '$msg_type' from command socket" ); $source->reply({ type => $msg_type, result => 'error_unknown_message_type' }); } } elsif ($sock == $backend_return_socket) { my $buf; my $peer = $sock->recv($buf, $Tirex::MAX_PACKET_SIZE); my $msg = Tirex::parse_msg($buf); my $job = $rendering_manager->done($msg); if ($job) { log_job($job); $job->notify(); $sock->send($buf, undef, $to_syncd) if ($to_syncd); } } elsif ($sock == $modtile_socket) { my $count = 0; # readability on a stream socket means we can accept, but not # necessarily read. while(my $newsock = $sock->accept()) { syslog('debug', 'connection from mod_tile accepted') if ($Tirex::DEBUG); $newsock->blocking(0); my $source = Tirex::Source::ModTile->new($newsock); $want_read->add([$newsock, $source]); $source->set_timeout($now + $accept_timeout); ++$count; } if (!$count) { syslog('err', "could not accept() from mod_tile: $!"); } } else { # must be one of the mod_tile sockets that we accepted then; # notify the source that it may read something. my $status = $source->readable($sock); $source->set_timeout($now + $read_timeout); if ($status == Tirex::Source::STATUS_MESSAGE_COMPLETE) { # source returns true, this indicates it doesn't want to # read more and can prepare a job # $want_read->remove([$sock]); -- leave in select so we get notified when other side closes $source->set_timeout($now + 86400); my $job = $source->make_job($sock); if (!defined($job)) { $want_read->remove([$sock]); $sock->close(); next; } my $already_rendering_job = $rendering_manager->requests_by_metatile($job); if ($job->has_notify()) { # give source a chance to re-insert itself into our write # queue later. slightly inelegant, should rather hand over # reference to self but we're not an object $source->set_request_write_callback( sub { if ($sock->opened) { $want_read->remove([$sock, $source]); $want_write->add([$sock, $source]); $source->set_timeout(time() + $write_timeout); } }); # the following serves the sole purpose of keeping Perl from # garbage-collecting our socket... # fixme: respect timeout if specified in request $already_rendering_job->add_notify($source) if defined($already_rendering_job); } else { # drop the connection if notification has not been requested $want_read->remove([$sock]); $sock->close(); } unless (defined($already_rendering_job)) { $queue->add($job); } } elsif ($status == Tirex::Source::STATUS_SOCKET_CLOSED) { syslog('debug', 'other side closed mod_tile socket %d', $sock->fileno) if ($Tirex::DEBUG); $want_read->remove([$sock]); $sock->close(); } } } # now handle writes. currently the UDP based writing is done elsewhere, but # the Unix domain socket writing needs to be part of the select loop. foreach my $handle (@$writable) { my ($sock, $source) = @$handle; my $status= $source->writable($sock); if ($status == Tirex::Source::STATUS_MESSAGE_COMPLETE) { # source is done writing, and the socket goes back to want-read # mode syslog('debug', 'sent answer on mod_tile socket %d', $sock->fileno) if ($Tirex::DEBUG); $want_write->remove([$sock]); $want_read->add([$sock, $source]); $source->set_timeout($now + $read_timeout); } elsif ($status == Tirex::Source::STATUS_SOCKET_CLOSED) { $want_write->remove([$sock]); $sock->close(); } else { # keep on writing $source->set_timeout($now + $write_timeout); } } foreach my $handle ($want_write->handles) { my ($socket, $source) = @$handle; next unless defined($source); # udp sockets don't have source if ($source->get_timeout() < $now && $source->get_timeout() > 0) { # timeout on writing the result. close. syslog('warning', 'timeout writing to socket %d; discarding response', $socket->fileno); $want_write->remove([$socket]); $want_read->remove([$socket]); $socket->close(); $source->set_timeout(0); # avoid tight loop in case of problems } } foreach my $handle ($want_read->handles) { my ($socket, $source) = @$handle; next unless defined($source); # udp sockets don't have source if ($source->get_timeout() < $now && $source->get_timeout() > 0) { # timeout on reading a request. close. syslog('warning', 'timeout reading from socket %d; closing connection', $socket->fileno); $want_read->remove([$socket]); $socket->close(); $source->set_timeout(0); # avoid tight loop in case of problems } } $rendering_manager->schedule(); # processes anything added } } #----------------------------------------------------------------------------- sub log_job { my $job = shift; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); my $logfile = Tirex::Config::get('master_logfile', $Tirex::MASTER_LOGFILE); if (open(my $logfh, '>>', $logfile)) { printf $logfh "%04d-%02d-%02dT%02d:%02d:%02d id=%s map=%s x=%d y=%d z=%d prio=%d request_time=%s expire=%s sources=%s render_time=%d success=%s\n", 1900+$year, 1+$mon, $mday, $hour, $min, $sec, $job->get_id(), $job->get_map(), $job->get_x(), $job->get_y(), $job->get_z(), $job->get_prio(), $job->{'request_time'}, defined $job->{'expire'} ? $job->{'expire'} : '', $job->sources_as_string(), $job->{'render_time'}, $job->get_success(); close($logfh); } else { syslog('err', "Can't write to logfile '$logfile': $!"); } } #----------------------------------------------------------------------------- # Read renderer and map config #----------------------------------------------------------------------------- sub read_renderer_and_map_config { Tirex::Map->clear(); Tirex::Renderer->clear(); Tirex::Renderer->read_config_dir($config_dir); foreach my $renderer (Tirex::Renderer->all()) { syslog('info', $renderer->to_s()); foreach my $map ($renderer->get_maps()) { syslog('info', ' %s', $map->to_s()); } } } #----------------------------------------------------------------------------- # clean up sockets, files, shm sub cleanup { defined($rendering_manager) && $rendering_manager->log_stats(); unlink($modtile_socket_name); unlink($master_socket_name); unlink($pidfile); $status->destroy(); syslog('info', 'tirex-master shutting down'); } sub sigint_handler { syslog('info', 'SIGINT (CTRL-C) received'); cleanup(); exit(0); } sub sigterm_handler { syslog('info', 'SIGTERM received'); cleanup(); exit(0); } sub sighup_handler { syslog('info', 'SIGHUP received'); # XXX ignore for the time being $SIG{HUP} = \&sighup_handler; } __END__ =head1 NAME tirex-master - tirex master daemon =head1 SYNOPSIS tirex-master [OPTIONS] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-d>, B<--debug> Run in debug mode. You'll see the actual messages sent and received and other debug messages. =item B<-f>, B<--foreground> Run in foreground. E.g. when started from systemd service =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =back =head1 DESCRIPTION The tirex master process gets requests for metatiles from mod_tile and other sources, queues them and sends them on to the rendering backends. It has a sophisticated queueing and rendering manager that decides what is to be rendered when. Unless the --debug option is given, tirex-master will detach from the terminal. =head1 FILES =over 8 =item F The configuration file. =item F The renderer configuration files. =item F The map configuration files. =item F Default location for jobs logfile. It contains one line for each metatile rendered. =back =head1 SEE ALSO See the tirex-send manpage for the list of commands you can send to a running tirex-master. L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ------------------------------------------------------------------ tirex-0.7.0/bin/tirex-rendering-control000077500000000000000000000175571412262531100201470ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-rendering-control # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Errno; use Getopt::Long qw( :config gnu_getopt ); use IO::Socket; use JSON; use Pod::Usage qw(); use Socket; use Tirex; use Tirex::Status; #----------------------------------------------------------------------------- # Reading command line and config #----------------------------------------------------------------------------- my %opts = (); GetOptions( \%opts, 'help|h', 'debug|d', 'config|c=s', 'stop', 'continue' ) or exit(4); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-rendering-control - send messages to control rendering\n", -exitval => 0 ); } if (!$opts{'stop'} && !$opts{'continue'}) { print STDERR "You need to have the --stop or --continue option!\n"; exit(4); } $Tirex::DEBUG = 1 if ($opts{'debug'}); my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- my $status = eval { Tirex::Status->new() }; if ($@) { print STDERR "Cannot open shared memory. Is the tirex-master running?\n"; exit(3); } my $s = eval { JSON::from_json($status->read()); }; if ($@) { print STDERR "Cannot read shared memory. Is the tirex-master running?\n"; exit(3); } my $bucketlist = $s->{'rm'}->{'buckets'}; my @cmd_buckets = @ARGV; my @all_buckets = map { $_->{'name'} } @$bucketlist; my @livebuckets = grep { $_->{'minprio'} == 1 } @$bucketlist; my $livebucket = $livebuckets[0]->{'name'}; my %all_buckets = map { $_ => 1 } @all_buckets; my @want_buckets; if (scalar(@cmd_buckets) == 1 && $cmd_buckets[0] eq 'ALL') { @want_buckets = @all_buckets; } elsif (scalar(@cmd_buckets) == 0 && $opts{'stop'}) { @want_buckets = grep { $_ ne $livebucket } @all_buckets; } elsif (scalar(@cmd_buckets) == 0 && $opts{'continue'}) { @want_buckets = @all_buckets; } else { foreach my $b (@cmd_buckets) { if (! $all_buckets{$b}) { print STDERR "Unknown bucket: $b\n"; exit(4); } } @want_buckets = @cmd_buckets; } printf STDERR "Buckets cmdline: (%s), all: (%s), live: (%s), want: (%s)\n", join(', ', @cmd_buckets), join(', ', @all_buckets), $livebucket, join(', ', @want_buckets) if ($Tirex::DEBUG); #----------------------------------------------------------------------------- my $master_socket_name = Tirex::Config::get('socket_dir', $Tirex::SOCKET_DIR) . '/master.sock'; my $socket = IO::Socket::UNIX->new( Type => SOCK_DGRAM, Local => '', Peer => $master_socket_name, ) or die("Cannot open connection to master: $!\n"); #----------------------------------------------------------------------------- foreach my $bucket (@want_buckets) { send_command($socket, $opts{'stop'} ? 'stop' : 'continue', $bucket); } if ($opts{'stop'}) { while (busy($status, $bucketlist, @want_buckets)) { sleep(1); } } exit 0; #----------------------------------------------------------------------------- sub busy { my $status = shift; my $bucketlist = shift; my %buckets = map { $_ => 1 } @_; my $s = eval { JSON::from_json($status->read()); }; if ($@) { print STDERR "Cannot read shared memory. Is the tirex-master running?\n"; exit(3); } my @rendering_prios = map { $_->{'prio'} } @{$s->{'rm'}->{'rendering'}}; foreach my $prio (@rendering_prios) { foreach my $bucket (@$bucketlist) { if ($bucket->{'minprio'} <= $prio && ($prio >= $bucket->{'maxprio'} || $bucket->{'maxprio'} == 0)) { if ($buckets{$bucket->{'name'}}) { print STDERR "still rendering: prio=$prio bucket=$bucket->{'name'}\n" if ($Tirex::DEBUG); return 1; } } } } return 0; } sub send_command { my ($socket, $command, $bucket) = @_; my $request = Tirex::Message->new( type => "${command}_rendering_bucket", id => "tirex-rendering-control.$$", bucket => $bucket, ); print STDERR "sending: ", $request->to_s(), "\n" if ($Tirex::DEBUG); $request->send($socket) or die("Cannot send message: $!"); my $reply = Tirex::Message->new_from_socket($socket); if (! defined $reply) { if ($!{'ECONNREFUSED'}) { print STDERR "Could not send request to server. Is it running?\n"; } else { print STDERR "Error reading answer: $!\n"; } exit(3); } print STDERR "got answer: ", $reply->to_s(), "\n" if ($Tirex::DEBUG); } __END__ =head1 NAME tirex-rendering-control - send messages to control rendering =head1 SYNOPSIS tirex-rendering-control [OPTIONS] [BUCKET...] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-d>, B<--debug> Run in debug mode. =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =item B<--stop> Send stop_rendering_bucket commands to tirex-master for each bucket given on the command line. If no bucket was specified, it is send for all buckets except the one containing the priority 1, which is usually the one getting live requests from the web server. You can specify the special bucket name 'ALL' to send the command for all buckets. =item B<--continue> Send continue_rendering_bucket commands to tirex-master for each bucket given on the command line. If no bucket was specified (or "ALL"), it is send for all buckets. =back =head1 DESCRIPTION Sometimes you want to stop rendering tiles temporarily for some reason. For instance because you want to update the database, re-start the rendering daemon or do some resource-intensive job on the machine that would conflict with rendering. This command sends the running tirex-master commands to stop (or continue) rendering for the specified buckets. When using --stop, this command will block and wait for currently rendering tiles to be finished. So when the command returns (with return code 0) you can be sure that you can do whatever you want to do without the rendering interfering. =head1 FILES =over 8 =item F The configuration file. =back =head1 DIAGNOSTICS tirex-rendering-control returns =over 8 =item 0 if everything was alright, =item 3 if the server could not be reached, =item 4 if there was an error parsing the command line. =back =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ---------------------------------------------------------------------------- tirex-0.7.0/bin/tirex-send000077500000000000000000000166461412262531100154430ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-send # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Errno; use Getopt::Long qw( :config gnu_getopt ); use IO::Socket; use Pod::Usage qw(); use Socket; use Tirex; #----------------------------------------------------------------------------- # Reading command line and config #----------------------------------------------------------------------------- my %opts = (); GetOptions( \%opts, 'help|h', 'debug|d', 'config|c=s', 'to|t=s', 'wait|w=i' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-send - send messages to a tirex daemon\n", -exitval => 0 ); } $Tirex::DEBUG = 1 if ($opts{'debug'}); my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- $opts{'to'} ||= 'master'; my $socket; if ($opts{'to'} eq 'master') { my $master_socket_name = Tirex::Config::get('socket_dir', $Tirex::SOCKET_DIR) . '/master.sock'; $socket = IO::Socket::UNIX->new( Type => SOCK_DGRAM, Local => '', Peer => $master_socket_name, ) or die("Cannot open connection to master: $!\n"); } elsif ($opts{'to'} eq 'syncd') { $socket = IO::Socket::INET->new( LocalAddr => 'localhost', Proto => 'udp' ); $socket->connect( Socket::pack_sockaddr_in($Tirex::SYNCD_UDP_PORT, Socket::inet_aton('localhost')) ); } elsif ($opts{'to'} =~ /^[0-9]+$/) { $socket = IO::Socket::INET->new( LocalAddr => 'localhost', Proto => 'udp' ); $socket->connect( Socket::pack_sockaddr_in($opts{'to'}, Socket::inet_aton('localhost')) ); } else { print STDERR "Unknown destination for --to option: $opts{'to'}\n"; exit(4); } #----------------------------------------------------------------------------- my $DEFAULT_TIMEOUT = 5; my $timeout = defined $opts{'wait'} ? $opts{'wait'} : ($opts{'to'} eq 'syncd' ? 0 : $DEFAULT_TIMEOUT); #----------------------------------------------------------------------------- my $type = shift; if (! $type) { print STDERR "missing message type\n"; exit(4); } my %msg = ( type => $type ); # don't add id if we are not waiting for response $msg{'id'} = "tirex-send.$$" unless ($timeout == 0); foreach my $field (@ARGV) { my ($k, $v) = split(/=/, $field, 2); $msg{$k} = $v; } #----------------------------------------------------------------------------- my $request = Tirex::Message->new(%msg); print STDERR "sending: ", $request->to_s(), "\n" if ($Tirex::DEBUG); $request->send($socket); exit(0) if ($timeout == 0); local $SIG{ALRM} = sub { print STDERR "timeout waiting for answer\n"; exit(3) }; alarm($timeout); my $reply = Tirex::Message->new_from_socket($socket); if (! defined $reply) { if ($!{'ECONNREFUSED'}) { print STDERR "Could not send request to server. Is it running?\n"; } else { print STDERR "Error reading answer: $!\n"; } exit(3); } print STDERR "got answer: ", $reply->to_s(), "\n" if ($Tirex::DEBUG); if ($reply->unknown_message_type()) { print STDERR "unknown message type: $type\n"; exit(2); } exit($reply->ok() ? 0 : 1); __END__ =head1 NAME tirex-send - send messages to a tirex daemon =head1 SYNOPSIS tirex-send [OPTIONS] TYPE [KEY=VALUE ...] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-d>, B<--debug> Run in debug mode. You'll see the actual messages sent and received. =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =item B<-t>, B<--to=DESTINATION> Talk to DESTINATION, which can be 'master' (default), 'syncd', or a port number. =item B<-w>, B<--wait=SECONDS> Wait SECONDS for the answer after sending the message. Default is 5 seconds. If set to 0, don't wait for an answer at all. This is the default when the destination is set to syncd. =back =head1 DESCRIPTION tirex-send sends a message through a UDP socket to a tirex daemon running on the local machine and waits for the answer. It can be used to check whether a daemon is running, activate debugging in the daemon, send a render request, etc. Messages are normally sent to the tirex-master daemon, but can also be sent to a sync daemon or one of the rendering backends. You have to give the TYPE of the message on the command line and optionally some fields in the format KEY=VALUE. =head1 MESSAGES FOR MASTER The following messages can be send to a master daemon: =over 8 =item ping Check whether server is alive. =item debug Activate debugging in the daemon. =item nodebug Deactivate debugging in the daemon. =item quit Quit server now (unclean!). =item shutdown Clean shutdown (no yet implemented XXX) =item reload_config Reload renderer and map config. =item reset_max_queue_size Reset max queue size indicator in shared memory. =item stop_rendering_bucket Stop feeding jobs from a bucket to the renderer. Currently rendering jobs are unaffected. Give the name of the bucket as "bucket=NAME". =item continue_rendering_bucket Continue rendering jobs from this bucket. Give the name of the bucket as "bucket=NAME". =item metatile_enqueue_request Add metatile to job queue. =item metatile_remove_request Remove job from queue. =back =head1 MESSAGES FOR SYNCD The following messages can be send to a syncd server: =over 8 =item metatile_render_request Syncs the metatile file. =back =head1 MESSAGES TO BACKENDS The following messages can be send to a rendering backend: =over 8 =item metatile_render_request Renders the metatile requested. =back =head1 FILES =over 8 =item F The configuration file. =back =head1 DIAGNOSTICS tirex-send returns =over 8 =item 0 if the message was answered with 'ok', =item 1 if the message was answered with an error, =item 2 if the error was 'unknown_message_type', =item 3 if the message could not be sent or if there was a timeout waiting for the answer, =item 4 if there was an error parsing the command line. =back =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ---------------------------------------------------------------------------- tirex-0.7.0/bin/tirex-status000077500000000000000000000317241412262531100160270ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-status # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Getopt::Long qw( :config gnu_getopt ); use JSON; use List::Util qw(); use Pod::Usage qw(); use Term::ANSIColor qw(:constants); use Tirex; use Tirex::Status; #----------------------------------------------------------------------------- # Reading command line #----------------------------------------------------------------------------- my %opts = ( raw => 0, once => 0 ); GetOptions( \%opts, 'help|h', 'once|o', 'raw|r', 'extended|e', 'times|t' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-status - show status of tirex master\n", -exitval => 0 ); } $opts{'once'} = 1 if ($opts{'raw'}); $opts{'once'} = 1 if ($opts{'times'}); #----------------------------------------------------------------------------- my $status = eval { Tirex::Status->new(); }; die("Can't connect to shared memory. Is the tirex-master running?\n") if ($@); my $clear = $opts{'once'} ? '' : ''; #----------------------------------------------------------------------------- while (1) { my $s = $status->read(); if (defined $s) { $s = format_status(JSON::from_json($s), $opts{'times'}) unless ($opts{'raw'}); print $clear, $s; } else { die("Can't read from shared memory. Did the tirex-master die?\n"); } last if ($opts{'once'}); sleep(1); } exit(0); #----------------------------------------------------------------------------- sub prettydate { my $time = shift; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec); } sub field { my $format = shift; my $var = shift; return sprintf(BOLD . $format . RESET, $var); } sub duration { my ($seconds, $milli) = @_; my $m = ""; if($milli && $seconds) { $m = sprintf(".%03d", $seconds%1000) if $seconds < 60000; $seconds = int($seconds/1000); } if($seconds >= 86400) { my $d = int($seconds/86400); $seconds -= $d*86400; my $h = int($seconds/3600); $seconds -= $h*3600; my $minutes = int($seconds/60); return sprintf("%dd+%d:%02d:%02d%s", $d, $h, $minutes, $seconds-$minutes*60, $m); } elsif($seconds >= 3600) { my $h = int($seconds/3600); $seconds -= $h*3600; my $minutes = int($seconds/60); return sprintf("%d:%02d:%02d%s", $h, $minutes, $seconds-$minutes*60, $m); } elsif($seconds >= 60) { my $minutes = int($seconds/60); return sprintf("%d:%02d%s", $minutes, $seconds-$minutes*60, $m); } return "$seconds$m"; } sub max { my ($a, $b) = @_; return ($a > $b) ? $a : $b; } sub format_status { my $d = shift; my $times = shift; my $r = ""; if (defined($times)) { my @styles = keys %{$d->{'rm'}->{'stats'}->{'sum_render_time'}}; my $maxzoom = 0; map { $maxzoom = max($maxzoom, scalar(@{$d->{'rm'}->{'stats'}->{'sum_render_time'}->{$_}})); } @styles; $r = "style zoom |"; for (my $i = 0; $i < $maxzoom; $i++) { $r .= sprintf("%7d ", $i); } $r .= "\n"; $r .= "---------------+"; $r .= "----------" x $maxzoom; $r .= "\n"; foreach (@styles) { $r .= sprintf("%-15s|", $_); for (my $i = 0; $i < $maxzoom; $i++) { my $time = $d->{'rm'}->{'stats'}->{'sum_render_time'}->{$_}->[$i]; my $count = $d->{'rm'}->{'stats'}->{'count_rendered'}->{$_}->[$i]; if (defined($time) && defined($count)) { $r .= sprintf "%7.1fs", $time / $count / 1000 + 0.05; } else { $r .= " " x 8; } } $r .= "\n"; } return $r; } return "\n " . BOLD . UNDERLINE . "Tirex Master Status" . RESET . ' (updated=' . ($d->{'updated'} < time()-2 ? RED : '') . BOLD . prettydate($d->{'updated'}) . RESET . ")\n\n" . format_master_server($d) . format_stats($d->{'rm'}->{'stats'}) . format_queue($d->{'queue'}) . format_buckets($d->{'rm'}, $d->{'queue'}) . format_rendering($d->{'rm'}) . ($opts{'extended'} ? format_renderers($d->{'renderers'}) . format_maps($d->{'maps'}) : ''); } sub format_master_server { my $data = shift; my $text = " Master server:\n started=" . BOLD . prettydate($data->{'started'}) . RESET . " pid=" . BOLD . $data->{'pid'} . RESET "\n"; return "$text\n"; } sub format_stats { my $stats = shift; my $text = " Statistics:\n"; foreach my $statkey (sort keys %$stats, "mean_render_time") { my $statvalue = $stats->{$statkey}; if ($statkey eq "mean_render_time") { foreach my $map (sort keys %{$stats->{"count_rendered"}}) { $text .= " $statkey" . "[$map]="; my $n = scalar(@{$stats->{"count_rendered"}{$map}}) - 1; $text .= join(', ', map { my $c=$stats->{"count_rendered"}{$map}[$_]; duration($c ? ($stats->{"sum_render_time"}{$map}[$_] / $c) : 0, 1)} (0 .. $n)); $text .= "\n"; } } elsif (ref($statvalue) eq '') { $text .= " $statkey=$statvalue\n"; } elsif (ref($statvalue) eq 'HASH') { foreach my $map (sort keys %$statvalue) { $text .= " $statkey" . "[$map]="; if($statkey =~ /_time$/) { $text .= join(', ', map { duration($_ || 0, 1) } @{$statvalue->{$map}}); } else { $text .= join(', ', map { $_ || 0 } @{$statvalue->{$map}}); } $text .= "\n"; } } else { $text .= "?\n"; } } return "$text\n"; } sub format_queue { my $q = shift; my $text = " Queue:\n " . UNDERLINE . "Prio Size Maxsize Age\n" . RESET; foreach my $pq (@{$q->{'prioqueues'}}) { $text .= ' ' . field('%4d', $pq->{'prio'}) . ' ' . field('%6d', $pq->{'size'}) . ' ' . field('%7d', $pq->{'maxsize'}); $text .= ' ' . field('%15s', duration($pq->{'age_last'}) . '-' . duration($pq->{'age_first'})) if (defined $pq->{'age_last'}); $text .= "\n"; } $text .= ' ' . UNDERLINE . " \n" . RESET; $text .= ' all ' . field('%6d', $q->{'size'}) . ' ' . field('%7d', $q->{'maxsize'}); my $min_age_last = List::Util::min(grep { defined $_ } map { $_->{'age_last' } } @{$q->{'prioqueues'}}); my $max_age_first = List::Util::max(grep { defined $_ } map { $_->{'age_first'} } @{$q->{'prioqueues'}}); $text .= ' ' . field('%15s', duration($min_age_last) . '-' . duration($max_age_first)) if (defined($min_age_last) || defined($max_age_first)); return "$text\n\n"; } sub format_buckets { my $rm = shift; my $q = shift; my $text = " Buckets: (load=" . $rm->{'load'} . ")\n " . UNDERLINE . "Name Priority Rendering MaxRend Maxload Active Can Queued Age\n" . RESET; foreach my $b (@{$rm->{'buckets'}}) { $b->{'queued'} = 0; my @queues_for_this_bucket = grep { $b->{'minprio'} <= $_->{'prio'} && ($b->{'maxprio'} == 0 || $_->{'prio'} <= $b->{'maxprio'}) } @{$q->{'prioqueues'}}; foreach my $pq (@queues_for_this_bucket) { $b->{'queued'} += $pq->{'size'}; } my $min_age_last = List::Util::min(grep { defined $_ } map { $_->{'age_last' } } @queues_for_this_bucket); my $max_age_first = List::Util::max(grep { defined $_ } map { $_->{'age_first'} } @queues_for_this_bucket); $text .= ' ' . field('%-20s', $b->{'name'}) . ' ' . field('%3d', $b->{'minprio'}) . '-' . field('%4s', $b->{'maxprio'} ? $b->{'maxprio'} : '') . ' ' . field('%9d', $b->{'numproc'}) . ' ' . field('%7d', $b->{'maxproc'}) . ' ' . ($rm->{'load'} > $b->{'maxload'} ? RED : '') . field('%7d', $b->{'maxload'}) . RESET . ' ' . field('%3s', $b->{'active'} ? 'yes' : RED . ' no' . RESET) . ' ' . field('%3s', $b->{'can_render'} ? 'yes' : ' no') . ' ' . field('%6d', $b->{'queued'}); $text .= ' ' . field('%15s', duration($min_age_last) . '-' . duration($max_age_first)) if (defined($min_age_last) || defined($max_age_first)); $text .= "\n"; } return "$text\n"; } sub format_rendering { my $rm = shift; my $text = " Currently rendering: (num=" . $rm->{'num_rendering'} . ")\n " . UNDERLINE . "Map X Y Z Prio Age\n" . RESET; foreach my $r (@{$rm->{'rendering'}}) { $text .= ' ' . field("%-20s", $r->{'map'}) . ' ' . field("%10d", $r->{'x'}) . ' ' . field("%10d", $r->{'y'}) . ' ' . field("%2d", $r->{'z'}) . ' ' . field("%4d", $r->{'prio'}) . ' ' . field("%5d", $r->{'age'}) . "\n"; } return "$text\n"; } sub format_renderers { my $renderers = shift; my $text = " Renderers:\n " . UNDERLINE . "Name Port Procs Path \n" . RESET; foreach my $r (@$renderers) { $text .= ' ' . field("%-10s", $r->{'name'}) . ' ' . field("%5d", $r->{'port'}) . ' ' . field(" %2d", $r->{'procs'}) . ' ' . field(" %-30s", $r->{'path'}) . ' ' . "\n"; } return "$text\n"; } sub format_maps { my $maps = shift; my $text = " Maps:\n " . UNDERLINE . "Name Renderer Zoom Tiledir \n" . RESET; foreach my $r (@$maps) { $text .= ' ' . field("%-20s", $r->{'name'}) . ' ' . field("%-10s", $r->{'renderer'}) . ' ' . field("%2d", $r->{'minz'}) . field("-%2d", $r->{'maxz'}) . ' ' . field(" %-30s", $r->{'tiledir'}) . ' ' . "\n"; } return "$text\n"; } __END__ =head1 NAME tirex-status - show status of tirex master =head1 SYNOPSIS tirex-status [OPTIONS] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-o>, B<--once> Show status only once, default is to show it once per second. =item B<-t>, B<--times> Show a table of rendering times by style and zoom level. Implies --once. =item B<-n>, B<--numbers> Show a table of rendering counts by style and zoom level. Implies --once. =item B<-r>, B<--raw> Return status in raw JSON format instead of in ANSI coloured human readable form. Implies --once. =item B<-e>, B<--extended> Show renderer and map config, too. =back =head1 DESCRIPTION Reads out the status of the running tirex-master process through shared memory and displays it. The display is formatted for human consumption on a terminal and uses ANSI control codes for colour, unless you specify the --raw option. =head1 DIAGNOSTICS tirex-status returns 1 if there was an error, 0 otherwise. =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ------------------------------------------------------------------ tirex-0.7.0/bin/tirex-syncd000077500000000000000000000131361412262531100156210ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-syncd # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Getopt::Long qw( :config gnu_getopt ); use IO::Socket; use POSIX 'setsid'; use Pod::Usage qw(); use Socket; use Tirex; use Tirex::Map; #----------------------------------------------------------------------------- my %opts = (); GetOptions( \%opts, 'help|h', 'debug|d', 'config|c=s', 'dry-run|n' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-syncd - sync rendered tiles to backup host\n", -exitval => 0 ); } $Tirex::DEBUG = 1 if ($opts{'debug'}); my $dryrun = $opts{'dry-run'}; my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); Tirex::Renderer->read_config_dir($config_dir); my $syncd_udp_port = Tirex::Config::get('syncd_udp_port', $Tirex::SYNCD_UDP_PORT, qr{^[1-9][0-9]{1,4}$}); my $delay = Tirex::Config::get('syncd_aggregate_delay', $Tirex::SYNCD_AGGREGATE_DELAY, qr{^\d+$}); my $syncd_command = Tirex::Config::get('syncd_command', $Tirex::SYNCD_COMMAND); my $socket = IO::Socket::INET->new( LocalAddr => 'localhost', LocalPort => $syncd_udp_port, Proto => 'udp', ReuseAddr => 1, Blocking => 0 ) or die("Can't open UDP socket: :$!\n"); my $host = Tirex::Config::get('sync_to_host'); if (! defined($host) || $host eq '') { die("Please set 'sync_to_host' config option in $config_file\n"); } my @hosts = split(/[ ,]/, $host); #----------------------------------------------------------------------------- # daemonize and create pidfile #----------------------------------------------------------------------------- if (!$Tirex::DEBUG) { chdir('/') or die("Cannot chdir to /: $!"); open(STDIN, '<', '/dev/null') or die("Cannot read /dev/null: $!"); open(STDOUT, '>', '/dev/null') or die("Cannot write to /dev/null: $!"); defined(my $pid = fork) or die("Cannot fork: $!"); exit(0) if $pid; setsid() or die("Cannot start a new session: $!"); open(STDERR, '>&STDOUT') or die("Cannot dup stdout: $!"); } my $pidfile = Tirex::Config::get('syncd_pidfile', $Tirex::SYNCD_PIDFILE); open(my $pidfh, '>', $pidfile) or syslog('err', "Can't open pidfile '$pidfile' for writing: $!\n"); print $pidfh "$$\n"; close($pidfh); while (1) { my $tiles = []; while (1) { my $msg = eval { Tirex::Message->new_from_socket($socket); }; last unless $msg; syslog('debug', 'got request: %s', $msg->to_s()) if ($Tirex::DEBUG); my $metatile = $msg->to_metatile(); my $filename = Tirex::Map->get_map_for_metatile($metatile)->get_tiledir() . '/' . $metatile->get_filename(); $filename = $1 if ($filename =~ m!^/(.*)!); push(@$tiles, $filename); } if (!scalar(@$tiles)) { syslog('debug', 'sleeping') if ($Tirex::DEBUG); sleep $delay; syslog('debug', 'waking up') if ($Tirex::DEBUG); next; } syslog('debug', 'acting on %d tiles', scalar(@$tiles)) if ($Tirex::DEBUG); my $names = join(" ", @$tiles); # fixme these could be run in parallel foreach my $host(@hosts) { my $command = $syncd_command; $command =~ s/%HOST%/$host/g; $command =~ s/%FILES%/$names/g; syslog('debug', "executing $command") if ($Tirex::DEBUG); system "$command" unless($dryrun); sleep 2; syslog('debug', "command done") if ($Tirex::DEBUG); } } __END__ =head1 NAME tirex-syncd - sync rendered tiles to backup host =head1 SYNOPSIS tirex-syncd [OPTIONS] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-d>, B<--debug> Run in debug mode. =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =item B<-n>, B<--dry-run> Do not actually copy anything. =back =head1 DESCRIPTION If you have several tile servers that you want to keep in sync (for instance as backup when one fails), you can use the tirex-syncd. The syncd will be notified by the master when a tile has been rendered and copy it to another server. =head1 FILES =over 8 =item F The configuration file. =back =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ------------------------------------------------------------------ tirex-0.7.0/bin/tirex-tiledir-check000077500000000000000000000223161412262531100172100ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-tiledir-check # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use File::Find; use File::stat; use Getopt::Long qw( :config gnu_getopt ); use IO::File; use IO::Handle; use IO::Seekable; use JSON; use List::Util qw(); use Pod::Usage qw(); use Tirex; use Tirex::Renderer; use Tirex::Map; #----------------------------------------------------------------------------- my $EMPTY_TILE_SIZE = 7124; my %opts = ( list => '', stats => '' ); GetOptions( \%opts, 'help|h', 'config|c=s', 'list|l=s', 'stats|s=s', 'minz|z=i', 'maxz|Z=i' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-tiledir-check - check tile dir\n", -exitval => 0 ); } die("--list and --stats can't both be -\n") if ($opts{'list'} eq '-' && $opts{'stats'} eq '-'); my $fh_list; my $fh_stats; if ($opts{'list'} eq '-') { $fh_list = IO::Handle->new(); $fh_list->fdopen(fileno(STDOUT), 'w'); } elsif ($opts{'list'}) { $fh_list = IO::File->new($opts{'list'}, 'w') or die("Can't open list file '$opts{'list'}': $!\n"); } if ($opts{'stats'} eq '-') { $fh_stats = IO::Handle->new(); $fh_stats->fdopen(fileno(STDOUT), 'w'); } elsif ($opts{'stats'}) { $fh_stats = IO::File->new($opts{'stats'}, 'a') or die("Can't open stats file '$opts{'stats'}': $!\n"); } die("missing map parameter\n") unless (defined $ARGV[0]); my $mapname = shift; my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); Tirex::Renderer->read_config_dir($config_dir); my $map = Tirex::Map->get($mapname); die("unknown map: $mapname\n") unless (defined $map); #----------------------------------------------------------------------------- my $REGEX_DIR = qr{^(/[0-9]+){0,5}$}; my $REGEX_FILE = qr{^(/[0-9]+){0,5}/[0-9]+\.meta$}; my $REGEX_TMP = qr{^(/[0-9]+){0,5}/[0-9]+\.meta\.[0-9]+\.tmp$}; my $BLOCKSIZE = 512; my $mtdir = $map->get_tiledir(); chop $mtdir if ($mtdir =~ qr{/$}); my %stats; my $exitcode = 0; my $list = ''; my $minz = $opts{'minz'}; $minz = 0 unless defined($minz); my $maxz = $opts{'maxz'}; $maxz = 19 unless defined($maxz); for (my $i=$minz; $i<=$maxz; $i++) { next unless (-d "$mtdir/$i"); find({ wanted => \&process, follow_fast => 1, no_chdir => 1 }, "$mtdir/$i"); if ($fh_list) { $fh_list->print($list); } $list = ''; } if ($fh_stats) { my $json = JSON::to_json(\%stats, { pretty => 1 }); unless ($opts{'stats'} eq '-') { $fh_stats->truncate(0); $fh_stats->seek(0, SEEK_SET); } $fh_stats->print("$json\n"); } exit($exitcode); #----------------------------------------------------------------------------- sub error { my $errmsg = shift; print STDERR "$errmsg\n"; $exitcode = 1; } sub process { (my $path = $_) =~ s/^$mtdir//; # directories if (-d $_) { return if ($path eq ''); if ($path !~ $REGEX_DIR) { $File::Find::prune = 1; error("Unknown directory format: $path"); } return; } my $stat = stat($_); # is a tmp file if ($path =~ $REGEX_TMP) { # older than 1 minute error("Old tmp file: $path") if ($stat->mtime() < time() - 60); return; } # not a metatile file if ($path !~ $REGEX_FILE) { error("Unknown file format: $path"); return; } my $metatile = Tirex::Metatile->new_from_filename_and_map($path, $mapname); if (!$stat) { error("Can't stat file: $path"); return; } my $age = time() - $stat->mtime(); my $size = $stat->size(); my $blocks = $stat->blocks(); $list .= join(',', $age, $size, $blocks, $metatile->to_s()) . "\n" if ($fh_list); return unless ($fh_stats); if (! defined $stats{$metatile->get_map()}->[$metatile->get_z()]) { $stats{$metatile->get_map()}->[$metatile->get_z()] = { 'minage' => 999_999_999, 'maxage' => 0, 'sumage' => 0, 'minsize' => 999_999_999, 'maxsize' => 0, 'sumsize' => 0, 'minblocks' => 999_999_999, 'maxblocks' => 0, 'sumblocks' => 0, 'count' => 0, 'countempty' => 0, }; } my $s = $stats{$metatile->get_map()}->[$metatile->get_z()]; $s->{'minage'} = 0 + List::Util::min($s->{'minage'} , $age); $s->{'maxage'} = 0 + List::Util::max($s->{'maxage'} , $age); $s->{'minsize'} = 0 + List::Util::min($s->{'minsize'} , $size); $s->{'maxsize'} = 0 + List::Util::max($s->{'maxsize'} , $size); $s->{'minblocks'} = 0 + List::Util::min($s->{'minblocks'}, $blocks); $s->{'maxblocks'} = 0 + List::Util::max($s->{'maxblocks'}, $blocks); $s->{'sumage'} += $age; $s->{'sumsize'} += $size; $s->{'sumblocks'} += $blocks; $s->{'count'}++; $s->{'countempty'}++ if ($size == $EMPTY_TILE_SIZE); } __END__ =head1 NAME tirex-tiledir-check - check tile dir =head1 SYNOPSIS tirex-tiledir-check [OPTIONS] MAP =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =item B<-l>, B<--list=FILE> Write list of tiles to FILE. See below for format. =item B<-s>, B<--stats=FILE> Write stats about tiles to FILE. See below for format. =item B<-z>, B<--minz=i> Start processing at min. zoom level i (default=0) =item B<-Z>, B<--maxz=i> Stop processing at max. zoom level i (default=19) =back =head1 DESCRIPTION Walks recursively through the tiles directory for the given MAP and checks for wrong directory or file names or files that can't be accessed. If problems are found, messages are written to STDERR. When the --list and/or --stats options are given, it outputs information about each tile and/or generates statistics, respectively. The list and stats are written to the filenames given, if you give '-' as the filename STDOUT is used. You can only use STDOUT for either the list file or the stats file. Caution: This command will go through all metatiles on your disk and stat each file. On a lightly loaded machine with lots of RAM this can go pretty quick, but under high IO loads it might take a long time. Take this into account if you want to run it regularly from cron or similar. =head1 LIST FILE FORMAT The list file is in CSV format with one line per metatile. The fields are: =over 8 =item age Age of metatile in seconds. =item size Size in bytes of the metatile. =item blocks Number of blocks used for this metatile. =item metatile Description of metatile. Format: map=foo x=8 y=0 z=10 =back This data can be read with other programs to create statistics etc. =head1 STATS FILE FORMAT The stats file is in JSON format. It contains a hash with the names of all maps as keys and a list of zoom levels as their values. For each zoom level there is a nested hash containing the statistics: =over 8 =item count - Number of tiles =item maxage - Maximum age in seconds =item maxblocks - Maximum number of blocks =item maxsize - Maximum file size in bytes =item minage - Minimum age in seconds =item minblocks - Minimum number of blocks =item minsize - Minimum file size in bytes =item sumage - Sum of ages in seconds =item sumblocks - Sum of number of blocks =item sumsize - Sum of file size in bytes =back The sumage, sumblocks, and sumsize values can be divided by count to get the average. This file can be displayed on a human readable format with the Program tirex-tiledir-stat. It is also read by several Munin plugins. =head1 FILES =over 8 =item F The configuration file. =back =head1 DIAGNOSTICS Returns 0 if no errors were found or 1 if there were errors. If there were errors parsing the command line 2 is returned. =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ------------------------------------------------------------------ tirex-0.7.0/bin/tirex-tiledir-stat000077500000000000000000000164601412262531100171110ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # tirex-tiledir-stat # #----------------------------------------------------------------------------- # See end of this file for documentation. #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Data::Dumper; use Getopt::Long qw( :config gnu_getopt ); use JSON; use List::Util qw(); use Pod::Usage qw(); use Tirex; #----------------------------------------------------------------------------- my %opts = (); GetOptions( \%opts, 'help|h', 'config|c=s' ) or exit(2); if ($opts{'help'}) { Pod::Usage::pod2usage( -verbose => 1, -msg => "tirex-tiledir-stat - create statistics about tile dir\n", -exitval => 0 ); } my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- my $BLOCKSIZE = 512; if (scalar(@ARGV) == 0) { $ARGV[0] = Tirex::Config::get('stats_dir', '/var/cache/tirex/stats') . '/tiles.stats'; } my $json = join('', <>); $json =~ s/\n\}\n\n\{/,/g; # join individual files my $stats = JSON::from_json($json); print <<"EOF"; ------------------Tiles------------------- ----------Age---------- ----------Size--------- ---------Blocks-------- Block Map Zoom Count Max Empty Exist% Empty% Min Avg Max Min Avg Max Min Avg Max Eff % -------------------------------------------------------------------------------------------------------------------------------------------------------- EOF my $sum = { count => 0, countempty => 0, maxcount => 0, minage => 999_999_999, minsize => 999_999_999, minblocks => 999_999_999, maxage => 0, maxsize => 0, maxblocks => 0 }; foreach my $map (sort(keys %$stats)) { foreach my $z (0 .. scalar(@{$stats->{$map}})-1) { my $s = $stats->{$map}->[$z]; next unless (defined $s); my $maxcount = $z <= 3 ? 1 : 4 ** $z / 64; #XXX my $exist = sprintf('%.2f', $s->{'count'} / $maxcount * 100); my $empty = sprintf('%.2f', $s->{'countempty'} / $maxcount * 100); $s->{'avgage'} = int($s->{'sumage'} / $s->{'count'}); $s->{'avgsize'} = int($s->{'sumsize'} / $s->{'count'}); $s->{'avgblocks'} = int($s->{'sumblocks'} / $s->{'count'}); printf "%-20s %2d %10d %11d %6d %6s %6s %7s %7s %7s %7d %7d %7d %7d %7d %7d %3d\n", $map, $z, $s->{'count'}, $maxcount, $s->{'countempty'}, $exist, $empty, format_time($s->{'minage'}), format_time($s->{'avgage'}), format_time($s->{'maxage'}), $s->{'minsize'}, $s->{'avgsize'} , $s->{'maxsize'}, $s->{'minblocks'}, $s->{'avgblocks'}, $s->{'maxblocks'}, int($s->{'sumsize'} / ($BLOCKSIZE * $s->{'sumblocks'}) * 100+0.5); $sum->{'count'} += $s->{'count'}; $sum->{'countempty'} += $s->{'countempty'}; $sum->{'maxcount'} += $maxcount; $sum->{'minage'} = List::Util::min($sum->{'minage'} , $s->{'minage'}); $sum->{'minsize'} = List::Util::min($sum->{'minsize'} , $s->{'minsize'}); $sum->{'minblocks'} = List::Util::min($sum->{'minblocks'} , $s->{'minblocks'}); $sum->{'maxage'} = List::Util::max($sum->{'maxage'} , $s->{'maxage'}); $sum->{'maxsize'} = List::Util::max($sum->{'maxsize'} , $s->{'maxsize'}); $sum->{'maxblocks'} = List::Util::max($sum->{'maxblocks'} , $s->{'maxblocks'}); $sum->{'sumage'} += $s->{'sumage'}; $sum->{'sumsize'} += $s->{'sumsize'}; $sum->{'sumblocks'} += $s->{'sumblocks'}; } } print <<"EOF"; -------------------------------------------------------------------------------------------------------------------------------------------------------- EOF my $s = $sum; my $exist = sprintf('%.2f', $s->{'count'} / $s->{'maxcount'} * 100); my $empty = sprintf('%.2f', $s->{'countempty'} / $s->{'maxcount'} * 100); $s->{'avgage'} = int($s->{'sumage'} / $s->{'count'}); $s->{'avgsize'} = int($s->{'sumsize'} / $s->{'count'}); $s->{'avgblocks'} = int($s->{'sumblocks'} / $s->{'count'}); printf "%-20s ALL %10d %11d %6d %6s %6s %7s %7s %7s %7d %7d %7d %7d %7d %7d %3d\n", 'ALL', $s->{'count'}, $s->{'maxcount'}, $s->{'countempty'}, $exist, $empty, format_time($s->{'minage'}), format_time($s->{'avgage'}), format_time($s->{'maxage'}), $s->{'minsize'}, $s->{'avgsize'} , $s->{'maxsize'}, $s->{'minblocks'}, $s->{'avgblocks'}, $s->{'maxblocks'}, int($s->{'sumsize'} / ($BLOCKSIZE * $s->{'sumblocks'}) * 100+0.5); exit(0); sub format_time { my $seconds = shift; if ($seconds >= 60*60*24) { my $hours = int($seconds / (60*60)); return sprintf("%2dd%2dh", int($hours / 24), int($hours % 24)); } elsif ($seconds >= 60*60) { my $minutes = int($seconds / 60); return sprintf("%2dh%2dm", int($minutes / 60), int($minutes % 60)); } elsif ($seconds >= 60) { return sprintf("%2dm%2ds", int($seconds / 60), int($seconds % 60)); } else { return $seconds . 's'; } } #----------------------------------------------------------------------------- __END__ =head1 NAME tirex-tiledir-stat - display statistics about tile dir =head1 SYNOPSIS tirex-tiledir-stat [OPTIONS] [STATSFILE] =head1 OPTIONS =over 8 =item B<-h>, B<--help> Display help message. =item B<-c>, B<--config=DIR> Use the config directory DIR instead of /etc/tirex. =back =head1 DESCRIPTION Read a JSON stats file generated by tirex-tiledir-check and output a nicely formatted table with the data. Reads F if no stats file is given on the command line. =head1 FILES =over 8 =item F The configuration file. =item F Default statistics file location. =back =head1 DIAGNOSTICS Returns 0 if no errors were found or 1 if there were errors. If there were errors parsing the command line 2 is returned. =head1 SEE ALSO L =head1 AUTHORS Frederik Ramm , Jochen Topf and possibly others. =cut #-- THE END ------------------------------------------------------------------ tirex-0.7.0/debian/000077500000000000000000000000001412262531100140705ustar00rootroot00000000000000tirex-0.7.0/debian/changelog000066400000000000000000000057641412262531100157560ustar00rootroot00000000000000tirex (0.6.4) focal; urgency=medium * allow adding a dot and zoom level after mapfile parameter to override map file for specific zoom levels -- Frederik Ramm Fri, 13 Aug 2021 11:55:10 +0200 tirex (0.6.3) bionic; urgency=medium * modify TMS backend to take template URL parameter -- Frederik Ramm Mon, 29 Jun 2020 15:57:07 +0200 tirex (0.6.2) bionic; urgency=medium * add TMS backend -- Frederik Ramm Wed, 21 Aug 2019 16:10:48 +0200 tirex (0.6.1) bionic; urgency=medium * add openseamap backend -- Frederik Ramm Wed, 21 Aug 2019 16:10:48 +0200 tirex (0.6.0) bionic; urgency=medium * tirex-syncd to use ssh master, also allow multiple destinations -- Frederik Ramm Sat, 02 Mar 2019 14:23:23 +0000 tirex (0.5.2) bionic; urgency=medium * die after serving X requests -- Frederik Ramm Mon, 14 Dec 2015 20:06:23 +0000 tirex (0.5.1) bionic; urgency=medium * jpeg tiles * mapnik 3 -- Frederik Ramm Mon, 14 Dec 2015 20:06:23 +0000 tirex (0.4.1precise1) precise; urgency=low * support (alomst) arbitrary sized meta tiles * support for Mapnik2 -- Frederik Ramm Tue, 26 Jun 2012 21:45:22 +0000 tirex (0.3.1debian1) precise; urgency=low * fix epsg3857 bug -- Frederik Ramm Tue, 08 May 2012 20:27:04 +0000 tirex (0.3.0) jaunty; urgency=low * add mapserver backend -- Sven Geggus Fri, 03 Feb 2012 19:43:24 +0100 tirex (0.2.0) jaunty; urgency=low * new renderd architecture allows several different renderers at the same time * lots of other small improvements -- Frederik Ramm Mon, 3 May 2010 10:30:44 +0100 tirex (0.1.7) jaunty; urgency=low * bugfixes on zoom level 1+2 -- Frederik Ramm Mon, 15 Mar 2010 21:48:44 +0100 tirex (0.1.6) jaunty; urgency=low * munin plugin overhaul -- Frederik Ramm Fri, 26 Feb 2010 22:38:03 +0100 tirex (0.1.5) jaunty; urgency=low * new commands to stop/continue buckets -- Frederik Ramm Wed, 24 Feb 2010 15:03:40 +0100 tirex (0.1.4) jaunty; urgency=low * new features+utilities, manpages -- Frederik Ramm Thu, 18 Feb 2010 12:02:16 +0100 tirex (0.1.3) jaunty; urgency=low * lots of new features -- Frederik Ramm Mon, 15 Feb 2010 19:17:53 +0100 tirex (0.1.2) jaunty; urgency=low * fixed renderd starter dying -- Frederik Ramm Wed, 10 Feb 2010 17:15:52 +0100 tirex (0.1.1) jaunty; urgency=low * new build -- Frederik Ramm Wed, 10 Feb 2010 11:40:21 +0100 tirex (0.1) unstable; urgency=low * Initial release. -- Frederik Ramm Thu, 04 Feb 2010 00:57:52 +0100 tirex-0.7.0/debian/compat000066400000000000000000000000021412262531100152660ustar00rootroot000000000000007 tirex-0.7.0/debian/control000066400000000000000000000073601412262531100155010ustar00rootroot00000000000000Source: tirex Section: web Priority: extra Maintainer: Frederik Ramm Build-Depends: debhelper (>= 7), libboost-program-options-dev, libmapnik-dev Homepage: http://wiki.openstreetmap.org/wiki/Tirex Standards-Version: 3.8.0 Package: tirex-core Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, libjson-perl, libipc-sharelite-perl, libgd-gd2-perl, adduser Provides: tirex-master Suggests: libapache-mod-tile, tirex-backend-mapnik Description: Core of the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This package contains the master daemon, rendering manager, test backend, and assorted utility programs. Package: tirex-munin-plugin Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core, munin-node Description: Munin plugin for the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This package contains plugins that help to graph Tirex activity with Munin. Package: tirex-nagios-plugin Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core Suggests: nagios3 | nagios-nrpe-server Description: Nagios plugins for the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This package contains plugins that help to monitor Tirex activity with Nagios. Package: tirex-backend-wms Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core, libwww-perl Description: WMS backend for the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This is the WMS backend for fetching maps from a WMS server. Package: tirex-backend-mapserver Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core, libwww-perl, libmapscript-perl, ttf-dejavu-core Description: Mapserver backend for the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This is the Mapserver backend for rendering maps with Mapserver. Package: tirex-backend-mapnik Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core Description: Mapnik backend for the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This is the Mapnik backend for creating maps with the Mapnik renderer. Package: tirex-backend-openseamap Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core, default-jre-headless | java8-runtime-headless Description: OpenSeaMap backend for the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This is the OpenSeaMap backend for creating maps with the OpenSeaMap renderer. Package: tirex-backend-tms Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core, libwww-perl, libnet-https-nb-perl, libhttp-async-perl Description: WMS backend for the Tirex tile rendering system The Tirex suite of programs manages map tile rendering and caching. This is the WMS backend for fetching maps from a WMS server. Package: tirex-syncd Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-core Description: Tirex sync daemon The Tirex suite of programs manages map tile rendering and caching. This is the sync daemon which listens for messages of tiles fully rendered, and then copies the metatile to another server. The sync daemon is intended to be used in setups where you have multiple tile servers with their own rendering queues. Package: tirex-example-map Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, tirex-backend-mapnik Description: Tirex example map data The Tirex suite of programs manages map tile rendering and caching. This package includes example map data and an example config for the Mapnik renderer. tirex-0.7.0/debian/copyright000066400000000000000000000020121412262531100160160ustar00rootroot00000000000000Tirex Rendering System Copyright (C) 2010 Frederik Ramm and Jochen Topf Initially developed by Geofabrik GmbH, Karlsruhe, Germany, with support from Enaikoon GmbH, Berlin, Germany. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. On Debian systems, the license text can usually be found in /usr/share/common-licenses. tirex-0.7.0/debian/etc/000077500000000000000000000000001412262531100146435ustar00rootroot00000000000000tirex-0.7.0/debian/etc/logrotate.d/000077500000000000000000000000001412262531100170655ustar00rootroot00000000000000tirex-0.7.0/debian/etc/logrotate.d/tirex-master000066400000000000000000000002301412262531100214270ustar00rootroot00000000000000# /etc/logrotate.d/tirex-master /var/log/tirex/jobs.log { daily rotate 35 dateext compress missingok notifempty create 0644 tirex tirex } tirex-0.7.0/debian/rules000077500000000000000000000042011412262531100151450ustar00rootroot00000000000000#!/usr/bin/make -f configure: configure-stamp configure-stamp: dh_testdir # commands to configure the package. touch configure-stamp build: build-stamp build-stamp: configure-stamp dh_testdir # commands to compile the package. $(MAKE) touch $@ clean: dh_testdir dh_testroot rm -f build-stamp configure-stamp # commands to clean up after the build process. $(MAKE) clean dh_clean install: build dh_testdir dh_testroot dh_prep dh_installdirs # commands to install the package into debian/tmp $(MAKE) DESTDIR=$(CURDIR)/debian/tmp install-all # Build architecture-independent files here. binary-indep: build install # We have nothing to do by default. # Build architecture-dependent files here. binary-arch: build install dh_testdir dh_testroot dh_installchangelogs dh_installdocs dh_installexamples # the following line handles all packages *except* tirex-core dh_install --no-package tirex-core --sourcedir=$(CURDIR)/debian/tmp # the following line handles *only* the tirex-core package, it excludes some perl libraries that should not end up in this package dh_install --no-package tirex-backend-wms --no-package tirex-backend-mapserver --no-package tirex-backend-openseamap --no-package tirex-backend-mapnik --no-package tirex-munin-plugin --no-package tirex-nagios-plugin --no-package tirex-syncd --no-package --tirex-example-map --exclude Munin --exclude Tirex::Backend::WMS --exclude Tirex::Backend::Mapserver --exclude Tirex::Backend::OpenSeaMap --exclude Tirex::Backend::TMS --exclude Backend/WMS --exclude Backend/Mapserver --exclude Backend/OpenSeaMap --exclude Backend/TMS --sourcedir=$(CURDIR)/debian/tmp # dh_installmenu # dh_installdebconf # dh_installlogrotate # dh_installemacsen # dh_installpam # dh_installmime # dh_python dh_installinit --name=tirex-master dh_installinit --name=tirex-backend-manager dh_installinit # dh_installcron # dh_installinfo dh_installman dh_link dh_strip dh_compress dh_fixperms # dh_perl # dh_makeshlibs dh_installdeb dh_shlibdeps dh_gencontrol dh_md5sums dh_builddeb binary: binary-indep binary-arch .PHONY: build clean binary-indep binary-arch binary install configure tirex-0.7.0/debian/tirex-backend-manager.service000066400000000000000000000005271412262531100216060ustar00rootroot00000000000000[Unit] Description=Backend of tirex rendering system After=network.target auditd.service Before=apache2.service tirex-master.service [Service] ExecStart=/usr/bin/tirex-backend-manager -f ExecReload=/bin/kill -HUP $MAINPID KillMode=process Restart=on-failure User=tirex Group=tirex RuntimeDirectory=tirex [Install] WantedBy=multi-user.target tirex-0.7.0/debian/tirex-backend-mapnik.install000066400000000000000000000001001412262531100214440ustar00rootroot00000000000000usr/libexec/tirex-backend-mapnik etc/tirex/renderer/mapnik.conf tirex-0.7.0/debian/tirex-backend-mapnik.postinst000066400000000000000000000001521412262531100216700ustar00rootroot00000000000000#! /bin/sh set -e [ -d /etc/tirex/renderer/mapnik ] || mkdir -p /etc/tirex/renderer/mapnik #DEBHELPER# tirex-0.7.0/debian/tirex-backend-mapserver.install000066400000000000000000000004321412262531100222010ustar00rootroot00000000000000usr/libexec/tirex-backend-mapserver usr/share/perl5/Tirex/Backend/Mapserver.pm usr/share/man/man3/Tirex::Backend::Mapserver.3pm etc/tirex/renderer/mapserver.conf etc/tirex/renderer/mapserver/msdemo.conf etc/tirex/renderer/mapserver/msdemo.map etc/tirex/renderer/mapserver/fonts.lst tirex-0.7.0/debian/tirex-backend-tms.install000066400000000000000000000002561412262531100210040ustar00rootroot00000000000000usr/libexec/tirex-backend-tms usr/share/perl5/Tirex/Backend/TMS.pm usr/share/man/man3/Tirex::Backend::TMS.3pm etc/tirex/renderer/tms.conf etc/tirex/renderer/tms/demotms.conf tirex-0.7.0/debian/tirex-backend-wms.install000066400000000000000000000002561412262531100210070ustar00rootroot00000000000000usr/libexec/tirex-backend-wms usr/share/perl5/Tirex/Backend/WMS.pm usr/share/man/man3/Tirex::Backend::WMS.3pm etc/tirex/renderer/wms.conf etc/tirex/renderer/wms/demowms.conf tirex-0.7.0/debian/tirex-core.install000066400000000000000000000013621412262531100175430ustar00rootroot00000000000000usr/bin/tirex-backend-manager usr/bin/tirex-batch usr/bin/tirex-check-config usr/bin/tirex-master usr/bin/tirex-rendering-control usr/bin/tirex-send usr/bin/tirex-status usr/bin/tirex-tiledir-check usr/bin/tirex-tiledir-stat usr/share/man/man1/tirex-backend-manager.1 usr/share/man/man1/tirex-batch.1 usr/share/man/man1/tirex-check-config.1 usr/share/man/man1/tirex-master.1 usr/share/man/man1/tirex-rendering-control.1 usr/share/man/man1/tirex-send.1 usr/share/man/man1/tirex-status.1 usr/share/man/man1/tirex-tiledir-check.1 usr/share/man/man1/tirex-tiledir-stat.1 usr/share/man/man3 usr/share/perl5 etc/tirex/tirex.conf etc/tirex/renderer/test.conf etc/tirex/renderer/test/checkerboard.conf etc/logrotate.d/tirex-master usr/libexec/tirex-backend-test tirex-0.7.0/debian/tirex-core.postinst000066400000000000000000000011341412262531100177550ustar00rootroot00000000000000#! /bin/sh set -e if ! getent passwd tirex >/dev/null; then adduser --group --system --no-create-home --shell /bin/bash --home /var/lib/tirex tirex fi for dir in /var/run/tirex /var/log/tirex /var/lib/tirex /var/cache/tirex/stats /var/lib/tirex/tiles /var/lib/tirex/tiles/test; do [ -d $dir ] || mkdir -p $dir && chown tirex:tirex $dir done dpkg-statoverride --list /var/log/tirex >/dev/null || \ dpkg-statoverride --update --add tirex adm 0750 /var/log/tirex dpkg-statoverride --list /run/tirex >/dev/null || \ dpkg-statoverride --update --add tirex root 0755 /run/tirex #DEBHELPER# tirex-0.7.0/debian/tirex-core.tirex-backend-manager.init000066400000000000000000000066131412262531100231730ustar00rootroot00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: tirex-backend-manager # Required-Start: # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Mapnik rendering daemon for Tirex. # Description: Mapnik rendering daemon for Tirex. ### END INIT INFO # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Mapnik rendering daemon for Tirex" NAME=tirex-backend-manager DAEMON=/usr/bin/$NAME PIDFILE=/run/tirex/$NAME.pid DAEMON_ARGS="" SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { [ -d `dirname $PIDFILE` ] || (mkdir -p `dirname $PIDFILE` ; chown tirex:tirex `dirname $PIDFILE`) # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --chuid tirex --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --chuid tirex --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --user tirex --pidfile $PIDFILE RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --user tirex --pidfile $PIDFILE return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; reload|force-reload) log_daemon_msg "Reloading $DESC" "$NAME" do_reload log_end_msg $? ;; restart) log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 exit 3 ;; esac : tirex-0.7.0/debian/tirex-core.tirex-backend-manager.simple-init000077500000000000000000000033101412262531100244540ustar00rootroot00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: tirex-backend-manager # Required-Start: # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Mapnik rendering daemon for Tirex. # Description: Mapnik rendering daemon for Tirex. ### END INIT INFO # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Mapnik rendering daemon for Tirex" NAME=tirex-backend-manager DAEMON=/usr/bin/$NAME PIDFILE=/run/tirex/$NAME.pid DAEMON_ARGS="" SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # # Function that starts the daemon/service # do_start() { [ -d `dirname $PIDFILE` ] || (mkdir -p `dirname $PIDFILE` ; chown tirex:tirex `dirname $PIDFILE`) # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started su tirex -c $DAEMON } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred kill `cat $PIDFILE` } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # kill -HUP `cat $PIDFILE` } case "$1" in start) do_start ;; stop) do_stop ;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # do_stop do_start ;; *) echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 exit 3 ;; esac : tirex-0.7.0/debian/tirex-core.tirex-master.init000066400000000000000000000070671412262531100214730ustar00rootroot00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: tirex-master # Required-Start: # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Tirex master server # Description: Tirex master server ### END INIT INFO # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Tirex master server" NAME=tirex-master DAEMON=/usr/bin/$NAME DAEMON_ARGS="" PIDFILE=/run/tirex/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { [ -d `dirname $PIDFILE` ] || (mkdir -p `dirname $PIDFILE` ; chown tirex:tirex `dirname $PIDFILE`) # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --chuid tirex --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --chuid tirex --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --user tirex --pidfile $PIDFILE RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --user tirex --pidfile $PIDFILE return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 exit 3 ;; esac : tirex-0.7.0/debian/tirex-core.tirex-master.simple-init000077500000000000000000000032121412262531100227510ustar00rootroot00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: tirex-master # Required-Start: # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Tirex master server # Description: Tirex master server ### END INIT INFO # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Tirex master server" NAME=tirex-master DAEMON=/usr/bin/$NAME DAEMON_ARGS="" PIDFILE=/run/tirex/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # # Function that starts the daemon/service # do_start() { [ -d `dirname $PIDFILE` ] || (mkdir -p `dirname $PIDFILE` ; chown tirex:tirex `dirname $PIDFILE`) # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started su tirex -c $DAEMON } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred kill `cat $PIDFILE` } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # kill -HUP `cat $PIDFILE` } case "$1" in start) do_start ;; stop) do_stop ;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # do_stop do_start ;; *) echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 exit 3 ;; esac : tirex-0.7.0/debian/tirex-example-map.install000066400000000000000000000001031412262531100210110ustar00rootroot00000000000000etc/tirex/renderer/mapnik/example.conf usr/share/tirex/example-map tirex-0.7.0/debian/tirex-example-map.postinst000066400000000000000000000002461412262531100212360ustar00rootroot00000000000000#! /bin/sh set -e [ -d /var/cache/tirex/tiles/example ] || mkdir -p /var/cache/tirex/tiles/example && chown tirex:tirex /var/cache/tirex/tiles/example #DEBHELPER# tirex-0.7.0/debian/tirex-master.service000066400000000000000000000005001412262531100200710ustar00rootroot00000000000000[Unit] Description=Master process of tirex rendering system After=network.target auditd.service Before=apache2.service [Service] ExecStart=/usr/bin/tirex-master -f ExecReload=/bin/kill -HUP $MAINPID KillMode=process Restart=on-failure User=tirex Group=tirex RuntimeDirectory=tirex [Install] WantedBy=multi-user.target tirex-0.7.0/debian/tirex-munin-plugin.install000066400000000000000000000001661412262531100212360ustar00rootroot00000000000000usr/share/man/man3/Tirex::Munin* usr/share/perl5/Tirex/Munin usr/share/perl5/Tirex/Munin.pm usr/share/munin/plugins/* tirex-0.7.0/debian/tirex-munin-plugin.postinst000066400000000000000000000010641412262531100214510ustar00rootroot00000000000000#! /bin/sh set -e if [ -d /etc/munin/plugins ] then for i in /usr/share/munin/plugins/tirex* do if [ -f /etc/munin/plugins/`basename $i` ] then # is a file, leave alone : else # does not exist or is a link ln -sf $i /etc/munin/plugins fi done if [ -x /etc/init.d/munin-node ] then if which invoke-rc.d >/dev/null 2>&1; then invoke-rc.d munin-node restart else /etc/init.d/munin-node restart fi fi fi #DEBHELPER# tirex-0.7.0/debian/tirex-munin-plugin.prerm000066400000000000000000000010761412262531100207160ustar00rootroot00000000000000#! /bin/sh set -e if [ -d /etc/munin/plugins ] then for i in /usr/share/munin/plugins/tirex* do if [ -f /etc/munin/plugins/`basename $i` ] then # is a file, leave alone : else # does not exist or is a link rm -f /etc/munin/plugins/`basename $i` fi done if [ -x /etc/init.d/munin-node ] then if which invoke-rc.d >/dev/null 2>&1; then invoke-rc.d munin-node restart else /etc/init.d/munin-node restart fi fi fi #DEBHELPER# tirex-0.7.0/debian/tirex-nagios-plugin.install000066400000000000000000000000551412262531100213650ustar00rootroot00000000000000usr/lib/nagios/plugins/* etc/nagios/nrpe.d/* tirex-0.7.0/debian/tirex-nagios-plugin.postinst000066400000000000000000000003571412262531100216070ustar00rootroot00000000000000#! /bin/sh set -e if [ -x /etc/init.d/nagios-nrpe-server ] then if which invoke-rc.d >/dev/null 2>&1; then invoke-rc.d nagios-nrpe-server restart else /etc/init.d/nagios-nrpe-server restart fi fi #DEBHELPER# tirex-0.7.0/debian/tirex-nagios-plugin.postrm000066400000000000000000000003571412262531100212500ustar00rootroot00000000000000#! /bin/sh set -e if [ -x /etc/init.d/nagios-nrpe-server ] then if which invoke-rc.d >/dev/null 2>&1; then invoke-rc.d nagios-nrpe-server restart else /etc/init.d/nagios-nrpe-server restart fi fi #DEBHELPER# tirex-0.7.0/debian/tirex-syncd.init000066400000000000000000000070571412262531100172370ustar00rootroot00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: tirex-syncd # Required-Start: # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Tirex sync server # Description: Tirex sync server ### END INIT INFO # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Tirex sync server" NAME=tirex-syncd DAEMON=/usr/bin/$NAME DAEMON_ARGS="" PIDFILE=/run/tirex/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { [ -d `dirname $PIDFILE` ] || (mkdir -p `dirname $PIDFILE` ; chown tirex:tirex `dirname $PIDFILE`) # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --chuid tirex --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --chuid tirex --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --user tirex --pidfile $PIDFILE RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --user tirex --pidfile $PIDFILE return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 exit 3 ;; esac : tirex-0.7.0/debian/tirex-syncd.install000066400000000000000000000000731412262531100177310ustar00rootroot00000000000000/usr/libexec/tirex-syncd /usr/share/man/man1/tirex-syncd.1 tirex-0.7.0/doc/000077500000000000000000000000001412262531100134135ustar00rootroot00000000000000tirex-0.7.0/doc/overview.odg000066400000000000000000000300651412262531100157600ustar00rootroot00000000000000PKv¬<Ÿ.Ä++mimetypeapplication/vnd.oasis.opendocument.graphicsPKv¬¼Ã®‚X‡aBk)DO[²|£­h¼\íF~Rr,t ŒcȘßêR7Šž-2ÖÓ .ºcùgªmâ]1#AªÊ BüóŒzJ©Ï>Sý¯Ý©ÚêÊ@—êŸÌ}¯|§fíÔŽ ÐŒû£aÀG9`b,­UyÁç5çÝH·yU¡§:T—Шâc{PÎÄŠŠÐþö èÞ´€_¼€ÎË]ÀƒÕ‚oã*õ‹C„hðr Œ>ŒqŽbv­o?™Æ'"Ë­ü*XÕB¿$~ð% üø•.¥¥, qϲÁë·´¨ö˜õi)øÙ™™û„.P˜„*~¹Rå" K.ÕIÅü(Î(_+(¢¡:¹ˆ¯¶ôóçÀ0Ò¦Ÿð¤üû^\ æói—§-‘zÆ©>À`x¿{QóØ~…¯ëŠG9Ú_ü;ôJŒ¢jûmŠ5Èý¬¯Óä„Þ×ÕÃç·û~ùyG¦w°®wÊäÞï K}nkð~ÏÆN晡äçÈI\ÖLØç Qe„ÞѤ&ÌÛu’ÐJ•X¨âVTixpx©þï¿ÿiç%5²›Ý£Ô@ôØëÝ6½Í“fŠë98 ! á’5íVl-ØLÍvþȧ~l2'LÕü¢© ²ðh”F9£ÈŠ.Ò%¯IãLKü[ÐiyÞ”ùò²Î†™™¯!ûŒy¯!çl™3ûU"äž !ëUâ3?>¯”§½³!d¿Rž^œ¡×ÉÓÄ8DÎË"j©º§õÞ‡_uÅmnÛ›ú)×Õž¡ã³.‘«‹ðïI{²´;¦àË꣒0þ¬®û•Fþ:Á\‚ ëÆ¡•+¿ û ñ‡gíá@ç(Ó`^“ø[|ÿ¾òu%N9êå0›Sæ Ìœ¦S"»¹Ù⺢tmSE½òrwm¦öí×CbwUÅŒÞk5æ(Öà_Œk—Ñ,¤,|‚–S¿ÈâχÛåàÒl…ÊCmIó”VlËñº‹éæ— 4C1‰kÔŸõŠ~µ‚µb´ VEƒÉòÔ;"°í ˆ<žQ¿¼T¯#K¹ŽløqàÇm$¶Ìåë,Ô *?[¶‡¿iÆQL([»QµS)ø;o¡±9 ÿ#ü\GžrÿÊ?x¹˜Ô \“f†ëoºi¯=®@ˆ*k¬šþ{ÍïË›%j×s–®·0îà~òPá{Cù©Œ³®#¸ø¡ˆuâ?þx¼óa#0Zå'‹¸–>w-÷¸²õPÐ~¾ŽL帞ýP¯Ê‹jÎC5~2§=®è>TÇ€4çg@WóMðQ­EÏ0U${¶#‘ÁºÃ¤æPq­y‹—º@F¨vjËM û†‘ëì'ªÝkàââtç©=õþ½¨»ÒaÞh*d¾?› ˆaqHF;k¶è¸2¹„É%L.ar “Kx™.Áí É!X¼j½¾å ÑD¯½Nô:Ñë §Ww,½š»»­Ç®¿°|SNô:ÑëD¯½¾xzµÆÒ«{æàxèS'tâ׉_'~øõ™ò«9_G?A\tÂ×ùÙµG¯ž×áÖe²ÆuÎv³‘]¾P’/ÞnÅ>J5¸\·;\ðiàgûêØýÊ»þ³b|¸ÝðÓ¨ÙÔ‚*ƒ“·Ù°h;<×Û%Äh@‘_`£üÂDACô«ôwipa $øåz»™:MˇÚ=¸‘‡MÝ>‘½9k»:!'¶¡hÈÚ'r5‡WCíÉ×áçÇ;Ückìj€ÞY 8͉ڶ‡QºóïÒ×özÛ>«ðÑKoÄï5ÏÃÆ§ç~ð™fá>Náã>Náã· ó,ƒœ³Cäjõ’kÏër0´fw´fO‚ï—I¯À½;kóñ{ª%²Ý½73íEûKsâ·ðø°åwDñ.¦ã¿éQ#îÄåBYØÝÁ¤Ä^`P¬@$HÀñbY qí…fAǯZ¾onÁûÚ`3{Ñs.ù*€o.céôáhöâèÂÑêÁÑ”p4`\ÊÜA5âÍ-@o±Ð\Ó&ÔŒ„Ñ>ŒsA«‹ !2„uå!­^C Ú= ;çœ%0(°¿Ý…òŒð#¦  =3»ÚRÀtÌ í^É!Ç$&"eƒµÄs1Ð01¹ƒ{q`>#åÀ’à›Vó#–Ø‹¢Ó‹¢uŠÊ›ÙB¬+ÐŒ¢‡›X\"3_cr+ ­o0d6N/6…;2k²ãa6ãvNkÜqùŒ©^9çÎd:‡5âxxbgñÑÙðâÙÖ¼.-Õ%ü³.áµ¢•¤;»Ö%õŽÕÑ$‰‹’¶ÑIk>4}ì}Â0]‘¤è#’ݧ5 ³ó§=Ú ìÉ ^´AxŠ0góÑaMñ¢-‚Ô¿™Û¾>ž$ÜÉ$F›Ä±Ë=_„å °'äïý)ÓÁPµ/wwÑ—ÛÆû‘†mE¦mõ¢§›õ޶s²‡m^ç•ñg Þd/Åíf­n4ûEÇÿcY·ÇíšcYáwy0+N]Á§®ßrWÙ]%~xܾZLûjľeøgGvñ‹øß2Ñ;éDïùw®þPKk€È ±gPKv¬< styles.xmlÝË’Û6ò¾_¡bj÷ñ!Íd¤õ8•¬³{‰S©u²×D‚<$ÁÁ‘äŸØãþß~Iº‚%R¦lÙ3™qÕL©D¿Ý¢_}·Ë³É#“ŽNoŠX$¼Xß{¿ýúOrç}÷ú/¯Dšò˜-×9+©Ô>cÕ˜‹ji÷^-‹¥ ¯–ÍYµTñR”¬°LK—z©2ý°±ìšØåVl§Æ2#m‡—®ÆŸ¬‰]îDÒíXf¤›ºì©˼«2’ ‹¼¤ŠI±Ëxñpïm”*—¾¿Ýn§ÛÙTȵ. _c[ã–®¬e¦©’ØgÃÃ*?œ†¾¥Í™¢cåCZW¤¢ÎWLŽ6 UôÄ«¥d€º—ãäòtâëq=:º×fŽ7TŽŽ3MÜ •Y2>Tf‰Ë›Sµðïÿú×ÛŸq%ó±g!mÇT±äåh5 µË/„hEE“ìZÜ(æ¾ùìPoÏ’o%WL:äñYò˜fqkq‘÷ èB({ÄoõÎy6Zk ZðѦGÚ“P•hüA o|ÉJ!Ukt|Ñ…S¢ö”$¥í1@YMõ2ê³Ü'ø˜Ý>²|ãMšâí4ŒÈ{m»ƒi ¯_a­[®%M8°Oô'”íÞûWû= ~=ƒIxUftOº‹nzT1Fe ƒìdjÌ7þiP v‹HõO‡‡ŠW{<þê0õch±ÆÓíÓWB&X×ï7jn¨Š7®Ž?d4~@üõ†­%cU¯®šrLº4ÂÇ£X¯ªð0E ì»Á4 ¢8oàRت´R¦<˄ۚ¹¢þ˜—j1-b¹‘ ¢é«ʨŸ;ûwÎþq7»¹–ÅÚ²ª}©%ÎK”ØÀª ¦6ƒ>X…GS”¢øIФ.§òIW°ï¥[oÅxùÈÙö±eÀRQ0™Õ4FB„m¢àCû¼JIñÀHB«Žc~Ë”¤)Ä‘ö Q²·´t“QãÉbeÙ„ªB÷#ÉX±ÆrLƒ›°õ â¢.i4Dê¸ÕÀA/Ó–Ò:k._K)T#ZH·rÃcÏÒ–TR #¥„l–ŠÃuÍ ðDh­DUR<‡'LRš•ê5de]h‡A(‘- 6xLI²’ŒÂåLÎÑ.ƒU—ä"ÁÔ•D­,*fa°ÀàER?ÑΔfku€ è*Ê ,à«Õ’£^'ÚÖ#póJÄ–èÛ\R²MRa$²Vü-Eõ÷_9ÒÉÏl;ù·Èia€ =Y³‚âHס(9Ô…{ï‘JŽ×ÆÃQÿF‰æ¥Ò° JL yyï%†(u¡$ÈòæÇ¾#¡NÓ ú†½§ÿ©'ïhQ}THËYí+ÅòSY-þ ±CB[#zô÷a·³¨F‹)DÁú¤Ã>š±Ý'iÔòêÔR kÕ’ôêÕb{4kqZ·CŒvÒÔ§›³¦Î`š'T&ÞG2¹ùìF¶Sæà9"ã‰)ŠMåÛòD׬)¸·‹˜âKL{´ŒÓY[Ž:ø2ûŸIÓöÐavÝ–{˜±/µ:••t±ˆcìà%Mp&%PtËnð ±Jᕯ—±T!æ!ùzã`Œ]74Ñ×Rž@…ê \t*¦È®«d¹ïEZ•îüÎÕ£hÅžœ —,ƒkfˆÉªÎ2¦&‰pÝ[ôGƒ"8Ü{ÿÿßÛ8rr%¨xWÆwïöùJdÞ%…ÕT»ù¾0ù甹@Õè“T5<ºç‘ƒÁG·Ø[t”Æä¼ ]OÇ·n÷ý3Yhv5 ÁÿEZh~E ݽH Ý\ÍBÑtþ"-t{5 Í^¤}¾½ž}^h¾»š…æ/´N/®h¡—Y§Ãàj&ºùsjÝÌZþÐÈtv-gܲF…Í ÐL Ð ]¨Dº°viÀz¹±a†ß¬ª-ÇõúÃÚt„ÝFQ+<°°”Dm¤¨×Òl²ÌD~ì¶ïaNή»ëïš]‡ÙÓeŽY{Ø©ËÝhD †’fKÔ'÷¶±®}^ñLÁ¬I`ô,ô^ɵÕóÚ¦ã¸HâíÖÄÕÊQø Á³[¬ ¨è¬O†” ÑqÁòrCM€ì–$Ë8n³3YȈÕ{«-WP@Íþ¹w/c×£T¶_„““±ýsW7ÓðæÒíM»6?³×ùöü^ǤÉÙµÎ'XÓ¤ö—4gS;yÅWí—&×^Ù|‚æP‘qŸõU7 ´ËâÓç+·ÉÊÏ’r%’ýSJzþ¦Ót·[¨nkõ¾® 4Øàò÷ëi7t#2ße| ]¥åéþr5Í-çù*yÁµ¯{k³wá‘æàÊéÏ4çó‹U ŸB§Áo S§/ì ѹžp‚<í ã³ÈtÙ±îˆ.wGôeÜÑw= ß—t|¦q¼XbúÆšóhÈœQ8ÞF—%†µÑ“”ª§3Rwæ¶CòJdÉå¦ûBeåÙšnÞ³®à ’<þ\‹æŒVµüùê6ÉK‡ÄèxH„!°—¯3F†Ðñ(¾ Fê‚«fÂ눨/Ñý£— ›ø>¾N‹°Æ\3’Ñ=ŒbWüò6ðzhÎÄ×@/8›.öv Õç5†‹:@»+ŒfTo䓌"$?¼oJÊ]û8Z GàÛ¤<©„Í«éÐÚÄžDÙŠÆk)jð¿q•y1rĴ纫Aä´R:$÷‡WIA)áÊ÷ðÙ}µÏhݾx‚?È;†ÆÌïÕ0i 1*Ev†¢It¬žšÊ?ÖÃ¥QmÜñÒóÑ¡ âó½Îk‰Äq®0ò‘-ýþÿÄñúPKäèwùˆ2PKv¬<"^Õ"««meta.xml Jochen Topf2010-05-02T21:26:462010-05-12T16:44:16Jochen TopfPT01H01M11S4OpenOffice.org/3.0$Unix OpenOffice.org_project/300m15$Build-9379PKv¬<Thumbnails/thumbnail.pngíÖû7 pžÞ=É-JLíò¤F©,­åU‹(ÓÜSHab#¯G¹ÄH„r™ûýÎ.ŒÖK„4c›;ÃcmËmhÌÓ9ï9ïÏïðœó9çû=ß¿à›pn©°Ws¯ŒŒŒ‚Õ5˜ÝÏ$ÉÈÈÊìùõg[Ósedö­`æa9‚1t’ö0cI4.4Õ’;¢ï¼³V/ûL¿DY^¬™ÄZ´ ð³I’ûfÂb»Š•^ƒ–~•æöªl^zhMÒ˸\r)y+»\¦¾pÀ Z,­×·´°Û·Ï4AVØë'ôÆÚÇr·ƒVÆ[د&@¯îúüçÅ9™'„rí¸ÿ›þ`¹}…›ý¼‡g發{M§±|,]ÀuÏüàŇ\wC ½~Ö»DÍàý‡C=<&f/EØÚ*ÝIJ¼´˜ÚDæƒiÈ@¡5' î‰MÏS]uù(å?! +é`xÓ*ËÃqÕÚø®<ÓdÐmÊ'”ËÃ2ìÖ`žoAóžù5»øß&QŒu”?fHW7“óW™Àlâ¾î›¹åöVý9Lg0Õ9ÀÚü5;Ù8EVU²[fK JÈoϺgGú¢—ÝçYU˜-QÜIšP5rõÔLã…Îò3q…œhçIB¶®ù Æ b_ìR⊤…‡ûÑ}œN¾„ ÇŒSíw#0[Cs¡@­awJ ŒŽèUHÿ®ÿ ñ8 |SdÜžÉ@ÛR8­rBâÒævÏɹ7{l ‹ cU›TÉf¨'LéB÷:Ùž²Bµ#*ñÒR8S±w#luTyÛhì }ÚÚÖ ÂÆ'7±ÞaE¹×ŠjÏöÛ¦ªç\âBÄèw5\Êæ|^8Ö1³?>x ÚS;˜àÐ':ÉT?HÃZ¹)´Tót¾jPíF¤&3/úYÔÇ(’dŸv]ĵù,M‹» ù3ÆÉç+2¥5+éáG¼Ð me%í™=&!,±V@ý9·G‚µ"´ë±ˆ¡äÜ>ÒŸcýÃ"÷ 7Koô+Ž<£rjSQ’¹Ê9¾Ó Fq9(Ôqÿ«‹—\ªÉ̯ðh¥Üf3³ÆžKÃÈ«+«Ý6»ˆ e‘”žd7Úý:…fÎs=ÒIº€Ù¦@~°ªiÕ—pc- ÷z/…‰†: É%­R¢uÛ7aÑß&/&Úßê1l ¨Ž¢Ì¶ðixÉA~*¾Ê×|øB"#¹P–0Wb,~?ç€ûëÞRæú“ÎwÄU6±š®Õž6v!Ï|GÇÊB"ç*M•lJ¾f`2žçyÉ¿¶7ñ5íùŒjàPöyÜÆw¡{Üw¥ˆ(W‰\µ0àÎT+à ¨^z@ìÎfE½éÎè¥ûm«cø#‰k«\ùšö¦¯®xÿŒ$K3 #N´~^c©mÔ_c áÌùßɈ«6!Q@&$2ŒÆñq’NG/Ú/1˜YžjÆ­7]&¡}Ø&jáÆiÓ+ËfŽ\/ýŒÚùÅÐ"ðëȦˆð5–7±ÁHm»w`}†èNÿ>XðVä >b\X¼Wl‡÷Ú~Yßh "‡8^ïìÄýÛúUÔty¦ŽUBJkq>?G¢jvXÃ|YO"¬˜z"ŒÚñÞ¦°¾FÓªCzت* ÀÜü3>é¸Ü}ShlÞ†Õ"]rKI«ÜÕ~ês¸½aǘzì«îç¦ZâétæÛ=˜æPZ勃˜£EŠ:‹cݪtÔ­öB‚ PשÏj˜ý°N«éÂÚ³¾óKA=Õ"˜…w·Ü 1'¾8‡ëË› 6 ùsk¾Dx`ÒÚ¦u+fj¯¾Ë_?Z  ¶:Ý%#¬º‘A¶,E}gÍâ­`ÂæS¾Í÷‡Ž\™»±w² m0šúÕU²ã9Au•soHë+® ܱvtÌD@¦OüñÝ™¼fC}LÿÚWvß»]5ß«9¡ÑX ÒTšq,‚™5Wü5C°Ð\:Zϵòø-ƒZþa_C:¯cŠÌîHÒ«åÆä›µÈ,ÌüížÇÏÎ[Ýa"‚ÝO ½Ñߥ`ß¾ð´ƒ0Á”°±áƒ€g…Å8 œMHlu•¼—‚H k\ ¤à¼üÓm•u ý,Aã£kЭ…Y'ŸŠ¦è)¼rp䇟àYßgÍIt™)4mH-š¡-¤Þ¨¬4)TL"^„{ÎÓ-EÀà „ †_fUËf&_Â6Ÿr”œï%۟ݤz… cN¾ótß9rìç•®‰_€ z~»Úé9øÛùʵDÐÞ„dÎÔ¿>—©½;±„H„åIGø#Ë®)º7ï`_NŸ/=å Ë}ØÆû •h6ë,!2W”ö Ñ΋fâ´—åÜ©*q†oé‡k‘ òPCK¯B]’È) °ú=0ð®«ËÁ—.—%Ò‰äkûÆÝgFÝyÏ)‰\<æ÷I2ªBŠò¶8°Énö2Ÿ f÷KŽ„‚2ø2ÂÃ=‡´£Ïxy)Êvõá~ÆØ›Ðþ†­bàÙtú6µý¼_-lL.åò×&-f;Jæùî¬Åö¥0þÚ­w`îl¢´·íÞ¤¥û¢Û·$–Èöí}µöùŠ ¥zá#ïÍaû¾ß3ÆÂWÆà ™“ØOÊ£&1•‰Rò6{ssŸ:ú)ð6z–¡è¹Ü]62úC¹ ƒ¿ÉýÏE†o ¼\…ŠŸÉåÒùKñ“6p’˜BôŠà1ò­ìQÎÃ7—HŒ¼&<SH®\!Qò#£wuŽaSìÎã~ÁA€&JÏ€¿Æ=>ã@o “gÈ|–sð’»Ñ{`‰:€/1ñ‘øÛ˜¢³pLO‹/È’5®cgé1õÅ£L¸¢($HÄ›Ý×w]õO•ƒ¥©5rÞ<úð@°)¨ÇÈ&8¶¶†Oz/Ý¥ÓºÚœrÅa¢=Ú`£"ß1>èÓmY Á7‰RêâW®¨'KO]‘ü*Ò´™µFðÇ¢¤~ Õe†T÷ÿ"vY…v*btg¸ñ}|ÿ®n©8|)WŒ«GGó¹1Q7‚>ø‡7Eà c8Ц(ãå½Ê\e ÁÖzˆ^åÄgC|š€ ²t‰l[SÄàñ&Á‰âæ…›øVóvWÇâÛהʀ;fp£kzϺË䉅?×÷æ†çÆ‘óîÿkErqÅ W‘d4mêgȸ@g¥Ï¤uóŠÁß‘=zMeÆg×·öÈ祎í§ÃÐÓ®k#$ã1rTeAnΡ>|ã˜ýzÌâÑ£)‚|Ú§y ~:x¶^ŒDz iûœ1û‰†$—È2xØw87½ke„/H^ÞµÚ¸<ˆÜ?Ó¾2‡lFñ*àã3‹2–JÑ¡ï £AûLgå÷?\%ÀŸ;‚”ØñkÁz¬*‘á@å’¼Âl¿Mê7‡ŠòCÓÊ y×ÖÌÒ#­±Ò愚œè¸D*ZS†ç™ìö­ VO‰¶NK=Þ–þA®t¯ÇâÔОK-鯕g7EP—[OKð  ¬uEÉ!Ç»ÍÍÈ$êò‹§± ×°ÔŒ+40„(ˆ]±Ô ‡Þ«§0 1AñžÓÇ"¿ßQ~ Pû:Ñ¥e€Œ’M Zu.G#zÒqØÅMÍ 2p$×^ ^cÜ¡f §Âþ’geº¶×åxRßËãcÒ`oª”ÐÿÊSÅÄ6Øœ6öÞD˜—T™ #®¯f$ʧzÏÒÊe‘Œ„o"é§ ß·„g^ã¯TúueeÇÓ§_üÕÏŒû£oNg6íkíx~éÄ0:Æ×þÛçšQ›Ôûz¡ÆL;õZzf¯ZÞ€É]z>mæ‡õÚ·ùô±5Ÿö[‹›Gg®ÆgÓ³ÈÞæGõü6W@ÍͲyLÆßzÇÖbRÛ̳Ä×k_á´›^LÆ.¬¦½vÕŠçUìîËBí×Ǭ/ó/ý–ÕèŠùtNjÝÁ|Lº¤74Z£Zù¡ŸÍ»°>¾ äì;ýÙ’Q–|šÎ|:ðå5Ìɧƒ¡ÚŸ~ZÈÜ®e _é¢f ß/h¼ýFÿ‰VqÜÛ¦Ч ß-ä5GT¹ŠÊ†ì!²òNÞr>⟭6S'¿šHûLéPK:ºe#PKv¬<META-INF/manifest.xmlµ•ÍnÂ0 Çï ocean tirex-0.7.0/example-map/mapnik-example.conf000066400000000000000000000016451412262531100206410ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Konfiguration for Mapnik example map. # # /etc/tirex/renderer/mapnik/example.conf # #----------------------------------------------------------------------------- #----------------------------------------------------------------------------- # General configuration #----------------------------------------------------------------------------- # symbolic name of this map name=example # tile directory tiledir=/var/cache/tirex/tiles/example # minimum zoom level allowed #minz=0 # maximum zoom level allowed maxz=4 #----------------------------------------------------------------------------- # Backend specific configuration #----------------------------------------------------------------------------- mapfile=/usr/share/tirex/example-map/example.xml #-- THE END ------------------------------------------------------------------ tirex-0.7.0/example-map/ocean.gpkg000066400000000000000000005600001412262531100170140ustar00rootroot00000000000000SQLite format 3@ . 'ØGPKG .S`ö,ûö  ¥ ¥h\=mUndefined geographic SRSNONEundefinedundefined geographic coordinate reference system[ÿÿÿÿÿÿÿÿÿ;kUndefined cartesian SRSNONEÿundefinedundefined cartesian coordinate reference systemƒ¡f +„iWGS 84 geodeticEPSGæGEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]longitude/latitude coordinates in decimal degrees on the WGS 84 spheroid ­­Q  =oceanfeaturesocean2021-09-21T11:50:30.098ZÿLÀUg‚‚Ê@f€@Vxxxx{æ ÷÷ ocean ÷÷ ocean õõ ocean ÷÷ ocean ååoceangeomPOLYGONæ òò  oceangeom ÷÷ ocean     òëú Ý·|  š h ¶ k~ ¸w8ë¤Mòž‚JQ-„!triggergpkg_tile_matrix_zoom_level_updategpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_zoom_level_update' BEFORE UPDATE of zoom_level ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'update on table ''gpkg_tile_matrix'' violates constraint: zoom_level cannot be less than 0') WHERE (NEW.zoom_level < 0); END‚<Q-„triggergpkg_tile_matrix_zoom_level_insertgpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_zoom_level_insert' BEFORE INSERT ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'insert on table ''gpkg_tile_matrix'' violates constraint: zoom_level cannot be less than 0') WHERE (NEW.zoom_level < 0); END? S-indexsqlite_autoindex_gpkg_tile_matrix_1gpkg_tile_matrixƒC --†9tablegpkg_tile_matrixgpkg_tile_matrix CREATE TABLE gpkg_tile_matrix (table_name TEXT NOT NULL,zoom_level INTEGER NOT NULL,matrix_width INTEGER NOT NULL,matrix_height INTEGER NOT NULL,tile_width INTEGER NOT NULL,tile_height INTEGER NOT NULL,pixel_x_size DOUBLE NOT NULL,pixel_y_size DOUBLE NOT NULL,CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level),CONSTRAINT fk_tmm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name))ƒ 55…_tablegpkg_tile_matrix_setgpkg_tile_matrix_set CREATE TABLE gpkg_tile_matrix_set (table_name TEXT NOT NULL PRIMARY KEY,srs_id INTEGER NOT NULL,min_x DOUBLE NOT NULL,min_y DOUBLE NOT NULL,max_x DOUBLE NOT NULL,max_y DOUBLE NOT NULL,CONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),CONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id))G [5indexsqlite_autoindex_gpkg_tile_matrix_set_1gpkg_tile_matrix_set „77‡tablegpkg_geometry_columnsgpkg_geometry_columnsCREATE TABLE gpkg_geometry_columns (table_name TEXT NOT NULL,column_name TEXT NOT NULL,geometry_type_name TEXT NOT NULL,srs_id INTEGER NOT NULL,z TINYINT NOT NULL,m TINYINT NOT NULL,CONSTRAINT pk_geom_cols PRIMARY KEY (table_name, column_name),CONSTRAINT uk_gc_table_name UNIQUE (table_name),CONSTRAINT fk_gc_tn FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),CONSTRAINT fk_gc_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id))I ]7indexsqlite_autoindex_gpkg_geometry_columns_2gpkg_geometry_columns I]7indexsqlite_autoindex_gpkg_geometry_columns_1gpkg_geometry_columns //[tablegpkg_ogr_contentsgpkg_ogr_contentsCREATE TABLE gpkg_ogr_contents(table_name TEXT NOT NULL PRIMARY KEY,feature_count INTEGER DEFAULT NULL)AU/indexsqlite_autoindex_gpkg_ogr_contents_1gpkg_ogr_contentsƒ''…wtablegpkg_contentsgpkg_contentsCREATE TABLE gpkg_contents (table_name TEXT NOT NULL PRIMARY KEY,data_type TEXT NOT NULL,identifier TEXT UNIQUE,description TEXT DEFAULT '',last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),min_x DOUBLE, min_y DOUBLE,max_x DOUBLE, max_y DOUBLE,srs_id INTEGER,CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id))9M'indexsqlite_autoindex_gpkg_contents_2gpkg_contents9M'indexsqlite_autoindex_gpkg_contents_1gpkg_contents‚55ƒ)tablegpkg_spatial_ref_sysgpkg_spatial_ref_sysCREATE TABLE gpkg_spatial_ref_sys (srs_name TEXT NOT NULL,srs_id INTEGER NOT NULL PRIMARY KEY,organization TEXT NOT NULL,organization_coordsys_id INTEGER NOT NULL,definition TEXT NOT NULL,description TEXT) Ï 8¹ b„(Ü€4Ø8g _ ™ £ & ‚tableoceanoceanCREATE TABLE "ocean" ( "fid" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "geom" POLYGON, "ScaleRank" MEDIUMINT, "FeatureCla" TEXT(30))‚YU-„;triggergpkg_tile_matrix_pixel_y_size_updategpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_pixel_y_size_update' BEFORE UPDATE OF pixel_y_size ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'update on table ''gpkg_tile_matrix'' violates constraint: pixel_y_size must be greater than 0') WHERE NOT (NEW.pixel_y_size > 0); END‚IU-„triggergpkg_tile_matrix_pixel_y_size_insertgpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_pixel_y_size_insert' BEFORE INSERT ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'insert on table ''gpkg_tile_matrix'' violates constraint: pixel_y_size must be greater than 0') WHERE NOT (NEW.pixel_y_size > 0); END‚YU-„;triggergpkg_tile_matrix_pixel_x_size_updategpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_pixel_x_size_update' BEFORE UPDATE OF pixel_x_size ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'update on table ''gpkg_tile_matrix'' violates constraint: pixel_x_size must be greater than 0') WHERE NOT (NEW.pixel_x_size > 0); END‚IU-„triggergpkg_tile_matrix_pixel_x_size_insertgpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_pixel_x_size_insert' BEFORE INSERT ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'insert on table ''gpkg_tile_matrix'' violates constraint: pixel_x_size must be greater than 0') WHERE NOT (NEW.pixel_x_size > 0); END‚YW-„9triggergpkg_tile_matrix_matrix_height_updategpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_matrix_height_update' BEFORE UPDATE OF matrix_height ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'update on table ''gpkg_tile_matrix'' violates constraint: matrix_height cannot be less than 1') WHERE (NEW.matrix_height < 1); END‚HW-„triggergpkg_tile_matrix_matrix_height_insertgpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_matrix_height_insert' BEFORE INSERT ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'insert on table ''gpkg_tile_matrix'' violates constraint: matrix_height cannot be less than 1') WHERE (NEW.matrix_height < 1); END“‚TU-„1triggergpkg_tile_matrix_matrix_width_updategpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_matrix_width_update' BEFORE UPDATE OF matrix_width ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'update on table ''gpkg_tile_matrix'' violates constraint: matrix_width cannot be less than 1') WHERE (NEW.matrix_width < 1); END‚DU-„triggergpkg_tile_matrix_matrix_width_insertgpkg_tile_matrixCREATE TRIGGER 'gpkg_tile_matrix_matrix_width_insert' BEFORE INSERT ON 'gpkg_tile_matrix' FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'insert on table ''gpkg_tile_matrix'' violates constraint: matrix_width cannot be less than 1') WHERE (NEW.matrix_width < 1); END ´ }´…ŽqŠVGPæ€fÀ€f@Ë‚‚gUÀ{xxxxV@yé€f@ãº,ïyàQ@?yX¨5Wf@®´5ËÅQ@u¬‹Ûè\f@³Þ«v±Q@€f@=–ð8»´Q@€f@§æ&=Q@4Sf@}xxxYQ@ÿÿÿÿ+÷e@wxxxžwQ@›´e@}xxxÊsQ@ƒNe@{xxx¯…Q@D@e@{xxxAiQ@$Ze@wxxxX@Q@€|2e@wxxxá+Q@¾úd@{xxxÃdQ@€¾d@wxxx¯]Q@®d@wxxx=jQ@€îHd@{xxxhQ@d@yxxxu[Q@€­öc@{xxx¯mQ@’úc@{xxx{œQ@ïßc@yxxxð¶Q@€8 c@yxxx|ÁQ@c@yxxx`µQ@>Ëb@}xxxIæQ@°b@}xxxF R@€úŽa@{xxxÖ5R@€»da@yxxxR@Ö{a@yxxx²ÞQ@~Ga@yxxx«çQ@í/a@yxxx¹ÕQ@€üñ`@yxxxhéQ@€r»`@{xxx4ØQ@€ˆ`@{xxxþôQ@=i`@}xxxرQ@€ê6`@yxxxÔËQ@¹`@yxxx2þQ@€§!`@yxxxüR@ì`@{xxxòAR@¾_@yxxx©cR@TX_@wxxxQcR@Ð^@}xxx„nR@ØÌ^@wxxx¢=R@IÁ]@yxxx(GR@¯±]@yxxxeR@Xä\@{xxx¨oR@~\@}xxx‹eR@ça\@yxxxêTR@?A\@{xxxþ}R@£\@{xxxâqR@÷¨[@wxxx ‚R@›Y[@wxxxÿŠR@¯‰[@}xxxúR@à1\@wxxxƒÁR@¬x\@yxxxtÔR@˜ˆ\@yxxx¹õR@7U\@}xxx² S@òÄ[@{xxxê,S@Ú [@{xxxÃ-S@^ÏZ@yxxx2S@¾Z@yxxxÓ=S@ -Z@{xxx¡GS@E„Z@yxxxfWS@‚Z@{xxx"lS@kY@yxxxÞQS@BBY@yxxx¡6S@Ÿ0Y@wxxxS@ »X@wxxxS@i+X@yxxxúR@ ÷W@}xxxoS@ÿNW@wxxxS@¥9W@wxxxöðR@¥V@wxxxoèR@4V@yxxx®ÈR@®ÊU@wxxxíÆR@Ÿ€U@wxxxäœR@¡´U@{xxxn{R@ð)U@yxxx sR@T@{xxxàuR@¶ T@}xxxöhR@'T@wxxxÆ$R@`T@}xxxzïQ@¼éS@{xxxõR@édS@{xxx“R@ÌùR@{xxxi÷Q@üS@yxxxAÉQ@ÒR@yxxxôÔQ@¾ëR@{xxx³R@ÊR@yxxx26R@2ªR@wxxx¼4R@¹R@{xxx;R@xFR@yxxxÜQ@˜™R@yxxxé§Q@†fR@yxxx¤gQ@èuR@wxxx DQ@æ»R@{xxxÉ>Q@ žR@}xxxˆQ@VÃR@{xxx$ðP@ð‹R@yxxxªÑP@ózR@}xxxÿ±P@†4R@yxxx¡P@R@{xxx†ŠP@íÑQ@yxxxõ“P@GOR@{xxxÜîP@¿jR@wxxx“Q@$$R@{xxxÎ@Q@®2R@{xxx€˜Q@R@{xxx?ÅQ@GöQ@wxxx§ÙQ@ó2R@}xxxŽ R@š%R@wxxx'1R@+|Q@yxxx BR@LQ@{xxxt5R@‘"Q@yxxxGûQ@v¬P@{xxxUÁQ@e®P@wxxx׬Q@¢ÐP@wxxxèzQ@‡»P@{xxx’\Q@©Q@wxxxIVQ@‡ Q@{xxx´HQ@‘KQ@yxxxà&Q@Æ Q@{xxx`Q@ׇvGPæPWG@ R^K@ªYB@<†G@4ŽH@"¤D@ ÐJH@ŠçD@ ¿G@L~E@¢ËG@„ÔE@PWG@þMF@ „ÖG@ÒF@  RH@4çF@øŒH@üÿÿÿ!3G@ ^I@øMG@ –˜I@<†G@ dJ@üÿÿÿÿfG@x…J@2mG@FœJ@üÿÿÿ G@ <…J@*¡F@pJ@üÿÿÿI´F@¨I@üÿÿÿŸF@ ¦£I@èAF@ "'I@NF@ h+I@^$F@ rI@üÿÿÿûF@Ø«I@ ‘E@2@J@üÿÿÿieE@–XJ@Ò8E@ 9J@~E@R@J@DäD@HhJ@X‘D@VuJ@ïD@`ÜJ@üÿÿÿÇE@ K@’ÆD@ R^K@¾yD@ ÜíJ@ÈPD@ (uJ@2pD@ØXJ@üÿÿÿOD@ЭJ@ÖüC@ ðŒJ@6¥C@ÂðJ@ÞyC@$ÞJ@üóB@ úõJ@v™B@ ¸éJ@ˆ{B@ Î!J@ªYB@ ÐkI@¼oB@îI@ö¯B@™H@œÊB@qH@)C@¢mH@bhC@ –œH@üÿÿÿO†C@š²H@"³C@ÞÈH@üÿÿÿ‰D@J2I@Ö D@Þ I@^CD@ 8ÏH@TID@ŽH@"¤D@Ocean õõ ocean8P@wxxxNQ@†ÀO@{xxx‚bQ@ jFN@{xxxàuQ@ ÚN@wxxxÁ`Q@ ø‰N@{xxx¬;Q@„øM@yxxxKQ@ ¬fM@wxxxÙ7Q@ –¨L@{xxxPQ@ª¸K@yxxxŒQ@ ø\K@yxxx¯Q@2¾J@{xxxZ Q@ `@{xxxd‹Q@$K?@wxxx…œQ@`*<@yxxxWËQ@À^:@}xxx˜¾Q@ô‹8@yxxxlÁQ@7@{xxxgŒQ@ä`5@{xxxÏQ@ /3@}xxxÊsQ@˜o0@{xxx…#Q@È…-@yxxx[óP@ðÿÿÿo·(@wxxxÅwP@0%@}xxx”P@h!@öððð¹O@ Ц@úððð˜MO@ð÷@öððð>ûN@ °;@öðððØÓM@à©@öððð6JM@2@úððð M@ Ã @òððð 'M@  ¶$@òððð»M@ø &@îððð„lM@€“'@òððð€·L@@)@òððð@&L@ Àâ)@öðððB­K@ 3,@òððð&³K@XU-@öððð¨L@ €Â/@öðððJ L@œr0@òððð2„L@LÔ0@îððð[M@ŒÞ1@úððð yM@¨É2@öðððl N@ÜÔ1@òðððpPN@¤1@öððð ªN@Ù1@öðððà^O@hÇ3@îðððúÌO@¤^5@}xxxòP@¨65@{xxx$AP@ì.6@wxxxÌmP@Lç7@{xxxéP@LK9@wxxx­aP@èe9@wxxx›FP@»8@wxxx79P@Tq6@îððð çO@<‰5@òððð>—O@05@òððð²LO@øÿÿÿ‹5@òððð8ÙN@„R5@öððð"[N@xJ6@òððð1N@¤Þ6@òðððJëM@(8@úðððFN@øÿÿÿ[A:@öððð25N@ô<@òðððb?N@ =@òðððˆN@øÿÿÿ'û;@òðððλM@ó:@îððð¸M@DÝ9@öððð(ÍM@øÿÿÿ³š8@òððð’ºM@W7@òðððê–M@ @úððð‚ÂJ@àÿÿÿ¿¾@îððð¸ÞJ@ ðf@öðððÂ×J@Ÿ@úððð¬¼J@ L@öðððHÀJ@Ó@òððð´ŠJ@€€¤@òððð\ÎI@€`… @úððð6«I@€ @òðððò‘I@À9ú?òðððxI@@kõ?òððð<I@þÿÿÿ¨ï¿îðððj«H@ÿÿÿ?ïþ¿öðððTâH@ÿÿÿ?Ýù¿öðððnQH@Àÿÿÿ¿] ÀöðððZrH@àÿÿÿ^Àúððð€VH@àÿÿÿ_÷Àöððð0ùG@€ÿÿÿŸ´ÀòððððÇG@€ÿÿÿ?ÎÀöððð2‡G@Àó¿òðððØG@€ÿÿÿ¿%ö¿úðððØF@€ÿÿÿ¿kþ¿öðððµE@à# ÀîðððJ¹E@ dÀîððð–²E@°¥ÀöðððrÈE@ÀÿÿÿŸÀöððð ÇE@àÿÿÿ¯éÀòððð¾ÞE@ðÿÿÿ'É"ÀöðððZ‚E@ø!ÀòðððÔJE@ðÿÿÿÏ"ÀîðððªïD@àÿÿÿGû!Àöððð‚ÄD@à”!Àòðððˆ–D@àÿÿÿ‰!ÀîðððN`D@`ô!ÀîðððTD@¸"Àîððð˜ßC@Ðä"Àòððð ±C@ðÿÿÿ #ÀòðððV]C@àÿÿÿ/“"ÀöðððÔ,C@àÿÿÿ®!Àöððð!C@ðÿÿÿÿ}!ÀöðððTÒB@ðÿÿÿ/Ì!Àöððð(nB@àÿÿÿÿà Àîððð<|B@ lÀîððð>jB@àÿÿÿÐÀîðððx‹B@Àÿÿÿ¯Àòððð wB@àÿÿÿ_òÀîððð.B@àÿÿÿ/wÀòðððÄB@àÿÿÿ‚Àúððð$øA@ÀÿÿÿûÀòððð€(B@Àÿÿÿ¿yÀîððð¶UB@€ÿÿÿ_S ÀòðððFSB@à+Àúððð8UB@€ÿÿÿ?÷¿úððð¨·B@€Þå¿îððð&ÑB@åÝ¿îððð`$C@€¼?òðððx]C@üÿÿÿÕÑ¿öððð¢¦C@P»?úðððÒD@ç?îðððÂUD@€ïé?öðððÒ€D@@@¼@îðððà›D@@àP@öððð ñD@@€ã@úððð~;E@@àÍ@öððð”ˆE@ @:@òððð²E@@@öðððpE@@À½@îðððÂ×E@ Pg@òððð&áE@€Û @òðððŒF@ Ç!@òðððÒ-F@°g#@öððð–F@pf$@îððð¶ôE@0%@úððð,vE@@b&@òðððr,E@  6(@îðððÙD@ ¸Æ)@öðððVŸD@˜A+@úððð —D@,@öðððœcD@h-@òðððVLD@8ÿ-@öðððD@ÐÓ.@úðððD@p/@öððð–ÄC@ü0@òðððdzC@ ÀÈ/@îððð_C@ P`/@öðððlC@@^/@öðððFóB@à0@òððð"ýB@”¢0@úðððîjC@Œ 1@öððð‚rC@è+1@îðððLµC@àr0@îðððÂäC@¤Þ0@öðððŽ7D@øÿÿÿ½1@öððð|"D@$K2@îððð¼æC@ôz2@öðððŽD@p`2@öðððx,D@ð„1@öððð4oD@üÈ0@öðððð•D@PÇ/@îððð2ÄD@øÿÿÿ+0@îððð²ÝD@ HÚ/@òðððüùD@ I.@òððð0ùD@P,@îððð\`E@È +@öððð*ÊE@¸-)@îððð¦ F@ðÿÿÿß…(@öðððÎKF@ Ä(@úðððHpF@H¨(@îðððÒ¯F@H*@òððð<ÝF@à+@úðððšÊF@ n+@öðððú¾F@è[+@îðððè¼F@pP+@úððð|F@ç+@öðððœeF@ x„,@îðððàœF@¨Í-@öððð®ˆF@0×-@öðððx]F@¨À.@úððð 'F@XY.@öðððF@ø0@öðððà¿E@î0@úðððÔ™E@ˆ‚1@îðððÀkE@8s2@òðððd@öðððG@¸¿>@úððð”IG@à¬?@òðððXYG@|¾?@òððð )G@<¦@@òðððD G@FË@@îðððôëF@ ÂP@@öðððhÁF@ &:@@öðððܨF@Æ@@öðððhƒF@Ê©@@úððð>GF@ öð@@òððð6-F@¼žA@öðððFwF@ Ø*B@òðððpF@ ÚCB@öððð»F@ JÁA@öðððn³F@¬‚A@úðððNÒF@ ,{A@òðððì!G@ péA@öðððœQG@DaB@îððð`XG@j¶B@öðððÊG@¢C@öðððŒG@†C@úðððª G@è’C@öððð¨„G@<ÖB@öðððpPG@ ÖC@òðððÆG@ ³B@öððð¶²F@vVB@òðððBžF@ÅB@úðððSF@WC@öðððÊ"F@@úC@öððð¢¶E@&)D@öðððjE@pD@òððð°€E@ ºD@òððð‚QE@ ÚD@îððð6úD@ ìÆD@öððð‚ÃD@Î/D@úððð°€D@ ÁC@öðððŒD@ ‚,C@úððð`xD@ àtB@öðððâ©D@x•A@öðððE@´Á@@öðððbE@Ž,@@òððð0ÝD@X%?@îððð*ŠD@x==@òððð›D@ôÑ<@îðððÔ9D@´G;@úððð¶4D@À+:@îðððJºC@Î:@îððð"}C@|Q:@öððð”C@„ ;@úððð–ÒB@,¤;@òðððFSB@ »<@òððð”UB@8³=@öðððhB@ d>@öðððž B@(Ÿ>@îððð¶UB@ ³?@öðððhQB@0A@@öððð² B@ rA@öðððB@t[A@öðððÂdB@ ŠÆA@òðððRGB@˜B@öððð:RB@äA@îððð&"B@0B@öðððèA@ ÚóA@îðððn³A@ÎÿA@òððð|QA@fýA@öðððMA@ ¼½A@òðððØò@@ $A@òððð’Š@@œŒA@òðððD‰@@ NzA@öðððÜh@@X`A@öðððD@@ z>A@äáááå˜?@@ 8@@ìááá ?@ ¤@@äááá…@?@äõ?@ôáááéì>@(°?@äáááák?@ú>@Üááá1Œ?@X>@Üáááw?@ø®=@ìááá¹-?@Üé<@ôááá¡Ü>@Ts<@äááá?@øÿÿÿ+u;@ôááá!P?@Ì~:@äáááÑ“?@8*9@Üááá?@Ìë8@ìááá%ä?@øÿÿÿoí7@îððð@@ð›7@öðððê@@¤<7@úðððt@@Xå6@öðððªP@@‹5@öðððâj@@ÀÚ4@îðððlY@@T"4@òðððp@@Ò3@Üááá]¾?@øÿÿÿ£ 4@ÜáááEú>@ø’3@äááá„>@$3@äáááB>@h2@ôáááUÁ>@˜œ0@ìááá,?@ˆm/@äááá5^?@Ð}.@îðððÞ @@`Ö+@öðððZ@@¨**@öðððpo@@  S)@úðððnd@@ Pú&@úððð|@@7&@öðððz¤@@¸¶%@öðððTá@@ ð­$@îðððˆã@@¨L$@öðððD)A@¨%@îððð iA@á%@òðððlØA@È/%@òððð8øA@@3%@úðððn3B@@3&@öððð&rB@ Ð&@òðððºŠB@ ˆ\$@öððð [B@k$@òðððdœB@(#@òðððÀ«B@˜× @îðððxB@@Ðò@öðððNpB@àÿÿÿoR@òðððŽB@@0 @òðððB@@ÐG@öððð¦ZB@@C@îððð¬mB@€ K @òðððFcB@€x÷?îðððxLB@ à?òððð‚%B@üÿÿÿMÀ¿òððð°ðA@ÿÿÿVó¿òðððrÚA@€ÿÿÿÿ[Àúððð‚”A@€ÿÿÿŸÕÀöðððÜ•A@à Àòððð ²A@Àÿÿÿ/]ÀòðððD©A@pÆÀúðððžßA@àÿÿÿO¸Àúððð:àA@àÿÿÿ/úÀöðððœ‘A@àÿÿÿ_¦Àöððð A@àÀúððð*Ø@@ðÿÿÿ—P!Àîððð²@@ø™"Àúððð8G@@àÿÿÿŸÞ"ÀîðððÐ@@ðÿÿÿ¡#Àôááá]+?@àÿÿÿ/!#ÀÜáááéì=@ðÿÿÿ—Ì$Àäááá%=@@Í%ÀìáááñÒ<@¸`'Àìáááñ#<@ðÿÿÿÏ<)Àìááá±<@ G*Àìááὡ;@0Œ+ÀìáááQœ:@8á,Àôááá ?:@ðÿÿÿš-ÀäáááÅ 9@0¦-Àìáááa9@ðÿÿÿ·-.Àìáááƒ8@ðÿÿÿÚ.ÀäáááÑY8@÷/Àäááá·7@S0Àäáááu7@ðÿÿÿ C0ÀäáááÉ«6@øÿÿÿÏ–0Àäááái&6@$ù0Àäáááà5@øÿÿÿ71Àäáááýi5@<1ÀÜáááÙý4@L‰0ÀÜáááA4@G0Àìááá‘4@¬`0Àôáááé•3@ÄA0Àìááá¡3@ðÿÿÿs%0Àôááá©2@øÿÿÿ?E0Àìááá©(1@¸Œ0Àìáááeª0@ðÿÿÿ‡v0Àìáááu 0@ðÿÿÿ_³0ÀèÃÃÃû9/@øÿÿÿg/1ÀØÃÃËÒ-@øÿÿÿÿŸ1À¸ÃÃÃ[q-@H 1ÀÀÃÃÃûº,@øÿÿÿ³¶0ÀÀÃÃÃs,+@l×0ÀØÃÃÃSI*@øÿÿÿk­0ÀÈÃÃÃÓÀ(@ 0ÀÐÃÃÃCS(@ðÿÿÿO0ÀÀÃÃÛæ'@P0ÀØÃÃó˜'@øÿÿÿÏ0ÀàÃÃÃk'@T/ÀÈÃÃÃæ&@¸B.ÀØÃÃÃ&@àÿÿÿÏ­-ÀàÃÃã¼%@èb-ÀàÃÃÃËK%@àÿÿÿÏ(-ÀàÃÃÓi$@©,ÀÈÃÃÃË$@àÿÿÿß%,ÀØÃÃËÁ#@ðÿÿÿÏ^+ÀÈÃÃÃ#ù"@8~*ÀàÃÃÃ#Ê!@àÿÿÿ?*ÀàÃÃëO @àå)À°‡‡‡g)@0Û(À€‡‡‡·@àÿÿÿ—j'ÀÀ‡‡‡Gh@àÿÿÿ§à&À ‡‡‡G@àÿÿÿ߇%Àp‡‡‡—‡@ðÿÿÿ§Ó#À‡‡‡wW@àÿÿÿw"ÀЇ‡‡L@àÿÿÿåÀ€‡‡‡÷c@Àÿÿÿ/ÙÀ‡‡‡÷l@àÿÿÿ_ÀÀ‡‡‡÷Q@Àÿÿÿ_À°‡‡‡‡É@ÀÿÿÿoVÀ€‡‡‡ñ@€™À°‡‡‡×£@ÀÿÿÿÿÀ€‡‡‡·¯@€ÿÿÿ} À°‡‡‡wç@€ÿÿÿ?ÙÀ ‡‡‡çñ@€ÿÿÿoÿ¿°‡‡‡Ï@€ÿÿÿñ¿€‡‡‡ø@€>à¿p‡‡‡GW@@öð?‡‡‡·®@@Øý?°‡‡‡7‰@€ ˆ@À‡‡‡§@ ˜ @‡‡‡@€M@‡‡‡· @€"@€‡‡‡j@ s@p‡‡‡÷„@ à—@p‡‡‡W@àÿÿÿßÊ@ ‡‡‡î@ °T@‡‡‡gÓ@@0Ù@°‡‡‡‡@(!@‡‡‡'@Pú @ ‡‡‡'ó@ `}!@ ‡‡‡G`@ på!@ ¯*@Ï"@ OÏ @0—#@…@pL#@@o4@ ˆœ"@qò?`ü"@À^ð?(•"@{xxxªÐ? Ø"@‡‡‡‡ìÝ¿©!@€ÄÃÃC1é¿  ˜!@âáááéñ¿€Ï"@€ðððp8À ð!$@ ñððpÒÀ ø/&@àðððåÀðÿÿÿ‡Ô'@xxxH/À X](@PxxxX1Àðÿÿÿ¥(@€xxxønÀxt(@xxxè5À àt)@€xxxؽÀÀÝ)@pxxxHkÀðÿÿÿy*@(<<*@ <<<äŒ#ÀPÆ*@8<<<„Ã$À €_+@ <<<Œz%À 8z+@8<<<´œ&ÀxD+@(<<<ü(À8 *@0<<<Ôû(À z)@(<<<ÔJ*À)@H<<<¬+À øY(@0<<<,ê,ÀX?(@H<<<ìÅ-À°Ž'@H<<<¬š/À¸G'@ n®0Àøw'@$fO1À —'@ Î2À ¨7)@$¶ 3À `§)@$n®3Àðÿÿÿw´*@ Žá4Àȼ+@$µ5À ðƒ,@–6À xÅ,@:ª6À ðÐ,@ ~Ü7À|-@²f9À¸ú-@& :À Èk.@b;À04/@ZÔ;ÀXX0@•<À1@$^â=À„1@ ã=À ‘1@(æ»>ÀÀ82@ ~«?Àx?2@û7@Ààì1@KO@À@2@¥@À˜>2@ð@À¤`2@ ‡AÀ¸l2@ ÇAÀôÚ2@í9AÀ|13@E@&?ÀxŸ>@–n>ÀÔæ>@ ë=À`S?@ i=Àh…?@D=À @@ºÂ<À,;@@.O<ÀHJ@@vz;À@j@@ À:À Bu@@b9:À „T@@"(:ÀI@@$N¼9À´@@¢]9À¢A@Ó8À6…A@$Ž|8À¶ºA@‚!8ÀÂÍA@ú¶7À š¯A@$&‹7ÀVÄA@>7À ÈA@*6Àf±A@ö%6À ίA@^Ù5À Š–A@ 6C5À ØYA@Z4À ¬dA@$ÒÊ3À h™A@¢3À ÀòA@¾Ù2À $B@ úª2Àž´B@6˜1ÀèDC@ ú1Àò¹C@ ªº0Àn D@ æ0À=D@0<<M@ìááá¹k4@8nM@äááá5@ (¤M@Üáááõl5@ ž¸M@ÜáááÉ´5@4çM@ìááá]M6@pçM@äááá†6@œ¹M@ìáááé¦6@—M@Üáááõû6@Z]M@äááᱎ7@ŒM@äáááY½7@¦³L@ôáááÕÞ7@ 2lL@ôáááÁ;8@Î2L@ìááá™ê8@l!L@ìáááÙ´9@2L@ìáááIã9@,>L@ÜáááM:@Z.L@Üááá9c:@ L@ôááá :@ 4¸K@äáááIn9@ ¸XK@äááá%Ê8@ K@ìááá8@º³J@Üááá$8@ÞIJ@ìáááQ+8@²åI@äáááù8@ öàI@ìááá-I8@.ÊI@ìááá¹<8@ â±I@äááá…ž8@¨ÍI@Üááá59@ hËI@äáááõÊ9@ ¬¤I@ìááá=:@ºI@äááá±ÿ9@8_I@ìáááey9@²gI@Üááá!¿8@ ’TI@ÜáááÙý8@†CI@äáááÑQ9@ ¶I@ìááá™9@ €I@ôááá‘ï9@FI@ìáááÑD:@†I@Üáááq®:@ F¼H@äááá;@\¦H@ôáááùs;@bgH@ôáááq®;@B5H@Üááá9‹<@  H@ôáááIL=@vH@ôááᵆ=@ ¼üG@äáááµ÷=@¸HH@äááá)ë=@|xH@ôááá O>@ØÉH@äááá9ú=@ ¼I@ôááá½#>@2mI@äáááeÎ<@¬ÂI@ìáááÛ;@ê=J@ìááá•’;@ ¿J@ìááááÍ:@ˆ[K@ìáááñx:@¢ÜK@ôáááÙô:@ þ>L@ìááá‘";@ F|L@ìááá5õ:@ Ú²L@ÜáááM»9@PCM@ìáááš9@ ÜÎM@äááá1_9@ ®¿N@äáááí9@ îsO@ìáááÑ59@ô!P@ìááá•:9@Þ—P@ôáááÁj9@PÉP@ôáááɧ8@fÜP@ìáááÅï7@O Q@ìááá ¯7@aVQ@ôáááÅÕ6@FiQ@ìáááAq6@JQ@äááá½6@žQ@ôáááyÞ4@8ËQ@ìáááÍ¿4@](R@Üááá Y5@Å4R@äáááEi4@Š4R@Üááá533@¬GR@äááá¡ë1@2bR@ÈÃÃÃ÷/@iœR@èÃÃÃÓ7-@x§R@ÀÃÃÃø+@[·R@ØÃÃÛw)@ZÙR@ÈÃÃÃË‹'@ÇïR@ÈÃÃÛ™&@TS@ÈÃÃÃ3•$@õ%S@ÀÃÃÃ3È!@ŽbS@À‡‡‡7Ô@>|S@ÐÃÃÃC} @Ì‘S@ØÃÃÃÙ!@$ÌS@ÀÃÃój"@¬¸S@ÀÃÃÃ[#@ËÕS@èÃÃÃë™$@ëöS@ÐÃÃó²$@3÷S@ÐÃÃÓ(@ST@ÀÃÃÃûþ)@îT@¸ÃÃû§+@›T@ØÃÃãA.@ËT@ÈÃÃÃ3È/@²2T@¸ÃÃÃ;ã/@UlT@äáááQM0@>ŒT@ôáááeŒ0@XŒT@ìááá)1@ÌT@ìááὩ1@;üT@Üááá9K2@ÜCU@äááámx3@õŸU@ôáááµ$4@ÂU@äááá)¼4@r¾U@ìááá¹|5@X V@Üáááí±5@ä8V@ìááá±®5@BV@äááá% 6@ÏZV@ìáááAõ5@ïlV@ìáááMÙ5@=vV@äáááå6@w‘V@äáááÔ5@“¥V@ìáááub6@ÀŸV@äáááýË6@²ÚV@äááá½Á6@oõV@Üáááµ,6@ŸW@äááá…±5@NW@ôááá/5@–W@äááá¡©4@EW@ÜáááÕØ3@tjW@Üááá¸3@¡bW@ôáááµ[3@Ë”W@ìááá‰42@%¢W@ìáááÝD1@ŒW@ôááá¡0@¾³W@ÈÃÃÃ+—/@£×W@ØÃÃÓi/@a X@ÜáááEk0@ŠJX@ìááá¡ë0@7fX@äááá©0@ÆqX@ÈÃÃÃs¨-@£†X@ÈÃÃóC+@Ÿ X@ØÃÃÃ{:*@i›X@ÈÃÃó (@î°X@àÃÃóÝ&@DX@àÃÃÃU%@m£X@ÐÃÃÃÙ#@˜X@ÀÃÃÃcî!@›‰X@ÐÃÃï @¾•X@ ‡‡‡%@@ X@ÈÃÃÿ @@¿X@ ‡‡‡g™@CáX@ ‡‡‡GW@4ìX@€‡‡‡7\@Y@‡‡‡'Ó@šY@À‡‡‡7!@— Y@Ї‡‡‡7@®#Y@À‡‡‡7 @ƒ,Y@`Or@„QY@¯ @YY@`o@¸¤Y@^Wÿ?CáY@}ó?¦Z@@žŽô?ÞZ@ÀÞ÷ù?³öY@ Ï@)àY@€/C@}ÛY@ /ÿ @AÕY@@O¿ @ÜY@°‡‡‡—±@gØY@À‡‡‡'c@½Y@°‡‡‡—@×Y@€‡‡‡çz@ ‰Y@‡‡‡‡Ú@ágY@Ї‡‡î@AY@À‡‡‡÷d@eY@€‡‡‡‡¯@çY@ØÃÃÃó’ @î÷X@¸ÃÃÃ3f"@=ÎX@ØÃÃÃKv"@ÖÉX@ØÃÃÃÛè#@¦ÞX@¸ÃÃÃ+­%@2Y@èÃÃÙ(@DY@¸ÃÃÃÌ*@ >Y@àÃÃÃÏ*@>5Y@àÃÃÃÓ<)@ùkY@èÃÃÃsF)@r¥Y@ÀÃÃÃ[[(@ÎÅY@ÀÃÃÃsJ&@ÔßY@ÀÃÃó?%@hZ@ÈÃÃÃóô$@âDZ@ÐÃÃà Ò#@æ2Z@ÀÃÃÃ3w"@"JZ@ÈÃÃÃË.!@í™Z@àÃÃÓ #@#ÎZ@¸ÃÃÃc¶$@n[@ØÃÃÃ&@ÎL[@ÈÃÃÃ3Q'@vU[@¸ÃÃÃëÕ*@"8[@èÃÃÃk‰.@?[@äáááI0@)×Z@äáááq°0@R›Z@ôáááíþ1@`jZ@ôáááÍ 3@nxZ@ôááái¾3@Ä­Z@ÜáááI°4@8[@ìáááQ‹5@w![@äáááý´5@Tw[@Üááác5@,h[@äááá5@ôx[@äááá1F4@lœ[@äááá5U4@F²[@Üááá•c5@ÿõ[@äáááÍŠ5@nO\@äááá 6@¤s\@Üááá=Š6@É\@ìááá176@ä°\@äáááé¨6@ù\@äáááMÆ6@R]@äáááÁ7@ ª]@äááá Š8@yå]@äááá»9@P^@ìááá… ;@ H^@äááá <@Ïk^@äááá7<@|^@äááá…=@å…^@ôáááÓ=@9`^@äáááu">@êP^@ôááá«>@y^@Üáááåð>@z^@äááá¯?@ªN^@öðððÜ9@@¶'^@òððð(¯@@^@îððð-A@¯É]@òðððhsA@Šê]@òðððÍA@Æ(^@úððð6 B@­F^@îðððNRB@I¡^@îðððvB@è–^@öððð¹B@†m^@úððð„¼B@´4^@îððð\îB@üì]@òðððø’B@Yº]@îðððX¸B@6¸]@îðððÊñB@Òƒ]@òðððÐC@b]@òððð\]C@¼‚]@îððð™C@Á]@öððð<ŸC@ñè]@úðððäñC@21^@îðððèJD@þh^@òðððxD@ÌŠ^@öððð5D@X^@òðððúÞC@ƒe^@òððð"­C@€C^@òðððÒqC@kˆ^@òððð”C@ˆ·^@öððð–ÐC@_@òðððÊõC@3/_@òðððtÓC@T_@îðððˆÅC@ÀX_@îððð–°C@H_@òððð’kC@4N_@öððð,TC@?_@úððð&EC@–-_@öðððÌ C@Ê>_@öðððføB@^O_@úððð¬ìB@ŸQ_@öððð–ÔB@bd_@öððð4ßB@l_@òðððF÷B@.‹_@öðððæÞB@·_@îððð\qB@…‡_@òðððÎ[B@Ì£_@òððð”ÖA@î—_@îððð”vA@Ÿ_@òðððÞ0A@¼Ø_@òðððÔ;A@ó`@öðððìpA@€í"`@òððð€‰A@€ü.`@öðððØÏA@€¼.`@öðððTcB@Ð&`@òðððL¶B@€0 `@öðððPMC@"ò_@öðððt…C@¬Ø_@öðððDšC@"à_@òðððf¨C@%â_@öðððÒßC@ìý_@òððð4D@€E`@öððð>D@V `@òððð=D@&`@öðððªSD@’6`@òðððòoD@\5`@òðððàËD@€è>`@öðððl÷D@€ÍL`@òðððÊ"E@€öX`@îðððE@ó]`@òððð®EE@æˆ`@îðððZ£E@€`@òððð&eE@€.±`@öðððÐfE@ÓÛ`@öðððì±E@€}ð`@òðððŠýE@˜a@îðððN‘F@Ga@îðððZ&G@ÀQa@úðððæ~G@€üa@úððð8H@€ka@îðððÂI@€“a@òðððžI@#¬a@öððð„J@€ «a@òðððlŠJ@Ù|a@òððð8K@ÀYa@îðððŠK@EEa@öððð˜ßJ@1&a@òðððüJ@€ta@öððð6LK@€ ä`@öðððV\K@¬^a@îððð6ŠL@UÆa@öððð„M@˜/b@òðððªM@p‘b@úðððþ“M@¹b@öðððÞÒM@€Ñêb@úðððv¿M@€€èb@öðððäbM@ûc@úðððpM@€gac@òððð‚‘M@€úFc@òðððàM@—c@öððð޶N@€¬éc@öðððâN@€ãd@òðððœDN@€ Ud@úððð.ÑN@€Ehd@îððð :O@€(d@îðððnEO@nud@òðððúN@è;d@òðððÚ*N@€Òd@úððð>§M@€¨Ëc@öðððM@€ï™c@öðððréL@D˜c@öðððž­L@€C}c@öððð:aL@ámc@òððð¸¯K@€½c@öðððF“J@€qc@úðððŒØI@G™c@öððð\€I@gÇc@úðððš÷I@ýÐc@òððð¦yJ@²d@îðððâ˜J@Î d@öððð+K@€ÂCd@öðððjlK@€r6d@úððð‚£K@€&Dd@òððð˜L@€Úad@òðððTL@$fd@öððð¬ÍL@³Ad@òðððXêL@€@d@òðððM@€òfd@úðððö™M@Bqd@òððð"îM@€œd@òððð˜ÜM@âºd@öðððnN@€qÉd@òðððäãM@Ñe@òðððXHN@€–Je@òðððÒïM@ZVe@òðððü)N@€Í„e@îðððŒxN@€Ãµe@öððð|ÒN@€7Òe@òðððháN@€¨+f@îððð¾AO@Ngf@úðððÜ%O@€‘of@öðððÈGO@Ûkf@îðððº|O@]f@òððð4ŸO@Jf@wxxxTP@€)-f@wxxxf&P@¢Vf@}xxx´!P@Æf@yxxxÓ=P@€f@IÜ£ú)>P@€f@öððððîN@€f@òððððL@€f@úððððNI@€f@öðððð~F@€f@îðððð®C@€f@öððððÞ@@€f@äáááá<@€f@ôáááá}6@€f@ìááááÝ0@€f@ÀÃÃÃÃ{&@€f@°‡‡‡‡w@€f@àðððð€¿€f@0xxxxˆÀ€f@0<<<<„&À€f@ŒôP¹M0À9Õüw;mf@´—›Ñ'c0ÀóÑ„lcf@|)¶7q0Àà@MSf@0[¹s½¥0Àkw±¯3Wf@¨·ãG31Àყkf@à¨CÏ0À€f@¼bÊ@0À€f@ â0À€f@‚6À€f@"<À€f@á@À€f@ ±CÀ€f@FÀ€f@ QIÀ€f@!LÀ€f@ñNÀ€f@†‡‡‡‡àPÀ€f@‰‡‡‡‡HRÀ€f@…‡‡‡‡°SÀ€f@‡‡‡‡‡UÀ€f@=ïŒ/.UÀ¥g½êÞHf@¯Ý©CÅUÀ«¥ŸŠÿe@· £‰´ UÀ9‘¶°+§e@†õôÁUÀuÎûCe@p pUÀsé¨ü‰e@ð^ø^.UÀ›£døó,e@ŽŸŸìbõTÀo(PJ©e@™89†ÖTÀyB8ÿTÓd@¡d2Ì÷ÁTÀë…|£d@©+ç­TÀÁ’vd@°¼$XÖ™TÀ…[ 4¶Od@õ,b⃄TÀ»Ë#4d@_–e°lTÀ=Ò—+×#d@Ñ­°}ZRTÀ)ß 9ùc@¢C­à=TÀ38þ‘$ d@{‡ñ¤4%TÀA?8¿îd@YË5i` TÀ3㨼’d@' ‹¾GïSÀs?9†8d@e`2ÌéÊSÀQy¦Qud@mz-gÈSÀsýó:4¦d@ O!»›ºSÀ˶pÝßd@;{Ç“°SÀyB8ÿTÓd@ñÖ© ü”SÀÓÐútÊ—d@y»ÁÔ5ŒSÀ3Äd¿ˆd@ˆ•|¢uSÀ£f÷Úd@gÙF@Î]SÀŸä¨<­od@”¬Mú¹DSÀ»Ë^ od@þQ—æ,SÀ¢/rd@wÛãö SÀ™N¬YTzd@áDç“:øRÀUÇ~‡d@S\2’äÝRÀ/x鮞d@Zˆ+ØÓÉRÀ™O¬Ùž´d@3Ìoœÿ±RÀ9|¥ŸÃd@Ûy·}ê˜RÀÝk”Nfìd@xº^ ‹RÀÅËÀ4ÿd@º^‹ŒtRÀ½sλ1)e@S½ûÁƒjRÀ?~¥_<8e@ÅÔFÀ-PRÀãXæÆ„Ce@_ÔF@›9RÀI?ùîQe@d¿˜xÃRÀ‘ñÚbe@y™Ù0RÀ}Yæžfe@G!íQÀû6Ĥ Pe@å.ÖLÚQÀ­?¹Õ.e@¹zsÁÍQÀíy¦ž e@“ŠeE´¾QÀ­¬;äéd@‘I¾7íµQÀZƒ}©Ãd@1Ë Óè°QÀùá nd@ga ¶)²QÀ{,Š7õzd@Ëž g®QÀUÝnûUd@û4ð§¯QÀ£‘ð]A2d@Ç]lÿŸ¥QÀ“ ³Ðd@Ö7­¦ QÀ‹]võc@CâWQ€QÀ)4aÛÊåc@åŸ2ëfQÀÉw¥ÑÀc@åôe_QÀÉ!PÊô™c@ƒÐFÀYQÀî×À}c@{ °CJQÀW ´Nec@ñ¦ê°6QÀ—¶L-Ic@eÿܼs$QÀcu¥l4c@¬´^ Ç9QÀWqic@^Óãv„8QÀc ¢‚zïb@·!ÿ^}.QÀ1Ì—ë;Äb@eÿܼs$QÀܨ<Þšb@‡·ûÁ+QÀŸ#Š÷$wb@-¦ê°ÝQÀçh•¬Tb@“P•[ÑùPÀ¯¿À‡ý?b@Ñ‹„çPÀ¹®¯öAFb@°VÏH-ÏPÀ5Œ”±/b@·‚ÈŽ»PÀGÓnOø b@ã)Ö¶PÀM+a›úáa@ë×d–³PÀ‡ƒSçäÃa@!nG×´PÀÅHæÆæ™a@!nG×´PÀ57Õõ}a@KVÏÈš¸PÀ?ÇASa@ì˫۹PÀ5‹*‹º.a@ªÔ€-Ÿ½PÀõZ”Ça@µA!U²PÀÿžØRa@…×äPÀÿ4Õ5öû`@÷îhâ­‚PÀKþÌQö`@o´ûAÕePÀõš;œCâ`@p2­>PÀ¿?¹á`@ÿb(nPÀKéV„<Ø`@íÈŽ÷PÀj¥h»`@ÇÚºû’PÀ× Ýò`@]ïhb@™PÀÏ y&™y`@]ïhb@™PÀ5r¬Y`@OA!ÛPÀáPJ‰6`@‰ÅñÊ¥PÀS?y´`@~«$ž±PÀÚnFø_@è–vVϪPÀ~¸_À_@R‚ÈФPÀÖ¾Öäl†_@R‚ÈФPÀNo»|AJ_@‹Ylÿ‘®PÀÊ{/WÓ_@{j}M¨PÀ®X µ)Î^@óª…ŸPÀ[ªë€”^@R‚ÈФPÀvB_íái^@KVÏÈš¸PÀ&Êçu¾7^@C*Ö‚«ÌPÀŽ‹ÝžNõ]@£¨‡ç¯ÑPÀ:þ·ß¥]@ ”ÙŸjËPÀþ ÉðžX]@·‚ÈŽ»PÀ*_¿,]@è–vVϪPÀzƒ@hæ\@UÃoQ­PÀd»|m¹\@]ïhb@™PÀ’"oÖ˜\@db¨/…PÀJªœ÷²f\@1ò¥xPÀÊ8_m7\@”±^‹p†PÀf¡b ï[@*óˆPÀÚ¾Ã[@OA!ÛPÀÎMªë[@UÃoQ­PÀ^f''J[@ã)Ö¶PÀZbŠ5[@ªÔ€-Ÿ½PÀš ÞKÊZ@ªÔ€-Ÿ½PÀžî¯ž‹Z@çÅq]¼PÀŠD 5$:Z@¹,s¹}•PÀvQ†Z@δ`ê~PÀÊIG¢¢ÞY@È„eE\mPÀzw8FµY@?ÿ^”dPÀš_’  eY@÷p·ý;”PÀ°J¿,9Y@‰ÅñÊ¥PÀâ”bŠ–Y@·‚ÈŽ»PÀºVX³öíX@r‹oÐPÀ:c̈«X@ã«$§ÇPÀæ©­žpX@r‹oÐPÀê>ªk¬+X@r‹oÐPÀVAG¢òW@oS26ÙPÀª9 5 ÁW@ ”ÙŸjËPÀ¶þŸ:‹W@ã«$§ÇPÀ†NÜcW@yÀÒeìÍPÀ†èñLò&W@C*Ö‚«ÌPÀªe£1ÄåV@ã«$§ÇPÀî‹æW¨V@°VÏH-ÏPÀ.šs›ëjV@JØä(ÊPÀª9G¢5V@ªÔ€-Ÿ½PÀ:JX3ðV@óª…ŸPÀJ$™ZÿU@a\ ¶öPÀR½2t‡ÞU@KVÏÈš¸PÀž±¥&°U@JØä(ÊPÀj±%ôiU@­(;fÆPÀzŠœ÷F+U@yÀÒeìÍPÀj²øŸñT@Õ€­1ÔPÀB çõ°±T@yÀÒeìÍPÀŽêe'PƒT@9½5/õ×PÀ†æÈpö^T@©*Ö>ãPÀ&SiÄÜ;T@cº˜xøPÀ¢ò<ËõT@ãôQÀ>œçuIÇS@YÏF@hQÀî_mj›S@i@„Ê:-QÀF ÓGœˆS@Šü?EQÀu(FiS@éNø$$^QÀ>òÙ(S@&œ,hQÀàÈð)èR@tö³oQÀv²ÏªuŸR@Û"ÿÞ4rQÀVü#ZwR@w7­&zxQÀ2™ZUR@ÙxT´Ó—QÀzsÅÓ5ER@Ëž g®QÀB‡s›R@Üä6ÁQÀö>’ úQ@…°$žGÕQÀæ µ°äQ@G!íQÀ²lá—ÁQ@y™Ù0RÀ:îv¸¢wQ@o,9ÌyRÀòO@h®-Q@Ù‹„4 RÀFHûÊüP@àC„Ê#÷QÀ΂sáQ@Q[ÏÈÍÜQÀ®ž[Pw;Q@)ŸùÄQÀ®ß^>DQ@ÑL[nä«QÀFHûÊüP@”*&­QÀB_íôP@5¶^ ”QÀ²ÊT)&Q@ú¢Ï<|QÀ⊭ˆ”cQ@EQ—ïkQÀ"™ÚkQ@Où¢ÏOQÀ’<’ ›mQ@ô瑾É>QÀÒ—!cö8Q@}Ž!SüPÀ¢…RùP@}Ž!SüPÀ>/»ü[ºP@¡þܼN÷PÀ¶ßŸ”0~P@ÕS2’ÈïPÀJ‰J?„?P@|ƒÈŽAèPÀR_î¯YP@¥é.õvÚPÀ† öa˜O@4ÒãöÌôPÀ i$A™1O@`û?†WQÀœèr\¶N@7‹”ýPÀŸ‘áwMN@1‘<éìPÀü¸y–;øM@¥é.õvÚPÀ\L_M@Ù>„ÊðÒPÀt,ñ •M@M—vÖaÁPÀ¤¦Ã L@-s9¬PÀ¬Fc<”L@Y®ÁTyPÀ„àüq-L@δ`ê~PÀ„Ã'ÞµK@1ò¥xPÀ¤©Ü_KDK@•/pâtPÀ‚xÎJ@hˆüåyPÀÄ]ûä›NJ@-…eÅîƒPÀDjQåI@Y®ÁTyPÀDSÁwƒyI@åUÏH¢PÀ—¼q`I@KVÏÈš¸PÀœM$A'÷H@ã«$§ÇPÀ„®ípÐ~H@­(;fÆPÀÌôÎë,H@9½5/õ×PÀŒ®Š§Â¸G@ž½5¯‡îPÀ”ZÒˆm@G@Ñ‹„çPÀìiã™&ÜF@4ÒãöÌôPÀTÕƒmÚrF@Í'9LÙQÀ¤ä”~“F@/瑾¤QÀœ0°f'xE@Ïê.u.QÀ¼ƒ¼ÎúD@_Q•[ö&QÀ¤ñ ïuD@»˜øG@«ŒüópQÀ™×>@ú¢Ï<|QÀ˜û>v&=@Ÿ¡°ÃËQÀh¨èU³<@lL[îQ•QÀðõ tþ;@û²ÁÔžQÀP˜®è0ú9@1I¾·ZŸQÀøwÆ3c×8@1I¾·ZŸQÀÈý‹ª7@Ÿu·}Ü¡QÀ(¡ùfÄ‘6@”*&­QÀx—¿ùKì5@ÒÊ SVšQÀˆCÛös5@£` ¶…QÀ—ùf0`4@sxT4AQÀÐ>fB3@§Í© »yQÀè|èU£32@w7­&zxQÀ誉Î1@Ýc¦ìûzQÀàý*æ/@ªQ‚‚QÀð×–>æ@.@ÒÊ SVšQÀPÊ¿šQx-@ªQ‚‚QÀ°JåYvØ*@ L[n¿~QÀÀx¹þÎ(@™óhbNQÀ`ì“[è'@e b¨b©QÀ`¤"d¹¢%@‘I¾7íµQÀ „:¯ë$@1I¾·ZŸQÀ ª3uÞ #@sxT4AQÀÐáÚ‚fù @ߺ ŠQÀàÑí±ø@§Í© »yQÀ ým$úŠ@™óhbNQÀ€’Þa|@û²ÁÔžQÀÀt¾S¡@5ŠeÅ!¨QÀ`î,}dŽ@Çߺ.·QÀ€ïÆš\.@VF!ö¿QÀ€Û çÝ/þ?Y‡È½ÈQÀ‡CÇAÈë?O(»ÔQÀTôâøCÍ¿}„+XXéQÀügãˆå¿ï›vVÏQÀfÿïUºü¿¿³ÁÔ>ËQÀ¶£hPdÀ„+ØÅÒQÀ`Ï/Þ]ÀˆñË«ÞQÀàz,u?%Àå.ÖLÚQÀ†fâ÷)ÀÃrÇwÂQÀ 6²yÀ-^l2¼QÀ`L‚‚À…°$žGÕQÀ@¤ìžªÀG!íQÀ°‹Éí9!À´(;™êQÀPåC¸3"À…°$žGÕQÀ@g–ºo—$ÀUÈo„ÑQÀà¤.v &Àço}ãQÀ€nùƒ''ÀŸ@ç“,RÀPDZ¢F–(ÀqmàÙ@RÀ0t"ຟ*ÀjAç“Q.RÀÐ}"àNÑ,Àý–<é]=RÀ0Üw5Êä.À/À˜xèIRÀ€jbß0À³¹^ ú]RÀDt9w0ÀA¢ PxRÀ«h¿Ð.ÀÛ÷hb\‡RÀxÏ)g/À§$b¨p RÀdù$?¤0Àibl@³RÀ¨—!â…1À#ò.õ’ÈRÀØ:ÆñÝé2ÀÆ5¯£ÜRÀ ?Æñ§4Àµ‹°ëRÀhö‡…95ÀHqàY¼úRÀè5ªfu6Àzš<éFSÀ˜Ò tí7ÀwÛãö SÀp‡\îy9Ào-œ•SÀ¸÷üÁ ):ÀC†Ž!”SÀ@4Í+ƒ;ÀÒìô\ SÀÈÓýá<ÀÀ\êWщWSÀÐù;ë.@À(b¨YjSÀ´0oÉò@À+Xø$eySÀ<&ûCÌ©AÀ¾­MzqˆSÀÈ#^ uãAÀ­’eÅ=–SÀ¸Z.wõAÀâ€gåÅSÀÜì£èÑAÀ›x}ʸÝSÀ(Hºœ5×@À›x}ʸÝSÀŠpóŸ?Àb¡ÙÙ°ÓSÀš\î¯=Àûtà/ÑSÀš\î¯=À‘ ÝvéSÀ¬ 6AA=À÷ Ýö”ÿSÀˆãwÖ¿Œ<Àç1œO(TÀˆã=iÛ>À±î‡u&TÀä×É·'@À­°M4¿1TÀ‡Äsu1AÀ©ñôA†:TÀÀŸr;""BÀ ±M´QHTÀLK€/VCÀú•eÿVTÀ‰oïPÀFk}rÕPÀô S7êçPÀž½5¯‡îPÀ¢ÀÔÅaÛPÀc<ç“ QÀŽßYqhåPÀ/iàÙ2#QÀ€.u™zþPÀ½Q•Ûˆ=QÀ|C#áQÀå Q]UQÀ€2PÔ"QÀ±:J]qnQÀâo§QÀ |†‡QÀ*ÏH ÆQÀû²ÁÔžQÀÜkð·úPÀÇߺ.·QÀ ܼú'äPÀ%2s9CÐQÀæ“^ÐPÀ}„+XXéQÀº`¨ ”ÈPÀ màY®RÀERÀ†J9ýPÀÉ¿˜øU3RÀ°îÍ æ;QÀ,ñj!ARÀŠÊó`QÀ/À˜xèIRÀHœx6¡çQÀ‡|oQRÀœX5RÀ„Ñ©‰6ZRÀX8‡vRÀS½ûÁƒjRÀbQ]Žö¸RÀA¢ PxRÀNÊÔE3SÀL‘|”~RÀ–S]N:SÀ'ÿÞBiRÀüûjBA{SÀ»g¦lw[RÀ4;,ÓSÀQ|T´¼aRÀFFL=îTÀù)œ•§HRÀÜÜH ÿ+TÀéO[î:_RÀÀzp#^TÀæWQwRÀBØó™ªTÀ'ÿÞBiRÀ¶^úÄQøTÀQ|T´¼aRÀdAu™MLUÀéO[î:_RÀ*æåÖò€UÀ‹ý¢Ï%FRÀ ™Ên,ÑUÀ!QkLRÀ¤ï"VÀ,ñj!ARÀ,þ“^†NVÀ1jC£I$RÀb]έ…VÀ%Sø$2URÀÛÔ…êÚVÀ„Ñ©‰6ZRÀà]#¡WÀë{T4*KRÀÂç«ékWÀ·&ÿ^°RRÀ¦²Û?ÐÂWÀéO[î:_RÀblúÄŠXÀçühRÀLÎËlXÀ½¨Mz>dRÀ d#ᛇXÀX¨Mú«MRÀ 1ÕÊÈXÀ–jC#Ü:RÀÃÓÃtYÀ]“Ÿ2Ô0RÀÓE?¼fYÀV•Û–4RÀˆ(|¸ºYÀ]“Ÿ2Ô0RÀáw—;šëYÀ_Rø$ (RÀ%èÔE ÕYÀ‘ñê³WRÀÚsú„@ÇYÀ³;­&ˆoRÀÓo]Îæ¢YÀÛ÷hb\‡RÀ¶>I,PYÀ¯P[î_ŒRÀx4²Õ0YÀŸvGó¢RÀqˆExYÀÉàäD¸RÀÚòH`P)YÀ_ÏÈÛÓRÀjZqYÀ#ò.õ’ÈRÀU§-xŒ×YÀ•‹ÈË¿RÀçúå–8ZÀ._ÏHI½RÀ/±g¥‹‰ZÀ#ò.õ’ÈRÀ¾1UÌãZÀÁ´$žUÌRÀËFÇÁ-[À5 ªÆºRÀíU;¬>„[Àibl@³RÀn³Ê.µÐ[ÀÓËom›RÀxÌ-$\À• zó<®RÀÞNdH‚<\ÀÛy·}ê˜RÀšÞ&>S\Àuy·ýW‚RÀDÎío|\À}¥°CGnRÀ~¦¹aÁ\À⥰ÃÙ„RÀ»–¨ Ø ]À×8p#RÀÓ¶-8^]Àuy·ýW‚RÀG= É«]À¯P[î_ŒRÀí)k¼öì]ÀqŽeÅ/ŸRÀÑôš¶D^Àiàd²¡RÀ4²VNú£^À§$b¨p RÀ`°¹W¼_ÀqŽeÅ/ŸRÀïë&¸»Y_Àiàd²¡RÀK©âóÿ¸_ÀÓËom›RÀ6\+Ǿ`À7·ÁÔ'•RÀ”Íh±¼1`À:øhâîRÀ¥éP&œ]`ÀqŽeÅ/ŸRÀo<¸:ˆ`À!Åñæ“RÀÒ¾WfÜ·`Àblÿ­œRÀ8–ûVÌÍ`À¤ãºš©—RÀ‰‡ÜÝæ`À!Åñæ“RÀÜ‚M¹ aÀiàd²¡RÀ Ë.Ê20aÀËŸvÖ}¯RÀP 9aq[aÀ_õË+оRÀàs.°†aÀû zsÏÄRÀÞD¦Áp´aÀ+ vVÆRÀÆ&!VkÙaÀ†±‡g^ÖRÀJÒË N bÀ²ÚãöèâRÀÍhÈ£bÀ÷J!–ÍRÀpX·RyFbÀíÝ€-àØRÀJ=ÏúßObÀÞ@†sïRÀº­ È—DbÀzš<éFSÀPªoQWCbÀ›Vø$SÀž¿v™sbÀ1k¦l`%SÀnV™ó—bÀaÕ© ²:SÀ­øÀbÀaWø$@LSÀŽÙhT®êbÀO<p ZSÀ¾/¾©rcÀY+ÿÞP`SÀ!‚vHÅ7cÀ”¬Mú¹DSÀ~4[@ŠjcÀ—íôMSÀ¢¢û³/ŸcÀ¹'b(ÇSSÀ X}â¼cÀÀS[n¶?SÀw.¯ËcÀŸîMp9SÀZŨÁcÀ(™Ÿ2,‚SÀlvcÀ¿^‹¿˜SÀŽs«8cÀ“eEЬSÀ~4[@ŠjcÀÏK„„¤ÄSÀ¤Ń},cÀe`2ÌéÊSÀLsNÔòbÀb¡ÙÙ°ÓSÀ—ÖT±bÀ‘‰Ž[t×SÀv¨‚bÀÇ¡ÙYCêSÀÂX·/¦XbÀSIçMÒûSÀ–’$3^MbÀç1œO(TÀ%bgbÀœŸìy+TÀB¶›bÀ8X[(NCTÀê”$оÔbÀú•eÿVTÀeÝ cÀ?£‰Ë@TÀrœÇMcÀSÔJTÀ…ËWcÌšcÀÕQÑGTÀæÔ.'IicÀÎîW‹![TÀÑ‘p×PcÀ¿ä´qTÀÔs‹‘cÀ¿–eÿBƒTÀh_TæLcÀS™TÀbò³R5!cÀu;ÖÃÏTÀ¼ã(Â2cÀÿ89›ìTÀ¨.¾ÉäÜbÀaø‘xfúTÀÈÇWƒñÁbÀ¹JJ—{UÀubº‡ZbÀS Ÿì‡"UÀLž˜ÜaÀEòW‹ %UÀqØnrãaÀw6#CUÀdÝ¢reÀ+b••%ùTÀúD•sœeÀ!õôAoUÀíKÏÚº£eÀp pUÀ]‹v=ÌeÀ”G­à¹"UÀ¾ eŽúeÀp pUÀ=Cø&äýeÀü.ÿUÀ Å£µfÀ´}}ÊáUÀΩ^¥^fÀƒÖoƱ UÀB(6ÄfÀ9ªv„]UÀ59r•fÀ™Q„´äUÀ‰ÿ|$fÀªGUÀ! ‡y7(fÀyG­`„UÀ|0J¯àafÀ†s¦¦s UÀHéhô(~fÀA\[¨³.UÀ€fÀ=ïŒ/.UÀ€fÀ‡‡‡‡‡UÀ€fÀ…‡‡‡‡°SÀ€fÀ‰‡‡‡‡HRÀ€fÀ†‡‡‡‡àPÀ€fÀñNÀ€fÀ!LÀ€fÀ QIÀ€fÀFÀ€fÀ ±CÀ€fÀá@À€fÀ"<À€fÀ‚6À€fÀ â0À€fÀ¼bÊ@0Àe= [}fÀ|j+ú’‚0ÀïsÚàbyfÀLã?¨v0À€fÀŒôP¹M0À€fÀ0<<<<„&À€fÀ0xxxxˆÀ€fÀàðððð€¿€fÀ°‡‡‡‡w@€fÀÀÃÃÃÃ{&@€fÀìááááÝ0@€fÀôáááá}6@€fÀäáááá<@€fÀöððððÞ@@€fÀîðððð®C@€fÀöðððð~F@€fÀúððððNI@€fÀòððððL@€fÀöððððîN@€fÀ›[™,>P@E4ºƒØmfÀ¡÷¨hUYP@óåØG|fÀmúBqwP@®nõœôUfÀý[äG¥†P@V:Xÿç\fÀ¢ìÖÛnP@0[ë‹„KfÀ;”ÀvXP@DÝ 'fÀý.Ä`P@A!¡fÀ/Öà&LVP@PèyÿeÀõ¿ïˆ:P@iÆ¢éìÔeÀãÞÞÞÞ'P@y( ô‰¼eÀÕîÇ–ŽP@h\8’žeÀ'ú%‹¤P@ö(\‘eÀ/Ï öP@ŠÙÎ÷eÀQž"0[P@NŽ;¥ƒ\eÀã¶šbP@Zÿ[ɾW@·bš’UÂS@&Æ‚pX@‰Ç' Þ¯S@¶… %üX@·ç¢ÊÙ·S@ °@Y“—r9@;qðUŠT@x^µ"Të6@nS‡)T@øH¤oè5@Ñ~d°\T@ȯDå»t4@{‚§À%T@ÈÇ>6^1@ìDáT@0w®èVv2@9§À¿€öS@ £mA³å3@åaõS@ˆ…Œ?4@ÅçgM¿ãS@ÓcC7@óv*CÙS@ÐÇDåµì9@yÝ©œàS@À4«KRh;@smSŸT@°@Y“—r9@;qðUŠT@„XŽ¿·;À»|”ºàT@L&üR?‹AÀCcÏGÂèT@@Ä”H¢OCÀðœâT@Ü–VCâòCÀ¥Q*ŠËT@ äÕ9´EÀýõ}áÍT@€çÞÃaGÀEœ÷¨§T@èÀ9#JsGÀÍ|wÔAŒT@tçû©ñBFÀ[–aÁiT@ŒjŸŽÇLGÀg"‡1’~T@”á|~HÀPQžƒT@̾+‚ÿ1IÀ»åBŽ›T@Iô2Š…JÀ üŠÞRxT@Ìö¯¬4KÀ©`o ?ŒT@àð×dšLÀýúŽ­‹T@8¹ß¡($NÀêvŸT@L;ü5YSOÀI„(ÇpT@ìoB!OÀ7(|_TT@PbX9ØOÀ="Áý)MT@Ž@¼®ÉPÀU \ª{ T@ŽxQÀe©×ÖøT@$ËǺTPÀûêÖýïS@QÚ|mPÀå.2ªµØS@:,ÔšæWQÀy–{õ¹S@´è+H3JRÀ3"Áý)›S@^ºI SRÀQ¬ÃzL‚S@x]¿ÂQÀqà+hS@Hmâä°PÀ4 ˆWS@|ܵ1QÀ5¤úw&TS@pö´ÃÙQÀç§á@S@º¯çŒjQÀ{"jKÆS@j0 ÃG QÀåá­œfS@Nëÿ„PÀïÚNS@,½R–!²OÀ ˆò® S@l°þÏa¢NÀmiPÝS@¤vÛ…æJMÀmÅl“àR@,J^cLMÀ5¼ÉÈÅR@Œ5µl©LÀƒbÔ^í¬R@Ež$]LÀsaMiR@$°U‚Å©KÀ `:VÒÀ·¹LöxQ@ðì£S¿;À]ýº|”Q@ðÀ=~9À™…X¦PQ@X¦í_YY6Ào‹Á‡Q@¸tv28º7À=YJ?‹Q@øKàÖÝ\:ÀÿîÊöQ@¨ q¬‹39À)þ˜¯Q@¸Qòê‹9ÀçÖý ÛQ@°7ÛܘN8ÀïNç3I¶Q@°¬C9‰7À“§UQ@À’ãNéÀ5À¤^ò©Q@Ø”Öÿ!6À)=|ÝQ@˜o™Óeq7ÀáÏ™R@( 3mÿÊ8ÀiÁ¦wšR@ +MJAG8À'4ò"¼%R@ø×G§®L6ÀÑÙä™@ R@Ø ú'P6À3z˜¾'R@0ÓÙÉà7ÀµöçKSR@8 ^ô,6ÀýF#HSR@°nض(Ã4ÀýÜy‹0]R@¸Íp>o4À›TÄsR@¡J͘5Àµ„—‰ËR@ Ÿåyp_3À5­¾c’R@H×/Ø «4À·½¿êqÉR@ œKqU™3À;·²í]ÏR@“Œœ…Õ3À‡‘Îi¿S@˜£Çï­5À÷٪ͨ'S@ð`à¹÷4Àù‹‚‰è;S@Ð镲 y2ÀÕK5\>S@èßJvl¬3ÀÙÛ!WhS@È‚9z´3Àc.q¯S@Pffffæ2ÀÙS@0¢´7øº1ÀÉù»T@HÄwbÖ 4ÀªÒ¿Í T@€™™™™Ù0ÀÝÞÞÞÞT@@‘ c I0ÀKØ—$T@0)ËÇj(ÀñÖÛ!RT@pUŠ)À-§æzmT@°á’ãN‰/ÀÇ' ÞyT@èÍ67¦Ÿ4Às-<Ø aT@ø›k+7Àßi¢x>IT@Ø|?5^6ÀÍÇÐZznT@`!«[=ç6Ày6Æ÷n…T@@ ^×/Ø8À9´ã/ÖqT@ÊÛNÛ;À• Úç‡T@,g~e?À\ÇaÙ€T@Pffffæ?ÀMEEEEŒT@ز˜Ø|„:Àcb+…’T@è·’±6Àç£nV•T@€]¢zkØ4À™>ú­T@XŽ¿·;À»|”ºàT@4¦n‘I@Åé"T@ŒTÁwïÂI@‘H”Æ@,T@„[˜I@=L1}G:T@ó1µpŒH@H”ƹ/T@d‹ËNëBH@¯ÆE+g T@ä³'ÞÃ(H@Ed°¥1T@le©,JfG@[-¬‘ß0T@T ilF@¯Rkê7%T@¬Öæ6F‰G@5aF#T@¤kã™\@G@{Ä\KT@Œ“¢òËG@ß»nGT@3Ù¡`H@Y¥W³ T@\èZ|rH@³–¯î3T@Œ€Wt—åH@GåÊÖT@4¦n‘I@Åé"T@|ã“‘ÄL@Þg”­Q@tØÕ%¹ÏK@U a“âQ@d²ͬµK@$¬Q;R@ÔàüN~L@ïTBÉTR@ÄŒ=M@ëm @“R@,¹Ü_°ÊN@ãgÍ*ÐR@6¤2ôÉ(P@-T¥×¯îR@Ú%€Ž Q@‡¾¨tlS@ž;’ Š6Q@f¶¨V"S@EiD Q@q§]vœ;S@ ñ°¥€P@ùî>ñK3S@AiDåP@ž¨ô‘S@|³Ä•N@‘„;T—S@„Ö›¸/ïL@í¸xæR@Üx©,ãÐK@?ƒ;T®ÄR@CƃóK@Ù0ƒ5¡§R@¬ë ¤ÁJ@E3 luoR@T‡ ¼6K@¿/ƒ5¢gR@l—h…Ú8J@#¾E« 1R@18=J@%r¤'R@ÿk"VºI@#'IHlR@L‹‘á ÍI@Ã0 ìÚÝQ@”p ö¼´J@eHkj²ÌQ@lµP:´ÖJ@õÛG°Q@DÊÄõxL@wfðU÷§Q@|ã“‘ÄL@Þg”­Q@@Œ–ºs-ÀoÁÏ¥œP@Ò6/ö*0À‡û‰o/¡P@˜àpœrÌ1À/h*ÃP@ Í+3ÀeL,‘P@PØÂT‡“4ÀŠLeSnP@PžGŠ"6ÀÛÕÊ–½™P@°²!ˆ¦7À›í•EP@8`:Ì€S8À³V2–fP@ÐÀ=i8:6Àç0ZY³WP@HZ×5/6ÀkÔÊ–èDP@îüÁ}ô7À§® ¾€8P@0“¾JÇ5ÀÆV¼4P@¸®!RÃ6ÀÚô°ØùO@ '{sù3Àö#G’SÑO@P6)»ÿ§2À.Ikz¾O@p[…J`Ë1Àþuÿ0ÒÕO@ЖºÕÑ-ÀÓÊ–ÅP@Ð5{Ò.8+À¥0ZÙ“GP@À.jÁ±z-ÀÙ ›;sP@@Œ–ºs-ÀoÁÏ¥œP@¤üÿÿÿ£VÀxxxR_Q@øÿÿÿ WÀxxxFlQ@þÿÿÿ@áVÀyxxx¶‹Q@þÿÿÿ28WÀyxxxÞÓQ@úÿÿÿóxWÀwxxxðQ@þÿÿÿ\ÍWÀwxxxaúQ@úÿÿÿXÀxxxñËQ@úÿÿÿ)XÀxxx9…Q@þÿÿÿuÓWÀ}xxx[kQ@üÿÿÿåŽWÀxxxäCQ@üÿÿÿÖ«WÀ}xxxQ@þÿÿÿQßWÀwxxxFQ@úÿÿÿ XÀ}xxxAÒP@úÿÿÿ«XÀyxxxÌQ@úÿÿÿØjXÀ{xxx$Q@þÿÿÿ¿£XÀyxxxRQ@þÿÿÿ\œXÀxxxñP@ûÿÿÿ¹ùXÀƒxxx óP@]YÀ{xxxßèP@üÿÿÿ%ÎYÀxxx¼Q@ûÿÿÿŸZÀ}xxx¡Q@úÿÿÿïUZÀyxxxd#Q@þÿÿÿ™‰ZÀ{xxx­2Q@ûÿÿÿ̼ZÀxxxF,Q@úÿÿÿ² [ÀƒxxxR)Q@ùÿÿÿ4[ÀxxxkQ@ýÿÿÿ¶òZÀxxxEøP@ýÿÿÿT8[Àƒxxxã×P@ýÿÿÿŒ|[À{xxxBþP@üÿÿÿ³[ÀxxxóP@üÿÿÿÑ_\Àyxxx…ëP@þÿÿÿÓ\À{xxx>ùP@úÿÿÿvy\ÀwxxxQ@ÍÏ\Àƒxxxr9Q@úÿÿÿ|]À{xxxS5Q@þÿÿÿf]À}xxx1@Q@ýÿÿÿVü]Àxxx¨WQ@:^^Àxxx‰rQ@ùÿÿÿ¼«^Àxxx9vQ@ûÿÿÿèÃ^ÀxxxŒcQ@úÿÿÿˆ_Àwxxx YQ@þÿÿÿ/_À{xxx‰Q@gp_Àxxx<^Q@ýÿÿÿœÜ_À}xxxž—Q@l`ÀyxxxpžQ@ýÿÿ‘ `ÀxxxK€Q@ÿÿÿr#`ÀyxxxXqQ@üÿÿÿm9`ÀƒxxxÞ‹Q@üÿÿÿÌm`À{xxxë{Q@ÿÿÿÿ»`ÀxxxÏ_Q@€DÍ`À{xxx¡gQ@€ô`À}xxx£SQ@ýÿÿaÀƒxxxò8Q@þÿÿÿ{1aÀ}xxxÖ>Q@ýÿÿÿÚcaÀ}xxxŸ]Q@€ŒŸaÀxxx mQ@RÂaÀƒxxxþuQ@€ÜòaÀƒxxx;‰Q@þÿÿpbÀ{xxxÖ~Q@þÿÿ6bÀwxxx(‡Q@þÿÿÿŸsbÀxxx,Q@ ·bÀ}xxxe¡Q@®×bÀ{xxxÿšQ@üÿÿÿ£cÀ}xxxà¥Q@üÿÿ¸cÀyxxx˜´Q@ÍHM@€ÅöcÀúððð,vM@þÿÿÿßácÀöððð@5M@þÿÿÿŒÐcÀúðððÊcM@ÿÿÿÿ:ÆcÀñððÆMM@þÿÿU¡cÀöðððŽtM@ÿÿÿÿ›±cÀñððö(M@þÿÿÿ ·cÀñððêÇL@€ÁÎcÀòððð´šL@þÿÿæÕcÀúðððL@üÿÿAdÀñððt4L@ dÀúðððúÿK@¼9dÀñðð‚ñK@ÿÿÿÿÖ[dÀîððð~«K@þÿÿ%{dÀòðððþƒK@€&ždÀñðð2HK@ýÿÿ#™dÀñðð°2K@ÿÿÿ8bdÀòððð>WK@ÿÿÿÿ›GdÀúððð ‚K@ýÿÿÿ"'dÀîðððž­K@E dÀñððTÑK@NócÀöðððzÇK@þÿÿÝÍcÀñðð.þK@üÿÿÿ¿ÃcÀñððJ:L@€Ë‘cÀúðððd|L@þÿÿ݉cÀîðððµL@þÿÿÿÖicÀòðððÜL@üÿÿpGcÀþððð°M@ÿÿÿÿ2)cÀòðððžmM@€œ@cÀîðððÆ«M@þÿÿcÀñððÖN@€ªübÀúððð\N@àÓbÀñððZ£N@üÿÿÿËbÀñðð@ƒN@þÿÿíbÀòðððÚ[N@€ûbÀòðððPÞM@üÿÿÿëöbÀñððä’M@€vÓbÀúððð®M@J·bÀöðððDÙM@üÿÿÿC’bÀòðððøóM@€“€bÀúððð(üM@þÿÿ-‡bÀöðððUN@þÿÿÿ¨cbÀñðð.pN@þÿÿ=bÀòððð¦9N@þÿÿ®þaÀñððÚþM@ýÿÿaÒaÀöðððÀ N@hšaÀþðððÜM@Å{aÀþðððÊÃM@ÿÿÿ™9aÀñððô>M@€aÀþðððM@9á`ÀþðððöM@üÿÿÿ~Â`Àòððð²M@üÿÿÿ@±`ÀñððÖ•L@üÿÿÿÿ‡`ÀúðððP.L@ÿÿÿó~`Àþððð¬¾K@þÿÿÿ¾b`ÀñððÖ•K@ýÿÿ'Q`Àúððð®eK@üÿÿÿyP`ÀþðððÄ#K@ýÿÿÿÃ)`ÀöðððÖÆJ@ýÿÿ&$`Àöððð¤_J@kö_Àîððð$)J@ýÿÿÿ‡ÿ_Àñðð’ÚI@áÛ_ÀñððDiI@ûÿÿÿøg_ÀöðððF4I@þÿÿÿ@:_ÀöðððúüH@ùÿÿÿX¾^ÀñððHH@úÿÿÿµ^Àúðððô~H@þÿÿÿÿŸ^ÀòðððþH@Õ^Àñðð­G@ùÿÿÿ•¥^Àñðð>‹G@ÿÿÿÿ­Ç^ÀòðððH@;$_ÀþðððŠ/H@úÿÿÿú+_ÀöðððŽH@üÿÿÿR_Àöððð"ÛG@ùÿÿÿ_ÀþðððžmG@ýÿÿÿ‡ù^ÀñððòÁF@ _Àîððð ÙE@ùÿÿÿ"_ÀöðððaE@ùÿÿÿ¬ _ÀñððæþD@ùÿÿÿp _Àþððð ‘D@þÿÿÿx_Àúððð 'D@ùÿÿÿ]÷^Àñðð áC@ùÿÿÿˆî^ÀñððÂxC@þÿÿÿ½^Àöððð~ C@þÿÿÿÄ ^Àîððð:ãB@ýÿÿÿ £^Àþððð”ÅB@ûÿÿÿ»m^Àñðð¢B@úÿÿÿ¢/^Àòððð“A@ùÿÿÿÛ'^ÀñððÔLA@ùÿÿÿ‰^Àþððð*8A@ýÿÿÿÜ]ÀñððŒ+A@ÿÿÿÿ.Å]ÀòððððA@F¡]Àñðð‚A@úÿÿÿFš]ÀòðððÆÝ@@ýÿÿÿh|]ÀñððvÎ@@þÿÿÿïR]ÀþðððÞ„@@ùÿÿÿ+H]ÀòðððtC@@þÿÿÿ,.]Àâáá ?@þÿÿÿ‡]ÀüáááÔ>@üÿÿÿÊø\Àôááá1,>@ûÿÿÿ1á\ÀüáááIŒ=@ýÿÿÿ¢»\ÀôááámE=@^Š\ÀìáááÍŽ<@ÁŒ\ÀâááY<@ùÿÿÿ¤\Àäáááµ»;@ýÿÿÿÛ¾\Àüááá=Ê;@üÿÿÿ†Ã\Àôáááå¶;@ùÿÿÿÍ\ÀìáááA";@úÿÿÿSv\ÀüáááMä:@ýÿÿÿ/f\Àôááᙡ:@þÿÿÿ»]\Àüááá‘Â:@ýÿÿÿ¾1\ÀäáááUP:@ýÿÿÿ>\À âááù:@üÿÿÿˆ \ÀìáááEv9@þÿÿÿ¥ \Àâááñº8@úÿÿÿêê[À âááéy8@ûÿÿÿļ[Àôááá)þ7@ÿÿÿÿà’[ÀÜááá=l7@úÿÿÿ‚[Àôááá™Ð6@þÿÿÿªv[Àìááá]Ï6@ýÿÿÿ¼[[Àüááám-7@ùÿÿÿ.Z[À âáá=[7@þÿÿÿdq[Àìááá‘Í7@ýÿÿÿ‹[ÀâááåA8@ûÿÿÿë©[ÀâááYJ8@ÿÿÿÿp­[À âáá]Ñ8@üÿÿÿ7¿[ÀüáááQI9@þÿÿÿ6Ò[Àìáááu¹9@úÿÿÿtç[ÀÜááá•§:@ùÿÿÿ«\Àôáááá);@ýÿÿÿM\Àôááá„;@úÿÿÿ½0\Àìááá¡Å;@üÿÿÿ•=\ÀÜáááÁj<@úÿÿÿõH\Àâáá-g<@ûÿÿÿfQ\ÀÜááá!¿<@úÿÿÿ"[\ÀôáááiÑ<@úÿÿÿ®e\Àüááá­ =@üÿÿÿ.•\À âáá¾=@ùÿÿÿ «\Àäááá‰'>@úÿÿÿZ±\ÀôáááÉç>@ÿÿÿÿò»\À âááb?@ûÿÿÿ°±\Àüááá™Ê?@ýÿÿÿ)\Àâáá „?@Íw\Àìááá5?@úÿÿÿƒI\À âáá­)?@þÿÿÿ{J\ÀâááUÇ>@ýÿÿÿÎ3\ÀâááI>@úÿÿÿd\Àôááá5B=@ûÿÿÿš\Àâáá=ò<@ûÿÿÿœð[Àìáááµu<@sË[ÀôáááÙî;@ùÿÿÿ©[À âááÚ;@ûÿÿÿ™[Àìáááa';@úÿÿÿIs[Àäáááõª:@ùÿÿÿ©R[ÀôáááAo:@ýÿÿÿk\[Àôááá Ñ9@ûÿÿÿ¦P[Àäááቒ9@ýÿÿÿ¸[À âááý)9@úÿÿÿ•úZÀôáááeŠ8@þÿÿÿ;ºZÀâááuÂ7@üÿÿÿÕZÀìáááñÃ6@ùÿÿÿ`lZÀ âááÅB6@þÿÿÿ˜fZÀäáááíÜ5@úÿÿÿQZÀìáááñi5@SQZÀäááái5@ûÿÿÿ `ZÀäáááÏ4@úÿÿÿsYZÀâááý…4@úÿÿÿÎnZÀäááám4@ûÿÿÿŒ_ZÀâááIð3@úÿÿÿ|?ZÀüáááÙN3@ùÿÿÿ·úYÀ âáá½2@ýÿÿÿàYÀ âááµH2@þÿÿÿ˜žYÀäáááµ÷1@ûÿÿÿÈzYÀôáááeè1@ûÿÿÿ jYÀâáá ¤1@5YÀüááá­)1@ùÿÿÿ¡ìXÀâáá±²0@ÿÿÿÿ¦¼XÀâááÍŽ0@øÿÿÿÔ€XÀìáááY0@úÿÿÿÞPXÀÈÃÃÃSÑ/@üÿÿÿ«#XÀÈÃÃÃ[J/@úÿÿÿjXÀÄÃÃÓ|/@þÿÿÿÐWÀìááá¹0@C¬WÀäááá]10@þÿÿÿxWÀÄÃÃ3Ý/@úÿÿÿÿVWÀÄÃÃã6/@þÿÿÿ’WÀÐÃÃë-@üÿÿÿ#ìVÀÀÃÃÃk<,@àÎVÀÀÃÃÃÛÖ+@úÿÿÿò¦VÀøÃÃãÍ+@þÿÿÿ†VÀÀÃÃÃSt+@úÿÿÿýsVÀ ÄÃÃ[+@þÿÿÿmPVÀÄÃÛæ*@þÿÿÿö5VÀÄÃû€*@üÿÿÿíVÀØÃÃëO*@üÿÿÿÜùUÀÄÃÃH*@ÂòUÀÀÃÃãÀ*@þÿÿÿQßUÀÄÃÔ*@øÿÿÿBÔUÀÄÃÃëó)@þÿÿÿÙUÀÄÃÃËÏ)@úÿÿÿ­ãUÀØÃÃÃã*@úÿÿÿÈêUÀÄÃãÍ)@¸ÊUÀÀÃÃÃkæ(@øÿÿÿ¼¯UÀÄÃËE(@þÿÿÿ§¡UÀÄÃÃã˜'@úÿÿÿ¼ƒUÀÄÃÃ[Ê&@úÿÿÿ™mUÀØÃÃÃ)&@üÿÿÿD|UÀðÃÃÃ+Æ%@úÿÿÿ1jUÀ ÄÃÃó}%@úÿÿÿªrUÀèÃÃëÜ$@üÿÿÿsUÀøÃÃÃÓ@$@þÿÿÿHjUÀÄÃÛÙ#@ºUUÀøÃÃç#@þÿÿÿGUÀðÃÃÃ#@üÿÿÿS:UÀØÃÃÃ[“#@þÿÿÿo>UÀÈÃÃÃ+($@üÿÿÿ§-UÀðÃÃóÌ#@r)UÀèÃÃÃã6#@kUÀàÃÃÃ[õ"@üÿÿÿ;úTÀÄÃó"@þÿÿÿ|èTÀøÃÃÃ#"@úÿÿÿ)æTÀÄÃÃû¤!@þÿÿÿˆíTÀÄÃÃL!@ŠàTÀÄÃÓà @üÿÿÿͽTÀÄÃÃo @þÿÿÿt¶TÀÐÃÃÛ! @þÿÿÿz´TÀÄÃó @úÿÿÿ™TÀÈÃÃÑ @iˆTÀÄÃÛU @úÿÿÿ)nTÀðÃÃÃ3 @þÿÿÿ>aTÀ€‡‡‡7Ë@þÿÿÿ#LTÀð‡‡‡÷Ž@þÿÿÿÎCTÀˆ‡‡=@þÿÿÿº8TÀ0ˆ‡‡WÙ@üÿÿÿòTÀ‡‡‡‡ @´TÀ‡‡‡G¥@þÿÿÿ:TÀ ‡‡‡'(@üÿÿÿÃTÀÄÃÃû) @úÿÿÿ|TÀÄÃÛ” @þÿÿÿ† TÀÐÃÃÃk¦ @üÿÿÿ«ðSÀ ÄÃÃ'!@úÿÿÿ²ãSÀøÃÃÃ3Ù!@þÿÿÿ²ÇSÀÀÃÃÃÃù!@üÿÿÿϧSÀèÃÃÃkk!@þÿÿÿÞ›SÀØÃÃÃC @þÿÿÿ¥‹SÀøÃÃÃ;Ÿ @þÿÿÿu›SÀÄÃÃs @þÿÿÿÀSÀ ˆ‡‡'@þÿÿÿkxSÀà‡‡‡ÇÜ@úÿÿÿ^SÀ°‡‡‡7»@gTSÀˆ‡‡Y@þÿÿÿbSÀ@ˆ‡‡gL@úÿÿÿ¯SSÀ ˆ‡‡§£@üÿÿÿÂ_SÀ@ˆ‡‡WQ@üÿÿÿ+HSÀ€/»@üÿÿÿ©`SÀàψ @üÿÿÿ{SÀ@ï@úÿÿÿ]›SÀ@¯ø@úÿÿÿ_ªSÀo@üÿÿÿ‰§SÀž!ü?þÿÿÿj¿SÀ€ žíú?þÿÿÿ¼¶SÀÀ^öõ?þÿÿÿ¼âSÀ€A<<<0ï?þÿÿÿËTÀ€@<<¼Sè?úÿÿÿTTÀ~xxxŠÖ?þÿÿÿTÀ…‡‡‡¯Ò¿U%TÀÁÃÃCGí¿À;TÀÀàááá ñ¿úÿÿÿñ0TÀ€ááá¡’ÿ¿úÿÿÿî=TÀ`ððð Àúÿÿÿ˜TÀ@ððð0ŒÀüÿÿÿ"ÿSÀÀððððÔÀüÿÿÿJñSÀ ðððpSÀþÿÿÿ[TÀÀððððM Àúÿÿÿ_FTÀ@xxx¸-ÀúÿÿÿKZTÀ`xxxØúÀüÿÿÿG;TÀ xxxˆËÀúÿÿÿÿOTÀ`xxxˆ”Àþÿÿÿe"TÀ@xxx(3Àüÿÿÿ«ðSÀ@xxxhÏÀúÿÿÿ‰ÜSÀ0xxx˜ÁÀ]ÂSÀ0<<<,Ê Àúÿÿÿä…SÀ<<<œÅ$ÀúÿÿÿËFSÀ <<ÀÌêQÀÊí>ÀþÿÿÿÜQÀ ­6@Àþÿÿÿ%÷QÀmõ@Àüÿÿÿe#RÀ /ÂAÀüÿÿÿªJRÀçBÀüÿÿÿ¡eRÀ•BÀúÿÿÿY`RÀE%CÀúÿÿÿìMRÀ)¢CÀþÿÿÿUkRÀ©ùCÀþÿÿÿ$RÀËæDÀþÿÿÿ=•RÀ ÛEÀúÿÿÿálRÀá¯EÀþÿÿÿãXRÀEÀþÿÿÿî-RÀ2EÀüÿÿÿ`ORÀI;FÀúÿÿÿ–RÀ ?FÀüÿÿÿJ¬RÀÙâFÀúÿÿÿ=éRÀóSGÀþÿÿÿˆRÀGyGÀþÿÿÿ°ËRÀ /ÜGÀúÿÿÿèæRÀ KWHÀþÿÿÿ²ÞRÀ {1IÀúÿÿÿ€¾RÀ †IÀüÿÿÿ£ÐRÀ›ÑIÀúÿÿÿ—¼RÀ¯"JÀùlRÀókJÀúÿÿÿ´#RÀÅJÀúÿÿÿÛQÀ­îJÀúÿÿÿfÀQÀµëJÀþÿÿÿ¶QÀ'tJÀüÿÿÿT|QÀ éEJÀþÿÿÿ…]QÀk&JÀúÿÿÿ’$QÀ a'JÀš QÀÛ-JÀþÿÿÿ04QÀÃãIÀþÿÿÿÜHQÀ Ñ^IÀþÿÿÿ¢.QÀ á"IÀüÿÿÿ:ôPÀ aðHÀþÿÿÿ¢ÊPÀQZHÀúÿÿÿ PÀHÀþÿÿÿiPÀGŸGÀúÿÿÿ4¦PÀe…GÀúÿÿÿ&åPÀ­'GÀúÿÿÿÍÒPÀ³ÇFÀþÿÿÿ¡ PÀ!†FÀ,dPÀÅ…FÀúÿÿÿ UPÀ 9AFÀúÿÿÿ¢KPÀuÀEÀþÿÿÿ=PÀÝpEÀøÿÿÿŸºOÀ#IEÀÂàOÀ§EÀúÿÿÿjPÀ/EÀþÿÿÿ >PÀ{EÀþÿÿÿŒGPÀI‰DÀüÿÿÿÚ.PÀËgDÀøÿÿÿŸâOÀi–DÀv_OÀ»„DÀôÿÿÿ¯OÀ³WDÀüÿÿÿM*OÀ'DÀøÿÿÿOÀW·CÀôÿÿÿý*OÀkCÀôÿÿÿcžNÀåwCÀüÿÿÿ«MÀ?]CÀôÿÿÿáßLÀ™CÀüÿÿÿãdLÀstBÀüÿÿÿe^LÀï5BÀøÿÿÿ_®LÀ+þAÀüÿÿÿåœLÀ í¥AÀôÿÿÿi?MÀ K8AÀüÿÿÿ§6MÀ yõ@Àüÿÿÿ­èLÀ CÀøÿÿÿ+ËHÀ–;=Àøÿÿÿ·qHÀ®®<À¬THÀÂ1<ÀÂE2Àøÿÿÿ5¢CÀ>à1Àôÿÿÿ›”CÀv71ÀüÿÿÿïpCÀ<<<¼Y/ÀzCÀ<<RÀÄÃÃå'@þÿÿÿ-(RÀÄÃÛr'@þÿÿÿ‰ZRÀÀÃÃà p&@úÿÿÿžŒRÀÈÃÃûš&@üÿÿÿ´‘RÀÄÃà 0&@úÿÿÿ ºRÀÈÃÃÃS&&@üÿÿÿ¾ÞRÀÐÃÃû8%@þÿÿÿ‰êRÀÈÃÃÈ#@úÿÿÿ,ëRÀÄÃûÞ"@þÿÿÿ…SÀÄÃÃC¨"@‹5SÀÈÃÃÃÃB!@úÿÿÿ›VSÀÄÃà S!@°nSÀÈÃÃÓà!@þÿÿÿ“ƒSÀÈÃÃÓz"@þÿÿÿ SÀÀÃÃà Ó"@þÿÿÿ¼ÃSÀðÃÃÓä"@úÿÿÿZÁSÀèÃÃÃÛ#@þÿÿÿ°äSÀøÃÃÃó4#@üÿÿÿˆúSÀÄÃÃÛ›"@üÿÿÿf!TÀÄÃã4"@¡UÀØÃÃëù/@úÿÿÿ«KUÀÈÃÃÃSÍ/@úÿÿÿi\UÀØÃÃÃ;Á/@üÿÿÿºkUÀÄÃà ä/@üÿÿÿ€UÀèÃÃÓþ/@¡‡UÀØÃÃÃ;Å/@þÿÿÿ7œUÀèÃÃÛŒ/@úÿÿÿ͹UÀÈÃÃÃ3/@þÿÿÿ‡×UÀÈÃÃÃc­/@úÿÿÿváUÀÄÃÃû“/@üÿÿÿeçUÀÄÃÃý/@úÿÿÿ¶ùUÀØÃÃÃc¶/@þÿÿÿ¿VÀÄÃÃc\/@úÿÿÿeVÀÈÃÃÃ[p/@þÿÿÿ+!VÀØÃÃÃ˱/@þÿÿÿ±&VÀÄÃÃ{e/@úÿÿÿŽ;VÀÄÃÃÂ/@úÿÿÿß.VÀ âááµ90@úÿÿÿP#VÀâááÙA0@þÿÿÿ¾VÀäáááÉ…0@TVÀâáá1@úÿÿÿ]VÀäááá™1@þÿÿÿ© VÀ âáá1{1@BVÀôáááÑ¢1@úÿÿÿåVÀâáá…2@úÿÿÿÕVÀüááá%W2@úÿÿÿöVÀüáááQX2@þÿÿÿ2VÀüáááé}2@þÿÿÿËVÀüááá)‚2@úÿÿÿ”õUÀÜááái@2@úÿÿÿˆåUÀüááá%3@üÿÿÿòÛUÀôáááÕv3@þÿÿÿ½çUÀâááa£3@üÿÿÿ‡ØUÀâááI?4@úÿÿÿ"¶UÀìáááu×4@úÿÿÿ÷³UÀÜááá½R5@úÿÿÿQÃUÀâáá‰5@øÿÿÿ"êUÀäááá]s5@úÿÿÿÎ"VÀüáááE|5@úÿÿÿ{fVÀìáááá@5@úÿÿÿÔ‘VÀôáááÙý4@þÿÿÿäœVÀüááá³4@þÿÿÿ$¢VÀôáááõÛ3@üÿÿÿd±VÀ âááF3@ÚVÀâáá)Þ2@úÿÿÿbWÀäáááI²2@þÿÿÿN2WÀôáááA„2@úÿÿÿcWÀ âááej2@üÿÿÿ>›WÀ âááÑ"2@úÿÿÿ±µWÀüáááí2@þÿÿÿ§ùWÀìááááÑ2@úÿÿÿ±XÀüáááíO3@üÿÿÿ¡!XÀôáááõá3@üÿÿÿLXÀäááá‘ 4@üÿÿÿäXXÀìááág5@úÿÿÿ»lXÀâááñã5@ÕwXÀìáááo6@þÿÿÿ©qXÀ âáá¥ì6@úÿÿÿûlXÀìááá•C8@úÿÿÿËaXÀìáááÝû8@üÿÿÿIXÀôáááÛ9@üÿÿÿÞHXÀÜáááÜ9@øÿÿÿøHXÀìáááyÜ9@üÿÿÿUXÀ âáá©3:@üÿÿÿQXXÀâáá®:@üÿÿÿ­WXÀìááá1_;@úÿÿÿõHXÀôáááaÒ;@üÿÿÿ&XÀôááá™L<@þÿÿÿjæWÀìáááýº<@þÿÿÿ(¬WÀìáááÉx=@þÿÿÿKvWÀ âáá•´=@üÿÿÿ|NWÀ âááÆ=@üÿÿÿðWÀâááE‹=@þÿÿÿèVÀìááá9«=@úÿÿÿT¸VÀüáááñ#=@þÿÿÿä‰VÀäáááõ=@üÿÿÿßqVÀôáááL=@úÿÿÿZVÀüáááÅ&=@íMVÀìáááeH=@üÿÿÿ„[VÀôáááýz=@þÿÿÿyZVÀâááÑâ=@þÿÿÿ¶fVÀôááá+>@úÿÿÿŒKVÀìáááÍN>@þÿÿÿ¼VÀ âáái`>@üÿÿÿïáUÀäááá%D>@úÿÿÿ™™UÀüáááMd>@üÿÿÿxqUÀüáááù$>@üÿÿÿjRUÀìááá…­=@þÿÿÿöFUÀäáááÅ =@üÿÿÿeUÀìáááñ>@þÿÿÿiíTÀäááá­í=@úÿÿÿ„»TÀ âáá=@üÿÿÿ™©TÀôááᵊ<@þÿÿÿ¼¶TÀäáááÅà;@þÿÿÿ­TÀìááá¡|;@øÿÿÿ[TÀìáááɸ:@þÿÿÿpmTÀüááá¡Ü9@úÿÿÿUTÀôááὡ9@üÿÿÿKTÀ âáái19@úÿÿÿ„+TÀôáááa9@þÿÿÿaTÀìááá±29@üÿÿÿiTÀôáááÏ9@þÿÿÿ¡TÀ âáá‰2:@üÿÿÿTÀ âáá1ß:@þÿÿÿë!TÀôááá%<@øÿÿÿF"TÀôááá½v<@þÿÿÿ·>TÀôáááý+=@üÿÿÿTTÀìáááù>@úÿÿÿb_TÀ âááɸ>@úÿÿÿ…UTÀìááá¥n?@úÿÿÿ[7TÀúððð4@@úÿÿÿGTÀñðð @@@ÍSÀñðð:“@@þÿÿÿàÃSÀòððð.¾@@þÿÿÿy£SÀúððð0í@@þÿÿÿ‚ƒSÀñððfõ@@þÿÿÿqYSÀöððð|@A@úÿÿÿ<SÀöðððlfA@üÿÿÿŽîRÀñððnÅA@üÿÿÿŒ÷RÀñðð‚EB@úÿÿÿ1þRÀþðððÊqB@þÿÿÿŽSÀúððð¦zB@úÿÿÿLSÀþðððtôB@üÿÿÿp=SÀòðððÀC@þÿÿÿSÀñððš C@þÿÿÿ»"SÀñððÎZC@þÿÿÿeSÀñðð&’C@þÿÿÿåSÀñððÎ'C@þÿÿÿ4îRÀòðððâöB@ÿSÀòðððÌŸB@þÿÿÿ+üRÀúððð´šB@úÿÿÿ&ØRÀöðððìC@þÿÿÿ ÃRÀñððª2C@þÿÿÿWÅRÀúðððòbC@þÿÿÿzÔRÀþðððÔyC@ÌáRÀúððð¾¾C@ÍÌRÀþððð¾žC@þÿÿÿ¾¾RÀîððð˜C@üÿÿÿû¹RÀñðð2wC@úÿÿÿj‹RÀþðððºÙC@úÿÿÿ—}RÀòððð®5D@úÿÿÿmRÀúðððŒ;D@þÿÿÿñ|RÀúððð_D@üÿÿÿØ~RÀñððVOD@úÿÿÿVRÀúððð˜OD@úÿÿÿzüQÀöðððþuD@úÿÿÿoRÀöððð<ŽD@þÿÿÿpmRÀúðððvD@þÿÿÿ8RÀñðð0›D@úÿÿÿàRÀñðð‚¡D@þÿÿÿ ÷QÀòðððê§D@üÿÿÿ²ÇQÀöððð<¾D@üÿÿÿõ¨QÀþðððÀ»D@þÿÿÿÂ}QÀñðð|ÐD@üÿÿÿ¡xQÀúðððõD@úÿÿÿÖ‹QÀöððð‚E@üÿÿÿ…QÀþðððÊâD@þÿÿÿ­ŸQÀñððþåD@úÿÿÿÌ´QÀúðððÔ)E@úÿÿÿ&´QÀöððð²mE@üÿÿÿ(¬QÀöðððÊ‚E@þÿÿÿm‡QÀîððð€ÖE@þÿÿÿÖCQÀîðððdüE@üÿÿÿQÀñðð”(F@þÿÿÿ»½PÀúððð”fF@þÿÿÿÊÈPÀòðððŠF@ªPÀþððð  F@úÿÿÿ:PÀñððT¤F@þÿÿÿXŠPÀñððx:F@üÿÿÿå‡PÀöððð"ÎE@úÿÿÿLWPÀöððð¼ÄE@üÿÿÿÇPÀöðððì F@üÿÿÿ™ OÀþððð¼TF@üÿÿÿ…NÀöðððæ F@üÿÿÿÁæMÀòðððÂôF@üÿÿÿk9NÀöððð #G@üÿÿÿQBNÀöðððòG@øÿÿÿ¥ÂNÀúððððF@ôÿÿÿ-–OÀòðððŠÝF@úÿÿÿ6PÀþðððxG@þÿÿÿ3PÀñðð~G@dGPÀñððH@úÿÿÿñ PÀþðððú]H@úÿÿÿ˜CPÀñððÀœH@[£PÀþðððúH@þÿÿÿ™)QÀöðððZ%H@úÿÿÿTQÀîððð(}G@þÿÿÿ¯ÆQÀöðððhG@}QÀöðððHÞG@úÿÿÿµ QÀñðð´‡H@úÿÿÿÏPÀöðððhÀH@úÿÿÿˆ™PÀþððð<I@þÿÿÿ?WPÀúððð%I@üÿÿÿeîOÀñðð2$I@üÿÿÿÜNÀñðð> I@üÿÿÿ;NÀöðððI@øÿÿÿ+cMÀúððð*‡I@ôÿÿÿ=LÀñðð¨´I@ôÿÿÿ_4LÀòððð–áI@üÿÿÿu×KÀþððð¸J@ôÿÿÿÍàKÀúððð¡J@øÿÿÿ;LÀòðððÒÑJ@üÿÿÿëwLÀòðððÐâJ@ôÿÿÿ¥ªLÀñðð"OK@ôÿÿÿÏüLÀöðððöwK@øÿÿÿçÈMÀòððð™K@øÿÿÿ÷;NÀñðð4âK@ôÿÿÿ7æNÀñððd*L@À²NÀþðððÈzL@ôÿÿÿM@OÀñððTM@üÿÿÿÿæOÀúðððš·M@úÿÿÿW%PÀúðððè)N@±OPÀþðððdîM@úÿÿÿéŒPÀöððð&aM@üÿÿÿ•éPÀþðððM@þÿÿÿ÷QÀþðððzeM@üÿÿÿlRQÀúðððxyM@þÿÿÿ²gQÀñððDN@üÿÿÿÈeQÀñððІN@þÿÿÿê×QÀîððð|N@úÿÿÿSëQÀúððð.ÂN@úÿÿÿ$:RÀòðððd O@þÿÿÿÀuRÀúðððÂ7O@úÿÿÿêRÀþðððO@þÿÿÿŽìRÀöððð–"O@GZSÀöðððfEO@Ö†SÀöðððÜ'O@úÿÿÿsqSÀñððò_N@þÿÿÿ‹USÀöðððìM@üÿÿÿ¡SÀúððððeM@üÿÿÿVSSÀñððœM@þÿÿÿà'SÀúðððâ˜L@þÿÿÿ¥"SÀòðððTCL@þÿÿÿFSÀòððð$êK@üÿÿÿ¢ŽSÀþðððhK@úÿÿÿõSÀñððhTK@üÿÿÿòÇSÀñðð K@úÿÿÿ…¦SÀöðððâFJ@þÿÿÿ&ÉSÀþðððFÃI@þÿÿÿlúSÀþððð ™I@üÿÿÿ¤YTÀöððð&J@úÿÿÿÿ‡TÀòðððh¢J@üÿÿÿê›TÀòððð#K@úÿÿÿu‘TÀþðððì‘K@úÿÿÿ×TÀñððHžK@úÿÿÿÀ@UÀñðð®¥K@„UÀöððð˜ÛK@øÿÿÿ¿ÔUÀòðððÒþK@üÿÿÿ‹ VÀñððP;L@þÿÿÿ†BVÀòðððôkL@þÿÿÿs¹VÀþðððb£L@üÿÿÿWÀúðððŠL@üÿÿÿî0WÀþððð0ëL@üÿÿÿÂMWÀòððð cM@úÿÿÿΫWÀþðððfxM@úÿÿÿE¨WÀþððð N@úÿÿÿtWÀòðððøqN@þÿÿÿ JWÀñððO@þÿÿÿ¼ûVÀúðððÔiO@þÿÿÿG±VÀîðððÜyO@üÿÿÿ ­VÀñððÍO@úÿÿÿ…zVÀ}xxxP@þÿÿÿçVÀxxxÐP@úÿÿÿ®ÔUÀxxx1P@úÿÿÿÂUÀƒxxxMP@þÿÿÿS„UÀxxxƒP@üÿÿÿ=qUÀxxx3£P@üÿÿÿ/UÀxxxðP@ ÖTÀyxxxÑ™P@úÿÿÿ»XTÀwxxx‘ÆP@˜PTÀyxxx²åP@úÿÿÿ¶}TÀyxxxóQ@þÿÿÿNTÀxxx*Q@þÿÿÿñQTÀ{xxxØIQ@þÿÿÿÖ§TÀxxxšiQ@þÿÿÿrUÀ{xxxsQ@gaUÀwxxxìwQ@þÿÿÿædUÀ}xxx­1Q@úÿÿÿ–“UÀƒxxxrúP@üÿÿÿgÖUÀ}xxx0ÌP@üÿÿÿQVÀyxxx_÷P@üÿÿÿ@VÀ}xxxÖ&Q@úÿÿÿÃMVÀxxxPQ@üÿÿÿK£VÀ{xxxàQ@üÿÿÿ£VÀxxxR_Q@Ie—»ú£UÀ7õŒIR@kѨßtUÀwd0êrR@jY#¡ VÀ1¸ >åaR@xrnßð[VÀ§ ÄœÁGR@ØÒšX!VÀ³ÕíƒR@„˜-x×xVÀýáĶÍQ@²)$ïVÀMýì8oÍQ@pйÝÛ`VÀ[\²=°Q@®‚0¡+VÀ¿QB™Q@dÏš×ÃUÀñ};TQ@¢¸ìvIm}RÀÇ`ðU“\P@8ìög+SÀ¿«n‡eTP@ªà‚ mySÀ£7”FBSP@(K†ª”£SÀïø"$P@È7OpmSÀ»zØJ)P@îÁšXb´RÀ=Ð-`_P@\î“gµRÀ­Urî*P@޹`+6XRÀÕÿÃ\â P@•>ɸøQÀ>€ÖÔúÕO@®äYqRÀòå<;ݱO@&°&þÁQÀ’ÉTƒsO@:ÂÔÅ&8QÀNTzE3)O@ž±`«˜ŠPÀVü“öN@6Œ¡Ò•PÀ¾pnÊ"O@Ö-uÙ¨×PÀ¶ü‡¹pO@Tõ¹2QÀöSÝcÞO@Rè0Uš‘PÀB%ðéwO@@È«)â@PÀæ2X£h6UÀ{ÝÊ–?UR@Du¨LqUÀ™ÜÊ–§!R@e—»ú£UÀ7õŒIR@ î(\ÂSÀSáˆÜ FR@À„SÀ{xx¸1iR@€˜TÀwxx¸pR@þÿÿ_^5TÀyxx˜ÕkR@üÿÿÿ8TÀxxXËTR@@§ñSÀwxx8Û2R@þÿÿ¿ßSÀ{xx¸ø.R@ ™SÀyxx˜“7R@þÿÿ¿TSÀwxx¸96R@SÀyxxø[4R@î(\ÂSÀSáˆÜ FR@jh‘í|yTÀ†ðoìYO@Î:ªš xTÀÂ?÷7ºrO@ZDÝÐTÀæ½ ×ñsO@ AJ˜ÿTÀjÁ¤Jæ8O@î´Ã_“ñTÀâÎàFO@(ßlscÄTÀB`CRO@jh‘í|yTÀ†ðoìYO@Ô§ã1ÑSÀšÉPg@O@Q÷HáSÀ.ŽŽý~-O@²Î1 {ûSÀ2œ/HL0O@ž”‚n/TÀVH/¼ä O@À1w-TÀòKI O@Tÿç0_TÀþ‘X¤ÛÚN@ÎÄÎêSÀ*Ñä´ùÏN@Ô§ã1ÑSÀšÉPg@O@¸ù“ÞRJUÀg* l‰iP@ø«xöxUÀužÀnP@œŠdŽUÀÿr$#4P@ò†¹“–UÀ×2÷ÃP@– ù4ÎUÀFæ<;8ÄO@̲²#ywUÀöéÙq‚ÐO@˜«xvaUÀNöML¥…O@>zâ9mUÀÖ†BÝÇO@&ŠöÆTÀ“£4šýP@^S÷£TÀ ü“\ÒO@õ“Þl?TÀ®¦”³O@Va4òžTÀZ‘çåÝÛO@Z•g¥O4TÀñ¦ÑP&P@æ´ìkcTÀ@/ÇTüO@ü¶ÀiTÀó@kj™P@²gѨg²TÀ'‚ø‰0P@ŒÙó|øTÀ;Êór|FP@øln_²UÀ¿ÔÊ–CWP@.Þ«ér>UÀ¿þIdMP@¸ù“ÞRJUÀg* l‰iP@ ¦ò“j÷RÀ-ddÿÈP@¬ºÕsÒÍRÀUÈçÛP@ЬnõœÆRÀàHI¸äP@ÙÎ÷SÇRÀoÄu5"Q@XCâKùRÀÛ½…ÚQ@$ÞÈ<ò3SÀØEzúQ@”1w-!OSÀ3¿µ¼åP@ÔÙÉà(?SÀÁÊÅP@¦ò“j÷RÀ-ddÿÈP@ÿèÔ•ÏYÀsS¬ÃzuR@Çõ(\bYÀW鵂VR@÷÷YÀ!2Ç›¥,R@…ëQ¸žYÀýcÊ0—4R@ YÀ뵂O R@éÔ•ÏòYÀ àHI¸îQ@òKý¼©ÔXÀ; yGÖQ@vk`«—XÀ#&QØîÐQ@¨Gáz.XÀOéµéQ@Âõ(\"XÀ鵂O#R@YÀnƒXÀ¥úw&Ý>R@Láz®GXÀ-ÀYóŒ]R@¸…ëQXXÀíµ‚OpR@처Ø|ÊXÀ»_®îhR@ÿèÔ•ÏYÀsS¬ÃzuR@ J2*LWÀ'ø²Ëà0R@Â&S÷@WÀUJkjO=R@Æl4r  VÀq§ú,MvR@t÷¼záWÀq)IHß…R@æ¸í; WÀu)IȈR@f”V»ßWÀ—ÉOªvR@’ô‚M+XÀU™†Rw[R@Òa#á(XÀ8÷¥;R@:¬¡;ÚWÀç aSnR@¼Ú78‘WÀ鹨t R@J2*LWÀ'ø²Ëà0R@? ×£p^ÀÙQ@? ×£p^À—&ÀYóóQ@³GázÎ]ÀaóŒ&À R@“RÐí%1]Àmà·Ê¼MR@m±à\À1ÑÉûá]R@:È$#g%]Àdzt®ÑxR@a›c]ÀyS \‹R@Sp±¢^Àe±½¿êŽR@ K< lb^ÀÕ„]½3œR@Üù~j¼:_ÀWr÷â4’R@[Âõ(ü^ÀiÊ0—ýjR@Õ­£ª3_Àøê@R@È€ìõîe_À;||ë R@¸Ò¤t{_À™$xì÷Q@Iáz®ç^Ào¡Ô;ÕQ@-ÁâpæÅ^À©ºð,¹Q@? ×£p^ÀÙQ@-Ó·Ì鲊\ÀÓþ°N>GR@€ñ Ì\À÷á¶šSR@l&ßlsw]À‹´o—¦,R@0ɤ]À-ýôH,R@jÔC4ºÙ]ÀWð½h8ãQ@ )?©öi]À[ê]ÒQ@s¶¹1=]ÀqaBÓQ@vÜ)¬›]Àõ٪ͨ¹Q@îZB>èy]ÀÚp¢Q@¢˜õb(]ÀÅü…Ç Q@jffff–\ÀáÞÞÞÞ¥Q@f÷äa¡\À¥ò‘=ë–Q@|@Ù”+n\ÀQ'»BÈ‹Q@‚ö#EdÈ\ÀMNÏd¨ŽQ@Sè¼Æ.+]À•ÛfÓºƒQ@ö(\ÂU]À½‚Oé|Q@‹{,}è]À‰¯XlYÀa¡ñíQ@ Pˆ€C²YÀçÈ:ÅSŸQ@Mº ¾ZÀ­rƒJ¿Q@‚è‚ú–1ZÀ/Zb+ìQ@¯|–çÁYZÀïße/„*R@ÜÍSr¡ZÀKƒ„ÑUDR@?M„ áZÀTÄ’NR@†)t^[À-ùT3ER@ˆØ™BçëZÀ+K©R@áŒ(í [ÀGK§ éQ@f‡¿&k@[À{³èF(R@è·¯çz[À—ÅߟûGR@! ZÀ÷Œ&ÀYZR@q= ×£PZÀ¥Ô;nhR@fffff¦ZÀÝÞÞÞÞeR@\Âõ(¼ZÀ¹‚Oé\R@¸…ëQXZÀ﵂O0R@ ZÀ÷Œ&ÀYZR@ ¢#Ý›séWÀ>’Þ\FQ@ˆæByXÀû:à Õ^Q@¼Dv¬#XÀWÊ…þjQ@$«ÜJXÀK>5ƒvQ@Ê×ý÷XÀù¡^<¨ˆQ@šè‰³¶ºXÀ±‚¤›élQ@RŸóXÀg‘YQ@˜,„¡¢›XÀÛJɾP’Þ\FQ@ Ͻ‡ [À툳Ë` S@j:;#[À¹†Ââ*S@+í ¾0e[À5Öà&L2S@ùf›ÓŸ[À#C7¤úS@îcÌ]KD[Àq‹ÁÝR@öï3.´[ÀËHfžâR@Ü”+¼Ë%\Àóße/„S@iHÜcéÙ\ÀÁºÓFS@Æð1%]À³Ë`5 S@Å1wm]ÀqÃÑþ°ÍR@üÞ¦?û]Àã@ÂR@ü .VÔò[ÀãÞÞÞÞÉR@¬WÊ2Äw\À'á²³‘­R@B•šo\ÀGkµ0´˜R@®dÇF\ÀÓgwñ'šR@ÐÌÌÌÌl[ÀßÞÞÞÞµR@½‘yä”ZÀ âsÐοR@fodmZÀ7.Ã(ÞR@¬ñÒMbxZÀÿ§ÄƒýR@€‘Жs»ZÀ§rƒJS@׆ŠqôZÀÅcx•õR@ Ͻ‡ [À툳Ë` S@ üÿÿÿÿŸXÀ-ÀYóŒ-S@ wgí¤XÀ©¨Üí#%S@S| €ñþXÀ£þêÖ(S@²w¼W_YÀùC§S@úNÌz1¤YÀÏm2SS@†i>" YÀ{×ÅãR@"ÄÎ:7YÀÝ2„zèR@&2¬â8YÀÏ~¿A$ÃR@æ½jeÂóXÀQ§ è¸R@×£p=ŠXÀyxxxx¿R@øó¦"mXÀíÖý ïR@j›ÓoXÀÖóãS@üÿÿÿÿŸXÀ-ÀYóŒ-S@FOÈ«WÀó™é¼ES@üeÀ°/XÀãÚ©ÌIS@ºG;¬ÄGXÀéQ!Š/S@Žç˜ýWÀaXB¸S@üç«i4yWÀµ2ƒõæS@Λ-8ô8WÀ7•Låõ÷R@íå–+1WÀcP!:ØR@"ÜÔE WÀyú²KµR@&ÈÃ4ñpVÀ¥‚;Tw R@†èåVŸ VÀ³µn”˜R@XkѨ<†UÀ7Šo¶™R@ÀðY1¦ÎTÀo›€‘£R@¦m Ö¹|TÀÑŒ¸É›R@~€¹LTÀKÑV¼‰©R@rðö'_õSÀ¥E1ýŒºR@TþjB®TÀ³|gÕR@ìS@⳸æVÀ93ƒµK1S@üvn»dWÀ³ägM'1S@FOÈ«WÀó™é¼ES@A QÀ-Ð;qFÆT@–tÍ䛪QÀ#¨•(VÊT@ éšÉ75RÀ€ïßeÎT@:AÔ}îRÀ+á²³‘ÃT@SÀ{{{{{ÊT@Né`ýŸÓSÀÍÓÇT@`ffffFTÀWóŒ&ÀÀT@v®GášTÀU鵂¶T@îQ¸…ËTÀ&ÀYó“T@þòì£UÀãÞÞÞÞ¥T@`UÀQ¥Q7©T@\Qƒi¾UÀáÏ™]‘T@°BÊOª;VÀs5"Áý†T@fffff†VÀ·‚Oé„T@B†U¼‘åVÀoA˜„´xT@l`«‹×VÀ3SvÞbT@ÎÌÌÌÌŒVÀíµ‚OPT@Þî¬ÝvWVÀcÙu<6T@|a2UæUÀuòË ƒ T@hffffUÀûcÊ0—$T@_{fIvTÀc_"‡1T@¨Ï(ÚTÀçÞÞÞÞT@ú¯=³ UÀUë¦=Î T@V/†r¢»UÀ‘6:T@òü0Bx UÀñøØ—îS@R0*©FUÀ'¼æþ“ÕS@Žé KUÀkPÝóaS@”™™™™éUÀ k—}S@r= ×£VÀyS@ÆQòêñUÀÓñ³:âJS@.ı.ngVÀ[#+h’LzSÀûæ§á@S@Ø=yX¨çSÀí„@×g>S@¿ÔÏ›ðSÀ} "ÞãLS@Š%P6—SÀ#½Š5`S@Ò YÝxSÀsN•˜yS@‚72üSÀù–T.‹S@§èH.ÙRÀ³òtW¡S@ä„BSÀë Ùé°ÀS@ ïrßáRÀg™îÌS@Bæ‘?:SÀ c&ú%ÔS@´…ëQxRÀãÞ†?ÛS@þÝ ŠORÀ)ZbèS@îQ¸…ËQÀ³««««òS@–»–^QÀÇÄò&T@ö(\ÂõPÀ9T@J|'f½^PÀA ä_T@_˜LêPÀCqp’_T@¼ …8°PÀÅ)UKãmT@N7‰A`PÀůºØzT@ZÙÄhS@É÷Ô}d]À‰Õí\_S@0¥ï¦Æ]À•“¯®@`S@³$1J^À÷@÷Ì6S@$#”·¶^À±e¶èíS@•&1·¶^À±e¶èíS@ÖZëÿ_^ÀéÜ-`ùR@ZkŽù]À'(¬QàS@Ä5ß–‚]À[G1ýCS@oSdˆÉF]ÀÍs*ƒd!S@ŠAS÷}]À·™é›˜7S@2H¤µ ]Àe>ZÙÄhS@¨óÊö‹[À«Ä\lS@`æV¯v[ÀKuA<S@¹Î²£ìÐ[Àm›€B‰S@/t#¡_.\ÀGß-༂S@I 1b\Àð>ñTnS@ÐA”S@:òcÏ4YÀ×ÕV<³²S@*ŒEÃßáYÀCsŒ ÊS@Òh†ª_ZÀ/£#ÉÅÒS@’ ÷gÚZZÀïa|{>ºS@‘íw ZÀŸ†RÓªS@ökÂEKZÀÿnðÕÏ—S@f†ªÉ¼YÀu Õío•S@ž¡ÁsSYÀ‡]ß„¯€S@^b†ªðêXÀ”¯®yS@§Ê.ÚYÀw1æ>A”S@h ½úpj[Àå|d0ÿ¥S@¢¹¬½[Àù Õmô²S@yã`+à[ÀgêÄÞµS@…42¨!\ÀßuÁ´¢S@íÿH ±"\À).Iˆ“™S@Tkésg¸[ÀÃdrƒ™S@h ½úpj[Àå|d0ÿ¥S@æz®GÁUÀOéµéS@¤p= ×óUÀ•&ÀYóT@ÌÌÌÌÌ\VÀå&ê3 T@ÊýEÈVÀ ³yÅ-T@è%Ñ:WÀÑŒ`ŒñOT@¾4 ¯WÀ#ty¯LT@F‰$z“WÀ§Ïð>T@ù g³ÔWÀ¹÷‹‚‰9T@*–[Z XÀ R™ &T@PTo l-XÀ Ð_‘ T@¸zNzßXÀ):­„—ìS@p“V¾WÀ÷Ó.O×S@„¶œKIWÀëÅkÌ×S@¾c*ã{WÀ=ªÒ¿ÆS@ÞP1Îß|WÀ]àÔ°ˆ¯S@¸®_°8WÀ‹çÜ–q•S@t]¢z³VÀ¿nóo@S@~z¥,CBVÀ”hrÚ‘S@l‚¨ûÌUÀe'ž\üÁS@Þ_vOtUÀãù|=ÕS@æz®GÁUÀOéµéS@ Xô‚#õWÀ—LeƒS@ŒBž5ËãWÀÑ;½">šS@¢9ÇH0XÀÉþ²‹{°S@Vè3•UXÀSMγ·´S@€¥Ênr¨XÀ÷¬ú,F·S@v¥/b£XÀË*¬ÊœS@Â7*[ô‡XÀI¥ÀÿÅ„S@ ,SwÔSXÀ…óÛ§èuS@Xô‚#õWÀ—LeƒS@§œÂuWÀY&À`S@òl—;nWÀ}&[hS@ š ›WÀáÕmósS@Êê«iìXÀ¡# ãtS@®ßœÛ XÀcOkjÿbS@¾=?ë’WÀ¹R¡ê^S@§œÂuWÀY&À`S@Z/d7gWÀÑ-æ¾0¾R@ÞBf“~WÀ½ðÛ'rÒR@Ê1Õs¶WÀY^|{äèR@|+S7xXÀAËϦ×R@ 'Š4XÀ5'¦ÖºR@BynŸôæWÀuóx^&ªR@Z–óÊ ŠWÀ uÇya¥R@Z/d7gWÀÑ-æ¾0¾R@,@­iû? ÀÆYîŸ8PM@`Œˆ’ØÀ*¨ sEM@ÀKm= À> Ó7•OM@à±`µ%Àn•øöÀçL@€×ä“”™Àþ%X#lcL@@.z”À®<£¡$"L@@Œ¡xXÀºùû“ĦK@`:é!1ÀÂúû“JãK@Àã£ì^àÀG’ÀK@Ù’ÛTÀzw­xÓ†K@Õ’Ûm`À&G|/dK@IUM@ ÀêyJ¯©MK@Àµ@Ÿ`À"LQiýJ@Àô­?”¼ÀÎÖv¨µ²J@@§6LëQÀè‡9M¾J@\d~Àžæ‡9vjJ@@YUÑ®ãÀ*VÅÃ%J@@^áöµÀ¦ëÁ&×ýI@àXáöýïÀ~X£çÊI@@p5Q À.Ž2dxµI@`Máöm=À–kBÒ™I@ ÍG]4ÀRiBaI@àóÝYçúÀFøÒ7ÒùH@à$è0‡+ÀªiB²*I@Í@Ÿˆð Àv@´²+I@@Ûÿ÷r¦ÀB"ÃN$XI@@ÙsÒƒëÀÊN(ñ>I@üVÒW3é¿#/Ç#bI@”°gUœá?N¡à«ô`I@>Z¦2÷?FFQéü£I@\Ç¢Ïð?²Öß4æI@€B½Ëµõø?Ž\ÿ°½ J@€¼úÕŒçú?jƒ¾‰™]J@þO¾Þ?â½+*ûuJ@¤ãÀw­Ç?.ü5‹¨J@ª[ìÛ¿~h9ža:K@€ãFÑ×ñ¿úðNK@À#f^®À"FzÅkóK@0à·óÀúdÿ0–ûK@À$É'‰ÂÀ®®à«MnL@€zÖ“6Yÿ¿­Cu˜ÖL@€|6ȤpÀzM|C×L@À²™KÀƬCuºÅL@@­iû? ÀÆYîŸ8PM@!Ùæ(LÀb¼ÈààVI@ÔµÄóÓÌKÀB5@؆§I@àÉr»´KÀšŸCu=ÊI@Ü¡,|ïKÀê^œgÙÏI@pÚæŒ^LÀ*ûÒ·»£I@k©‹é­LÀò3@XáZI@`¿aª&2MÀ^‚øöH@€ÜIߥMÀ/ÇèAH@\n©‹öeMÀº»ì"H@ÀFMü±µMÀ”¦>òG@<É ¢MÀžÆÙq+ÌG@ |º¡©LÀ~jJ¯BÈG@È¢, LÀv‚•-èÏG@|¸aªF¥KÀAîΰG@°¶Äs­ÿKÀjC‹VªtG@œ–?ˆL³KÀ¦$k8pG@p]oÈKÀö)£¡;ßG@4Û ƒûJÀ.Û‡¹÷ÎG@4à[çKÀºËß>fG@<ë1¿ÂJÀf?îNG@؃Ë-ÚˆJÀ¶r!SØRG@hnæôRJÀì˜Ê}ÃG@Œ8Mü´zJÀoçåH@àZp‹JÀ*ˆ2äúVH@4¤PœäJÀz¾ÎAH@}’ÿ¼JÀÂÞ‡¹ÔžH@Ìž¬¤q¤³7@¦ž“Þ7bSÀ¬½Û`À7@*Ö²ìP}ëSÀŒnÎËÌÁ6@Jñöç™'TÀœ he7@®ÝH âYTÀ|9þaç7@Ö®²c)‘TÀt­*.7@¦Ôqüª TÀÝnŸ 7@ª¥ÛÑTÀ$[ „Šù6@,•Ê®ÎñTÀrk¤Ç6@~Å`+¾UÀÜË.·Ž6@¬Q†ªœUÀôæâyY26@°òYñd>UÀ´Í—ûCã5@ЋóŠ#UÀÌ$ÿÊ5@ž}pVUÀlóVTýæ5@£>É)úTÀ¼æâys%6@X©x6¥ßTÀ,MIà)6@&BP¨±TÀ”‰¶€ ®6@Í7OárTÀŒqkò 6@Ú–g%áŠTÀÄçâyû`6@€m VŠtTÀT‡¶€ /6@DŸ>I!TÀܳã_6@.€¹ë TÀ,ÔÑh­Ñ5@Vxp=ÒSÀlœÿ5@b…óJ®SÀDªuÙÿ–5@Âý¡æžSÀ”Kæ55@b’geɈSÀ¬+a+O»4@Èy§‡_SÀÔP „2ª4@ŒI†jrESÀtn¥o³g4@rl¨ÌYpSÀÜ­âØ3@š&dȶSÀ4îóŠÒñ3@ê6u™žèRÀô½]Ž‘Ý3@ØàÄŠ½RÀœ‡$Hê3@V$dHü’RÀ¼­L}Ç 4@Ü÷jÂd‹RÀì Ü?¿F4@ÆCéóÄ»RÀ sB¦…¯4@Ðw§òêRÀ¢Òº4@®=¯FIæRÀ40þa#5@jìö§t SÀÌxßÜX65@ÌÚX†!SÀkkÔ25@¾Îqü^ISÀLÚ ÖJ¦5@nbÑ(’SÀ4? ØD6@"~¹]<–SÀ ¤ž5ÿ€6@*¹ÝÒSÀ|/Äôd6@Ö²ìP}ëSÀŒnÎËÌÁ6@$¶\%RÀÄíóŠüÜ3@¯‰‡%LRÀ¬‡$Lè3@vnâ¹ZRÀì«L}›¡3@ŽÆÔÅ.2RÀÜЇ®y3@Z0Øbª2RÀ\Á—ûå3@â̳nRÀĸ]Žÿ¨2@®3uÙy,RÀ|[ÎËp2@¶g¨LÅ\RÀÄsJ„2@ž*Û¬—RÀüÓEC¨2@Ä.;lPRÀ2r<“U2@¾g% {RÀL-ÕÑ2@¼¡m]RÀ|'›˜ª52@’ï0Õ 6RÀÜgB¦(#2@B¦ÖRÀ¼¶]Žé42@Ôv¹ÝTíQÀ$*8Ïf 2@ »ý!êQÀŒa+Ò¿1@F  ÙQÀ\ñge—1@"‡Ê.ÿ¿QÀà¹jF2@j”>ÉÕªQÀ£¯F*k2@8¯Æ¡QÀ܆Ǒ-2@‚¡²ã†ˆQÀ¤ a+Ö<2@öè“Þü|QÀLÅÑh‡k2@HÍ«iïgQÀLsJX_2@¾õyŽJQÀ܇Ǒj2@º ¶À,QÀ$?æf22@.„Ê.YQÀLmßÜšš2@FäögÍ3QÀ4y¶€†ø2@±Ã4GPQÀüdÈÅ3@2¿7O7NQÀœÝ°N3@&d;qQÀ”i¥oõH3@Žåö'Ú|QÀt—ž5Å£3@Öåö'¸QÀ”xJW3@(‘¡¡³QÀŒ©¯F<ß3@0‹ge–åQÀ<+þakà3@DzVT—íQÀÄ yvÈ´3@¶\%RÀÄíóŠüÜ3@ ZzÓePÀô#þaB82@æ2¯]qPÀ|J½ºk2@‚áög’PÀìCƒM©2@Z4¯†qÆPÀ,ÿ'ƒ2@ƒÊîƒÏPÀ|v¶€¾]2@¥ìPÉËPÀ ž53ð1@–|Se¦PÀŒöœ:ù1@rÊî7vPÀÊ Ö¶÷1@ZzÓePÀô#þaB82@ ¾¯ìÃ9SÀ¬wS·&Ü1@4¹Ã4Ç SÀöœëà1@Æ‚ó bSÀl¶]Ž'2@ÞÑ3b9SÀ@æd2@ØÜåVtdSÀDÄôt{2@ÌÔsSÀ „2@â[—;ïSÀL$>r2@VëY1•SÀœ¬†êº72@ôWú„qSÀ¼ЇsÚ1@.^4²4MSÀL/r<^±1@¾¯ìÃ9SÀ¬wS·&Ü1@<áz®wNÀpâH¯4$@¸õ(\rNÀÈìS†±%@, ×£pNÀ¥>ØqÃ%@Уp= ×NÀ8¯|â€%@®GázÔNÀ@Øq ¥¶$@ ™™™™ùNÀ€ ¥>Ø)$@¸õ(\âNÀ°ÃÃÃÃû#@<áz®wNÀpâH¯4$@à“+a[úña@L:‹+ÀT½êóa@ ½Z@Ñ*À£Ù¨<µða@°iØBm¯)ÀÍ¢‚åa@Z‘øª(ÀsBÖµãa@<êêÓ'ÀÛÕ †¼Ûa@`¡P–'ÀwFIƒÙa@xW; ÍT&Às¢}Ða@X¨¬XZ%ÀùÀ]>™Äa@˜â`Ë&À—÷-¨ÊÃa@(Þ&^1¬&À=ŠT·½a@8Ä¡rÅ'À¢ØÒûµa@øïÔ(À'³S÷ºa@€½Zè)À3rBVÔ´a@°{éSêç)À÷x|â°a@8‰#Á¦i+À½—/V´a@Ø8Îk­Ž,À!8Õ5²a@p¬I£#-Àa1›Hx¶a@H+ $<.Àwب<¾¬a@Öa–²/ÀÛ²écŨa@4ŒM«e0À¥u‰F¢a@ìÎô)×0À#Ìœa@4ueg™`1À©PJã†a@9n¸1À•UZ¡Vha@L4¾Y?a1À[–/yca@ sÛ)1À½Ý⩹Ra@+„ìœÐ0ÀKÂúô³Ia@ PCEÝÐ0À Š7“2a@¤•$À-90À/!'n"a@È<÷ÇÂ/Àÿ*þr a@x6Z‘ø/À›¢‚ð`@êx.ÀCƒð·í`@èû&^‰r-À“Ü/ù`@@‘À÷êv,À#oBÖ{a@°jžÕw+ÀC?¹Æþ`@(éSbª*À°éãÅ a@`ÞMX™*ÀÅŠ*‹ìa@(¼a~Ê)À9r¬sa@xTÇ1p¸(À­'a[Âa@¸ÑM »'ÀYô-¨Da@ˆGð(ÀÒ ?ü`@°5ß|êð'À+Iƒ=é`@Ш‡…ƒ(À±&a[·Õ`@(}À÷æ'ÀéV”Ì`@á‰'í(À׸À‡ ±`@p²aî–'Àë#Gƒ=  `@15õÄ&À»Ý`n‹`@Ÿ‡ F&À?ªLícz`@ÈÝ&^i&À§ªL­Ô‘`@ /BFú8'ÀQU÷×h’`@xKÄŸ>(À9ðÝ…w`@@FSWŸ(Àó€ðÝ&g`@Ç¡rCb(ÀcvúÄS`@höš8Þ)À +›HßE`@˜©âI;*À }SçÜJ`@ȼó*6»*À=º]¾o<`@KêA+À/íq-`@øGBFžÛ,Àõ—;ã3`@Ð#æ¶Ãô-Àû&þ‘‰`@H‰L¾Ä-ÀÉ‚”‚ `@¨Ñ@Á-À6¬(ó_@rÜ’,ÀBìÏ*7Ä_@P¬Eã§+ÀNKü#I¥_@°ç²ƒì+À®(Ú$‰_@X7Îka5,À¶ÿ}rˆ_@Qê¶,Àr Uäk_@híOºTz,ÀBsX³âj_@ˆ& $d -À¦aG¢´J_@¨v; ™`-À¦UF;_@ eį*.À–-oM_@ð‹¯æ‘&/ÀÆ‚ić_@ØÝ;V0ÀÚD_íJô^@XŽêR0À–Ý5à^@,Ǻ¼Òš0À>%€ÿö^@DÏjÊ1ÀÒpX3ÃÛ^@¬cTVÞF1ÀrõCÎÀ^@4ïLÙi0ÀºËçu”^@„Ö¤cC1À• X’^@°Ö‘`Î1Àrû}rw^@àœ7·42À"PÓÇíi^@ÔŸÔ­¶2ÀRÇJ?—Y^@¼Òº¼~?3ÀÊ×[PÌ6^@Pâ.—%±3À†î¦Îˆó]@Lñ¢qü3ÀÆ.NÜ(Ð]@l·ö3Àvôà;8¿]@z+ún 4ÀBWªk‚µ]@„ò¢q†E4ÀZé ±Ž]@ ï;êa4ÀfcFB\]@ˆûÜÞRÁ4À†=_í¤J]@”1­H¡4Àê·s‹-]@ø¥‡‰¿µ4Àj©ÿÀ¡ü\@H)Ö¤³5ÀîâlasÝ\@¼íËÍá€5Àæ"ït©\@øª‡‰yÖ5Àj"okŽ\@ù;˜†6Àµs›•‰\@ª‡‰ŸÃ5À‚õ©#o\@ÝÞÖ{6ÀD6‘úu\@Ï uy7ÀÆ^?m\@(Àûcˆ‘7Àvb»|!`\@T z‹Ð7ÀZb»|/Y\@Pž< žd8ÀöTG¢h\@\l Ø6±8À®‹ )n\@|)Å“Ø9À¢,ë’Õ\@¡ŸÔgË9À:Œ çŽ\@Tz…N:À"–î/ö{\@ô0bÊdë9À>,º8\\@@f24*¡9ÀΜ(Ðq\@”×Ò«Ž:À@h±U\@tiô:ÀÆ·R^\@DöWó(:Àæiõé ƒ\@TLÑW;À"Rªë‹\@<ÔûcX <ÀgX³t§\@ÄÜ5ÑP†<À¦µs©\@ˆ§eg‰Ñ<À|ûÂ\@d»vx(x=À.uÌÏ¿\@”‘}²û >ÀÖ‰zULÊ\@(|24 œ>Àb ”"ì\@üIbÊæž?Àªf'Nó\@ª½ŠN@À Éðzë\@öæ‰WNt@À†8Â6½í\@„ݲ³H¢@À¾òLãâ\@@KS‡m¿@ÀÒÔø†Ã\@z:BvÛÐ@Àðà;·Á\@ÒLS‡6AÀ‘´Bä\@:MS‡…2AÀ¶2ˆÉ(]@(noE„AÀZè ˜éR]@̪âIQ„AÀæAü#™]@Ô™Ñ8X‰AÀ³9®] ]@ðÕÕ¦`AÀeFxÀ]@ÈyLMxõCè³^@ÎH¶Pö@À‚…û7ê^@âjØró@Ànš´BÚ_@ïÃÄý¾@À&†{/_@âyéƒß{@À†'Ú¬E_@¢½-ÈV^@Àáø†„‰_@`ýÔÕ³@À¶ïla•Æ_@@VÇa0%@À›ý¡Âµ`@dc­Hîô?À¹?9%1`@h^D™?ÀóoßLqj`@4.z ?Àû¾úô7‰`@ä,ÝÞ¬ý?À'ïq´Ÿ`@"Í>Ù~@À»ÍnÏÃÈ`@ú¹‘P@À;?¹¿Â`@h I°œm@Àµš;¡Ó`@ür “@À¬L­§ç`@^]d˜eú@À¬L­©æ`@çì T>AÀ›½]>¦ÿ`@mØrþrAÀÁIƒýç a@عóZ0 AÀ³ãæa@fqàiá@À÷M 4î9a@RÙ}Bt@ÀˆÔ{À«?97"c@$ï uÇ[>À+ËÞÙ*c@ìà5Ñjw=Àa†¶0c0c@*´‚Û=ÀÍì92c@Lå uK<Àߤ;ù"c@ü»MÁD;Àž¯.%c@X¡žK¦:À+†¶p[$c@Tí†V:ÀgOæÆ]c@Ðz™F9À‹ÜoZc@°À^-Vw8À]~ób@ˆ6Ö¤£8ÀOy&ÉÜb@ÐmCEsx7Àë•ÇÁE×b@Ðv·"i6Àuݨ[o÷2À‰ð Bb@äÑWóÐI2ÀK«@!Eb@ø^éÅ1À›…Sçq/À<þ¤èd@³n? /ÀådZarãd@PØggâ-Àçþó: ûd@LöP¹y0Àu³u ðæd@PõMÜÌ/ÀÓ|¥ŸÂåd@l2[+0ÀÙ ]|ðd@ðbÊ*›0Àçþó: ûd@LöP¹y0À +¹ü‡ôKf@È_a#Y1Àk`«Df@°Ï†XYƒ1ÀÇ»\Äw5f@¨P9‚°c1À¯Á4 )f@ø)±Ç »1Àošë4,f@@2@þ.,2À9ÉÈYØ=f@`±Ô×K2ÀƒÅá̯Qf@ð8/«(2À!÷XúVf@0öHߣ1À+¹ü‡ôKf@È_a#Y1À ñØ4"×ãd@$hCE+6ÀdZ!3Ód@”zñŒRµ5Àë¼L­õ¸d@,¤êR™5ÀC¨že¸®d@˜(]ìÎ4ÀÛK#¤ d@ÜÍãàw4À'‚ß ¸Žd@Tœ7× 4À›ÉÀ‡ò€d@PQ¹)4À«,Š7`…d@¼¼Òùs4ÀÛÑØšd@´:çµx(5À= h.¯d@̾5Ѱ5Àç²uIÆd@œž¯R#6Ào ]®×d@äWó‚h6ÀñØ4"×ãd@$hCE+6À$-(³“•Óe@èF|ãBÀ§˜ð]ŠÊe@Ü3kÒ £AÀ+ê¨<¤±e@ø@߬ÙAÀ‘¶°9 e@bê‰W¾:AÀ[.(Z”e@D2kÒÈDAÀer1Å»¡e@–)”.iŸAÀŸ‘qéºe@¤êì ¬BÀ5[æF5Êe@2\*+„EBÀ‰LXÉe@úëì \BÀ_JÕõMÖe@šI|ã×±BÀ›·u‰Ê×e@CžCÀ[[æÆdÒe@&'½Š)gCÀ9ôºE»e@¾¹íÒ“CÀù÷—^ºe@*þ11ÂCÀuhÎÜe@¦aûfõCÀ=G8¿Hçe@ƶâI×;DÀKs1ÅÔÔe@DÄÄ!¥DÀÅ?9,âe@ÈÐ-È’·DÀß·u‰ªçe@H]S‡)ÙDÀG~éef@^KBv!¦DÀÑÂL­Af@ ›hyNDÀÝf½ªXf@šqž DÀ£®žå !f@:qž±ñCÀ[AþQf@>pž ºCÀQ«¯Ÿ&f@  ÕÕ·“CÀó¢?f@ÌË-ÈY–CÀ…âјÊHf@ xu©¨KCÀ“ÑÀŒPf@Ô r ÚBÀ¶ØÒT@f@è'ZÁFËBÀŸ¤ÇÁ .f@ž@¥?üBÀ±áÑlf@ú+÷÷ÛñBÀçîEó«þe@´hÏ%ÈBÀ"yfâùe@Ж—ËRgBÀ=6'nqëe@ž_ÇaiDBÀAp”ŽÅêe@~Ç-ÈÒ›BÀ-(³“•Óe@èF|ãBÀ¡|馠e@:Ì‘²vDÀY½¯ö™e@ˆÕg5I@DÀKuÎ{ƒe@DI°p{DÀ§²ØÒ[~e@ÞKBvçÂDÀÍÞÑ;re@\užJãDÀÏ証$de@,6÷÷°BEÀû6Ä$ÌPe@ŒÞl…EÀ証^5e@n8÷÷#ÈEÀ»l”Žae@| aû×øEÀY2'n¸ e@º›híFÀ­k”N|ád@~rÇaBFÀ yéJÐd@Ôõ}4îFÀcƒß ©Õd@Üc¶P5GÀ¹Üјpød@òR¥?4&GÀànÏ) e@4›hiPGÀUÓút¢*e@´ðÛ#SGÀ{¢›:e@Н™.GÀ{tÎû»Se@vÕÕfõFÀwy¦ìee@Ρ]^ãsFÀ#]~ne@ùOê FÀWd½ê߉e@¬ûì ÞïEÀ£B›H¢e@XàlIîEÀ_ÎÀ‡Â–e@x;”.¶°EÀ©¯ §e@ìxF9}EÀ'T¬Ù ¼e@žF èEÀWT¬ÙóÇe@ I°žãDÀoô:ìÇe@7”.À­DÀ¯¢ÇA«¾e@6”.­wDÀ³k÷Wé§e@öÓÊþ«DÀ¡|馠e@:Ì‘²vDÀ«Ú¨<¼,b@‚·âIfDÀåsBÖÌb@VñOê*[DÀ‹¶púb@–ü&ŽÝ•DÀåXZaq)b@´kÇa\EÀÈð]Ò-b@€Â·ÜYEÀ1ÔnOŒAb@^НmÇEÀ3ú9Ub@š ¬yhËEÀKE¬ÙÙ[b@È1½ŠIÒEÀ£nérb@ŒK¥?yEÀßââé?}b@¸«Ñ8"œEÀhλ€b@ð ›h(5EÀ«¢„‹b@ÚC EÀ+‘* @‰b@b91eqDÀ½ââivb@æãÛ„hDÀ‘‰ð]b@¢bô DÀmåà¦Kb@šäÛ¯’DÀ«Ú¨<¼,b@‚·âIfDÀ1Ìeo¿;I@Ð+”þÈ +À4T^®ŒçH@hÆgŸÎ)À$y’ÅH@µó*Êô(ÀL-ŸUê˜H@ðÏx(À„~Wt‡nH@3¥þ(ÀÆ8ï*lH@è Z‘ä1*ÀTÈÕ%œ%H@(÷Ô­•+À,”¢ò<ïG@ØÉÊÎ"X+Àdƒ‘áªH@øDBFò2,ÀÄȱAÚG@ȯ¨¬„4-À¤Èr\ëpG@¨~ØBÙo.Àüšy–÷'G@xÜÊΚ“/À¬D$A¾ïF@€íÛß{š/Àˆh…YÀF@HÆP÷/À|I^®óxF@ˆ2[ 00À4+{'9F@È}ÙAx90ÀÔ,vùþ'F@¤?24ØÛ0ÀòYFûE@ø‚ÙAk1Àtpº=€F@„aéóV2À ÍIÀF@$|egcø2ÀŒ“Ü_q;F@€ˆ< ˜q3À¤Kûäé/F@ì5絤4ÀT@$A¼òE@°ŸM·Ö4À|ÿ|3dòE@€Yl¡ì+5À°aKv·E@ÀE¾YAX5Àþ|3‰ E@,ߺ¼Ð6À|1°f>¬E@< LÉ6À<øBÆPÙE@leg•7ÀŒŽ?)ÃáE@Ìn¦ x8À„õ¥F@(å€O"ÿ8À'<Œ²jF@e24¸Z9ÀŒ[o¿j´F@|Q¹œ9À4¥P:($G@Ð>sÛÍ/9ÀÜ!eèAŒG@DŒŽÃ,ó8À$¡ÍÆG@$Å“LÊ7À¼Ñ¬É"÷G@d­‡‰Wf6ÀôŸy–6FH@L¯^-R4ÀÜ~WtY…H@ía@€D; ‘û!ÀMƒý)éa@PAØBí À%õñ´üa@P¢oÞıÀ ?9Ðb@ \ÈиÀ½ù-(Ab@Pñ¼ZÀ& ÀÍŒ,Rb@X_#ÁÒæ!À£xߌUdb@èÔ`Ë]#Àgkkr7}b@¨Ó&^G$ÀO^÷WŠb@”P”$À9œ¯¹b@Àh†Š–Í$ÀõÒјèÀb@˜ÛÔMR%ÀÑ„¶0Öb@ÛÔ•.%ÀÇœï¦Ùb@€déSšš$Àݨ<¤·b@(Ù-Ã#À¥}|C=Áb@°l].›b#À×Pƒ=ˆ¨b@° vœ #À…^÷—Ñ©b@ˆ«¡rÏ("ÀYPÊ}—b@ð|³Ò9"À¿uBVµ‚b@˜ÄÔÑ À%Ô#fb@6¦®Î•À?ú-¨_b@PæajrëÀ}S ôƒ|b@àÈyµ8}ÀaðV½tb@pèÕD#^À¥…Sçk?b@€»³"AåÀß ;œ:b@0±³"ŠÀÿ@ã½(b@ðï”-‡À«´éã¯b@¨ìõÀ§³é#‡×a@à âY a À ?¹ a@À¼ð ÞÀ5šže§}a@ Ã…Ê­VÀcùÊÞêea@ Äù¤þyÀá Ja@@þ `û¿ÝÒ †.a@@-¯ycû¿%‡Ôb a@ÀÃ…À[”¯¤î`@À¤O ÀwÛâ)†Í`@À§);;8À7dx–Ä`@@[[îò¿-†¶0{$c@pÚItÀ§«uÉ£ c@ÀPÛèÀóV{c@€uq–X À…Æ]þ­c@ ®R—zü ÀkKI=úb@ ÊâÀ³ÂÀ\ïb@ Þ¼YNÀmk2Þb@€ ÊòÀï ¢‚/Õb@`V4ÿÀoòVLìb@ ¾|YÀ'Y½ê€þb@ öù¤>à ÀWåâ)þ c@ ošxUbÀ ¢pc@PE‡)Ó¼ÀɵL-yc@àHM¼JÀ-†¶0{$c@pÚItÀƒKIsÿb@GÙáòÀ_öóº2 c@PiÒ§ô€ÀojÎûÖ c@ AM¼òHÀ#Ôј`c@Ðá½ùÔ Àßäâ)6ñb@ u&N´À[…¶p»ôb@ ‰ôɶÀ}_÷—Þâb@èæUä|À+f1ÅÖÙb@P6ÈÐ@ÛÀ{98¿”Çb@@—ôÉv)À‘[ZáxÄb@€?ÙáÙ Àï5›Há¿b@€MM¼2#ÀÓ^÷×»b@\‡)Àuê—Œ©b@À8ÈÐ8^À£”ÇÁÛŒb@ àr{»ÈÀ+ôóº4Šb@@d$`‹À[oi{œb@ _M¼"#ÀƒñV¸¶b@@„Œ”LÀ¯Ï4â·Çb@PS<«ÙMÀ³('n$Øb@@šº\>^ÀõQƒý¤éb@`6Žc`eÀC"í±îb@Õþ 2FÀƒKIsÿb@GÙáòÀ [Ï—+)|c@Мã¸%PÀŸe”Ž£€c@€ZÙáq1À§F#‡qc@ ÅìðÕÀ÷Brlbc@0ƒF‚ÝLÀ‡a÷×QXc@0úZ0dÀíØnOáTc@àÙr{ë3Àßu¥ŸsPc@€x©Kí–À•¬u‰UWc@°c‡)ë¢ÀƒôVXec@P÷²C-Àƒê 3sc@0k°…жÀ[Ï—+)|c@Мã¸%PÀ:›È:±c@€€$`£lÀwÓ4âz¤c@àQÈРÀb÷×^‘c@ÀTŸt1nÀ;X 4¹c@ÿ¬èËÀ# oÝœc@p·‘—½ÀßQæ†Üªc@ ˆ^Íó¦À:›È:±c@€€$`£lÀ i?9üc@€Euyñ° À+Sçzôc@þ05} ÀwåEs=Úc@ î'ýéEÀýÜï„Ëc@`þÕD+PÀµá¨¼ÁÆc@H·¿w¸À;ýqÁÒc@Pàìh ÀÃhGäc@xÏš8²> À‘4a[[ýc@H_À÷Ö!Ài?9üc@€Euyñ° ÀcviÂ5d@°Šâm7#ÀÜnÏõ(d@ðša®A"ÀWËÞpd@X4dh¨ ÀŸ|BVd@X4dh¨ ÀAË^9d@€z vÚ!ÀÍÄ#Ñð0d@èÓÔ͕#ÀcviÂ5d@°Šâm7#À¯ÎútEd@(Ù-Ã#À«|BVd@h°¡r£<#À}|BV d@PÆìð0Ñ"Àß †~öc@ø$ €"À+Sçzôc@ø³>©çK#À›Er¬.ûc@HjÀ÷Æš#À‹ß †Íd@ Ή'•Î#À¯ÎútEd@(Ù-Ã#ÀnÀs›æ_@Àä7oêK$ÀnÜ[PªE_@ØÚš8ŠÍ"ÀŽsX³A{_@¨¡Ê΂:"ÀÂ" ”ó½_@`O¯æaZ!ÀjgäØÕ_@ ƒP©Ï À ¡QyC½_@¸®x0 Àr–zÕB©_@@¢<#Ð À¶•zÕœ|_@+Äwá Àbh…E_@-ÄT!À–½Öäþ=_@€sÑXÍ!Àªµœw¸þ^@¨àÔ¥º˜"Àb%K™Z3ã^@àì Ñ#ÀþÌçupÝ^@øKžÕ$Àv\ªkå^@ȲƒŒ¼$ÀnÀs›æ_@Àä7oêK$À"íˆÌ-^@éÔ¥æ~$ÀοÒ¡1^@Д‡µô#À’ ”?^@ ·Ûß/Y#ÀzÌ„¬žù]@Ù=½"ÀÚ1ë’ð½]@¸žaê!#ÀZ ƒá^@ßýªˆ$À"íˆÌ-^@éÔ¥æ~$À ê‹æ™y]@°Á&^95 ÀžUsh]@xã«IGê ÀÞÝ2ô[E]@p<žÕLî À¶­œw^/]@‰P"ÀFt/WÆQ]@ :dh,"ÀŽ`~]@¨ç«IoÔ!ÀÞ›î¯È]@SLžm!À–›î¯8¸]@ØK¯æñ“ ÀöÉð­]@èV†ŠÆ½ Àê‹æ™y]@°Á&^95 À Þ}ÌÓ¹^@8çå¶{4 À–þ©x€^@pÙÔ¥ð À¶¡‹æÝU^@,Ä !À²K6Ä-^@À§>©} À¢Qp~ðú]@€ûöÇè ÀB%w8)û]@¨'SW+£!Àb4ë’IP^@¹½Eâ!ÀZ‹@hr°^@ðm4ÒïP!ÀÞ}ÌÓ¹^@8çå¶{4 À~&ëç'[@p°‘Ï$Àú\»|([@©K•¸Àò6ü#[@`8Tö‡jÀ6’‹æõÐZ@³¢`ÚÀ*ÕÏ*NƒZ@Àt˜:äÀÂc’ dWZ@€cvRpÀ²¶J¿õ‘Z@ZŸt‘»À ÚZ@ÿÕDãsÀêò·ßÆ[@ Cà‡ÀòRäØd,[@`Rñ,x™Àž”‹æ^[[@ã³"éþÀ Qyƒ¥[@€BFC ÀÆ×2tiá[@ø:žÕìž ÀnÄ„¬Ñ#\@¸žg×Ä À¾}£1¾]\@°Euyå¶ ÀÛ2ô ¤\@¨—a*…!ÀÆB™Z'í\@À—-˜ Àºù·ß¦ž\@@z°…¢#ÀZ<ü#¤>\@àlÙáñhÀÆB6Y'\@$Ï 6ÑÀÞÅ!㜰[@ð_OÒäÀžbX³‚¢[@pj°…âŠÀ~&ëç'[@p°‘Ï$Àùj¥0×`@üæUäÀÊјC×`@B>ÁûÀÃ'îüÏ`@ Âí1ÐÀ§'nJÉ`@TOR*Àc&aÛ›Ã`@`k$`[šÀgO½j¹Æ`@0]<«1Àùj¥0×`@üæUäÀ#’¹çuTtZ@`6ŽcHqÀ’¨ÖdàvZ@`UûTAÀ¦u£1ó†Z@Y7¯uÀʼ„¬ÐgZ@@P«‰6Àòäà;Ó8Z@À-Ø!ÊÀîÚ‡"Z@A#ày¦ü¿²ÿÈð­Z@@Öµ?f}ñ¿f„´Â°Z@À †©iñ¿¶ÕlaÜY@?† 翆<ÓG¨õY@0`#!¥¸?~Î2ôêÄY@‹°hè²á?Z¹„¬ãŸY@ÀZ™Û1?ö?¦ë·ßjY@à@á›xš@~°J? )Y@®o—º@Ú²çujìX@à3®hT @Ê9®ÉX@ ðõI¨ @‚™œw —X@°t8/W @öÕ¦N_X@ êÃó@RHõûW@ ‡J—¹@²õÈðÀÒW@`rí°Ýâ@"KF`ØW@ ’I@œÙ@JéT#X@€²Ï{â@ÙCSKX@@F"Cvg @ž< 5ÆlX@ ¢6ñ-@ò’bŠ|¦X@@äY3 ý?F婾X@€]Ãð?ÖŒ(áÐX@@ÙíbÆ?f'Â6 Y@=Õ‚i忊¦s›Â9Y@@çÇàwÀŠë‹YY@à|âvÀÊ•½þ‰Y@@wS¶ú ÀÇø†d¥Y@ÐSûêÀþܦÎ÷Y@ ‡º\®.Àöïv-Z@@ôɶ†À’¹çuTtZ@`6ŽcHqÀ.ÚcF x]@ÀÓH"ý?zÕƒ]@7Њ?<@Þ6% T]@€Ž ô*Ï @¦“´Bsx]@0Ü;Ìa„@bI6‘’§]@P©l5á@æg»|$œ]@ø¯¦>Õ@Žºs›Ç]@Æ|s @îÕ[P¤Ë]@ ˆ8/'™@.1ë’@–]@ Ù¶à;Í@.ì¦Îl]@"û$¸ê@âD™Z-i]@€&^îÓ§@Z=ËKH]@0áBÛ­@žÿTh.]@Ý¥Ï~ª@–‡Ýž ]@°¦I@(Š@Ú ÉpØÜ\@ * 6Â@&xiÄe¦\@@ÎS$‘@*µs›\@êu9@¦îà» m\@ k3T÷@– f'¸?\@ )}ÃÀ@Æö·ßó[@ÀR ø_@Ú~@h¯×[@úQÙ"ƒ@æ_»|ÎÊ[@€’äYSzý?¾CÓGZ™[@ÀUN]ð|ú?<™Úrj[@À–>›øÿ?n,ºlD[@òÍ­Võ?k/Wø<[@› |û Ú?þ‰´BáE[@r•ðÝ¿=Ëšd[@@VóI½+õ¿6J 5Š„[@€Kn^Kžù¿öFp~SŽ[@`iåöÖ‰ÀÖ ^Ã[@À£µ`*vÀ4¶í[@àì3À¬9.\\@`E#°ä Àêã ˜rP\@À¡1 ÀÊ9_í\p\@@Ð:L\” ÀîÉ!cþ\@T‰g$ À#ïX·\@ mã¸vÀFvÌ ]@@ð…ÊR ÀâP 5z ]@p+š€À¦ ,º)"]@`ÞÐH/ïÀb‹zÕ×#]@ÇPWï÷¿Ê¸s›ba]@€•|Ýû鿦о^]@¼è«©¸?2g»|õs]@€z˜¾Ôè?T µÊ¿]@€£eê6›ì?ÚcF x]@ÀÓH"ý?-^5N\dO_@@‚X4Ä•ö?êÏ*9D_@@ÓÿAè(ú?Vx’ ö_@_„o"í?±ÿ@]»^@€*‹©Ï½ë?®u’ ­j^@ÀÒø;ð?‚¼s›°8^@@Qü¤±Ðô?êÉðH^@s^ÑÜá?NäÏ*Ýô]@à'¯Â?Þh»|²Ô]@@{l_Èõ¿2ë•Ë]@à87¯5>Àzr’ #±]@Àìmo{ÀJê ˜Å]@€Øt¹\ö ÀúWªëìß]@€þ3 Ànœî¯Ôé]@p·'ýéÞÀ:çla{×]@@›.7wÀ®J6‘úò]@€±?HºÀ0NÜ^@å²c%Àέˆö^@ U˜:dlÀΪbŠŒ^@ÀÒè“Ý„ÀΌݞ;>^@ ˜R—ZÀºï¦Î{9^@Àêè“ â Àž;%€ g^@ðÈÐxÉÀV^S_^@PTöÏTÀF'w8?o^@Pݬè;pÀŠ[ªk ®^@œ?HÈãÀ®U!^@A<«1*À2î ˜9¨^@@XM¼J’ÀÚÿ©cÊ^@`#·¿?eÀ*~Ì ñÊ^@D°…’ÄÀFÈJ¿f‘^@ÀÅÆq[M Àò?¶^@`Õ®&ý ÀV|̇`^@Àùø¤šþ¿>Ï„¬Ý˜^@@|>Èîfø¿Î^§´^@™¶î¿>§(‰Ð^@@BV Zñ¿‚Ú[ÐËÕ^@R [÷ã¿ú`äØs^^@€æ†gÛî¿Ö¸ßå;^@µåÕ¬ö¿’»s›^@€ißYÍäà¿*\G¢· ^@ør OÍ?éla¡C^@4ðUÞ×?¶TpþF®^@YÇ77Û?æÚ[Pßë^@•©ÛÍ?âÔ!cø_@è‰-àÚÚ?^5N\dO_@@‚X4Ä•ö? ÓÃ4"`@Àß^üñ?µOZ¡`@@ºî0G…ø? ú "`@€žÌ•ìù?¾;ë¬û_@€Ö@È¡T@ªVÓÇnæ_@@«ÌµÖü?îÍJ?‘Ù_@À2%!ð?ê²b “ì_@„Fz—Ñ¿§‚T3`@€[›‰í¿¹ÔEó( `@›S4Ž9é¿önFôý_@ãMv©Ð¿C'nØ`@Ȳ‰ïGÖ?/d¸Y`@¾Ãš€Ð?ÓÃ4"`@Àß^üñ? «¢@O`@€†0uøÐÀ{¥¯6ß+`@ÇÀ{ÀYg¥Y`@€Ât¹¼ÐÀr1o‡ù_@5¡²²6 À·OZáæ`@ âYà~ À³úÌ÷$`@ú3Ÿ÷ ÀMW”޲?`@à~‚-÷¢ Àvú¶Z`@À“¼šïÀ«¢@O`@€†0uøÐÀLü#óÏ_@à¿);½ ÀNÔ„¬ À_@À‹Í«È À¢­ÅSL_@÷ÐHÿ{ À:wõiË_@£Ùì Àýà»þ·_@  ¶ßdÀLü#óÏ_@à¿);½ À>D¶˜_@À‡ø&Ð @ÎÉðŸ“_@(/÷eŒ!@îç2ô@Ž_@˜NQ<Ž"@Jü#`Z_@ —•]#@„iD+^_@P¡¦nõ!@–@hï0_@ó^~ç!@’†{~&_@8¦n!@žQyÕõ^@ˆõ‰Ïv @jF6ß^@è†[ð•^!@ÉJ?P¼^@€½È­ @^Ÿî¯ý“^@ðFîOª @ŠÕ¾àz^@]º}B¼@:ÁÒx…^@àÒÎ+Š@VUÕ´^@€Ò÷‡áË@Î’zÕòÒ^@ ³Cu¤@V)w¸ ç^@ <Á·M@)˜_@ ®ÕeËh@Ö…ûü^@ ø„è@v>%_@°ŠçÁœ@æþ}r`Y_@:€zJ@–Ž@è¸k_@ -ÒÈ`*@fM™ZIW_@Ð,^îã@"Xpþ5u_@ðêßD¶˜_@À‡ø&Ð @ yÌ K ]@Èw­¨À"@+±¥âÁ]@ø<›¢ý#@ü©#ì]@ðµàÛ%@òF™Z¼à]@Ð-æ¹&@†¢(0¿]@h(»m¼$@"ƒ£1Ø]@˜¸·:Z#@ò=ˆj]@(›l"@¾Ó[P'K]@`°T¶ì· @yÌ K ]@Èw­¨À"@ ¢” Œx^@ b´âWÄ'@žþ©t‚^@¨J߬Ð&@ny/Wé&}^@Àc‹†zË%@J€iÄ*€^@ ÂT¶Ý$@† òLШ^@ _Pw%@Fw’ tÆ^@N@¹P&@Nw’ ±Ç^@°õ°E™&'@bC_íöž^@ð„s;Ø%'@¢” Œx^@ b´âWÄ'@Âô¦Î)`_@Ø-äxO(@º8ë‰N_@(æeÇ )@jÝž_@˜S›V)@Wpþ08_@` Õc’'@ÚL™Z9_@Яl‹Ð&@ƃ\_@èP@eù&@Vuõéa_@˜…­¨hÃ%@Šð ˜¦0_@ðQzuѨ%@2'ÚQ3_@à:id¸@$@B,ºÁQ_@p9Ì-n³$@†›´ÂB_@›[ðaï%@¦Z µÂ@_@¨j(½:›&@Â<ˆI$r_@pía&@Âô¦Î)`_@Ø-äxO(@ ^š´Bàþ^@ ÕŠ$@¶¤‹æü_@0×Èër&@þî žÕ^@˜ZîOª„$@f'íß^@x„L]Ý%@ ¸_¢¼^@X{ÖH¿%@Êà•½’µ^@°÷$ z$@Z„{‚¥^@€ç°Eò#@’ÁÒR˜^@HÓgi#@Ú•Œ¼¿^@à«} "@NᕽÕÓ^@xK´â·ž"@Žùà;âç^@¨ª 6â#@^š´Bàþ^@ ÕŠ$@žÀÒÀa^@¸Ÿçe*@¢&w8‡K^@xÅ×*@µ9.³^@ð©!ƒ‘ê*@š{Ì^5^@àN|wd)@Ò,ºÇP^@vÅóe(@žÀÒÀa^@¸Ÿçe*@!NØ[PT^@<ÓECì~2@â~iÄÐ-^@lyv82@έˆö^@LÏEC?—1@*×[ÐU^@D íP¿0@ŽÝ•½ø]@dïœýZ0@¢Qp~ðú]@ªrÐË.@J ‹æ^@HøÈÙì-@¶á2ô$^@¨JߨÆ,@‚ŒÝž_,^@¸~Q.-@b²œ÷y?^@È;ª Ä-@æòC{+^@ø}îO††,@ì ˜;(^@èû-oô_@›ç‹)@"úà;š _@hh¬ú)@:7ëºö^@ >æ€u*@N`G¢Ñü^@˜'617Œ+@þoX³á¬^@ÈGæ¨,@øà;’^@àW/÷{k,@Âu’ ¥n^@ÈäT¶ä£,@rdS`^@¨UXS«;.@ Ukj^@‰QrØ/@2NÜ%^@$\¥oA0@.œQy¡^@Ô%8ÏÑ1@†ºÖd'‹^@…Ç‘PÍ1@šoX³•^@ŒZÎËs72@ZM6‘¾^@t´ÀW~x2@†þ©|^@Ô|ðíÔ52@NØ[PT^@<ÓECì~2@ B£ÿ@µ•[@ÄòÊ.«2@Qy…¤[@\õÊ.c?3@Öª9®¤À[@ì¹ÀW °3@>UäØV²[@ìøÊ.»4@f„zÕŠ[@¬@¬©Í4@’¯s›žG[@L yvÐ3@zì}ò([@\dÈ\3@*4_íî)[@däVTÙ2@šƒzÕi^[@TÄÑh~02@B£ÿ@µ•[@ÄòÊ.«2@ V¸…ë‘Q@„#½VðÛHÀ¦p= ×£Q@†L没HÀš™™™™¡Q@Î-”ú`‰HÀŽëQ¸eQ@Ä-”ú`yHÀ¦p= ×;Q@QHÀòQ¸…7Q@æ²LkHÀ²Gáz.Q@ä²L HÀNáz®/Q@@BBBBäHÀV¸…ë‘Q@„#½VðÛHÀ pffffFMÀÝÛÛÛÛIÀ(3333“MÀ ÁIÀNÀ¡IÀœ™™™™™NÀáÛÛÛÛíIÀ™™™™YNÀpuuuu'JÀÔÌÌÌÌìMÀáÛÛÛÛíIÀ83333³MÀ«¨¨¨¨JÀdffffMÀCBBBBôIÀüÿÿÿÿßLÀyuuuuÇIÀpffffFMÀÝÛÛÛÛIÀ<B•(QÀCæ²RJÀ,w-!VQÀ+Ñ•¶fCJÀ`od‘QÀú•|šAxJÀL›8¹ß¥QÀ¬†“âÏJÀ³^ åÆQÀLJ´’ KÀî$éšÉRÀŠÖ8I¡ÜJÀ±.n£uRÀµ{¼>!‡JÀ”§:äfªRÀl_îAlJÀj4€·@RRÀ ±ýŸûJÀ†ZÓ¼ãRÀ‰‡lÎo@KÀn¥]ÀQÀ«ÔÓõò‡KÀú'¸XQ}QÀÚT‹6ušKÀ²Yõ¹ÚNQÀômÄAðÀKÀMg'ƒ QÀj_ÏKÀâ K< ÒPÀ¢.Už§KÀ¬±KTo½PÀäk™ºÙsKÀÊÌÌÌÌœPÀ¡KÀ`PÀ¢¨¨¨¨šKÀ.3333CPÀ—¨¨¨¨ZKÀÊÌÌÌÌœPÀ«¨¨¨¨:KÀüÿÿÿÿïPÀÛÛÛÛÛíJÀúÿÿÿÿQÀÊÛÛÛÛJÀ(3¢Ÿ“(QÀÑa¥ƒRJÀ<B•(QÀCæ²RJÀ¶÷XqcÀ¤ÀßÓ@3@â„BgcÀ|,@;3@\3ùf›ZcÀ4ÐY ìq3@ÔMÖYcÀôo³€3@,û®þacÀLà%ÜÏÙ3@‹)‘D/gcÀü'êpü3@:´TÞlcÀ ½Û`L4@1w-!ycÀ¼e€¯Œ=4@¬ç¤÷{cÀ ÅÁIB4@„}i}cÀ|›Þi*4@zßøÚ3{cÀ—Nø3@82üÁ€cÀœ¡±šRÎ3@˜çÁÝY‚cÀ¼îÂÕ±3@ÈYØÓ}cÀ¼&׸¢T3@޹k ù}cÀt—½ 3@Ђ}vcÀÎhOmè2@¶÷XqcÀ¤ÀßÓ@3@ 2|DL‰‚cÀÄ|«½¢4@ð=\rÜcÀÜ·þzÁ4@2/À>:ˆcÀ옳â¿è4@áE_Aš“cÀ¼hOm5@ˆ…ZÓ¼–cÀ„•"ë4@²Ò¤t–cÀ¤Ð«¥$Û4@ 2þ}Æ’cÀŒ¦’TÆ4@zz¥,CcÀÉXk4@2|DL‰‚cÀÄ|«½¢4@œPˆ€C˜cÀ¼ÂD'+5@q%;6¨cÀì¼øF65@¢JÍhªcÀ¤iJVé5@ øü0B™cÀì"ð+z5@œPˆ€C˜cÀ¼ÂD'+5@ "{½ûã´cÀ|¢[P5@ÇrK«!¾cÀìSOŠú¤5@~û:pÎÀcÀ,lª’lµ5@–eˆc]ÉcÀ„W6#’5@4w-!ÈcÀt6ú<ê‡5@Nl>® ÄcÀ4Ø?óÝM5@OŒJê¸cÀäc\ÞD5@Oé`ýŸ¶cÀdJVé’A5@"{½ûã´cÀ|¢[P5@D&9 ëcÀÜþ;Fù5@.σ»³ëcÀü{°0è46@¼ócÀÚÁ,X:6@<Ç€ìõ÷cÀŒ0"õB!6@ßÃ%ÇùcÀtTgY›6@ PSËÖîcÀܓփíß5@D&9 ëcÀÜþ;Fù5@/ܲݣà^À2Qbú8@H@ì ÷a û^À:Yœgð†H@¿Ý²Ýí:_ÀÊ[ÀÆ»H@š–ÑbZ_À¢5ÝŽŠøH@FƒRp_À"§´$I@(ô`åz¬_Àбñ¼A2I@5çìÊ¿Ó_ÀΠ૫EI@Òêx `À¾‰•aI@d s:`Àr\œgóCI@þÖ?æ`ÀöhÂKþH@Í8¥iëÁ_Àþ߇¹BçH@•ayf¶_À¶#ÌýÇÂH@¯¡}_ÀÊcs û•H@YH¶ºëi_ÀVUÿ°ŠhH@#¦â3Ó_Àš^ÖÔh.H@/ܲݣà^À2Qbú8@H@ qtb¸–`Àª”g÷K@š Ö—Â¥`ÀI´²²K@psÙT«§`À†X#áëJ@Xñ_¿¡`ÀŠ»Žó›³J@½ºº‰™‘`À^Ï<;¾‹J@ ÖÆ…`À‚ð^ÝÒPJ@ãde”}r`Àvï^ÝHJ@Öúa·ºe`Àrï^] J@Z5ÏW•`ÀºËŸù|J@Tô'êÿw`ÀÆ¡¦>MK@qtb¸–`Àª”g÷K@ Ë’‡¹3 cÀ*ªÛÄL@.Ó.G„cÀ’‘[À˜ÊL@0ÅÃcÀÂTQéRòL@Y3[ÀQ'cÀâ·øúL@¦Ð‘°h8cÀV­CuvçL@z&çÅxUcÀ®kœgù¹L@ú…_†PcÀšY‹V~L@ò,!³)@cÀêpÖÔú\L@Ë’‡¹3 cÀ*ªÛÄL@SÐWƒˆ²dÀâ 2dkóM@A•픵dÀ1õÙ…$N@ Û.'øÎdÀfBk0N@9¯5¡‘îdÀþ™Ê6N@âh”%ÛdÀ¶ù$ðp÷M@8“M,+ÆdÀ¢±Cu‚ßM@SÐWƒˆ²dÀâ 2dkóM@ 8¿©»iweÀ–yœgãO@ûÝ.ÇPyeÀ†]´²ã²O@~Nl±²qeÀ6†Bž§O@ÈàËý{UeÀ6ðß ¯O@|2„VzEÔ—O@[à+ñ0eÀÖ± ý{O@µ¥^½³eÀž0»ì—O@ÒWãeÀdO@·‘°×5eÀ.Îñ¼¶O@çMl1·OeÀZ‘çåå×O@2Ú‘p©ceÀf ªÛ½ÊO@8¿©»iweÀ–yœgãO@u€ÔDZ@‰ÖlŽ“S@*ƒ£äÕWZ@ï3IÕ­S@NFΞµY@Ã$> yÑS@¢€&†…Y@õ¸Q ¤ÕS@z-!ôPY@SÞ©)rÎS@vÄZ| ÜX@å_t"jzS@u€ÔDZ@‰ÖlŽ“S@£OÚâQÀí(»³½QÀöÙDo.QÀñzóÛ QÀLfEþCQÀ¡` 6D…QÀ¤áY±R_QÀ«K[nlhQÀ&ì0U]nQÀƒŸ2˜PQÀÆÃÔE5QÀîø¢ÏÄ8QÀ}óÊËQÀ!ÐFÀÌBQÀ"yïQÀß °Cæ`QÀ"X428îQÀÅÛdQ”QÀˆ­‰GûñQÀarÇ$¬QÀÞçö'ÈRÀ†o}ÀÌQÀ„>L½½NRÀC„J>ÊQÀÌÇÔÅœzRÀåí.uÄÑQÀÌÇÔÅœzRÀ{ê‘xÄÑQÀ<6uÙÎÀRÀ=@ç“ÙêQÀbßœ ½RÀÌ(œ•/RÀv‘œ)ŒRÀ”f¦lÿRÀú9¶DRÀ@†7RÀŒÅÔùQÀ¿˜xpRÀÆp0×RÀaQ—…RÀ Ž\ÛÄQÀ‘§MzÆ RÀ öùg}QÀe~ñê;RÀšœí12QÀb=JÝt RÀnø¤ï¥ QÀAŽ¡ óQÀÔdˆ]QÀé.Ö‚‹ÚQÀ£OÚâQÀí(»³½QÀVFžµÑ¾XÀ½sGCüQÀK×`+”YÀê(»?÷QÀ×ú‚Í mYÀ[´ÁÔwîQÀDTu™*•YÀWG!ÁùQÀDTu™*•YÀíC„„ÁùQÀWÐQsYÀå/Ö‚RÀg#!$2YÀY2¢ RÀÅO¦ÛXÀn–XÀn–ƒÊ§^Àé¹^‹ jRÀ÷ÐÛʧ^À¶ÁŽ jRÀd»-rÜg^ÀOº^ 3RÀí`¥Ü^À#Q—6†RÀ}Æ¡²Ò]ÀS'ÿ^éuRÀvÂ\X®]Àh@V_RÀ½‰—uÎú]À]”Ÿ²ŸjRÀBÖg‰M^À$½ûÁ—`RÀ ¤Ó‘p©æcÀž¡ÙYWàSÀ|¸©ÛoïcÀgl¹~ÃSÀ(`·ïàdÀj 6ì¬SÀ=é?ø×'dÀ¯»ÁTܘSÀL$­¸bcdÀe™Ÿ²ÒŽSÀÀ©˜ ÐvdÀ†U[SÀÀ©˜ ÐvdÀR¾ñ¦¦SÀ´Ä€Ÿ!bdÀ!Ô 5¸SÀeT†à`dÀK¼ÁŽø»SÀú—‡9NdÀ°¼Á‹ÒSÀ &JO$dÀ›â€géSÀ¤Ó‘p©æcÀž¡ÙYWàSÀ$‘PÏ“FÀ€Q¾·ŠƒSÀÄ~ØTGÀ’l¦l¾uSÀ|yõ`HÀ€Q¾·ŠƒSÀPÈTHÀŠ(b≃SÀPÈTHÀõ+ÿÞ‰ƒSÀ¤ï¥nJ§HÀ:s¹àSÀv‘@õHÀé´`r´SÀ sô «.IÀ¨ÇEÌSÀÉã~IÀkL„„ÝçSÀpîk3íIÀ“¶‡!/ýSÀÜ"~vþJÀ™8Ö<½TÀ˜²ÄsKÀ(!‹>)TÀœÇò mJÀã°M´e>TÀ,~Ë­µ=IÀ€sC](BTÀŒ¢'=v1HÀTJçÍ5TÀh¯8NÊ@GÀ»ô‘x‘&TÀÐýðlµpFÀñ@@DTÀX#M|¨ªEÀù49†3TÀ<|?¬¯EÀÕ7Ö<˜áSÀ°†¬¶¾EÀ¹$XÆSÀÐW€¯ÝõEÀ¹ª°Ã ŸSÀ$‘PÏ“FÀ€Q¾·ŠƒSÀ ¨}f:ÉMÀ­QÑTÀ°¯³bNNÀ1œÏÃèSÀªyõÉ‘NÀ~1œOVÿSÀdg51ñNÀ QQ¬TÀ,dâyiPÀ +bâgTÀàp¹Ý’PÀ›þhæTÀÚ•xvwoPÀÒ´µ#TÀÚ•xvwoPÀ?C­à6&TÀ6P4r=PÀn­°}ˆ;TÀèØr»° OÀËêºÔÅ7TÀ$²P™oNÀÍ+bâŒ@TÀTò÷&ÔîMÀÒ´µ#TÀ¨}f:ÉMÀ­QÑTÀ d=K^@ô‰¶€WÈ6@ €iÄÇq^@ô¨ÿÐb8@Þ¶9.á|^@ÜWTDý8@^ð¦Î®_^@$Ž|…I9@º\G¢u,^@\vÎ˹‡8@¢=ËË^@$÷¹IŒ7@š_äØ^@¼ œ|Î6@f'Ð/^@LÜ?Yö5@d=K^@ô‰¶€WÈ6@%ƒ¹#‘>Ÿa@jßon ‘B@Ÿ>#´ža@vtÏš6C@[y¦N¼a@*Åê‚–C@C'ĤA½a@JâÒ7ÞýC@¿VZ¡Î«a@^«e—e¯D@›“døÈ‰a@r¿ßæ—D@#&ĤD|a@ ’ GD@cå×Àa@2òãÈ·C@O)a¥ma@R¤e—•C@ézzq[a@jCõÛèB@·°éã,a@âÈàØhB@øÊž)a@Ž›+ªú¥B@ǯéc®õ`@æFjÂA@'ØE3wÓ`@âže—–ÜA@{wúÓ`@šÊ^]j¶A@'%a›K|`@Nü‘ç^A@5[1E\`@†yCu»A@—ÜoSK`@Ö\[ÀEÌ@@)PZ!-`@®Ö¤@@I ô:`@š÷‘M@@y ]SN`@¦|Õ'@@?9zF`@䯶€óh?@Û³#QöU`@d\þat?@Š7¦j`@MíP,q?@CüL€`@f2ä#’@@ç.8Ž_`@~âF’Qð@@ßçVD…`@.¶MÌÅò@@)öÊÞãª`@nMJ¯/A@ãìóºŠâ`@V‹T†LKA@«âßã`@2žNŸë@@uƒð`ù`@Vc•­oº@@+þ‘ö&a@jÖÒ·‹LA@·Ó †7_a@Îà©ÛdTA@ß݈a@¶®v¨žA@ÍŠ7Řa@+‹VÔêA@AEIP1“a@6 e—ø*B@ƒ¹#‘>Ÿa@jßon ‘B@ ínÔ`@~r  A@‹Üoî¼`@ŠÒ5§-A@¯–žeƯ`@òý.GÚ÷@@c3Õu”`@®žΨA@‹ªL­à‹`@¦Éû“Iº@@ç9£ž‹`@’·ê•}@@‹Š·y `@i4 Y@@…¿úô÷¨`@b±°•¤@@ ÊÑØ_¹`@>öôYÁÁ@@ÛÆ4b‚Æ`@òW#±˜@@Û«L-†Ø`@êë&æ@@ínÔ`@~r  A@Ï ýa@‚%@Ø9F@ï'Äd’äa@’Þ^]D@F@#hkòö¾a@BªŽó‡ÅF@÷Ê4¢€µa@rÀÙñÅaF@ûÃút-¬a@v¥Žóµ°E@ó…ðü‰a@P9ž™©E@߸#Q)za@’Gÿ0GE@“ïó:~a@¦ÔÁ&ØÇD@Ù7Õ5'¢a@Ú0QéÄÉD@#þgU‘³a@’=(ÓUE@5ÏÑâåa@fX#TþD@#ªÀèb@Þtøvs}E@ YZaa1b@Î>( } E@ ÞE3D*b@vÆß/0F@}¶0¡b@öÙÁ&ïùE@Ï ýa@‚%@Ø9F@‰}z¼ôa@~«·O¢^I@³FIP‡ça@žsJ/ËßI@ƒÝXèa@º•-Â]J@ÿ½À‡DÝa@–…¾‰ ÙJ@…¢ôÔa@fPîÆ-K@Wy|C¶Æa@ŽS‹VÍK@gílÓa@¦KQé~àJ@}S½j×µa@Ž(/Ç—¥J@Ç—«³a@n=zE­öI@á;rlÂÅa@®ÛML×xI@§?yXÄa@žÏš®ÍH@Q8Õ5ñ¼a@š_ÖÔêlH@[þg—Àa@RE‹VÌâG@o½À‡½a@ªW9žfG@ŸkéñÂa@*åû“¯úF@ àâ)í×a@Îr!SÂ]G@ñó:+ða@ªjçå—G@ÇûÊ^ña@Z+@Ø jG@Ÿ;œàÑa@¢ 9íG@»º#Ñåa@šÃŸ.¦H@çJæÆîb@‚§·Oë{H@‰}z¼ôa@~«·O¢^I@ ²—­ôMT@p“›øfÁ@Ö¥!ãÉhT@ ÙÞä@†.ªëmrT@°p.X#@¾†œ÷yST@0ýÒg£!@ÞY£1¯5T@øœl3…"@6‘u T@(æ°E±¡#@Rw(}ìS@è®T¶˜b @"®ø†Ö÷S@¼ äP@êU{KT@Àݶà#×@²—­ôMT@p“›øfÁ@ #x¹n /@æáon‡C@ Ø3uÂ…-@q2d[C@5šÛx{+@ž=ÿ°jC@0 x¹R$)@Ö%´2C@म‰¬Ü(@溰fÍB@@qI§+@ć9RŒB@Þ £«,@îHs‹‚~B@pg¨13.@VRJ¯LNB@°$ðªž.@6Is B@0*O] R.@øºlÇ·B@#x¹n /@æáon‡C@ ¾Då1³7@ÖT†.ÙA@y¡Õƒ7@²i´Ç¢A@P7q)¼8@¢"QéщA@KÍm˜¹8@2…³tA@ _AH=*:@þ £¡”A@(…¡=J:@êŒTW¥A@0†×¹¾9@æmÏû•A@°`ÍêÄ9@;œgA¬A@0|cjg9@ú´2WµA@07q%?8@b¹ML ®A@¾Då1³7@ÖT†.ÙA@ $6ûäÉIA@ÖéãÈçÔA@ìȱgÕ@@V4bz¶®A@t„³6y@@®Ã$pp°A@4[Wt·f@@¡ΑA@Tvùª]@@‰·OÝA@$*ÁwÚ @@’®v¨'ŒA@¤SÂ>@@n/ÅÃÀXA@Üvùj}@@2zCõ#HA@|¥8ïŸA@Î_[@#|A@ì-ÁwŸü@@.Íûn†A@DߥMó@@J²ßežA@$6ûäÉIA@ÖéãÈçÔA@ ÀªÐ«†k"@ŽÐ$ðÑ™D@z×åƒk!@棡#rD@°«§OëQ @R /ÇŠxD@°JÞÉÆ @Žæon]/D@€ 3uJÛ @n•Tð”C@ §Ð«&!@B ÌýürC@€ÉU—üm"@z!zŸC@`ö±&ËV#@rø6¥•C@`7µž#@š³<;ñ>D@ÀªÐ«†k"@ŽÐ$ðÑ™D@pª3uº#@JÈMÌuE@Àdï0®Ç"@~7î8€E@pJAéô}!@n ñ 2010-02-26 ISO-8859-1 87 ISO-8859-1 ÔÔ*= tableocean2021-09-21T11:50:30.096Z öûö  •®´u g — ¡ « .«-¢aûödZ•B);‚=triggerrtree_ocean_geom_deleteoceanCREATE TRIGGER "rtree_ocean_geom_delete" AFTER DELETE ON "ocean" WHEN old."geom" NOT NULL BEGIN DELETE FROM "rtree_ocean_geom" WHERE id = OLD."fid"; END‚(=ƒEtriggerrtree_ocean_geom_update4oceanCREATE TRIGGER "rtree_ocean_geom_update4" AFTER UPDATE ON "ocean" WHEN OLD."fid" != NEW."fid" AND (NEW."geom" ISNULL OR ST_IsEmpty(NEW."geom")) BEGIN DELETE FROM "rtree_ocean_geom" WHERE id IN (OLD."fid", NEW."fid"); ENDƒ'=…Utriggerrtree_ocean_geom_update3oceanCREATE TRIGGER "rtree_ocean_geom_update3" AFTER UPDATE ON "ocean" WHEN OLD."fid" != NEW."fid" AND (NEW."geom" NOTNULL AND NOT ST_IsEmpty(NEW."geom")) BEGIN DELETE FROM "rtree_ocean_geom" WHERE id = OLD."fid"; INSERT OR REPLACE INTO "rtree_ocean_geom" VALUES (NEW."fid",ST_MinX(NEW."geom"), ST_MaxX(NEW."geom"),ST_MinY(NEW."geom"), ST_MaxY(NEW."geom")); END‚&=ƒ;triggerrtree_ocean_geom_update2oceanCREATE TRIGGER "rtree_ocean_geom_update2" AFTER UPDATE OF "geom" ON "ocean" WHEN OLD."fid" = NEW."fid" AND (NEW."geom" ISNULL OR ST_IsEmpty(NEW."geom")) BEGIN DELETE FROM "rtree_ocean_geom" WHERE id = OLD."fid"; END‚c%=„}triggerrtree_ocean_geom_update1oceanCREATE TRIGGER "rtree_ocean_geom_update1" AFTER UPDATE OF "geom" ON "ocean" WHEN OLD."fid" = NEW."fid" AND (NEW."geom" NOTNULL AND NOT ST_IsEmpty(NEW."geom")) BEGIN INSERT OR REPLACE INTO "rtree_ocean_geom" VALUES (NEW."fid",ST_MinX(NEW."geom"), ST_MaxX(NEW."geom"),ST_MinY(NEW."geom"), ST_MaxY(NEW."geom")); END‚>$;„5triggerrtree_ocean_geom_insertoceanCREATE TRIGGER "rtree_ocean_geom_insert" AFTER INSERT ON "ocean" WHEN (new."geom" NOT NULL AND NOT ST_IsEmpty(NEW."geom")) BEGIN INSERT OR REPLACE INTO "rtree_ocean_geom" VALUES (NEW."fid",ST_MinX(NEW."geom"), ST_MaxX(NEW."geom"),ST_MinY(NEW."geom"), ST_MaxY(NEW."geom")); END#;;'tablertree_ocean_geom_parentrtree_ocean_geom_parent.CREATE TABLE "rtree_ocean_geom_parent"(nodeno INTEGER PRIMARY KEY,parentnode)|"77tablertree_ocean_geom_nodertree_ocean_geom_node-CREATE TABLE "rtree_ocean_geom_node"(nodeno INTEGER PRIMARY KEY,data)!99tablertree_ocean_geom_rowidrtree_ocean_geom_rowid+CREATE TABLE "rtree_ocean_geom_rowid"(rowid INTEGER PRIMARY KEY,nodeno){ --+tablertree_ocean_geomrtree_ocean_geomCREATE VIRTUAL TABLE "rtree_ocean_geom" USING rtree(id, minx, maxx, miny, maxy)sQƒ triggertrigger_delete_feature_count_oceanoceanCREATE TRIGGER "trigger_delete_feature_count_ocean" AFTER DELETE ON "ocean" BEGIN UPDATE gpkg_ogr_contents SET feature_count = feature_count - 1 WHERE lower(table_name) = lower('ocean'); ENDsQƒ triggertrigger_insert_feature_count_oceanoceanCREATE TRIGGER "trigger_insert_feature_count_ocean" AFTER INSERT ON "ocean" BEGIN UPDATE gpkg_ogr_contents SET feature_count = feature_count + 1 WHERE lower(table_name) = lower('ocean'); ENDƒM;;†1tablegpkg_metadata_referencegpkg_metadata_reference*CREATE TABLE gpkg_metadata_reference (reference_scope TEXT NOT NULL,table_name TEXT,column_name TEXT,row_id_value INTEGER,timestamp DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),md_file_id INTEGER NOT NULL,md_parent_id INTEGER,CONSTRAINT crmr_mfi_fk FOREIGN KEY (md_file_id) REFERENCES gpkg_metadata(id),CONSTRAINT crmr_mpi_fk FOREIGN KEY (md_parent_id) REFERENCES gpkg_metadata(id))‚ ''ƒUtablegpkg_metadatagpkg_metadata)CREATE TABLE gpkg_metadata (id INTEGER CONSTRAINT m_pk PRIMARY KEY ASC NOT NULL,md_scope TEXT NOT NULL DEFAULT 'dataset',md_standard_uri TEXT NOT NULL,mime_type TEXT NOT NULL DEFAULT 'text/xml',metadata TEXT NOT NULL DEFAULT '')=Q+indexsqlite_autoindex_gpkg_extensions_1gpkg_extensions(w++ƒ%tablegpkg_extensionsgpkg_extensions'CREATE TABLE gpkg_extensions (table_name TEXT,column_name TEXT,extension_name TEXT NOT NULL,definition TEXT NOT NULL,scope TEXT NOT NULL,CONSTRAINT ge_tce UNIQUE (table_name, column_name, extension_name))P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)  - -‰P“$B:º€BZò’BÍPB<1áÃ4C4«<B³ûÄ tirex-0.7.0/lib/000077500000000000000000000000001412262531100134145ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex.pm000066400000000000000000000075431412262531100150560ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Sys::Syslog; use Tirex::Message; use Tirex::Zoomrange; use Tirex::Metatile; use Tirex::Metatiles::Range; use Tirex::Job; use Tirex::Config; #----------------------------------------------------------------------------- package Tirex; our $DEBUG = 0; our $VERSION = '0.6.2'; =head1 NAME Tirex - Tirex Tile Rendering System =head1 SYNOPSIS use Tirex; =head1 DESCRIPTION There are only a few utility functions and config variables in the Tirex namespace. =cut # max size of UDP packets (this just defines the buffer size, no actual check is done) our $MAX_PACKET_SIZE = 512; # max zoom level we will ever allow our $MAX_ZOOM = 30; our $EXIT_CODE_RESTART = 9; our $EXIT_CODE_DISABLE = 10; our $STATS_DIR = '/var/cache/tirex/stats'; our $PIXEL_PER_TILE = 256; #----------------------------------------------------------------------------- # defaults for config variables (these can also be set in the config file, see there for documentation) our $METATILE_COLUMNS = 8; our $METATILE_ROWS = 8; our $TIREX_CONFIGDIR = '/etc/tirex'; our $TIREX_CONFIGFILENAME = 'tirex.conf'; our $TIREX_CONFIGFILE = $TIREX_CONFIGDIR . '/' . $TIREX_CONFIGFILENAME; our $SOCKET_DIR = '/run/tirex'; our $MASTER_SYSLOG_FACILITY = 'daemon'; our $MASTER_PIDFILE = '/run/tirex/tirex-master.pid'; our $MASTER_LOGFILE = '/var/log/tirex/jobs.log'; our $MASTER_RENDERING_TIMEOUT = 10; # minutes our $BACKEND_MANAGER_SYSLOG_FACILITY = 'daemon'; our $BACKEND_MANAGER_PIDFILE = '/run/tirex/tirex-backend-manager.pid'; our $BACKEND_MANAGER_ALIVE_TIMEOUT = 8; # minutes - make this a tad smaller than the above our $SYNCD_PIDFILE = '/run/tirex/tirex-syncd.pid'; our $SYNCD_UDP_PORT = 9323; our $SYNCD_AGGREGATE_DELAY = 5; our $SYNCD_COMMAND = qq(tar -C/ -cf - %FILES% | ssh %HOST% -oControlMaster=auto -oControlPersist=1h -oControlPath=$SOCKET_DIR/ssh-control-%h-%r-%p -Tq "tar -C/ -xf -"); our $MODTILE_SOCK = "/run/tirex/modtile.sock"; our $MODTILE_PERM = 0666; #----------------------------------------------------------------------------- =head1 METHODS =head2 Tirex::parse_msg($string) Parse a message with linefeed separated var=value assignments into a hash ref. Carriage returns are removed. =cut sub parse_msg { my $string = shift; my $msg; foreach (split(/\r?\n/m, $string)) { my ($k, $v) = split(/=/); $msg->{$k} = $v; } return $msg; } =head2 tirex::create_msg($msg) Create a message string with lines of the format key=value from a hash. =cut sub create_msg { my $msg = shift; my $prefixcount = shift || 0; my $string = ''; foreach my $k (sort(keys %$msg)) { $string .= (' ' x $prefixcount); $string .= "$k=$msg->{$k}\n"; } return $string; } =head2 Tirex::print_msg($msg) Create a message string with lines of the format key=value from a hash. Each line has two leading spaces. For debugging output. =cut sub print_msg { my $msg = shift; create_msg($msg, 2); } =head1 SEE ALSO L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L =cut 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/000077500000000000000000000000001412262531100145075ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Backend.pm000066400000000000000000000251551412262531100164040ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Backend.pm # #----------------------------------------------------------------------------- use strict; use warnings; use File::Path; use Time::HiRes; use Socket; use IO::Socket; use GD; use Tirex; use Tirex::Renderer; use Tirex::Map; #----------------------------------------------------------------------------- package Tirex::Backend; =head1 NAME Tirex::Backend - Generic Tirex rendering backend =head1 SYNOPSIS use Tirex::Backend::Test; my $backend = Tirex::Backend::Test->new(); $backend->main(); =head1 DESCRIPTION This is a parent class for rendering backends written in Perl. To use it create a subclass (such as Tirex::Backend::Test). =cut my $keep_running; =head1 METHODS =head2 Tirex::Backend->new($name) This class should not be instantiated. Create instances of a subclass instead. =cut sub new { my $class = shift; my $self = bless {} => $class; ($self->{'name'} = $0) =~ s{^.*/}{}; $self->set_status('initializing'); $self->init(); return $self; } =head2 $backend->check_map_config($map) Check a map config for this backend for syntactic and other validity. Should croak if it fails. Overwrite this in the subclass. =cut sub check_map_config { } =head2 $backend->set_status('text') Set status text which is shown on the ps output. =cut sub set_status { my $self = shift; my $text = shift; $0 = $self->{'name'} . ': ' . $text; } =head2 $backend->main() Core backend method. Call this directly after creating a subclass with new(). It will parse the config file(s), set everything up and then wait for rendering requests and fulfill them by calling create_metatile(). =cut sub main { my $self = shift; my $renderer_name = $ENV{'TIREX_BACKEND_NAME'} or $self->error_disable('missing TIREX_BACKEND_NAME'); my $port = $ENV{'TIREX_BACKEND_PORT'} or $self->error_disable('missing TIREX_BACKEND_PORT'); my $syslog_facility = $ENV{'TIREX_BACKEND_SYSLOG_FACILITY'} or $self->error_disable('missing TIREX_BACKEND_SYSLOG_FACILITY'); my $mapfiles = $ENV{'TIREX_BACKEND_MAP_CONFIGS'} or $self->error_disable('missing TIREX_BACKEND_MAP_CONFIGS'); my $alive_timeout = $ENV{'TIREX_BACKEND_ALIVE_TIMEOUT'} or $self->error_disable('missing TIREX_BACKEND_ALIVE_TIMEOUT'); my $pipe_fileno = $ENV{'TIREX_BACKEND_PIPE_FILENO'} or $self->error_disable('missing TIREX_BACKEND_PIPE_FILENO'); my $socket_fileno = $ENV{'TIREX_BACKEND_SOCKET_FILENO'}; $Tirex::DEBUG = 1 if (defined $ENV{'TIREX_BACKEND_DEBUG'}); my @mapfiles = split(' ', $mapfiles); #----------------------------------------------------------------------------- ::openlog('tirex-backend-' . $self->{'name'}, $Tirex::DEBUG ? 'pid|perror' : 'pid', $syslog_facility); ::syslog('info', 'Renderer started (name=%s)', $renderer_name); my $pipe = IO::Handle->new(); $pipe->fdopen($pipe_fileno, 'w') or $self->error_disable("Cannot access open pipe: $!"); $pipe->autoflush(1); my $renderer = Tirex::Renderer->new( name => $renderer_name, port => $port, path => $0, procs => 0 ); foreach my $file (@mapfiles) { eval { my $map = Tirex::Map->new_from_configfile($file, $renderer); $self->check_map_config($map); ::syslog('info', 'map config found: %s', $map->to_s()); }; my $err = $@; if ($err) { $self->error_disable("error reading map config '%s': %s", $file, $err); } } #----------------------------------------------------------------------------- my $socket; if ($socket_fileno) { ::syslog('debug', 'using existing socket %d', $socket_fileno) if ($Tirex::DEBUG); $socket = IO::Socket->new()->fdopen($socket_fileno, 'r+') or $self->error_disable("Cannot access open socket $socket_fileno: $!"); } else { ::syslog('debug', 'opening socket on port %d', $port) if ($Tirex::DEBUG); $socket = IO::Socket::INET->new( LocalAddr => 'localhost', LocalPort => $port, Proto => 'udp', ReuseAddr => 1, ) or $self->error_disable("Cannot open UDP socket: :$!"); } $SIG{'HUP'} = \&Tirex::Backend::signal_handler; $SIG{'TERM'} = \&Tirex::Backend::signal_handler; $SIG{'INT'} = \&Tirex::Backend::signal_handler; $SIG{'ALRM'} = \&Tirex::Backend::sigalrm_signal_handler; $keep_running = 1; while ($keep_running) { # send keepalive to parent $pipe->write('a', 1); alarm($alive_timeout); $self->set_status('idle'); # this will block waiting for new commands on socket # if a signal comes in (ALRM or from parent) it will return with EINTR my $msg = Tirex::Message->new_from_socket($socket); if (! $msg) { next if ($!{'EINTR'}); $self->error_restart("error reading from socket: $!"); } alarm(0); ::syslog('debug', 'got request: %s', $msg->to_s()) if ($Tirex::DEBUG); my $map = Tirex::Map->get($msg->{'map'}); $self->set_status(sprintf("request map=%s z=%d x=%d y=%d", $map->get_name(), $msg->{'z'}, $msg->{'x'}, $msg->{'y'})); if ($map) { my $metatile = $msg->to_metatile(); my $filename = $map->get_tiledir() . '/' . $metatile->get_filename(); ::syslog('debug', 'doing rendering (filename=%s)', $filename) if ($Tirex::DEBUG); my $t0 = [Time::HiRes::gettimeofday]; my $image = $self->create_metatile($map, $metatile); $self->set_status('writing metatile'); $self->write_metatile($image, $filename, $metatile); $msg = $msg->reply(); $msg->{'render_time'} = int(Time::HiRes::tv_interval($t0) * 1000); # in milliseconds ::syslog('debug', 'sending response: %s', $msg->to_s()) if ($Tirex::DEBUG); } else { ::syslog('err', 'unknown map: %s', $msg->{'map'}); $msg = $msg->reply('ERROR_UNKNOWN_MAP', "The map " . $msg->{'map'} . " is unknown to renderer " . $renderer_name); } $msg->send($socket) or $self->error_restart("error when sending: $!"); ::syslog('debug', 'done with request') if ($Tirex::DEBUG); } ::syslog('info', 'shutting down %s', $self->{'name'}); exit($Tirex::EXIT_CODE_RESTART); } =head2 $backend->create_metatile($map, $metatile) Create a metatile. This method has to be overwritten in subclasses. =cut sub create_metatile { my $self = shift; my $map = shift; my $metatile = shift; Carp::croak('Overwrite create_metatile() method in subclass!'); } =head2 $backend->write_metatile($image, $filename, $metatile) Takes a single image the size of a metatile, cuts it into tiles and then re-assembles those tiles into a metatile and write it to disk. =cut sub write_metatile { my $self = shift; my $image = shift; my $filename = shift; my $metatile = shift; # metatile header my $meta = 'META' . pack('llll', $Tirex::METATILE_COLUMNS * $Tirex::METATILE_ROWS, $metatile->get_x(), $metatile->get_y(), $metatile->get_z()); # cut metatile into tiles and create pngs for each my @pngs = (); foreach my $x (0..$Tirex::METATILE_COLUMNS-1) { foreach my $y (0..$Tirex::METATILE_ROWS-1) { my $tile; if ($image->isTrueColor()) { $tile = GD::Image->newTrueColor($Tirex::PIXEL_PER_TILE, $Tirex::PIXEL_PER_TILE); $tile->alphaBlending(0); $tile->saveAlpha(1); } else { $tile = GD::Image->newPalette($Tirex::PIXEL_PER_TILE, $Tirex::PIXEL_PER_TILE); } $tile->copy($image, 0, 0, $x * $Tirex::PIXEL_PER_TILE, $y * $Tirex::PIXEL_PER_TILE, $Tirex::PIXEL_PER_TILE, $Tirex::PIXEL_PER_TILE); push(@pngs, $tile->png()); } } # calculate and store byte offsets for each tile my $offset = length($meta) + ($Tirex::METATILE_COLUMNS * $Tirex::METATILE_ROWS * 2 * 4); # header + (number of tiles * (start offset and length) * 4 bytes for int32) foreach my $png (@pngs) { my $l = length($png); $meta .= pack('ll', $offset, $l); $offset += $l; } # add pngs to metatile $meta .= join('', @pngs); # check for directory and create if missing (my $dirname = $filename) =~ s{/[^/]*$}{}; if (! -d $dirname) { File::Path::mkpath($dirname) or $self->error_disable("Can't create path $dirname: $!"); } open(METATILE, '>', $filename) or $self->error_disable("Can't open $filename: $!"); binmode(METATILE); print METATILE $meta; close(METATILE); } =head2 $backend->create_error_image($map, $metatile) Create an error image in case a renderer didn't work. The error image is a black/yellow checkerboard pattern. This method can be overwritten in subclasses. =cut sub create_error_image { my $self = shift; my $map = shift; my $metatile = shift; my $image = GD::Image->new($Tirex::PIXEL_PER_TILE * $Tirex::METATILE_COLUMNS, $Tirex::PIXEL_PER_TILE * $Tirex::METATILE_ROWS); my $yellow = $image->colorAllocate(255, 255, 0); my $black = $image->colorAllocate( 0, 0, 0); my @color = ($yellow, $black); foreach my $x (0..2*$Tirex::METATILE_COLUMNS-1) { foreach my $y (0..2*$Tirex::METATILE_ROWS-1) { my $xpixel = $x * $Tirex::PIXEL_PER_TILE / 2; my $ypixel = $y * $Tirex::PIXEL_PER_TILE / 2; my $color_offset = ($x+$y) % 2; $image->filledRectangle($xpixel, $ypixel, $xpixel + $Tirex::PIXEL_PER_TILE - 1, $ypixel + $Tirex::PIXEL_PER_TILE - 1, $color[$color_offset]); } } return $image; } sub error_restart { my $self = shift; ::syslog('err', @_); exit($Tirex::EXIT_CODE_RESTART); } sub error_disable { my $self = shift; ::syslog('err', @_); exit($Tirex::EXIT_CODE_DISABLE); } #----------------------------------------------------------------------------- sub signal_handler { $keep_running = 0; $SIG{'HUP'} = \&signal_handler; $SIG{'INT'} = \&signal_handler; $SIG{'TERM'} = \&signal_handler; } sub sigalrm_signal_handler { $SIG{'ALRM'} = \&sigalrm_signal_handler; } #----------------------------------------------------------------------------- 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Backend/000077500000000000000000000000001412262531100160365ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Backend/Mapserver.pm000066400000000000000000000111331412262531100203370ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Backend/Mapserver.pm # #----------------------------------------------------------------------------- use strict; use warnings; use mapscript; use Tirex::Backend; #----------------------------------------------------------------------------- package Tirex::Backend::Mapserver; use base qw( Tirex::Backend ); =head1 NAME Tirex::Backend::Mapserver - Mapserver backend for Tirex =head1 DESCRIPTION Simple "renderer" that gets the map image from a Mapserver via Mapscript. Config parameters for the map file: =over 8 =item mapfile mapfile to use for Mapserver configuration =item layers list of comma-separated layers =item srs spatial reference system, 'EPSG:3857' etc. =item transparent TRUE or FALSE =back =head1 METHODS =head2 $backend->init() This method initializes things specific to this backend. =cut sub init { my $self = shift; mapscript::msIO_installStdoutToBuffer(); } =head2 $backend->create_metatile() Create a metatile. =cut sub create_metatile { my $self = shift; my $map = shift; my $metatile = shift; my $xc = $metatile->get_x(); my $yc = $metatile->get_y(); my $zoom = $metatile->get_z(); my ($left, $right, $top, $bottom, $size); my $pow = 2 ** $zoom; if ($pow <= $Tirex::METATILE_COLUMNS) { $left = -20037508.3392; $right = 20037508.3392; $top = 20037508.3392; $bottom = -20037508.3392; $size = $Tirex::PIXEL_PER_TILE * $pow; } else { my $factor = 40075016.6784 / $pow; my $yy = $pow - $yc - $Tirex::METATILE_ROWS; $left = ( $xc * $factor) - 20037508.3392; $right = (($xc+$Tirex::METATILE_COLUMNS) * $factor) - 20037508.3392; $bottom = ( $yy * $factor) - 20037508.3392; $top = (($yy+$Tirex::METATILE_ROWS) * $factor) - 20037508.3392; $size = $Tirex::PIXEL_PER_TILE * $Tirex::METATILE_COLUMNS; } ::syslog('debug', 'Mapserver request for layer(s) >>%s<< started with mapfile %s', $map->{'layers'}, $map->{'mapfile'}) if ($Tirex::DEBUG); my $req = new mapscript::OWSRequest(); $req->setParameter( 'SERVICE', 'WMS' ); $req->setParameter( 'VERSION', '1.1.1' ); $req->setParameter( 'REQUEST', 'GetMap' ); $req->setParameter( 'LAYERS', $map->{'layers'}); $req->setParameter( 'STYLES', '' ); $req->setParameter( 'SRS', $map->{'srs'} || 'EPSG:3857' ); $req->setParameter( 'BBOX', join(',', $left, $bottom, $right, $top)); $req->setParameter( 'WIDTH', "$size"); $req->setParameter( 'HEIGHT', "$size"); $req->setParameter( 'FORMAT', 'image/png'); $req->setParameter( 'TRANSPARENT', $map->{'transparent'} || 'FALSE' ); $req->setParameter( 'EXCEPTIONS', 'application/vnd.ogc.se_xml'); $self->set_status("mapserver request"); my $msmap = new mapscript::mapObj( $map->{'mapfile'} ); $msmap->OWSDispatch( $req ); my $content_type = mapscript::msIO_stripStdoutBufferContentType(); my $content = mapscript::msIO_getStdoutBufferBytes(); if ($content_type eq 'image/png') { # Bit 0 af Byte 25 in the png header is 1 for palette image, 0 for truecolor image: # http://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header my $palette_bit = unpack("x25 c1", $$content) & 1; my $palette_name; my $image; # Despite the description at http://search.cpan.org/~lds/GD-2.46/GD.pm # "Images created by reading PNG images will be truecolor if the image file itself is truecolor." # # auto-detection of input png does not seem to work so (at least for now) we need to do this manually by looking at the png header if ($palette_bit) { $image = GD::Image->newFromPngData($$content,0); $palette_name = "palette"; } else { $image = GD::Image->newFromPngData($$content,1); $palette_name = "truecolor"; } if ($image) { ::syslog('debug', 'Mapserver request was successful (got %s image)',$palette_name) if ($Tirex::DEBUG); return $image; } } ::syslog('err', 'Error on Mapserver request: content-type=%s', $content_type); if ($content_type eq 'application/vnd.ogc.se_xml' && $Tirex::DEBUG) { ::syslog('debug', 'Mapserver request returned: %s', $$content); } return $self->create_error_image($map, $metatile); } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Backend/OpenSeaMap.pm000066400000000000000000000053021412262531100203640ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Backend/OpenSeaMap.pm # #----------------------------------------------------------------------------- use strict; use warnings; use List::Util qw(); use Carp; use Tirex::Backend; #----------------------------------------------------------------------------- package Tirex::Backend::OpenSeaMap; use base qw( Tirex::Backend ); =head1 NAME Tirex::Backend::OpenSeaMap - OpenSeaMap backend for Tirex =head1 DESCRIPTION This backend calls an external Java program to generate an OpenSeaMap meta tile. The Java program is here: https://svn.openstreetmap.org/applications/editors/josm/plugins/seachart/jrenderpgsql/ Config parameters for the map file: =over 8 =item dburl the JDBC database connection URL, with username and password if needed =item jar the JAR file containing the JRenderPsql class to use =item scalefactor the scale factor (defaults to 1.0) =item tilesize the tile size (should be 256 * scalefactor to avoid issues) =back =head1 METHODS =head2 $backend->init() This method initializes things specific to this backend. =cut sub init { my $self = shift; $self->{'border_width'} = 6; GD::Image->trueColor(1); } =head2 $backend->check_map_config($map) =cut sub check_map_config { my $self = shift; my $map = shift; if ($Tirex::METATILE_COLUMNS != $Tirex::METATILE_ROWS) { Carp::croak("this plugin cannot work with non-square meta tiles"); } if (!defined($map->{'jar'})) { Carp::croak("must configure renderer in map file"); } if (!-f $map->{'jar'}) { Carp::croak("configured renderer " . $map->{'jar'} . " does not exist"); } if (!defined($map->{'dburl'})) { Carp::croak("must configure dburl in map file"); } } =head2 $backend->create_metatile() Create a metatile. =cut sub create_metatile { my $self = shift; my $map = shift; my $metatile = shift; my $xc = $metatile->get_x(); my $yc = $metatile->get_y(); my $zoom = $metatile->get_z(); my $pixel = $Tirex::PIXEL_PER_TILE; my $tmpfile = "/tmp/tirex-$$-openseamap.png"; my $cmdline = sprintf("java -jar %s --scale %f --tilesize %d '%s' %d %d %d %s", $map->{'jar'}, $map->{'scalefactor'}, $map->{'tilesize'} * $Tirex::METATILE_COLUMNS, $map->{'dburl'}, $zoom , $xc, $yc, $tmpfile); ::syslog('debug', 'OpenSeaMap request: %s', $cmdline) if ($Tirex::DEBUG); system($cmdline); my $image = GD::Image->new($tmpfile); $image->alphaBlending(0); $image->saveAlpha(1); unlink ($tmpfile); return $image; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Backend/TMS.pm000066400000000000000000000125201412262531100170370ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Backend/TMS.pm # #----------------------------------------------------------------------------- use strict; use warnings; use HTTP::Async; use HTTP::Request; use IPC::SysV qw(IPC_PRIVATE IPC_CREAT S_IWUSR S_IRUSR); use IPC::SharedMem; use List::Util qw(min max); use Tirex::Backend; #----------------------------------------------------------------------------- package Tirex::Backend::TMS; use base qw( Tirex::Backend ); =head1 NAME Tirex::Backend::TMS - TMS backend for Tirex =head1 DESCRIPTION Simple "renderer" that gets the map image from a TMS server. The TMS server is assumed to be a standard EPSG:3857 service with standard tiling scheme. Config parameters for the map file: =over 8 =item url url template (should contain {x}, {y}, {z} which are replaced) =item slots maximum number of parallel connections per backend process =back =head1 METHODS =head2 $backend->init() This method initializes things specific to this backend. =cut sub init { my $self = shift; $self->{'max_tile_size'} = 4 * 256 * 256 + 256; } =head2 $backend->create_metatile() Create a metatile. =cut sub create_metatile { my $self = shift; my $map = shift; my $metatile = shift; my $async = HTTP::Async->new; $async->slots(defined($map->{'slots)'} ? $map->{'slots)'} : 4)); my $async_index = {}; my $async_error = 0; my $async_array = []; my $limit = 2 ** $metatile->get_z(); my $url = $map->{'url'}; my $z = $metatile->get_z(); $url =~ s/{z}/$z/g; for (my $x = 0; $x < $Tirex::METATILE_COLUMNS; $x++) { my $xx = $metatile->get_x() + $x; next if ($xx >= $limit); my $url1 = $url; $url1 =~ s/{x}/$xx/g; for (my $y = 0; $y < $Tirex::METATILE_ROWS; $y++) { my $yy = $metatile->get_y() + $y; next if ($yy >= $limit); my $url2 = $url1; $url2 =~ s/{y}/$yy/g; ::syslog('debug', 'TMS request: %s', $url2) if ($Tirex::DEBUG); my $request = HTTP::Request->new('GET', $url2, [ 'User-Agent' => 'tirex-backend-tms/' . $Tirex::VERSION ]); my $async_id = $async->add($request); $async_index->{$async_id} = $x*$Tirex::METATILE_ROWS+$y; } } while (my ($response, $async_id) = $async->wait_for_next_response) { if ($response->is_success() && $response->header('Content-type') eq 'image/png') { my $size = length($response->content()); if ($size > $self->{'max_tile_size'}) { ::syslog('err', 'Error on TMS request: %d bytes returned but limit is %d', $size, $self->{'max_tile_size'} ); $async_error = 1; last; } else { my $index = $async_index->{$async_id}; ::syslog('debug', 'TMS request was successful (got %d bytes), saving at index %d', $size, $index) if ($Tirex::DEBUG); $async_array->[$index] = $response->content(); } } else { ::syslog('err', 'Error on TMS request: status=%d (%s) content-type=%s', $response->code(), $response->message(), $response->header('Content-type')); $async_error = 1; last; } } $async->remove_all(); return undef if ($async_error); return $async_array; } # write_metatile is not normally overloaded by backends; the standard implementation # cuts a large image returned by create_metatile into smaller PNGs. However the TMS # service does not bother to create a large metatile, it simply keeps the 64 tiles # in an array and saves them into the meta tile. sub write_metatile { my $self = shift; my $image = shift; my $filename = shift; my $metatile = shift; # metatile header my $limit = 2 ** $metatile->get_z(); my $meta = 'META' . pack('llll', $Tirex::METATILE_ROWS * $Tirex::METATILE_COLUMNS, $metatile->get_x(), $metatile->get_y(), $metatile->get_z()); my @pngs = (); my $offset = length($meta) + ($Tirex::METATILE_COLUMNS * $Tirex::METATILE_ROWS * 2 * 4); # header + (number of tiles * (start offset and length) * 4 bytes for int32) # this builds the offset table in the meta tile's header foreach my $x (0..$Tirex::METATILE_COLUMNS-1) { my $xx = $metatile->get_x() + $x; foreach my $y (0..$Tirex::METATILE_ROWS-1) { my $yy = $metatile->get_y() + $y; my $size = ($x >= $limit || $y >= $limit) ? 0 : length($image->[$x*$Tirex::METATILE_ROWS+$y]); $meta .= pack('ll', $offset, $size); $offset += $size; } } # add pngs to metatile $meta .= join('', grep { defined($_) } @$image); # check for directory and create if missing (my $dirname = $filename) =~ s{/[^/]*$}{}; if (! -d $dirname) { File::Path::mkpath($dirname) or $self->error_disable("Can't create path $dirname: $!"); } open(METATILE, '>', $filename) or $self->error_disable("Can't open $filename: $!"); binmode(METATILE); print METATILE $meta; close(METATILE); } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Backend/Test.pm000066400000000000000000000106171412262531100173200ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Backend/Test.pm # #----------------------------------------------------------------------------- use strict; use warnings; use List::Util qw(); use Carp; use Tirex::Backend; #----------------------------------------------------------------------------- package Tirex::Backend::Test; use base qw( Tirex::Backend ); =head1 NAME Tirex::Backend::Test - Test backend for Tirex =head1 DESCRIPTION This backend creates a checkerboard test pattern for testing the Tirex tile rendering system. It will be called from tirex-backend-manager if you configure it in /etc/tirex/renderer/test.conf. It has no renderer specific configuration option. There is one renderer specific configuration option for maps called "sleep". It gives the time in seconds the renderer should sleep before notifying the tirex-master to simulate a longer rendering time. =head1 METHODS =head2 $backend->init() This method initializes things specific to this backend. =cut sub init { my $self = shift; $self->{'border_width'} = 6; } =head2 $backend->check_map_config($map) =cut sub check_map_config { my $self = shift; my $map = shift; if (defined $map->{'sleep'}) { Carp::croak("parameter 'sleep' needs integer argument between 0 and 999 (is '" . $map->{'sleep'} . "')") unless ($map->{'sleep'} =~ /^[0-9]{1,3}$/); $map->{'sleep'} = 0 + $map->{'sleep'}; # force to integer } else { $map->{'sleep'} = 0; # default: no sleep } } =head2 $backend->create_metatile() Create a metatile. The metatile will have a checkerboard pattern with the tile numbers and zoom level in each tile and an additional red border around the whole metatile. =cut sub create_metatile { my $self = shift; my $map = shift; my $metatile = shift; my $xc = $metatile->get_x(); my $yc = $metatile->get_y(); my $zoom = $metatile->get_z(); my $pixel = $Tirex::PIXEL_PER_TILE; my $image = GD::Image->new($pixel * $Tirex::METATILE_COLUMNS, $pixel * $Tirex::METATILE_ROWS); my $white = $image->colorAllocate(255, 255, 255); my $black = $image->colorAllocate( 0, 0, 0); my $red = $image->colorAllocate(200, 0, 0); # for border around metatile my @color = ($white, $black); my $font = GD::Font->Large; my $text_y_offset1 = $pixel/2 - (1.5 * $font->height()); my $text_y_offset2 = $pixel/2 + (0.5 * $font->height()); # create checkerboard pattern and write tile numbers and zoom into it foreach my $x (0..$Tirex::METATILE_COLUMNS-1) { foreach my $y (0..$Tirex::METATILE_ROWS-1) { my $xpixel = $x * $pixel; my $ypixel = $y * $pixel; my $color_offset = ($x+$y) % 2; $image->filledRectangle($xpixel, $ypixel, $xpixel + $pixel - 1, $ypixel + $pixel - 1, $color[$color_offset]); my $text1 = sprintf("x=%d y=%d", $xc + $x, $yc + $y); my $text2 = sprintf("zoom=%d", $zoom); my $text_x_offset1 = $pixel/2 - (length($text1)/2) * $font->width(); my $text_x_offset2 = $pixel/2 - (length($text2)/2) * $font->width(); $image->string($font, $xpixel + $text_x_offset1, $ypixel + $text_y_offset1, $text1, $color[1 - $color_offset]); $image->string($font, $xpixel + $text_x_offset2, $ypixel + $text_y_offset2, $text2, $color[1 - $color_offset]); } } # draw border around metatile my $xmax = $pixel * List::Util::min(2**$zoom, $Tirex::METATILE_COLUMNS) - 1; my $ymax = $pixel * List::Util::min(2**$zoom, $Tirex::METATILE_ROWS) - 1; $image->filledRectangle( 0, 0, $xmax, $self->{'border_width'}, $red); $image->filledRectangle( 0, 0, $self->{'border_width'}, $ymax, $red); $image->filledRectangle($xmax - $self->{'border_width'}, 0, $xmax, $ymax, $red); $image->filledRectangle( 0, $ymax - $self->{'border_width'}, $xmax, $ymax, $red); # sleep to simulate longer rendering time if so configured sleep($map->{'sleep'}); return $image; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Backend/WMS.pm000066400000000000000000000113371412262531100170470ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Backend/WMS.pm # #----------------------------------------------------------------------------- use strict; use warnings; use LWP; use Tirex::Backend; #----------------------------------------------------------------------------- package Tirex::Backend::WMS; use base qw( Tirex::Backend ); =head1 NAME Tirex::Backend::WMS - WMS backend for Tirex =head1 DESCRIPTION Simple "renderer" that gets the map image from a WMS server. The WMS server must support the right SRS (EPSG:3857 or the informal EPSG:900913). Only WMS 1.1.1 is currently supported. Config parameters for the map file: =over 8 =item url url prefix =item layers list of comma-separated layers =item srs spatial reference system, 'EPSG:3857' etc. =item transparent TRUE or FALSE =back =head1 METHODS =head2 $backend->init() This method initializes things specific to this backend. =cut sub init { my $self = shift; $self->{'ua'} = LWP::UserAgent->new(); $self->{'ua'}->agent('tirex-backend-wms/' . $Tirex::VERSION); my $timeout = $ENV{'TIREX_BACKEND_ALIVE_TIMEOUT'} - 10; # timeout from backend-manager minus 10 seconds $self->{'ua'}->timeout($timeout); } =head2 $backend->create_metatile() Create a metatile. =cut sub create_metatile { my $self = shift; my $map = shift; my $metatile = shift; my $xc = $metatile->get_x(); my $yc = $metatile->get_y(); my $zoom = $metatile->get_z(); my ($left, $right, $top, $bottom, $size); my $pow = 2 ** $zoom; if ($pow <= $Tirex::METATILE_COLUMNS) { $left = -20037508.3392; $right = 20037508.3392; $top = 20037508.3392; $bottom = -20037508.3392; $size = $Tirex::PIXEL_PER_TILE * $pow; } else { my $factor = 40075016.6784 / $pow; my $yy = $pow - $yc - $Tirex::METATILE_ROWS; $left = ( $xc * $factor) - 20037508.3392; $right = (($xc+$Tirex::METATILE_COLUMNS) * $factor) - 20037508.3392; $bottom = ( $yy * $factor) - 20037508.3392; $top = (($yy+$Tirex::METATILE_ROWS) * $factor) - 20037508.3392; $size = $Tirex::PIXEL_PER_TILE * $Tirex::METATILE_COLUMNS; } my %wms_request = ( SERVICE => 'WMS', VERSION => '1.1.1', REQUEST => 'GetMap', LAYERS => $map->{'layers'}, STYLES => '', SRS => $map->{'srs'} || 'EPSG:3857', BBOX => join(',', $left, $bottom, $right, $top), WIDTH => $size, HEIGHT => $size, FORMAT => 'image/png', TRANSPARENT => $map->{'transparent'} || 'FALSE', EXCEPTIONS => 'application/vnd.ogc.se_xml', ); my $request = $map->{'url'} . join('&', map { $_ . '=' . $wms_request{$_} } sort keys %wms_request); ::syslog('debug', 'WMS request: %s', $request) if ($Tirex::DEBUG); $self->set_status("wms request"); my $response = $self->{'ua'}->request(HTTP::Request->new(GET => $request)); if ($response->is_success() && $response->header('Content-type') eq 'image/png') { # Bit 0 af Byte 25 in the png header is 1 for palette image, 0 for truecolor image: # http://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header my $palette_bit = unpack("x25 c1", $response->content()) & 1; my $palette_name; my $image; # Despite the description at http://search.cpan.org/~lds/GD-2.46/GD.pm # "Images created by reading PNG images will be truecolor if the image file itself is truecolor." # # auto-detection of input png does not seem to work so (at least for now) we need to do this manually by looking at the png header if ($palette_bit) { $image = GD::Image->newFromPngData($response->content(),0); $palette_name = "palette"; } else { $image = GD::Image->newFromPngData($response->content(),1); $palette_name = "truecolor"; } if ($image) { ::syslog('debug', 'WMS request was successful (got %s image)',$palette_name) if ($Tirex::DEBUG); return $image; } } ::syslog('err', 'Error on WMS request: status=%d (%s) content-type=%s', $response->code(), $response->message(), $response->header('Content-type')); if ($response->header('Content-type') eq 'application/vnd.ogc.se_xml' && $Tirex::DEBUG) { ::syslog('debug', 'WMS request returned: %s', $response->content()); } return $self->create_error_image($map, $metatile); } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Config.pm000066400000000000000000000066271412262531100162650ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Config.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; #----------------------------------------------------------------------------- package Tirex::Config; =head1 NAME Tirex::Config - Configuration =head1 SYNOPSIS my $config_dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR; my $config_file = $config_dir . '/' . $Tirex::TIREX_CONFIGFILENAME; Tirex::Config::init($config_file); Tirex::Config::dump_to_syslog(); my $some_var = Tirex::Config::get('some_option', $Tirex::SOME_OPTION); =head1 DESCRIPTION Methods for reading the main Tirex configuration file (usually /etc/tirex/tirex.conf) and accessing the config options. =cut our $confhash = {}; =head1 METHODS =head2 init($configfilename [,$prefix]) Loads the given config file. Dies on failure. If $prefix is given, all keys are prepended with $prefix and a dot. Does not overwrite already loaded data. =cut sub init { my $configname = shift; my $prefix = shift; $prefix = defined($prefix) ? "$prefix." : ""; open(my $configfh, '<', $configname) or die("cannot open configuration file '$configname'"); while (<$configfh>) { chomp; parse_line($configname, $prefix, $_); } close($configfh); } sub parse_line { my $configname = shift; my $prefix = shift; my $line = shift; $line =~ s/#.*$//; if ($line =~ /^([a-z0-9_]+)\s*=\s*(\S*)\s*$/) { $confhash->{$prefix.$1} = $2; } elsif ($line =~ /^([a-z0-9_]+)\s+(.*?)\s*$/) { my $obj = $1; my @attrs = split(/\s+/, $2); my %attrs = (); foreach my $attr (@attrs) { if ($attr =~ /^([a-z0-9_]+)=(.*)$/) { $attrs{$1} = $2; } } push(@{$confhash->{$prefix.$obj}}, \%attrs); } elsif ($line =~ /^\s*$/) { # ignore empty lines } else { Carp::croak("error reading config file '$configname' in line $."); } } =head2 dump_to_syslog() Dump config to syslog. =cut sub dump_to_syslog { foreach my $key (sort(keys %$confhash)) { my $value = $confhash->{$key}; if (ref($value) eq 'ARRAY') { my @values = (); foreach my $item (@$value) { push(@values, '{' . join(' ', map { "$_=$item->{$_}"; } sort keys %$item) . '}'); } $value = '[' . join(',', @values) . ']'; } ::syslog('info', 'Config %s=%s', $key, $value); } } =head2 get($key [,$default [, $pattern]]) Returns the value of config key $key, or $default if the value is unset. If $pattern is available the value is checked against it. Will croak if it doesn't match. =cut sub get { my $key = shift; my $default = shift; my $pattern = shift; my $value = defined($confhash->{$key}) ? $confhash->{$key} : $default; if (defined $pattern) { Carp::croak("config value for '$key' doesn't match pattern '$pattern'") unless ($value =~ $pattern); } return $value; } =head2 get_int($key [,$default]) Returns the value of config key $key as integer, or $default if the value is unset. =cut sub get_int { my $key = shift; return sprintf "%d", defined($confhash->{$key}) ? $confhash->{$key} : shift; } 1; tirex-0.7.0/lib/Tirex/Job.pm000066400000000000000000000202401412262531100155550ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Job.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use List::Util qw(); #----------------------------------------------------------------------------- package Tirex::Job; =head1 NAME Tirex::Job - A Tirex rendering job =head1 SYNOPSIS my $job = Tirex::Job->new( metatile => $metatile, prio => 5, expire => time() + 60*60 ); =head1 DESCRIPTION Tirex jobs consist of a metatile, rendering priority and an expire time plus some optional additional info. They are created when rendering requests reach the master and put into the queue. =head1 METHODS =head2 Tirex::Job->new( ... ) Create new job. A job always needs the following parameters: metatile -- the metatile prio -- priority (integer >= 1, 1 is highest priority It can have any or all of the following parameters: expire -- the time when this job will expire (seconds since epoch) request_time -- the time when this request came in (seconds since epoch, will be set to current time if not set) =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; Carp::croak("need prio for new job") unless (defined $self->{'prio'}); Carp::croak("prio must be integer 1 or larger") unless ($self->{'prio'} =~ /^[1-9][0-9]*$/ && $self->{'prio'} >= 1); Carp::croak("need metatile for new job") unless (defined $self->{'metatile'}); $self->{'notify'} = []; $self->{'success'} = 0; $self->{'request_time'} = time unless (defined $self->{'request_time'}); $self->{'id'} = $self->{'request_time'} . "_" . ($self + 0); return $self; } =head2 $job->same_tile($other_job) Returns true if both jobs concern the same metatile, false otherwise. The same tile means: same map, same x and y coordinates and same zoom level. =cut sub same_tile { my $self = shift; my $other = shift; return $self->{'metatile'}->equals($other->{'metatile'}); } =head2 $job->expired() Returns 1 if this job is expired, 0 otherwise. If the job doesn't have an expire time, it returns undef; =cut sub expired { my $self = shift; return undef unless (defined $self->{'expire'}); return ($self->{'expire'} < time() ? 1 : 0); } =head2 $job->to_s( foo => 'bar' ) Creates a message string from the job. It contains the fields id, map, x, y, and z from the job plus all the fields given as argument to this method. =cut sub to_s { my $self = shift; my $m = $self->to_msg(@_); my $content = ''; foreach my $k (sort(keys %$m)) { if ($k eq 'metatile') { foreach my $mk (sort keys %{$m->{'metatile'}}) { $content .= "$mk=$m->{'metatile'}->{$mk}\n"; } } else { $content .= "$k=$m->{$k}\n"; } } return $content; } =head2 $job->to_msg( foo => 'bar' ) Creates a message from the job. It contains the fields id, map, x, y, z, prio from the job and, if available, the field priority. All the fields given as argument to this method will also be added. You must give a type argument, otherwise this will croak. =cut sub to_msg { my $self = shift; my %args = @_; $args{'id'} = $self->get_id() unless (exists $args{'id'}); $args{'map'} = $self->get_map(); $args{'x'} = $self->get_x(); $args{'y'} = $self->get_y(); $args{'z'} = $self->get_z(); $args{'prio'} = $self->get_prio(); $args{'expire'} = $self->{'expire'} if ($self->{'expire'}); return Tirex::Message->new(%args); } =head2 $job->get_id() Get unique id of this job. =cut sub get_id { my $self = shift; return $self->{'id'}; } =head2 $job->get_prio() Get priority of this job. =cut sub get_prio { my $self = shift; return $self->{'prio'}; } =head2 $job->set_prio($prio) Set priority of this job. =cut sub set_prio { my $self = shift; $self->{'prio'} = shift; return; } =head2 $job->get_metatile() Get metatile of this job. =cut sub get_metatile { my $self = shift; return $self->{'metatile'}; } sub get_x { my $self = shift; return $self->{'metatile'}->get_x(); } sub get_y { my $self = shift; return $self->{'metatile'}->get_y(); } sub get_z { my $self = shift; return $self->{'metatile'}->get_z(); } sub get_map { my $self = shift; return $self->{'metatile'}->get_map(); } =head2 $job->get_pos() Get position of this job in the priority queue. Returns undef if the job is in no queue. =cut sub get_pos { my $self = shift; return $self->{'pos'}; } =head2 $job->set_pos($pos) Set position of this job in the priority queue. Set to undef if the job is in no queue. =cut sub set_pos { my $self = shift; $self->{'pos'} = shift; return; } =head2 $job->get_bucket() Get bucket for this job. =cut sub get_bucket { my $self = shift; return $self->{'bucket'}; } =head2 $job->set_bucket($bucket) Set bucket for this job. =cut sub set_bucket { my $self = shift; $self->{'bucket'} = shift; return; } =head2 $job->get_success() Get success flag for this job. =cut sub get_success { my $self = shift; return $self->{'success'}; } =head2 $job->set_success($success) Set success flag for this job. =cut sub set_success { my $self = shift; $self->{'success'} = shift; return; } =head2 $job->hash_key() Create a hash key (string) from the contents of this job. Only the attributes describing the tile are included. So that this hash key is unique for any tile. =cut sub hash_key { my $self = shift; return $self->{'metatile'}->to_s(); } =head2 $job->merge($secondjob) Merge two jobs for the same metatile into one. This method will look into both jobs and create a new job from the data. The old jobs are not changed. * priority of the new job will be the minimum of the priorities of the old jobs * expire will be the maximum of the expire times of the old jobs, if there is no expire time for at least one job, the result will have no expire time either * notify will be the concatenation of both notifies * request_time will be the minimum of both request times This methods assumes that the metatiles are the same, it does no check. =cut sub merge { my $self = shift; my $other = shift; my $job = Tirex::Job->new( metatile => $self->{'metatile'}, request_time => List::Util::min($self->{'request_time'}, $other->{'request_time'}), prio => List::Util::min($self->get_prio(), $other->get_prio()), expire => (defined($self->{'expire'}) && defined($other->{'expire'})) ? List::Util::max($self->{'expire'}, $other->{'expire'}) : undef, ); foreach my $n (@{$self->{'notify'} }) { $job->add_notify($n); } foreach my $n (@{$other->{'notify'}}) { $job->add_notify($n); } return $job; } =head2 $job->add_notify($source) Add a Tirex::Source to be notified when this job is done. =cut sub add_notify { my $self = shift; my $notify = shift; push(@{$self->{'notify'}}, $notify); } =head2 $job->has_notify() Returns true if any sources are waiting to be notified of this job's completion. =cut sub has_notify { my $self = shift; return defined($self->{'notify'}) && scalar(@{$self->{'notify'}}) > 0; } =head2 $job->notify() Notify all sources that this job is done. Returns an array reference with the result of the notify calls on the sources. Clears the notify list. =cut sub notify { my $self = shift; my @results = (); foreach my $source (@{$self->{'notify'}}) { push(@results, $source->notify($self)); } $self->{'notify'} = []; return \@results; } =head2 $job->sources_as_string() The sources of this job as a string (for logging). =cut sub sources_as_string { my $self = shift; return join('', map { $_->name($self); } @{$self->{'notify'}}); } =head2 $job->age() Returns age of this job in seconds (ie. the difference between current and request time). =cut sub age { my $self = shift; return time() - $self->{'request_time'}; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Manager.pm000066400000000000000000000273621412262531100164310ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Manager.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use IO::Socket::INET; use Tirex; use Tirex::Manager::Bucket; use Tirex::Manager::RenderingJobs; #----------------------------------------------------------------------------- package Tirex::Manager; =head1 NAME Tirex::Manager - Rendering manager =head1 SYNOPSIS use Tirex::Manager; my $queue = Tirex::Queue->new(); my $rm = Tirex::Manager->new( queue => $queue ); =head1 DESCRIPTION The rendering manager takes jobs from the queue and runs them. It takes into account how many parallel renderings are allowed, the priority of the jobs etc. =head1 METHODS =head2 Tirex::Manager->new( queue => $queue ) Create a new rendering manager. Parameters are: queue the queue with the rendering requests (see Tirex::Queue) =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; Carp::croak("need queue parameter") unless (defined $self->{'queue'}); $self->{'stats'} = { 'count_requested' => 0, 'count_expired' => 0, 'count_timeouted' => 0, 'count_error' => 0, 'count_rendered' => {}, 'sum_render_time' => {}, 'max_render_time' => {}, }; $self->{'rendering_timeout'} = Tirex::Config::get('master_rendering_timeout', $Tirex::MASTER_RENDERING_TIMEOUT) * 60; # config is in minutes, but we need in seconds $self->{'next_timeout_check'} = time() + $self->{'rendering_timeout'}; $self->{'rendering_jobs'} = Tirex::Manager::RenderingJobs->new( timeout => $self->{'rendering_timeout'} ); $self->{'socket'} = IO::Socket::INET->new( LocalAddr => 'localhost', Proto => 'udp') or Carp::croak("Can't open backend return UDP socket: $!\n"); $self->{'buckets'} = []; $self->{'load'} = 0; $self->{'last_load_check'} = 0; return $self; } =head2 $rm->add_bucket(name => $name, minprio => $minprio, maxproc => $maxproc, maxload => $maxload) Creates and adds a bucket with the given config. It always keeps buckets sorted according to minprio. Returns the created bucket or undef on error. =cut sub add_bucket { my $self = shift; my @config = @_; my $bucket = Tirex::Manager::Bucket->new(@config); if ($bucket) { # re-sort all buckets including the new one my @buckets = sort { $a->get_minprio() <=> $b->get_minprio() } (@{$self->{'buckets'}}, $bucket); # re-calculate maxprio for all buckets foreach my $n (1 .. scalar(@buckets)-1) { $buckets[$n-1]->set_maxprio( $buckets[$n]->get_minprio() - 1 ); } $self->{'buckets'} = \@buckets; } return $bucket; } =head2 $rm->set_active_flag_on_bucket($active, $bucketname) Returns true if successful, ie. the bucket did exists, false if unsuccessful. =cut sub set_active_flag_on_bucket { my $self = shift; my $active = shift; my $name = shift; foreach my $bucket (@{$self->{'buckets'}}) { if ($bucket->get_name() eq $name) { $bucket->set_active($active); return 1; } } return; } =head2 $rm->get_socket() Return socket where the rendering manager expects responses from the rendering backends. This should be added to select calls in main loop. =cut sub get_socket { my $self = shift; return $self->{'socket'}; } sub requests_by_metatile { my $self = shift; my $job = shift; return $self->{'rendering_jobs'}->find_by_metatile($job->hash_key()); } =head2 $rm->schedule() Runs as many jobs as there are in the queue (as long as the resources don't run out). This will also check for timeouts on the jobs that are currently rendering. =cut sub schedule { my $self = shift; # if we haven't done this for a while check currently rendering jobs for timeouts if (time() >= $self->{'next_timeout_check'}) { $self->{'stats'}->{'count_timeouted'} += $self->{'rendering_jobs'}->check_timeout(); $self->{'next_timeout_check'} = time() + $self->{'rendering_timeout'} / 10; } while ($self->run()) {}; } =head2 $rm->run() Check whether there is a job that can be rendered and that there are enough resources and then render the job. Returns undef if there is no job to run 0 if there aren't enough resources 1 if a job was started 2 if a job was expired =cut sub run { my $self = shift; my $job = $self->{'queue'}->peek(); return undef unless (defined $job); # if the job is expired, just remove it from the queue and return if ($job->expired()) { $self->{'queue'}->next(); # ignore result, we already got the job from call to peek() above ::syslog('debug', 'job is expired id=%s prio=%s map=%s x=%d y=%d z=%d', $job->get_id(), $job->get_prio(), $job->get_map(), $job->get_x(), $job->get_y(), $job->get_z()) if ($Tirex::DEBUG); $self->{'stats'}->{'count_expired'}++; return 2; } my $prio = $job->get_prio(); my $bucket; # the current system load my $current_load = $self->get_load(); # Check if buckets can render. Start at bucket with lowest priority and end at the # bucket with the right priority for this job. If any of the buckets can't render # we stop there. If all of them can render we go on. foreach my $b (@{$self->{'buckets'}}) { return 0 unless ( $b->can_render($self->{'rendering_jobs'}->count(), $current_load) ); # break from loop if the currently looked at bucket is the right for the priority of this job if ($b->for_prio($prio)) { $bucket = $b; last; } } # we found the right bucket and it can render # remove job from queue, ignore result, we already got the job from call to peek() above $self->{'queue'}->next(); ::syslog('debug', 'request rendering of job id=%s prio=%s map=%s x=%d y=%d z=%d', $job->get_id(), $job->get_prio(), $job->get_map(), $job->get_x(), $job->get_y(), $job->get_z()) if ($Tirex::DEBUG); # do all the necessary housekeeping... $self->{'rendering_jobs'}->add($job); $bucket->add_job($job); # update statistics $self->{'stats'}->{'count_requested'}++; # and actually send the job to the renderer $self->send($job); # return success return 1; } =head2 $rm->send($job) Send a job to the rendering daemon. =cut sub send { my $self = shift; my $job = shift; my $map = Tirex::Map->get($job->get_map()); my $port = $map->get_renderer()->get_port(); my $sock; eval { $sock = Socket::pack_sockaddr_in($port, Socket::INADDR_LOOPBACK) }; if (!defined($sock)) { ::syslog('warning', 'cannot send request %s, renderer port is undef', $job->get_id()); return; }; ::syslog('debug', 'sending request to port %d id=%s prio=%s map=%s x=%d y=%d z=%d', $port, $job->get_id(), $job->get_prio(), $job->get_map(), $job->get_x(), $job->get_y(), $job->get_z()) if ($Tirex::DEBUG); return $self->{'socket'}->send( $job->to_s( type => 'metatile_render_request' ), undef, $sock ); } =head2 $rm->done($msg) This is called when a message comes back from the backend that a job was rendered. Returns the job or undef if the job was not found. =cut sub done { my $self = shift; my $msg = shift; my $job = $self->{'rendering_jobs'}->find_by_id($msg->{'id'}); # if the job is found in our records, we remove it. if ($job) { my $success = (defined($msg->{'result'}) && $msg->{'result'} eq 'ok'); $job->set_success($success); if ($success) { ::syslog('debug', 'job rendering done id=%s map=%s x=%d y=%d z=%d', $job->get_id(), $job->get_map(), $job->get_x(), $job->get_y(), $job->get_z()) if ($Tirex::DEBUG); # update statistics $self->{'stats'}->{'count_rendered' }->{$job->get_map()}->[$job->get_z()] ||= 0; $self->{'stats'}->{'count_rendered' }->{$job->get_map()}->[$job->get_z()]++; $self->{'stats'}->{'sum_render_time'}->{$job->get_map()}->[$job->get_z()] ||= 0; $self->{'stats'}->{'sum_render_time'}->{$job->get_map()}->[$job->get_z()] += $msg->{'render_time'}; my $max = $self->{'stats'}->{'max_render_time'}->{$job->get_map()}->[$job->get_z()] || 0; $max = $msg->{'render_time'} if $msg->{'render_time'} > $max; $self->{'stats'}->{'max_render_time'}->{$job->get_map()}->[$job->get_z()] = $max; $job->{'render_time'} = $msg->{'render_time'}; } else { ::syslog('warning', 'job rendering error id=%s map=%s x=%d y=%d z=%d result=%s errmsg=%s', $job->get_id(), $job->get_map(), $job->get_x(), $job->get_y(), $job->get_z(), $msg->{'result'}, $msg->{'errmsg'}); # update statistics $self->{'stats'}->{'count_error' }++; $job->{'render_time'} = 0; } $self->{'rendering_jobs'}->remove($job); $job->get_bucket()->remove_job($job); } # if the job is not found, our records are confused and we log a warning but ignore the job # this can happen if there was a timeout waiting for the backend, but later the backend came # through and send the answer or if the master was restarted and gets a response for a tile from # the backend that was requested by an earlier master process. else { ::syslog('warning', 'Job for id %s not found (timeout? restart of master?)', $msg->{'id'}); } $self->schedule(); return $job; } =head2 $rm->log_stats() Write statistics to log file. =cut sub log_stats { my $self = shift; my $stats = $self->{'stats'}; foreach my $statkey (sort keys %$stats ) { my $statvalue = $stats->{$statkey}; if (ref($statvalue) eq '') { ::syslog('info', 'stat %s=%s', $statkey, $statvalue); } elsif (ref($statvalue) eq 'HASH') { foreach my $map (sort keys %$statvalue) { my $text = join(', ', map { $_ || 0 } @{$statvalue->{$map}}); ::syslog('info', 'stat %s[%s]=%s', $statkey, $map, $text); } } } } =head2 $rm->status() Return status of the rendering manager. =cut sub status { my $self = shift; my $current_load = $self->get_load(); # 0 + in the following to force numbers for JSON my $status = { load => 0 + $current_load, num_rendering => 0 + $self->{'rendering_jobs'}->count(), stats => $self->{'stats'}, buckets => [], rendering => [], }; foreach my $bucket (@{$self->{'buckets'}}) { push(@{$status->{'buckets'}}, $bucket->status($self->{'rendering_jobs'}->count(), $current_load)); } $status->{'rendering'} = $self->{'rendering_jobs'}->status(); return $status; } =head2 $rm->get_load_from_uptime() Get system load by calling the 'uptime' command. This is called by get_load() if /proc/loadavg is not available. =cut sub get_load_from_uptime { my $uptime = `uptime`; if ($uptime =~ m/load average: ([0-9\.]+), ([0-9\.]+), ([0-9\.]+)/) { return $2; } return 0; } =head2 $rm->get_load() Get current load on the machine. If /proc/loadavg is not readable for some reason, it always returns 0. =cut sub get_load { my $self = shift; return $self->{'load'} if ($self->{'last_load_check'} == time()); open(my $loadavg, '<', '/proc/loadavg') or return $self->get_load_from_uptime(); ($self->{'load'} = <$loadavg>) =~ s/ .*\n//; close($loadavg); $self->{'last_load_check'} = time(); return $self->{'load'}; } =head1 SEE ALSO L =cut 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Manager/000077500000000000000000000000001412262531100160615ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Manager/Bucket.pm000066400000000000000000000114331412262531100176360ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Manager/Bucket.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use JSON; use Tirex; #----------------------------------------------------------------------------- package Tirex::Manager::Bucket; =head1 NAME Tirex::Manager::Bucket - Rendering buckets for different priorities =head1 SYNOPSIS use Tirex::Manager::Bucket; my $bucket = Tirex::Manager::Bucket->new( name => 'test', minprio => 1, maxproc => 8, maxload => 20 ); =head1 DESCRIPTION To simplify the configuration of rendering parameters for different zoom levels, they are configured through Buckets. One Bucket contains the configuration for a range of zoom levels. =head1 METHODS =head2 Tirex::Manager::Bucket->new( name => $name, minprio => $minprio, maxproc => $maxproc, maxload => $maxload ) Create new rendering bucket. =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; Carp::croak("need 'name' parameter for bucket") unless ($self->{'name'}); Carp::croak("need 'minprio' parameter for bucket") unless ($self->{'minprio'}); Carp::croak("need 'maxproc' parameter for bucket") unless ($self->{'maxproc'}); Carp::croak("need 'maxload' parameter for bucket") unless ($self->{'maxload'}); $self->{'numproc'} = 0; $self->{'active'} = 1; return $self; } =head2 $bucket->add_job($job) Add a Tirex::Job to bucket. =cut sub add_job { my $self = shift; my $job = shift; $job->set_bucket($self); $self->{'numproc'}++; return; } =head2 $bucket->remove_job($job) Remove a Tirex::Job from bucket. =cut sub remove_job { my $self = shift; my $job = shift; $job->set_bucket(undef); $self->{'numproc'}--; return; } =head2 $bucket->get_name() Get name of bucket. =cut sub get_name { my $self = shift; return $self->{'name'}; } =head2 $bucket->get_numproc() Returns the number of rendering processes currently working on jobs in this bucket. =cut sub get_numproc { my $self = shift; return $self->{'numproc'}; } =head2 $bucket->get_minprio() Get minimum priority for this bucket. =cut sub get_minprio { my $self = shift; return $self->{'minprio'}; } =head2 $bucket->get_maxprio() Get maximum priority for this bucket. The maximum priority has to be set with set_maxprio() before this works. =cut sub get_maxprio { my $self = shift; return $self->{'maxprio'}; } =head2 $bucket->set_maxprio($maxprio) Set maximum priority for this bucket. =cut sub set_maxprio { my $self = shift; $self->{'maxprio'} = shift; return; } =head2 $bucket->get_active() Get active flag for this bucket. =cut sub get_active { my $self = shift; return $self->{'active'}; } =head2 $bucket->set_active($active) Set active flag for this bucket. =cut sub set_active { my $self = shift; my $active = shift; $self->{'active'} = $active ? 1 : 0; return; } =head2 $bucket->for_prio($prio) Check whether this bucket is the right one for the given priority, ie. whether the priority is between min- and maxprio. =cut sub for_prio { my $self = shift; my $prio = shift; if (defined $self->{'maxprio'}) { return $self->{'minprio'} <= $prio && $prio <= $self->{'maxprio'}; } else { return $self->{'minprio'} <= $prio; } } =head2 $bucket->can_render($num_rendering, $current_load) Finds out if a job in this rendering bucket can be rendered. Returns 1 if it can be rendered 0 if there are already maxproc or more rendering processes or if bucket is not active undef if the load is higher or equal than maxload =cut sub can_render { my $self = shift; my $num_rendering = shift; my $current_load = shift; return 0 if (! $self->{'active'}); return 0 if ($num_rendering >= $self->{'maxproc'}); return $current_load >= $self->{'maxload'} ? undef : 1; } =head2 $bucket->status($num_rendering, $current_load) Return status of bucket. =cut sub status { my $self = shift; my $num_rendering = shift; my $current_load = shift; # 0 + in the following to force numbers for JSON return { name => $self->{'name'}, minprio => 0 + $self->get_minprio(), maxprio => defined($self->get_maxprio()) ? 0 + $self->get_maxprio() : 0, numproc => 0 + $self->get_numproc(), maxproc => 0 + $self->{'maxproc'}, maxload => 0 + $self->{'maxload'}, active => $self->get_active(), can_render => $self->can_render($num_rendering, $current_load) ? JSON::true : JSON::false, }; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Manager/RenderingJobs.pm000066400000000000000000000073211412262531100211550ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Manager/RenderingJobs.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use Tirex; #----------------------------------------------------------------------------- package Tirex::Manager::RenderingJobs; =head1 NAME Tirex::Manager::Rendering - Hold currently rendering jobs =head1 SYNOPSIS use Tirex::Manager:RenderingJobs; =head1 DESCRIPTION Keeps the list of currently rendering jobs. =head1 METHODS =head2 Tirex::Manager::RenderingJobs->new( timeout => 120 ) Create new object. =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; Carp::croak('missing or illegal timeout parameter when creating object of class Tirex::Manager::RenderingJobs') unless (defined($self->{'timeout'}) && $self->{'timeout'} =~ /^[0-9]+$/); $self->{'requests_by_id'} = {}; $self->{'requests_by_metatile'} = {}; return $self; } =head2 $rj->count() Returns the number of jobs currently rendering. =cut sub count { my $self = shift; return scalar(keys %{$self->{'requests_by_id'}}); } =head2 $rj->add($job) Add a job. Returns the job added. =cut sub add { my $self = shift; my $job = shift; $job->{'rendering_requested'} = time(); $self->{'requests_by_id' }->{ $job->get_id() } = $job; $self->{'requests_by_metatile'}->{ $job->hash_key() } = $job; return $job; } =head2 $rj->remove($job) Remove a job. Returns the job removed. =cut sub remove { my $self = shift; my $job = shift; delete($self->{'requests_by_id' }->{ $job->get_id() }); delete($self->{'requests_by_metatile'}->{ $job->hash_key() }); return $job; } =head2 $rj->find_by_id($id) Find a currently rendering job by its id. =cut sub find_by_id { my $self = shift; my $id = shift; return $self->{'requests_by_id'}->{$id}; } =head2 $rj->find_by_metatile($hash) Find a currently rendering job for some metatile by the metatile hash. =cut sub find_by_metatile { my $self = shift; my $hash = shift; return $self->{'requests_by_metatile'}->{$hash}; } =head2 $rj->check_timeout() Check if there are any jobs older than the timeout and remove them. They will have been killed by tirex-backend-manager in the mean time. Returns the number of jobs removed. =cut sub check_timeout { my $self = shift; my $count = 0; my $timeout = time() - $self->{'timeout'}; foreach my $job (values %{$self->{'requests_by_id'}}) { if ($job->{'rendering_requested'} < $timeout) { my $bucket = $job->get_bucket(); $bucket->remove_job($job) if (defined $bucket); $self->remove($job); ::syslog('err', 'Job with id=%s timed out on rendering list (%s)', $job->get_id(), $job->get_metatile()->to_s()); $count++; $job->notify(); } } return $count; } =head2 $rj->status() Return status. =cut sub status { my $self = shift; # 0 + in the following to force numbers for JSON my @status = map { { map => $_->get_map(), x => 0 + $_->get_x(), y => 0 + $_->get_y(), z => 0 + $_->get_z(), prio => 0 + $_->get_prio(), age => time() - $_->{'rendering_requested'}, }; } sort { $a->get_prio() == $b->get_prio() ? $a->{'rendering_requested'} <=> $b->{'rendering_requested'} : $a->get_prio() <=> $b->get_prio(); } values %{$self->{'requests_by_id'}}; return \@status; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Manager/Test.pm000066400000000000000000000032171412262531100173410ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Manager/Test.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex; use Tirex::Manager; #----------------------------------------------------------------------------- package Tirex::Manager::Test; use base qw( Tirex::Manager ); =head1 NAME Tirex::Manager::Test - Dummy rendering manager for testing =head1 SYNOPSIS use Tirex::Manager::Test; my $queue = Tirex::Queue->new(); my $rm = Tirex::Manager::Test->new( queue => $queue ); $rm->set_load(1.5); print $rm->get_load(); $rm->schedule(); =head1 DESCRIPTION This is a dummy version of the L rendering manager class for testing. It is a child class of the normal L class and behaves just like it except that you can set the "system load" with set_load() and this load is returned by the get_load() method. This way the system load can be simulated in tests. This class also has a dummy version of the send() method that doesn't actually send the message. =head1 METHODS =head2 $rm->get_load() Get the load that was set with set_load(). =cut sub get_load { my $self = shift; return $self->{'load'} || 0; } =head2 $rm->set_load($load) Set the load. =cut sub set_load { my $self = shift; my $load = shift; $self->{'load'} = $load; return; } =head2 $rm->send($job) Simulate sending a job to the rendering daemon. =cut sub send { # do nothing return; } =head1 SEE ALSO L =cut 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Map.pm000066400000000000000000000113631412262531100155660ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Map.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use Tirex::Renderer; #----------------------------------------------------------------------------- package Tirex::Map; # a hash with all configured maps our %Maps; =head1 NAME Tirex::Map - A Tirex map configuration =head1 SYNOPSIS my $map = Tirex::Map->new(); =head1 DESCRIPTION A Tirex map configuration. It always contains the name, tile directory and zoom range for this map. Depending on the backend there can be more options. =head1 METHODS =head2 Tirex::Map->get('foo') Get map by name. =cut sub get { my $class = shift; my $name = shift; return $Maps{$name}; } =head2 Tirex::Map->clear(); Clear list of maps. =cut sub clear { %Maps = (); } =head2 Tirex::Map->get_map_for_metatile($metatile) Get map for a metatile. Will croak if the map named in the metatile does not exist. Will also croak if the zoom given in the metatile is out of range. =cut sub get_map_for_metatile { my $class = shift; my $metatile = shift; my $map = $Maps{$metatile->get_map()}; Carp::croak("map with name '" . $metatile->get_map() . "' not found") unless (defined $map); Carp::croak('zoom out of range') if ($metatile->get_z() < $map->get_minz()); Carp::croak('zoom out of range') if ($metatile->get_z() > $map->get_maxz()); return $map; } =head2 Tirex::Map->new( ... ) Create new map configuration. Default values for minimum zoom (minz) is 0, for maximum zoom (maxz) it's 17. =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; Carp::croak('missing name' ) unless (defined $self->{'name' }); Carp::croak('missing renderer') unless (defined $self->{'renderer'}); Carp::croak('missing tiledir' ) unless (defined $self->{'tiledir' }); Carp::croak("map with name $self->{'name'} exists") if ($Maps{$self->{'name'}}); $self->{'minz'} = 0 unless (defined $self->{'minz'}); $self->{'maxz'} = 17 unless (defined $self->{'maxz'}); $Maps{$self->{'name'}} = $self; return $self; } =head2 Tirex::Map->new_from_configfile($filename, $renderer) Create new map config from a file for a given renderer. Croaks if the file does not exist. =cut sub new_from_configfile { my $class = shift; my $filename = shift; my $renderer = shift; my %config; open(my $cfgfh, '<', $filename) or Carp::croak("Can't open map config file '$filename': $!"); while (<$cfgfh>) { s/#.*$//; next if (/^\s*$/); if (/^([a-z0-9_]+)\s*=\s*(\S*)\s*$/) { $config{$1} = $2; } } close($cfgfh); $config{'filename'} = $filename; $config{'renderer'} = $renderer; return $class->new(%config); } =head2 $map->get_name() Get name of this map. =cut sub get_name { return shift->{'name'}; } =head2 $map->get_renderer() Get renderer of this map. =cut sub get_renderer { return shift->{'renderer'}; } =head2 $map->get_filename() Get filename of config file for this map. This only works if the map was created from a config file. Otherwise it will return undef. =cut sub get_filename { return shift->{'filename'}; } =head2 $map->get_tiledir() Get tile directory of this map. =cut sub get_tiledir { return shift->{'tiledir'}; } =head2 $map->get_minz() Get minimum zoom value of this map. =cut sub get_minz { return shift->{'minz'}; } =head2 $map->get_maxz() Get maximum zoom of this map. =cut sub get_maxz { return shift->{'maxz'}; } =head2 $map->to_s(); Return human readable description of this map. =cut sub to_s { my $self = shift; my $s = sprintf("Map %s: renderer=%s tiledir=%s zoom=%d-%d", $self->get_name(), $self->get_renderer()->get_name(), $self->get_tiledir(), $self->get_minz(), $self->get_maxz()); foreach my $key (sort keys %$self) { $s .= " $key=$self->{$key}" unless ($key =~ /^(name|renderer|tiledir|minz|maxz|filename)$/); } return $s; } =head2 $map->to_hash(); Return parameters of this map as hash. =cut sub to_hash { my $self = shift; my %hash = %$self; $hash{'minz'} = 0 + $self->get_minz(); # force integer (so that it works in JSON) $hash{'maxz'} = 0 + $self->get_maxz(); $hash{'renderer'} = $self->get_renderer()->get_name(); return \%hash; } =head2 Tirex::Map->status(); Return status of all configured maps. =cut sub status { my $self = shift; my @status = (); foreach my $map (sort { $a->get_name() cmp $b->get_name() } values %Maps) { push(@status, $map->to_hash()); } return \@status; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Message.pm000066400000000000000000000104611412262531100164330ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Message.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; #----------------------------------------------------------------------------- package Tirex::Message; =head1 NAME Tirex::Message - A message =head1 SYNOPSIS my $msg = Tirex::Message->new( ... ); =head1 DESCRIPTION Messages are used to exchange requests and replies between different components of the Tirex system. "On the wire" they consist of several lines (separated by an optional carriage return and a newline). Each line has the form "key=value". No spaces are allowed before or after the key or equals sign. =head1 METHODS =head2 Tirex::Message->new( type => '...', field1key => "field2value", ... ) Create new message. You always need a type for the message, all other fields are optional. Will croak if there is no type given. =cut sub new { my $proto = shift; my $class = ref($proto) || $proto; my %args = @_; my $self = bless \%args => $class; Carp::croak("need type for new message") unless (defined $self->{'type'}); return $self; } =head2 Tirex::Message->new_from_string("type=foo\nbar=baz\n"); Create message object from string. =cut sub new_from_string { my $class = shift; my $string = shift; my %hash; foreach my $line (split(/\r?\n/, $string)) { my ($k, $v) = split(/=/, $line, 2); $hash{$k} = $v; } return $class->new(%hash); } =head2 Tirex::Message->new_from_socket($socket) Read a datagram from given socket and create new message from it. =cut # XXX error handling? sub new_from_socket { my $class = shift; my $socket = shift; my $buf; if ($socket->recv($buf, $Tirex::MAX_PACKET_SIZE)) { return $class->new_from_string($buf); } else { return; } } =head2 $msg->reply([RESULT[, ERRMSG]]) Create new message with reply to old one. If RESULT is not given it defaults to 'ok'. If ERRMSG is given, it is attached to the message. You can't send a reply to a reply, so if the original message contains a 'result' field, this method croaks. =cut sub reply { my $self = shift; my $result = shift; my $errmsg = shift; Carp::croak("can't reply to reply") if ($self->{'result'}); $result = 'ok' unless ($result); my %fields = (%$self, result => $result); $fields{'errmsg'} = $errmsg if ($errmsg); return $self->new(%fields); } =head2 $msg->serialize() Serialize this message into a string with lines of the format key=value. If a value is undefined the field is not added. =cut sub serialize { my $self = shift; return $self->_to_s('', "\n"); } =head2 $msg->to_s() Return string version of this message, for instance for debugging. Format is key=value separated by spaces. If a value is undefined the field is not added. =cut sub to_s { my $self = shift; return $self->_to_s(' ', ''); } sub _to_s { my $self = shift; my $joinstring = shift; my $endstring = shift; return join($joinstring, map { defined($self->{$_}) ? "$_=$self->{$_}$endstring" : '' } sort(keys %$self) ); } =head2 $msg->send($socket, $dest) Send message through $socket to $dest. =cut sub send { my $self = shift; my $socket = shift; my $dest = shift; return $socket->send($self->serialize(), undef, $dest); } =head2 $msg->to_metatile() Create metatile from message. Croaks when the message can't be made into a valid metatile. =cut sub to_metatile { my $self = shift; return Tirex::Metatile->new( map => $self->{'map'}, x => $self->{'x'}, y => $self->{'y'}, z => $self->{'z'} ); } =head2 $msg->ok() Is this message a positive reply (contains 'result=ok')? =cut sub ok { my $self = shift; return unless (defined $self->{'result'}); return $self->{'result'} eq 'ok'; } =head2 $msg->unknown_message_type() Is this an error message for an unknown message type (contains 'result=error_unknown_command')? =cut sub unknown_message_type { my $self = shift; return unless (defined $self->{'result'}); return $self->{'result'} eq 'error_unknown_command'; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Metatile.pm000066400000000000000000000202431412262531100166120ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Metatile.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use Math::Trig; use File::stat; #----------------------------------------------------------------------------- package Tirex::Metatile; =head1 NAME Tirex::Metatile - A Metatile =head1 SYNOPSIS my $mt = Tirex::Metatile->new( map => 'osm', x => 16, y => 12, z=> 12 ); =head1 DESCRIPTION A metatile. =head1 METHODS =head2 Tirex::Metatile->new( ... ) Create new metatile object. A metatile always needs the following parameters: map the map config to use for rendering x metatile x coordinate y metatile y coordinate z zoom level You can give any x and y coordinate in the range 0 .. 2^z-1. It will be rounded down to the next tile coordinate. Croaks if there is a problem with the parameters. =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; Carp::croak("need map for new metatile") unless (defined $self->{'map'}); Carp::croak("need x for new metatile") unless (defined $self->{'x'} ); Carp::croak("need y for new metatile") unless (defined $self->{'y'} ); Carp::croak("need z for new metatile") unless (defined $self->{'z'} ); Carp::croak("z must be between 0 and $Tirex::MAX_ZOOM (but is $self->{'z'})") unless ( 0 <= $self->{'z'} && $self->{'z'} <= $Tirex::MAX_ZOOM ); my $limit = 2 ** $self->{'z'} - 1; Carp::croak("x must be between 0 and 2^z-1 (but is $self->{'x'})") unless ( 0 <= $self->{'x'} && $self->{'x'} <= $limit ); Carp::croak("y must be between 0 and 2^z-1 (but is $self->{'y'})") unless ( 0 <= $self->{'y'} && $self->{'y'} <= $limit ); $self->{'x'} -= $self->{'x'} % Tirex::Config::get('metatile_columns', $Tirex::METATILE_COLUMNS); $self->{'y'} -= $self->{'y'} % Tirex::Config::get('metatile_rows', $Tirex::METATILE_ROWS ); return $self; } =head2 Tirex::Metatile->new_from_filename_and_map($filename, $map) Create metatile from filename. The first directory element must be the zoom level. Optionally, the filename can start with '/' or './'. =cut sub new_from_filename_and_map { my $class = shift; my $filename = shift; my $map = shift; # remove leading / or ./ $filename =~ s{^\.?/}{}; # remove trailing .meta $filename =~ s{\.meta$}{}; my @path_components = split('/', $filename); my $z = shift @path_components; my $x = 0; my $y = 0; Carp::croak("not a valid metatile filename: too many or too few components") unless (scalar(@path_components) == 5); while (defined (my $c = shift @path_components)) { Carp::croak("failed to parse tile path (invalid component '$c')") if ($c < 0 || $c > 255); $x <<= 4; $y <<= 4; $x |= ($c & 0xf0) >> 4; $y |= ($c & 0x0f); } return $class->new( map => $map, z => $z, x => $x, y => $y ); } =head2 Tirex::Metatile->new_from_lon_lat(map => $map, lon => $lon, lat => $lat, z => $z) Create metatile from zoom, longitude, and latitude. =cut sub new_from_lon_lat { my $class = shift; my %args = @_; my $map = $args{'map'}; my $z = $args{'z'}; my $lon = $args{'lon'}; my $lat = $args{'lat'}; Carp::croak('need map for new metatile') unless (defined $map); Carp::croak('need z for new metatile') unless (defined $z ); Carp::croak('need lon for new metatile') unless (defined $lon); Carp::croak('need lat for new metatile') unless (defined $lat); Carp::croak('lon must be between -180 and 180') unless (-180 <= $lon && $lon <= 180); Carp::croak('lat must be between -85.05112 and 85.05113') unless ( -85.05112 <= $lat && $lat <= 85.05113); Carp::croak("z must be between 0 and $Tirex::MAX_ZOOM (but is $z)") unless ( 0 <= $z && $z <= $Tirex::MAX_ZOOM ); my $x = lon2x(1, $z, $lon); my $y = lat2y(1, $z, $lat); return $class->new( map => $map, z => $z, x => $x, y => $y ); } # Tirex::Metatile::lon2x sub lon2x { my $mtx = shift; my $zoom = shift; my $lon = shift; my $limit = 2 ** $zoom; my $x = int( ($lon+180) / 360 * $limit ); $x = $limit-1 if ($x >= $limit); # need this so that +180 comes out to max x return int($x / $mtx) * $mtx; } # Tirex::Metatile::lat2y sub lat2y { my $mty = shift; my $zoom = shift; my $lat = shift; $lat = -85.05113 if ($lat < -85.05113); $lat = 85.05113 if ($lat > 85.05113); my $limit = 2 ** $zoom; $lat = $lat * Math::Trig::pi / 180; # degree -> radians my $y = int( ( 1 - log(Math::Trig::tan($lat) + Math::Trig::sec($lat)) / Math::Trig::pi ) / 2 * $limit ); $y = $limit-1 if ($y >= $limit); # need this so that -85.05113 comes out to max y return int($y / $mty) * $mty; } =head2 $mt->get_x() Get x coordinate. =head2 $mt->get_y() Get y coordinate. =head2 $mt->get_x() Get zoom. =head2 $mt->get_map() Get map. =cut sub get_x { my $self = shift; return $self->{'x'}; } sub get_y { my $self = shift; return $self->{'y'}; } sub get_z { my $self = shift; return $self->{'z'}; } sub get_map { my $self = shift; return $self->{'map'}; } =head2 $mt->to_s() Return string describing this metatile in the format 'map=MAP z=Z x=X y=Y' =cut sub to_s { my $self = shift; return join(' ', map { "$_=$self->{$_}"; } qw( map z x y )); } =head2 $mt->equals($other_metatile) Returns true if both metatiles are the same, false otherwise. The same tile means: same map, same x and y coordinates and same zoom level. =cut sub equals { my $self = shift; my $other = shift; return (($self->{'map'} eq $other->{'map'}) && ($self->{'x'} == $other->{'x'} ) && ($self->{'y'} == $other->{'y'} ) && ($self->{'z'} == $other->{'z'} )); } =head2 $mt->up() Return the metatile one zoom level above this metatile that contains this metatile. =cut sub up { my $self = shift; return if ($self->{'z'} == 0); my $x = int($self->{'x'} / 2); my $y = int($self->{'y'} / 2); $x -= $x % Tirex::Config::get('metatile_columns', $Tirex::METATILE_COLUMNS); $y -= $y % Tirex::Config::get('metatile_rows', $Tirex::METATILE_ROWS ); return Tirex::Metatile->new( map => $self->{'map'}, z => $self->{'z'} - 1, x => $x, y => $y ); } =head2 $mt->get_filename() Return filename for this metatile. Format is something like: [zoom]/[path].meta zoom zoom level path path with 4 directory elements and a filename based on x and y coordinates =cut sub get_filename { my $self = shift; my @path_components; my $x = $self->{'x'}; my $y = $self->{'y'}; foreach (0..4) { my $v = $x & 0x0f; $v <<= 4; $v |= ($y & 0x0f); $x >>= 4; $y >>= 4; unshift(@path_components, $v); } unshift(@path_components, $self->{'z'}); return join('/', @path_components) . '.meta'; } =head2 $mt->exists() Does the metatile file for this metatile exist? =cut sub exists { my $self = shift; my $s = $self->_stat() or return 0; return 1; } =head2 $mt->older($time) Is the metatile file older than the given time? Returns 2 if the file doesn't exist. =cut sub older { my $self = shift; my $time = shift; my $s = $self->_stat() or return 2; return $s->mtime() < $time; } =head2 $mt->newer($time) Is the metatile file newer than the given time? Returns 2 if the file doesn't exist. =cut sub newer { my $self = shift; my $time = shift; my $s = $self->_stat() or return 2; return $s->mtime() > $time; } =head2 $mt->size() Return size of the metatile file. =cut sub size { my $self = shift; my $s = $self->_stat() or return 0; return $s->size(); } # call stat on the metatile file and memoize the result sub _stat { my $self = shift; my $fn = Tirex::Map->get($self->get_map())->get_tiledir() . '/' . $self->get_filename(); $self->{'stat'} = File::stat::stat($fn) unless (defined $self->{'stat'}); return $self->{'stat'}; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Metatiles/000077500000000000000000000000001412262531100164365ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Metatiles/Range.pm000066400000000000000000000316671412262531100200450ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Metatiles/Range.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use List::Util qw(); use Math::Trig; #----------------------------------------------------------------------------- package Tirex::Metatiles::Range; =head1 NAME Tirex::Metatiles::Range - range of metatiles =head1 SYNOPSIS use Tirex::Metatiles::Range; $range = Tirex::Metatiles::Range->new( z => '3-4', lon => '8-9', lat => '48-49' ); =head1 DESCRIPTION A range of metatiles for one or more maps, one or more zoom levels and an x/y range for a lon/lat bounding box. Is used to easily iterate over all those metatiles. =head1 METHODS =head2 Tirex::Metatiles::Range->new(...) Create new range. =cut sub new { my $class = shift; my %args = (); my $self = bless \%args => $class; my %options = @_; $self->{'mtx'} = Tirex::Config::get_int('metatile_columns', $Tirex::METATILE_COLUMNS); $self->{'mty'} = Tirex::Config::get_int('metatile_rows', $Tirex::METATILE_ROWS ); $self->{'mtz'} = 1; # XXX argh Carp::croak("you cannot have parameters 'z' and 'zmin'/'zmax'") if ( exists($options{'z'}) && ( exists($options{'zmin'}) || exists($options{'zmax'}) ) ); Carp::croak("you cannot have parameters 'y' and 'ymin'/'ymax'") if ( exists($options{'y'}) && ( exists($options{'ymin'}) || exists($options{'ymax'}) ) ); Carp::croak("you cannot have parameters 'x' and 'xmin'/'xmax'") if ( exists($options{'x'}) && ( exists($options{'xmin'}) || exists($options{'xmax'}) ) ); foreach my $var ('xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax') { Carp::croak("$var must be zero or positive integer") if ( exists($options{$var}) && ($options{$var} !~ /^[0-9]+$/) ); } foreach my $var ('lonmin', 'lonmax', 'latmin', 'latmax') { Carp::croak("$var must be legal degree value") if ( exists($options{$var}) && ($options{$var} !~ /^-?[0-9]+(.[0-9]+)?$/) ); Carp::croak("$var must be legal longitude value") if ( $var =~ /^lon/ && exists($options{$var}) && ($options{$var} < -180 || $options{$var} > 180) ); Carp::croak("$var must be legal latitude value" ) if ( $var =~ /^lat/ && exists($options{$var}) && ($options{$var} < -90 || $options{$var} > 90) ); } foreach my $var ('x', 'y', 'z', 'lon', 'lat') { Carp::croak("'${var}min' but missing '${var}max'") if ( exists($options{$var . 'min'}) && ! exists($options{$var . 'max'}) ); Carp::croak("'${var}max' but missing '${var}min'") if ( exists($options{$var . 'max'}) && ! exists($options{$var . 'min'}) ); } foreach my $key (keys %options) { my $value = $options{$key}; if ($key eq 'init') { $self->_parse_init($value); } else { $self->_parse_key_value($key, $options{$key}); } } # make sure we have all needed parameters Carp::croak("missing 'map' parameter") if ( ! exists($self->{'maps'})); Carp::croak("missing 'z' or 'zmin'/'zmax' parameter") if ( ! exists($self->{'z'}) && ! exists($self->{'zmin'}) && ! exists($self->{'zmax'}) ); Carp::croak("missing 'x' or 'xmin'/'xmax' or 'lon' or 'lonmin/lonmax' or 'bbox' parameter") if ( ! exists($self->{'x'}) && ! exists($self->{'xmin'}) && ! exists($self->{'xmax'}) && ! exists($self->{'lon'}) && ! exists($self->{'lonmin'}) && ! exists($self->{'lonmax'}) ); Carp::croak("missing 'y' or 'ymin'/'ymax' or 'lat' or 'latmin/latmax' or 'bbox' parameter") if ( ! exists($self->{'y'}) && ! exists($self->{'ymin'}) && ! exists($self->{'ymax'}) && ! exists($self->{'lat'}) && ! exists($self->{'latmin'}) && ! exists($self->{'latmax'}) ); # make sure min is always smaller than max ($self->{'zmin' }, $self->{'zmax' }) = (List::Util::min($self->{'zmin' }, $self->{'zmax' }), List::Util::max($self->{'zmin' }, $self->{'zmax' })) if (defined $self->{'zmin' }); ($self->{'ymin' }, $self->{'ymax' }) = (List::Util::min($self->{'ymin' }, $self->{'ymax' }), List::Util::max($self->{'ymin' }, $self->{'ymax' })) if (defined $self->{'ymin' }); ($self->{'xmin' }, $self->{'xmax' }) = (List::Util::min($self->{'xmin' }, $self->{'xmax' }), List::Util::max($self->{'xmin' }, $self->{'xmax' })) if (defined $self->{'xmin' }); ($self->{'lonmin'}, $self->{'lonmax'}) = (List::Util::min($self->{'lonmin'}, $self->{'lonmax'}), List::Util::max($self->{'lonmin'}, $self->{'lonmax'})) if (defined $self->{'lonmin'}); ($self->{'latmin'}, $self->{'latmax'}) = (List::Util::min($self->{'latmin'}, $self->{'latmax'}), List::Util::max($self->{'latmin'}, $self->{'latmax'})) if (defined $self->{'latmin'}); # if there is only one zoom level, calculate x/y range now if ($self->{'zmin'} == $self->{'zmax'}) { ($self->{'xmin'}, $self->{'xmax'}) = $self->_get_range_x($self->{'zmin'}); ($self->{'ymin'}, $self->{'ymax'}) = $self->_get_range_y($self->{'zmin'}); } $self->reset(); return $self; } sub _parse_key_value { my $self = shift; my $key = shift; my $value = shift; if ($key eq 'map') { $self->{'maps'} = ref($value) eq '' ? [split(',', $value)] : $value; } elsif ($key eq 'z') { $self->_parse_int_range('z', $value); } elsif ($key eq 'x') { $self->_parse_int_range('x', $value); } elsif ($key eq 'y') { $self->_parse_int_range('y', $value); } elsif ($key eq 'zmin') { $self->{'zmin'} = $value; } elsif ($key eq 'zmax') { $self->{'zmax'} = $value; } elsif ($key eq 'xmin') { $self->{'xmin'} = int($value / $self->{'mtx'}) * $self->{'mtx'}; } elsif ($key eq 'xmax') { $self->{'xmax'} = int($value / $self->{'mtx'}) * $self->{'mtx'}; } elsif ($key eq 'ymin') { $self->{'ymin'} = int($value / $self->{'mty'}) * $self->{'mty'}; } elsif ($key eq 'ymax') { $self->{'ymax'} = int($value / $self->{'mty'}) * $self->{'mty'}; } elsif ($key eq 'lon') { $self->_parse_degree_range('lon', $value); } elsif ($key eq 'lat') { $self->_parse_degree_range('lat', $value); } elsif ($key eq 'lonmin') { $self->{'lonmin'} = $value; } elsif ($key eq 'lonmax') { $self->{'lonmax'} = $value; } elsif ($key eq 'latmin') { $self->{'latmin'} = $value; } elsif ($key eq 'latmax') { $self->{'latmax'} = $value; } elsif ($key eq 'bbox') { $self->_parse_bbox($value); } elsif ($key eq 'init') { $self->_parse_init($value); } else { Carp::croak("unknown parameter: '$key'"); } } sub _parse_init { my $self = shift; my $init = shift; foreach my $parameter (split(/\s+/, $init)) { if ($parameter =~ /^([^=]+)=(.+)$/) { $self->_parse_key_value($1, $2); } else { Carp::croak("can't parse init string: '$init'"); } } return; } =head2 $range->reset() Reset range, so that the next call to next() will return the first metatile in the range. Returns range itself. =cut sub reset { my $self = shift; $self->{'finished'} = 0; $self->{'current_map_pos'} = 0; $self->{'current_z'} = $self->{'zmin'}; ($self->{'ymin_for_current_z'}, $self->{'ymax_for_current_z'}) = $self->_get_range_y($self->{'current_z'}); ($self->{'xmin_for_current_z'}, $self->{'xmax_for_current_z'}) = $self->_get_range_x($self->{'current_z'}); $self->{'current_y'} = $self->{'ymin_for_current_z'}; $self->{'current_x'} = $self->{'xmin_for_current_z'}; $self->{'metatiles'} = 0; return $self; } sub _get_range_x { my $self = shift; my $zoom = shift; return ($self->{'xmin'}, $self->{'xmax'}) if (defined $self->{'xmin'}); if (defined $self->{'lonmin'}) { return ( Tirex::Metatile::lon2x($self->{'mtx'}, $zoom, $self->{'lonmin'}), Tirex::Metatile::lon2x($self->{'mtx'}, $zoom, $self->{'lonmax'}) ); } Carp::croak("should not be here"); } sub _get_range_y { my $self = shift; my $zoom = shift; return ($self->{'ymin'}, $self->{'ymax'}) if (defined $self->{'ymin'}); if (defined $self->{'latmin'}) { return ( Tirex::Metatile::lat2y($self->{'mty'}, $zoom, $self->{'latmax'}), # latitude increases from south to north, but tile numbers from north to south! Tirex::Metatile::lat2y($self->{'mty'}, $zoom, $self->{'latmin'}) ); } Carp::croak("should not be here"); } =head2 $range->count() Calculates how many metatiles there are in this range. =cut sub count { my $self = shift; my $maps = scalar(@{$self->{'maps'}}); my $tiles = 0; foreach my $zoom ($self->{'zmin'} .. $self->{'zmax'}) { my ($ymin, $ymax) = $self->_get_range_y($zoom); my ($xmin, $xmax) = $self->_get_range_x($zoom); $tiles += (int(($ymax - $ymin)/$self->{'mtx'}) + 1) * (int(($xmax - $xmin)/$self->{'mty'}) + 1); } return $maps * $tiles; } =head2 $range->get_metatiles() Return the number of metatiles you already got out of this range with next(). =cut sub get_metatiles { my $self = shift; return $self->{'metatiles'}; } =head2 $range->to_s() Return string describing the range. =cut sub to_s { my $self = shift; if ($self->{'lonmin'}) { return sprintf('maps=%s z=%s lon=%s lat=%s', join(',', @{$self->{'maps'}}), _range_to_s($self->{'zmin'}, $self->{'zmax'}), _range_to_s($self->{'lonmin'}, $self->{'lonmax'}), _range_to_s($self->{'latmin'}, $self->{'latmax'}) ); } else { return sprintf('maps=%s z=%s x=%s y=%s', join(',', @{$self->{'maps'}}), _range_to_s($self->{'zmin'}, $self->{'zmax'}), _range_to_s($self->{'xmin'}, $self->{'xmax'}), _range_to_s($self->{'ymin'}, $self->{'ymax'}) ); } } sub _range_to_s { my $min = shift; my $max = shift; return $min == $max ? $min : "$min,$max"; } =head2 $range->next() Get next metatile from the range. Returns undef if there are no more metatiles. =cut sub next { my $self = shift; return undef if ($self->{'finished'}); my $metatile = Tirex::Metatile->new( map => $self->{'maps'}->[$self->{'current_map_pos'}], x => $self->{'current_x'}, y => $self->{'current_y'}, z => $self->{'current_z'} ); $self->{'current_map_pos'}++; if ($self->{'current_map_pos'} >= scalar(@{$self->{'maps'}})) { $self->{'current_x'} += $self->{'mtx'}; if ($self->{'current_x'} > $self->{'xmax_for_current_z'}) { $self->{'current_y'} += $self->{'mty'}; if ($self->{'current_y'} > $self->{'ymax_for_current_z'}) { $self->{'current_z'}++; if ($self->{'current_z'} > $self->{'zmax'}) { $self->{'finished'} = 1; return $metatile; } ($self->{'ymin_for_current_z'}, $self->{'ymax_for_current_z'}) = $self->_get_range_y($self->{'current_z'}); ($self->{'xmin_for_current_z'}, $self->{'xmax_for_current_z'}) = $self->_get_range_x($self->{'current_z'}); $self->{'current_y'} = $self->{'ymin_for_current_z'}; } $self->{'current_x'} = $self->{'xmin_for_current_z'}; } $self->{'current_map_pos'} = 0; } $self->{'metatiles'}++; return $metatile; } sub _parse_int_range { my $self = shift; my $var = shift; my $val = shift; if ($val =~ /^[0-9]+$/) { $self->{$var . 'min'} = int($val / $self->{"mt$var"}) * $self->{"mt$var"}; $self->{$var . 'max'} = int($val / $self->{"mt$var"}) * $self->{"mt$var"}; } elsif ($val =~ /^([0-9]+)\s*[:,-]\s*([0-9]+)$/) { $self->{$var . 'min'} = int($1 / $self->{"mt$var"}) * $self->{"mt$var"}; $self->{$var . 'max'} = int($2 / $self->{"mt$var"}) * $self->{"mt$var"}; } else { Carp::croak("wrong format for '$var'"); } return; } sub _parse_degree_range { my $self = shift; my $var = shift; my $val = shift; if ($val =~ /^-?[0-9]+(\.[0-9]+)?$/) { $self->{$var . 'min'} = $val; $self->{$var . 'max'} = $val; } elsif ($val =~ /^(-?[0-9]+(?:\.[0-9]+)?)\s*[:,]\s*(-?[0-9]+(?:\.[0-9]+)?)$/) { $self->{$var . 'min'} = $1; $self->{$var . 'max'} = $2; } else { Carp::croak("wrong format for '$var'"); } return; } sub _parse_bbox { my $self = shift; my $val = shift; if ($val =~ /^(-?[0-9]+(?:\.[0-9]+)?)\s*[:,]\s*(-?[0-9]+(?:\.[0-9]+)?)\s*[:,]\s*(-?[0-9]+(?:\.[0-9]+)?)\s*[:,]\s*(-?[0-9]+(?:\.[0-9]+)?)$/) { $self->{'lonmin'} = $1; $self->{'lonmax'} = $3; $self->{'latmin'} = $2; $self->{'latmax'} = $4; } else { Carp::croak("wrong format for 'bbox'"); } return } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin.pm000066400000000000000000000055151412262531100161410ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex; #----------------------------------------------------------------------------- package Tirex::Munin; =head1 NAME Tirex::Munin - Parent class for Munin scripts =head1 SYNOPSIS my $m = Tirex::Munin::SomeSubclass->new(...) if ($ARGV[0] eq 'config') { print $m->config(); } else { $m->init(...) or die; print $m->fetch(); } =head1 DESCRIPTION Parent class for Munin scripts. This class is never instantiated, create subclasses instead. =head1 METHODS =head2 Tirex::Munin::SomeSubclass->new( map => [ 'map1', 'map2' ], z => [$zr1, $zr2] ) Create new munin object. =cut sub new { my $class = shift; die("never instantiate Tirex::Munin, create a subclass instead") if ($class eq 'Tirex::Munin'); my %args = @_; my $self = bless \%args => $class; $self->{'zoomranges'} = []; foreach my $zr (@{$self->{'z'}}) { if (ref($zr) eq '' && $zr =~ /^[0-9]+$/) { push(@{$self->{'zoomranges'}}, Tirex::Zoomrange->new("z$zr", $zr, $zr)); } elsif ($zr =~ /^([0-9]+)-([0-9]+)$/) { push(@{$self->{'zoomranges'}}, Tirex::Zoomrange->new("z$zr", $1, $2)); } else { push(@{$self->{'zoomranges'}}, $zr); } } $self->init(%args); return $self; } =head2 $m->do(...) Do the right Munin action depending on command line. All command line args are passed to init_data(). =cut sub do { my $self = shift; if (defined($ARGV[0]) && $ARGV[0] eq 'config') { print $self->config(); } else { $self->init_data(@_); print $self->fetch(); } return; } =head2 $m->init() Initialize config source. This method is called from new(), in the Tirex::Munin class it does nothing, but can be overwritten in subclasses. =cut sub init { } =head2 $m->config() Return config in Munin format. This method must be overwritten in subclasses. =cut sub config { die("overwrite config() in subclass"); } =head2 $m->init_data() Initialize data source. This method must be called before fetch(), in the Tirex::Munin class it does nothing, but can be overwritten in subclasses. =cut sub init_data { } =head2 $m->fetch() Return data in Munin format. This method must be overwritten in subclasses. =cut sub fetch { die("overwrite fetch() in subclass"); } =head2 Tirex::Munin::make_id($name) Create Id from name by changing all characters except A-Z, a-z, and 0-9 to a _ (underscore). =cut sub make_id { my $name = shift; (my $id = $name) =~ tr/A-Za-z0-9/_/cs; return $id; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/000077500000000000000000000000001412262531100155755ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Munin/Status.pm000066400000000000000000000020011412262531100174070ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Status.pm # #----------------------------------------------------------------------------- use strict; use warnings; use JSON; use Tirex::Status; use Tirex::Munin; #----------------------------------------------------------------------------- package Tirex::Munin::Status; use base qw( Tirex::Munin ); =head1 NAME Tirex::Munin::Status - Parent class for Tirex munin classes using status =head1 SYNOPSIS my $m = Tirex::Munin::Status::SomeSubclass->new(...) $m->init(); =head1 DESCRIPTION Parent class for Tirex munin classes using status. =head1 METHODS =head2 $m->init_config() Initialize data source from status in shared memory. =cut sub init { my $self = shift; eval { my $status = Tirex::Status->new()->read(); $self->{'status'} = JSON::from_json($status); return 1; }; return 0; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Status/000077500000000000000000000000001412262531100170605ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Munin/Status/ActiveRequests.pm000066400000000000000000000036511412262531100223720ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Status/ActiveRequests.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status; #----------------------------------------------------------------------------- package Tirex::Munin::Status::ActiveRequests; use base qw( Tirex::Munin::Status ); =head1 NAME Tirex::Munin::Status::ActiveRequests - Currently rendering requests =head1 DESCRIPTION Munin plugin for number of metatile requests currently rendering. This is reported per bucket (ie. range of priorities). =cut sub config { my $self = shift; my $config = <{'minprio'} <=> $a->{'minprio'} } @{$self->{'status'}->{'rm'}->{'buckets'}}) { my $id = Tirex::Munin::make_id($bucket->{'name'}); $config .= sprintf("req_%s.info Number of requests currently processing for bucket %s (stacked graph)\n", $id, $bucket->{'name'}); $config .= sprintf("req_%s.label rendering in %s\n", $id, $bucket->{'name'}); $config .= sprintf("req_%s.type GAUGE\n", $id); $config .= sprintf("req_%s.draw %s\n", $id, $draw); $draw = 'STACK'; } return $config; } sub fetch { my $self = shift; my $data = ''; foreach my $bucket (sort { $b->{'minprio'} <=> $a->{'minprio'} } @{$self->{'status'}->{'rm'}->{'buckets'}}) { my $id = Tirex::Munin::make_id($bucket->{'name'}); $data .= sprintf("req_%s.value %d\n", $id, $bucket->{'numproc'}); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Status/QueuedRequests.pm000066400000000000000000000045671412262531100224160ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Status/QueuedRequests.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status; #----------------------------------------------------------------------------- package Tirex::Munin::Status::QueuedRequests; use base qw( Tirex::Munin::Status ); =head1 NAME Tirex::Munin::Status::QueuedRequests - Queued requests =head1 DESCRIPTION Munin plugin for number of metatile requests queued for rendering. =cut sub config { my $self = shift; my $config = <{'minprio'} <=> $a->{'minprio'} } @{$self->{'status'}->{'rm'}->{'buckets'}}) { my $id = Tirex::Munin::make_id($bucket->{'name'}); $config .= sprintf("req_%s.info Number of requests currently queued in bucket %s (prio %s-%s)\n", $id, $bucket->{'name'}, $bucket->{'minprio'}, ($bucket->{'maxprio'} > 0) ? $bucket->{'maxprio'} : ''); $config .= sprintf("req_%s.label queued in %s\n", $id, $bucket->{'name'}); $config .= sprintf("req_%s.type GAUGE\n", $id); $config .= sprintf("req_%s.draw %s\n", $id, $draw); $draw = 'STACK'; } return $config; } sub fetch { my $self = shift; my @sorted_buckets = sort { $a->{'minprio'} <=> $b->{'minprio'} } @{$self->{'status'}->{'rm'}->{'buckets'}}; my $sum = {}; foreach my $pq (@{$self->{'status'}->{'queue'}->{'prioqueues'}}) { my $prio = $pq->{'prio'}; foreach my $bucket (@sorted_buckets) { if (($bucket->{'minprio'} <= $prio) && ($bucket->{'maxprio'} == 0 || $bucket->{'maxprio'} >= $prio)) { $sum->{$bucket->{'name'}} += $pq->{'size'}; last; } } } my $data = ''; foreach my $bucket (@sorted_buckets) { my $id = Tirex::Munin::make_id($bucket->{'name'}); $data .= sprintf("req_%s.value %d\n", $id, $sum->{$bucket->{'name'}} // 0); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Status/QueuedRequestsAge.pm000066400000000000000000000047401412262531100230240ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Status/QueuedRequestsAge.pm # #----------------------------------------------------------------------------- use strict; use warnings; use List::Util; use Tirex::Munin::Status; #----------------------------------------------------------------------------- package Tirex::Munin::Status::QueuedRequestsAge; use base qw( Tirex::Munin::Status ); =head1 NAME Tirex::Munin::Status::QueuedRequestsAge - Age of queued requests =head1 DESCRIPTION Munin plugin for age of oldest requests in the queue for each bucket (ie. range of priorities). =cut sub config { my $self = shift; my $config = <{'minprio'} <=> $a->{'minprio'} } @{$self->{'status'}->{'rm'}->{'buckets'}}) { my $id = Tirex::Munin::make_id($bucket->{'name'}); $config .= sprintf("age_%s.info Age of oldest request queued in bucket %s (prio %s-%s)\n", $id, $bucket->{'name'}, $bucket->{'minprio'}, ($bucket->{'maxprio'} > 0) ? $bucket->{'maxprio'} : ''); $config .= sprintf("age_%s.label queued in %s\n", $id, $bucket->{'name'}); $config .= sprintf("age_%s.type GAUGE\n", $id); $config .= sprintf("age_%s.draw LINE2\n", $id); } return $config; } sub fetch { my $self = shift; my @sorted_buckets = sort { $a->{'minprio'} <=> $b->{'minprio'} } @{$self->{'status'}->{'rm'}->{'buckets'}}; my $age = {}; foreach my $pq (@{$self->{'status'}->{'queue'}->{'prioqueues'}}) { my $prio = $pq->{'prio'}; foreach my $bucket (@sorted_buckets) { if (($bucket->{'minprio'} <= $prio) && ($bucket->{'maxprio'} == 0 || $bucket->{'maxprio'} >= $prio)) { $age->{$bucket->{'name'}} += List::Util::max($age->{$bucket->{'name'}} // 0, $pq->{'age_first'} // 0); last; } } } my $data = ''; foreach my $bucket (@sorted_buckets) { my $id = Tirex::Munin::make_id($bucket->{'name'}); $data .= sprintf("age_%s.value %s\n", $id, $bucket->{'numproc'} == 0 ? 'U' : $age->{$bucket->{'name'}} // 0); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Status/RenderTime.pm000066400000000000000000000047271412262531100214660ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Status/RenderTime.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status; #----------------------------------------------------------------------------- package Tirex::Munin::Status::RenderTime; use base qw( Tirex::Munin::Status ); =head1 NAME Tirex::Munin::Status::RenderTime - Rendering time for requests =head1 DESCRIPTION Munin plugin for milliseconds each second spend rendering tiles for a map. =cut sub config { my $self = shift; my $map = $self->{'map'}; my $config = ''; if ($map eq '*') { $config .= "graph_title Render time\n"; } else { $config .= sprintf("graph_title Render time for map %s\n", $map); } $config .= <{'zoomranges'}}) { my $id = $zoomrange->get_id(); $config .= sprintf("%s.info Time spend rendering per second for zoom levels %s\n", $id, $zoomrange->to_s()); $config .= sprintf("%s.label %s\n", $id, $zoomrange->get_name()); $config .= sprintf("%s.type DERIVE\n", $id); $config .= sprintf("%s.min 0\n", $id); } return $config; } sub fetch { my $self = shift; my $data = ''; foreach my $zoomrange (@{$self->{'zoomranges'}}) { my $sum = 0; foreach my $z ($zoomrange->get_min() .. $zoomrange->get_max()) { if ($self->{'map'} eq '*') { my $maps = $self->{'status'}->{'rm'}->{'stats'}->{'sum_render_time'}; while ( my ($map, $stats) = each( %$maps ) ) { $sum += ($stats->[$z] // 0); } } else { $sum += ($self->{'status'}->{'rm'}->{'stats'}->{'sum_render_time'}->{$self->{'map'}}->[$z] // 0); } } $data .= sprintf("%s.value %d\n", $zoomrange->get_id(), $sum); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Status/RequestsRendered.pm000066400000000000000000000052671412262531100227140ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Status/RequestsRendered.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status; #----------------------------------------------------------------------------- package Tirex::Munin::Status::RequestsRendered; use base qw( Tirex::Munin::Status ); =head1 NAME Tirex::Munin::Status::RequestsRendered - Number of requests rendered =head1 DESCRIPTION Munin plugin for number of metatile requests rendered per second or minute for a map. =cut sub config { my $self = shift; my $map = $self->{'map'}; my $per = $self->{'per'} // 'second'; my $config = ''; if ($map eq '*') { $config .= "graph_title Requests rendered\n"; } else { $config .= sprintf("graph_title Requests rendered for map %s\n", $map); } $config .= <{'zoomranges'}}) { my $id = $zoomrange->get_id(); my $type = $zoomrange eq $self->{'zoomranges'}->[0] ? 'AREA' : 'STACK'; $config .= sprintf("%s.info Zoomlevel %s\n", $id, $zoomrange->to_s()); $config .= sprintf("%s.label %s\n", $id, $zoomrange->get_name()); $config .= sprintf("%s.type DERIVE\n", $id); $config .= sprintf("%s.min 0\n", $id); $config .= sprintf("%s.draw %s\n", $id, $type); } return $config; } sub fetch { my $self = shift; my $data = ''; foreach my $zoomrange (@{$self->{'zoomranges'}}) { my $sum = 0; foreach my $z ($zoomrange->get_min() .. $zoomrange->get_max()) { if ($self->{'map'} eq '*') { my $maps = $self->{'status'}->{'rm'}->{'stats'}->{'count_rendered'}; while ( my ($map, $stats) = each( %$maps ) ) { $sum += ($stats->[$z] // 0); } } else { $sum += ($self->{'status'}->{'rm'}->{'stats'}->{'count_rendered'}->{$self->{'map'}}->[$z] // 0); } } $data .= sprintf("%s.value %d\n", $zoomrange->get_id(), $sum); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Tiledir.pm000066400000000000000000000023231412262531100175270ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Tiledir.pm # #----------------------------------------------------------------------------- use strict; use warnings; use JSON; use Tirex::Munin; #----------------------------------------------------------------------------- package Tirex::Munin::Tiledir; use base qw( Tirex::Munin ); =head1 NAME Tirex::Munin::Tiledir - Parent class for Tirex munin classes using tiledir stats =head1 SYNOPSIS my $m = Tirex::Munin::Tiledir::SomeSubclass->new(...) $m->init(); =head1 DESCRIPTION Parent class for Tirex munin classes using tiledir statistics. =head1 METHODS =head2 $m->init_data([ statsfile => '...' ]) Initialize data source from stats file. If no stats file is given, 'stats_dir' from the config is used. =cut sub init_data { my $self = shift; my %args = @_; $self->{'statsfile'} = $args{'statsfile'} || (Tirex::Config::get('stats_dir', $Tirex::STATS_DIR) . '/tiles.stats'); open(STATS, '<', $self->{'statsfile'}) or return; $self->{'stats'} = JSON::from_json(join('', )); close(STATS); return 1; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Tiledir/000077500000000000000000000000001412262531100171715ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Munin/Tiledir/Diskusage.pm000066400000000000000000000040631412262531100214510ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Tiledir/Diskusage.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Tiledir; #----------------------------------------------------------------------------- package Tirex::Munin::Tiledir::Diskusage; use base qw( Tirex::Munin::Tiledir ); =head1 NAME Tirex::Munin::Tiledir::Diskusage - Diskusage of tiledir =head1 DESCRIPTION Munint plugin for sum of bytes in all metatile files for a map and specified zoom levels or zoom level ranges. Actual disk usage will be a bit higher because of file system blocks. =cut sub config { my $self = shift; my $map = $self->{'map'}; my $config = <{'zoomranges'}}) { my $id = $zoomrange->get_id(); my $type = $zoomrange eq $self->{'zoomranges'}->[0] ? 'AREA' : 'STACK'; $config .= sprintf("%s.info Zoomlevel %s\n", $id, $zoomrange->to_s()); $config .= sprintf("%s.label %s\n", $id, $zoomrange->get_name()); $config .= sprintf("%s.type GAUGE\n", $id); $config .= sprintf("%s.draw %s\n", $id, $type); } return $config; } sub fetch { my $self = shift; my $data = ''; foreach my $zoomrange (@{$self->{'zoomranges'}}) { my $sum = 0; foreach my $z ($zoomrange->get_min() .. $zoomrange->get_max()) { $sum += ($self->{'stats'}->{$self->{'map'}}->[$z]->{'sumsize'} // 0); } $data .= sprintf("%s.value %d\n", $zoomrange->get_id(), $sum); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Tiledir/Tileage.pm000066400000000000000000000033751412262531100211110ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Tiledir/Tileage.pm # #----------------------------------------------------------------------------- use strict; use warnings; use List::Util; use Tirex::Munin::Tiledir; #----------------------------------------------------------------------------- package Tirex::Munin::Tiledir::Tileage; use base qw( Tirex::Munin::Tiledir ); =head1 NAME Tirex::Munin::Tiledir::Tileage - Age of tiles =head1 DESCRIPTION Munin plugin for max age of metatiles on disk for a map and specified zoom levels or zoom level ranges. =cut sub config { my $self = shift; my $map = $self->{'map'}; my $config = <{'zoomranges'}}) { $config .= sprintf("%s.info Zoomlevel %s\n", $zoomrange->get_id(), $zoomrange->to_s()); $config .= sprintf("%s.label %s\n", $zoomrange->get_id(), $zoomrange->get_name()); $config .= sprintf("%s.type GAUGE\n", $zoomrange->get_id()); } return $config; } sub fetch { my $self = shift; my $data = ''; foreach my $zoomrange (@{$self->{'zoomranges'}}) { my $max = 0; foreach my $z ($zoomrange->get_min() .. $zoomrange->get_max()) { $max = List::Util::max($max, $self->{'stats'}->{$self->{'map'}}->[$z]->{'maxage'} // 0); } $data .= sprintf("%s.value %d\n", $zoomrange->get_id(), int($max / 3600)); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Munin/Tiledir/Tilecount.pm000066400000000000000000000047111412262531100215000ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Munin/Tiledir/Tilecount.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Tiledir; #----------------------------------------------------------------------------- package Tirex::Munin::Tiledir::Tilecount; use base qw( Tirex::Munin::Tiledir ); =head1 NAME Tirex::Munin::Tiledir::Tilecount - Number of tiles =head1 DESCRIPTION Munin plugin for number of metatiles on disk for a map and specified zoom levels or zoom level ranges. =cut sub config { my $self = shift; my $map = $self->{'map'}; my $rel = $self->{'relative'} ? 'relative' : 'absolute/stacked'; my $label = $self->{'relative'} ? 'percentage of world covered' : 'number of metatiles'; my $config = <{'relative'}); foreach my $zoomrange (@{$self->{'zoomranges'}}) { my $id = $zoomrange->get_id(); $config .= sprintf("%s.info Zoomlevel %s\n", $id, $zoomrange->to_s()); $config .= sprintf("%s.label %s\n", $id, $zoomrange->get_name()); $config .= sprintf("%s.type GAUGE\n", $id); if (! $self->{'relative'}) { my $type = $zoomrange eq $self->{'zoomranges'}->[0] ? 'AREA' : 'STACK'; $config .= sprintf("%s.draw %s\n", $id, $type); } } return $config; } sub fetch { my $self = shift; my $data = ''; foreach my $zoomrange (@{$self->{'zoomranges'}}) { my $sum = 0; my $total = 0; foreach my $z ($zoomrange->get_min() .. $zoomrange->get_max()) { $sum += ($self->{'stats'}->{$self->{'map'}}->[$z]->{'count'} // 0); $total += ($z < 4) ? 1 : 4 ** ($z - 3); # XXX metatile size } if ($self->{'relative'}) { $sum *= 100 / $total if ($self->{'relative'}); $sum = sprintf("%.2f", $sum); } else { $sum = sprintf("%d", $sum); } $data .= sprintf("%s.value %s\n", $zoomrange->get_id(), $sum); } return $data; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/PrioQueue.pm000066400000000000000000000135461412262531100167740ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/PrioQueue.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use Data::Dumper; use Tirex::Job; #----------------------------------------------------------------------------- package Tirex::PrioQueue; =head1 NAME Tirex::PrioQueue - Queue for one priority =head1 SYNOPSIS use Tirex::PrioQueue; my $pq = Tirex::PrioQueue->new(prio => 7); $pq->add($job); $pq->remove($job); $job = $pq->next(); =head1 DESCRIPTION PrioQueues hold all jobs with a certain priority. They are never accessed directly, only through a L object. =head1 METHODS =head2 Tirex::PrioQueue->new(prio => $prio); Create new priority queue object. =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; return undef unless (defined($self->{'prio'}) && $self->{'prio'} =~ /^[0-9]+$/); return $self->reset(); } =head2 $pq->size() Returns the size of the priority queue. =cut sub size { my $self = shift; return $self->{'size'}; } =head2 $pq->empty() Is the priority queue empty? Returns true if the queue is empty, false otherwise. =cut sub empty { my $self = shift; return $self->{'size'} == 0; } =head2 $pq->reset() Reset the queue. All jobs on the queue will be lost! Returns priority queue itself, so that calls can be chained. =cut sub reset { my $self = shift; $self->{'queue'} = []; $self->{'offset'} = 0; $self->{'size'} = 0; $self->{'maxsize'} = 0; return $self; } =head2 $pq->add($job) Add job to priority queue. The job will only be added if the job priority and the queue priority are the same. This method will *not* check whether a job for the same metatile is already in the queue. Returns the job if it was added, undef otherwise. =cut sub add { my $self = shift; my $job = shift; return if (ref($job) ne 'Tirex::Job'); return if ($job->get_prio() != $self->{'prio'}); my $q = $self->{'queue'}; push(@$q, $job); $job->set_pos($self->{'offset'} + scalar(@$q) - 1); $self->{'size'}++; $self->{'maxsize'} = $self->{'size'} if ($self->{'size'} > $self->{'maxsize'}); return $job; } =head2 $pq->remove($job) Remove a job from the priority queue. Returns the job or undef if the job was not on this queue. =cut sub remove { my $self = shift; my $job = shift; return unless (defined $job->get_pos()); my $pos = $job->get_pos() - $self->{'offset'}; $self->{'queue'}->[$pos] = undef; $job->set_pos(undef); $self->{'size'}--; $self->clean(); return $job; } =head2 $pq->clean() The priority queue can have empty (undef) items in it where there was a real job that was removed when another job for the same metatile came in. This method will clean those empty items from the beginning and end of the queue. It is called from remove() and next() methods to ensure that there are no empty items at the beginning or end at any time. Returns priority queue itself, so that calls can be chained. =cut sub clean { my $self = shift; my $q = $self->{'queue'}; # remove undefs from end of queue while (scalar(@$q) > 0 && ! defined($q->[-1])) { pop(@$q); } # remove undefs from beginning of queue while (scalar(@$q) > 0 && ! defined($q->[0])) { shift(@$q); $self->{'offset'}++; } return $self; } =head2 $pq->peek() Get first element of the priority queue without removing it. Returns false if the queue is empty. =cut sub peek { my $self = shift; return $self->{'queue'}->[0]; } =head2 $pq->next() Remove and return first element of the priority queue. Returns false if there are no jobs in the queue. =cut sub next { my $self = shift; my $q = $self->{'queue'}; return if ($self->empty()); $self->{'size'}--; $self->{'offset'}++; my $job = shift(@$q); $job->set_pos(undef); $self->clean(); return $job; } =head2 $pq->age_first() Returns age (in seconds) of first job in the priority queue. Age is the difference between current and request time. Returns false if the priority queue is empty. =cut sub age_first { my $self = shift; return if ($self->empty()); return $self->peek()->age(); } =head2 $pq->age_last() Returns age (in seconds) of last job in the priority queue. Age is the difference between current and request time. Returns false if the priority queue is empty. =cut sub age_last { my $self = shift; return if ($self->empty()); return $self->{'queue'}->[-1]->age(); } =head2 $pq->reset_maxsize() Reset maxsize. New maxsize will be equal to current size. Returns new maxsize; =cut sub reset_maxsize { my $self = shift; $self->{'maxsize'} = $self->{'size'}; return $self->{'maxsize'}; } =head2 $pq->remove_jobs_for_unknown_maps() Remove all jobs from this prioqueue where the map is undefined. This can happen after a reload of the config file, when a map was deleted from it. =cut sub remove_jobs_for_unknown_maps { my $self = shift; my @jobs = grep { ! defined Tirex::Map->get($_->get_map()) } @{$self->{'queue'}}; foreach my $job (@jobs) { $self->remove($job); } } =head2 $pq->status() Return status of the priority queue. =cut sub status { my $self = shift; # 0 + in the following to force integer values for JSON my %status = ( size => 0 + $self->size(), maxsize => 0 + $self->{'maxsize'}, prio => 0 + $self->{'prio'}, ); unless ($self->empty()) { $status{'age_last'} = 0 + $self->age_last(); $status{'age_first'} = 0 + $self->age_first(); } return \%status; } =head1 SEE ALSO L, L =cut 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Queue.pm000066400000000000000000000133461412262531100161400ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Queue.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use Data::Dumper; use Tirex::Job; use Tirex::PrioQueue; #----------------------------------------------------------------------------- package Tirex::Queue; =head1 NAME Tirex::Queue - Job queue for Tirex system =head1 SYNOPSIS use Tirex::Queue; my $q = Tirex::Queue->new(); $q->add( Tirex::Job->new(...) ); my $job = $q->next(); =head1 DESCRIPTION Tirex::Queue implements a prioritized queue of L items. =head1 METHODS =head2 Tirex::Queue->new() Create new Tirex queue object. Normally, this can not fail. =cut sub new { my $class = shift; my %args = (); my $self = bless \%args => $class; return $self->reset(); } =head2 $queue->reset() Reset the queue. All jobs on the queue will be lost! Returns queue itself, so that calls can be chained. =cut sub reset { my $self = shift; $self->{'queues'} = []; $self->{'jobs'} = {}; $self->{'size'} = 0; $self->{'maxsize'} = 0; return $self; } =head2 $queue->size() Returns the size of the queue. There can be jobs on the queue that are already expired, they will still be counted in this size value. =cut sub size { my $self = shift; return $self->{'size'}; } =head2 $queue->empty() Is the queue empty? Returns true if the queue is empty, false otherwise. =cut sub empty { my $self = shift; return $self->size() == 0; } =head2 $queue->status() Return status of the queue. =cut sub status { my $self = shift; my @pq = map { $_->status(); } grep { defined($_); } @{$self->{'queues'}}; my %status = ( size => 0 + $self->{'size'}, # force integer for JSON maxsize => 0 + $self->{'maxsize'}, # force integer for JSON prioqueues => \@pq, ); return \%status; } =head2 $queue->add($job1, $job2, ...) Adds one or more jobs to the queue. You can also call it with an array reference and all jobs inside the array will be added to the queue. Returns queue itself, so that calls can be chained. =cut sub add { my $self = shift; while (defined(my $job = shift)) { if (ref($job) eq 'ARRAY') { foreach my $j (@$job) { Carp::croak('Can only add objects of type Tirex::Job to queue!') unless (ref($j) eq 'Tirex::Job'); $self->_add($j); } } elsif (ref($job) eq 'Tirex::Job') { $self->_add($job); } else { Carp::croak('Can only add objects of type Tirex::Job to queue!'); } } return $self; } sub _add { my $self = shift; my $newjob = shift; my $oldjob = $self->remove($newjob); $newjob = $oldjob->merge($newjob) if ($oldjob); my $prio = $newjob->get_prio(); $self->{'queues'}->[$prio] = Tirex::PrioQueue->new(prio => $prio) unless (defined($self->{'queues'}->[$prio])); $self->{'queues'}->[$prio]->add($newjob); $self->{'jobs'}->{$newjob->hash_key()} = $newjob; $self->{'size'}++; $self->{'maxsize'} = $self->{'size'} if ($self->{'size'} > $self->{'maxsize'}); } sub remove { my $self = shift; my $job = shift; my $oldjob = $self->{'jobs'}->{$job->hash_key()}; if (defined $oldjob) { $self->{'queues'}->[$oldjob->get_prio()]->remove($oldjob); delete $self->{'jobs'}->{$oldjob->hash_key()}; $self->{'size'}--; } return $oldjob; } =head2 $queue->in_queue($job) Is this metatile already in the queue? Returns the job that is in the queue or undef, if its not in there. =cut sub in_queue { my $self = shift; my $job = shift; return $self->{'jobs'}->{$job->hash_key()}; } =head2 $queue->next() Removes the topmost job from the queue and return it. Returns undef if the queue is empty. =cut sub next { my $self = shift; my $q; foreach my $q (@{$self->{'queues'}}) { if (defined($q) && !$q->empty()) { my $job = $q->next(); delete($self->{'jobs'}->{$job->hash_key()}); $self->{'size'}--; return $job; } } return undef; } =head2 $queue->peek() Peek at the topmost job from the queue and return it. Does not remove the job from queue. Returns undef if the queue is empty. =cut sub peek { my $self = shift; my $q; foreach my $q (@{$self->{'queues'}}) { return $q->peek() if (defined($q) && ! $q->empty()); } return undef; } =head2 $pq->reset_maxsize() Reset maxsize. New maxsize will be equal to current size. Returns new maxsize; =cut sub reset_maxsize { my $self = shift; $self->{'maxsize'} = $self->{'size'}; foreach my $q (@{$self->{'queues'}}) { $q->reset_maxsize() if (defined $q); } return $self->{'maxsize'}; } # calculate queue size (debugging only) sub _calc_size { my $self = shift; my $sum = 0; foreach my $q (@{$self->{'queues'}}) { if (defined($q)) { foreach my $j (@$q) { $sum++ if (defined($j)); } } } return $sum; } =head2 $pq->remove_jobs_for_unknown_maps() Remove all jobs from this prioqueue where the map is undefined. This can happen after a reload of the config file, when a map was deleted from it. =cut sub remove_jobs_for_unknown_maps { my $self = shift; foreach my $prioqueue (@{$self->{'queues'}}) { $prioqueue->remove_jobs_for_unknown_maps() if (defined $prioqueue); } } =head1 SEE ALSO L, L =cut 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Renderer.pm000066400000000000000000000166711412262531100166260ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Renderer.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; #----------------------------------------------------------------------------- package Tirex::Renderer; # a hash with all configured renderers our %Renderers; =head1 NAME Tirex::Renderer - A Tirex renderer config =head1 SYNOPSIS my $r = Tirex::Renderer->new( ... ); =head1 DESCRIPTION Tirex can work with several rendering backends such as Mapnik or WMS. A backend can be started with different configurations, for the Mapnik backend you need to configure the font directory for instance. This class defines methods for reading the config files (/etc/tirex/renderer/*.conf) describing the backend configurations and managing those renderers. See the class L and its subclasses for the actual code of some backends. =head1 METHODS =head2 Tirex::Renderer->read_config_dir($dir) Read all renderer configs in given config directory. =cut sub read_config_dir { my $class = shift; my $dir = shift; foreach my $file (glob("$dir/renderer/*.conf")) { $class->new_from_configfile($file); } } =head2 Tirex::Renderer->get('foo') Get renderer by name. =cut sub get { my $class = shift; my $name = shift; return $Renderers{$name}; } =head2 Tirex::Renderer->all(); Return sorted (by name) list of all configured renderers. =cut sub all { return sort { $a->get_name() cmp $b->get_name() } values %Renderers; } =head2 Tirex::Renderer->clear(); Clear list of renderers. =cut sub clear { %Renderers = (); } =head2 Tirex::Renderer->enabled(); Return sorted (by name) list of all enabled renderers. =cut sub enabled { return grep { $_->is_enabled(); } all(); } =head2 Tirex::Renderer->new( name => 'foo', path => '/path/to/exec', port => 1234, procs => 3, ... ) Create new renderer config. Every renderer has at least these general options: name, path, port, and procs. Will croak if they are not all present. In addition it can have zero or more options specific to this renderer. Will croak if a renderer configuration already exists under the same name. =cut sub new { my $class = shift; my %args = @_; my $self = bless {} => $class; Carp::croak("missing name") unless (defined $args{'name'} ); Carp::croak("missing path") unless (defined $args{'path'} ); Carp::croak("missing port") unless (defined $args{'port'} ); Carp::croak("missing procs") unless (defined $args{'procs'} ); Carp::croak("renderer with name $args{'name'} already exists") if ($Renderers{$args{'name'}}); foreach my $cfg ( qw( name path port procs syslog_facility debug filename ) ) { $self->{$cfg} = $args{$cfg}; delete $args{$cfg}; } # set default values $self->{'syslog_facility'} = $Tirex::BACKEND_MANAGER_SYSLOG_FACILITY unless ($self->{'syslog_facility'}); $self->{'debug'} = 0 unless ($self->{'debug'}); $self->{'maps'} = []; $self->{'enabled'} = 1; $self->{'config'} = \%args; $Renderers{$self->{'name'}} = $self; return $self; } =head2 Tirex::Renderer->new_from_configfile($filename) Create new renderer config from a file. Croaks if the file does not exist. =cut sub new_from_configfile { my $class = shift; my $filename = shift; my %config = ( filename => $filename ); open(my $cfgfh, '<', $filename) or Carp::croak("Can't open renderer config file '$filename': $!"); while (<$cfgfh>) { s/#.*$//; next if (/^\s*$/); if (/^([a-z0-9_]+)\s*=\s*(\S*)\s*$/) { $config{$1} = $2; } } close($cfgfh); my $renderer = $class->new(%config); $renderer->read_map_config(); return $renderer; } =head2 $rend->read_map_config() Read all map configs for this renderer. =cut sub read_map_config { my $self = shift; (my $dirname = $self->{'filename'}) =~ s/\.conf$//; return unless (-d $dirname); my @maps; foreach my $file (glob("$dirname/*.conf")) { push(@maps, Tirex::Map->new_from_configfile($file, $self)); } $self->{'maps'} = \@maps; } =head2 $rend->get_maps() Get array of map configs for this renderer. =cut sub get_maps { return @{shift->{'maps'}}; } =head2 $rend->get_config() Return hash with renderer-specific configuration. =cut sub get_config { my $self = shift; return $self->{'config'}; } =head2 $rend->get_name(); Get name of this renderer. =cut sub get_name { return shift->{'name' }; } =head2 $rend->get_debug(); Get debug flag of this renderer. =cut sub get_debug { return shift->{'debug' }; } =head2 $rend->get_path(); Get path of this renderer. =cut sub get_path { return shift->{'path' }; } =head2 $rend->get_port(); Get port of this renderer. =cut sub get_port { return shift->{'port' }; } =head2 $rend->get_procs(); Get procs of this renderer. =cut sub get_procs { return shift->{'procs'}; } =head2 $rend->get_syslog_facility(); Get syslog facility of this renderer. =cut sub get_syslog_facility { return shift->{'syslog_facility'}; } =head2 $rend->is_enabled() Is this renderer enabled? =cut sub is_enabled { return shift->{'enabled'}; } =head2 $rend->disable(); Disable this renderer. =cut sub disable { shift->{'enabled'} = 0; } =head2 $rend->enable(); Enable this renderer. =cut sub enable { shift->{'enabled'} = 1; } =head2 $rend->to_s(); Return human readable description of this renderer. =cut sub to_s { my $self = shift; my $s = sprintf("Renderer %s:", $self->get_name()); foreach my $key ( qw( port procs path syslog_facility debug ) ) { $s .= " $key=$self->{$key}"; } foreach my $key ( sort keys %{$self->{'config'}} ) { $s .= " $key=$self->{'config'}->{$key}"; } return $s; } =head2 $rend->to_hash(); Return parameters of this renderer as hash. =cut sub to_hash { my $self = shift; my %hash = %{$self->{'config'}}; $hash{'name'} = $self->get_name(); $hash{'path'} = $self->get_path(); $hash{'syslog_facility'} = $self->get_syslog_facility(); $hash{'debug'} = 0 + $self->get_debug(); # force integer (so that it works in JSON) $hash{'port'} = 0 + $self->get_port(); $hash{'procs'} = 0 + $self->get_procs(); $hash{'maps'} = [map { $_->get_name(); } $self->get_maps()]; return \%hash; } =head2 Tirex::Renderer->status(); Return status of all configured renderers. =cut sub status { my $self = shift; my @status = (); foreach my $renderer (sort { $a->get_name() cmp $b->get_name() } values %Renderers) { push(@status, $renderer->to_hash()); } return \@status; } =head2 $rend->add_worker($pid); Add process id to list of currently running workers. =cut sub add_worker { my $self = shift; my $pid = shift; $self->{'workers'}->{$pid} = 1; } =head2 $rend->remove_worker($pid); Remove process id from list of currently running workers. =cut sub remove_worker { my $self = shift; my $pid = shift; delete $self->{'workers'}->{$pid}; } =head2 $rend->num_workers(); Return number of currently running workers. =cut sub num_workers { my $self = shift; return scalar(keys %{$self->{'workers'}}); } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Source.pm000066400000000000000000000035071412262531100163120ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Source.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use IO::Socket; use Tirex::Source::Command; use Tirex::Source::ModTile; #----------------------------------------------------------------------------- package Tirex::Source; =head1 NAME Tirex::Source - A source of a job request =head1 SYNOPSIS my $source = Tirex::Source::...->new(); $source->readable($socket); $source->writable($socket); $source->notify(); =head1 DESCRIPTION This is a virtual parent class. Only instantiate subclasses: L, L, L =head1 METHODS Each subclass must define the following methods: =head2 Tirex::Source::...->new() Create new object of this source class. =head2 $source->readable($socket) =head2 $source->writable($socket) These methods are called if the socket that was associated to this source object becomes readable or writable. The source will return true from any of these if reading/writing has been completed, and false if waits for another chance to read/write. =head2 $source->notify() This method is called once a tile has been rendered to notify the source. =cut #use constant { # STATUS_CONTINUE => 0, # STATUS_SOCKET_CLOSED => 1, # STATUS_MESSAGE_COMPLETE => 2 #} sub STATUS_CONTINUE { return 0; } sub STATUS_SOCKET_CLOSED { return 1; } sub STATUS_MESSAGE_COMPLETE { return 2; } sub set_timeout { my $self = shift; my $to = shift; $self->{'timeout'} = $to; } sub get_timeout { my $self = shift; return $self->{'timeout'}; } # XXX overwrite this in subclass sub name { return '?'; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Source/000077500000000000000000000000001412262531100157475ustar00rootroot00000000000000tirex-0.7.0/lib/Tirex/Source/Command.pm000066400000000000000000000074331412262531100176720ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Source/Command.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use IO::Socket; use IO::Socket::UNIX; #----------------------------------------------------------------------------- package Tirex::Source::Command; use base qw( Tirex::Source ); =head1 NAME Tirex::Source -- A source of a job request =head1 SYNOPSIS my $source = Tirex::Source::Command->new(); $source->notify(); =head1 DESCRIPTION Source using Tirex messages sent through UDP. =head1 METHODS =head2 Tirex::Source::Command->new( socket => $socket ) Create source object for UDP connection. =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; return $self; } sub readable { my $self = shift; my $sock = shift; my $buf; my $peer = $sock->recv($buf, $Tirex::MAX_PACKET_SIZE); $self->{'peer'} = $peer; my $args = Tirex::parse_msg($buf); foreach (keys %$args) { $self->{$_} = $args->{$_}; }; return &Tirex::Source::STATUS_MESSAGE_COMPLETE; } sub get_msg_type { my $self = shift; return $self->{'type'}; } sub make_job { my $self = shift; my $metatile = eval { my $mt = Tirex::Metatile->new( map => $self->{'map'}, x => $self->{'x'}, y => $self->{'y'}, z => $self->{'z'} ); Tirex::Map->get_map_for_metatile($mt); return $mt; }; # if we couldn't create the metatile... if ($@) { ::syslog('warning', $@) if ($Tirex::DEBUG); # and the client wanted an answer... if (defined $self->{'id'}) { # send error message $self->reply({ type => $self->{'type'}, map => $self->{'map'}, x => $self->{'x'}, y => $self->{'y'}, z => $self->{'z'}, prio => $self->{'prio'}, result => 'error_illegal_metatile' }); } return; } my $job = eval { Tirex::Job->new( metatile => $metatile, prio => $self->{'prio'} ); }; # if we couldn't create the job... if ($@) { # and the client wanted an answer... if (defined $self->{'id'}) { # send error message $self->reply({ type => $self->{'type'}, map => $self->{'map'}, x => $self->{'x'}, y => $self->{'y'}, z => $self->{'z'}, prio => $self->{'prio'}, result => 'error_illegal_prio' }); } return undef; } $job->add_notify($self) if (defined $self->{id}); return $job; } =head2 $source->notify($job) Send notify that a tile was rendered back to source. =cut sub notify { my $self = shift; my $job = shift; my $msg = $job->to_msg( type => 'metatile_enqueue_request', id => $self->{'id'}, result => $job->{'success'} ? 'ok' : 'error' ); return $self->reply($msg); } =head2 $source->reply($msg) Send a reply message to this source. The id is automatically filled in. The parameter is a hash with the message. Returns the result of the sockets send method. =cut sub reply { my $self = shift; my $msg = shift; $msg->{'id'} = $self->{'id'} if (defined $self->{'id'}); my $peer = $self->{'peer'}; return $self->{'socket'}->send( Tirex::create_msg($msg), 0, $peer ); } sub name { return 'C'; } #----------------------------------------------------------------------------- 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Source/ModTile.pm000066400000000000000000000167741412262531100176610ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Source/ModTile.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use IO::Socket::UNIX; use Socket; #----------------------------------------------------------------------------- package Tirex::Source::ModTile; use base qw( Tirex::Source ); =head1 NAME Tirex::Source::ModTile -- mod_tile a a source of job requests =head1 SYNOPSIS my $source = &Tirex::Source::ModTile->new(); $source->notify(); =head1 DESCRIPTION Source using Tirex messages sent through mod_tile (Unix domain socket) =head1 METHODS =head2 Tirex::Source::ModTile->new() Create source object for Unix domain socket connection. =cut sub new { my $class = shift; my $socket = shift; my $self = bless { 'read_buffer' => "", 'socket' => $socket } => $class; return $self; } =head2 $source->readable($sock) Indicates to this source that the given socket is readable, and has data for this source. The source will return STATUS_CONTINUE if it expects to continue reading, or STATUS_MESSAGE_COMPLETE if reading is complete. STATUS_SOCKET_CLOSED indicates that the peer has closed the connection. =cut sub readable { my $self = shift; my $sock = shift; # we want 64 bytes. nothing else will do. recv is not guaranteed to # return that. my $tmp; return &Tirex::Source::STATUS_SOCKET_CLOSED unless defined($sock->recv($tmp, 64 - length($self->{read_buffer}))); return &Tirex::Source::STATUS_SOCKET_CLOSED unless length($tmp); $self->{read_buffer} .= $tmp; return &Tirex::Source::STATUS_CONTINUE if (length($self->{read_buffer}) < 64); # request fully read. ($self->{ver}, $self->{cmd}, $self->{x}, $self->{y}, $self->{z}, $self->{map}) = unpack("lllllZ*", $self->{read_buffer}); $self->{read_buffer} = ""; ::syslog('debug', 'read request from mod_tile: ver=%d cmd=%d x=%d y=%d z=%d map=%s', $self->{ver}, $self->{cmd}, $self->{x}, $self->{y}, $self->{z}, $self->{map}) if ($Tirex::DEBUG); return &Tirex::Source::STATUS_MESSAGE_COMPLETE; } =head2 $source->make_job($sock) Returns a new Tirex::Job object created from the data read by this source. Also decides whether or not this source would like to be notified of job completion, and if yes, flags the job accordingly. These are the priority levels used in the Apache mod_tile and how they are translated in Tirex. (Note that in mod_tile the numbers do not imply higher or lower priority.) =head3 cmdRender (1 in mod_tile, 2 in Tirex) This priority is used by mod_tile if a "very old" tile is requested. The configuration option ModTileVeryOldThreshold controls what counts as "very old". Unless the system load is higher than ModTileMaxLoadOld, a render request is triggered and mod_tile waits for up to ModTileRequestTimeout seconds. If a tile is not produced within that time, the old tile is returned. =head3 cmdDirty (2 in mod_tile, 10 in Tirex) This priority is used by mod_tile if =over =item * the /dirty URL is manually called for a tile =item * an "old" or "very old" tile is requested but the load is too high to render it right away (the old tile will be delivered instead) =item * a "missing" tile is requested but the load is too high to render it right away (a 404 error will be issued) =back =head3 cmdRenderPrio (5 in mod_tile, 1 in Tirex) This priority is used by mod_tile if a "missing" tile is requested. Unless the system load is higher than ModTileMaxLoadMissing, a render request is triggered and mod_tile waits for up to ModTileMissingRequestTimeout seconds. If a tile is not produced within that time, a 404 error will be issued. =head3 cmdRenderBulk (6 in mod_tile, 20 in Tirex) Used for all "missing tile" render requests triggered by mod_tile if "ModTileBulkMode On" is set in the Apache configuration. In this mode, requests for old tiles are never issued. Never used if "ModTileBulkMode Off" (the default). =head3 cmdRenderLow (7 in mod_tile, 25 in Tirex) This priority is used by mod_tile if an "old" tile is requested. An "old" tile is one that is older than three days or, if a planet_import_complete file is present, older than this file. Unless the system load is higher than ModTileMaxLoadOld, a render request is triggered and mod_tile waits for up to ModTileRequestTimeout seconds. If a tile is not produced within that time, the old tile is returned. =cut sub make_job { my $self = shift; my $sock = shift; my $metatile = eval { my $mt = Tirex::Metatile->new( map => $self->{'map'}, x => $self->{'x'}, y => $self->{'y'}, z => $self->{'z'} ); Tirex::Map->get_map_for_metatile($mt); return $mt; }; # return error if we cannot create the metatile if ($@) { ::syslog('warning', $@); return; } my $job = eval { Tirex::Job->new( metatile => $metatile, # enum protoCmd { cmdIgnore, cmdRender, cmdDirty, cmdDone, cmdNotDone, cmdRenderPrio, cmdRenderBulk, cmdRenderLow }; 'prio' => [99, 2, 10, 99, 99, 1, 20, 25]->[$self->{'cmd'}] ); }; # return error if we can't create the job if ($@) { ::syslog('warning', $@); return; } $job->add_notify($self); return $job; } =head2 $source->set_request_write_callback(\&wcb} Specifies a function to be called if this source ever wants to receive writable events. =cut sub set_request_write_callback { my $self = shift; my $callback = shift; $self->{request_write_callback} = $callback; } =head2 $source->notify($job) Prepares a notification message about successful tile rendering and informs the main select loop to give us a chance to write. =cut sub notify { my $self = shift; my $job = shift; # enum protoCmd { cmdIgnore, cmdRender, cmdDirty, cmdDone, cmdNotDone, cmdRenderPrio, cmdRenderBulk, cmdRenderLow }; $self->{write_buffer} = pack("lllllZ*", 2, # protocol version $job->get_success() ? 3 : 4, # cmdDone or cmdNotDone $self->{'x'}, $self->{'y'}, $self->{'z'}, # x, y, z $self->{"map"} # map ); $self->{write_buffer} .= chr(0) x 64; $self->{write_buffer} = substr($self->{write_buffer}, 0, 64); &{$self->{request_write_callback}}(); delete $self->{request_write_callback}; return 1; } =head2 $source->writable($sock) Indicates to this source that the given socket is writable. The source will attempt to send the prepared notification message, and return STATUS_MESSAGE_COMPLETE if the message has been fully sent. It will return STATUS_CONTINUE if sending needs to continue later. A return value of STATUS_SOCKET_CLOSED indicates that the peer has closed the connection. =cut sub writable { my $self = shift; my $sock = shift; my $bytes_sent = eval { $sock->send($self->{write_buffer}, Socket::MSG_NOSIGNAL) }; if (!defined($bytes_sent)) { # other side is gone. no use trying to continue. return &Tirex::Source::STATUS_SOCKET_CLOSED; } if ($bytes_sent < length($self->{write_buffer})) { $self->{write_buffer} = substr($self->{write_buffer}, $bytes_sent); return &Tirex::Source::STATUS_CONTINUE; } delete $self->{write_buffer}; return &Tirex::Source::STATUS_MESSAGE_COMPLETE; } sub name { return 'M'; } #----------------------------------------------------------------------------- 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Source/Test.pm000066400000000000000000000021141412262531100172220ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Source/Test.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use IO::Socket; #----------------------------------------------------------------------------- package Tirex::Source::Test; use base qw( Tirex::Source ); =head1 NAME Tirex::Source::Test -- Dummy source for testing =head1 SYNOPSIS my $source = Tirex::Source::Test->new('x'); $source->notify(); =head1 DESCRIPTION Dummy source for testing notifies. =head1 METHODS =head2 Tirex::Source::Test->new($x) Create new object. The parameter will be returned on notify calls. =cut sub new { my $class = shift; my %args = (); my $self = bless \%args => $class; $self->{'para'} = shift; return $self; } =head2 $source->notify() Returns the parameter that was given when the class was created. =cut sub notify { my $self = shift; return $self->{'para'}; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Status.pm000066400000000000000000000054651412262531100163420ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Status.pm # #----------------------------------------------------------------------------- use strict; use warnings; use Carp; use IPC::SysV qw(IPC_RMID); use IPC::ShareLite; use JSON; #----------------------------------------------------------------------------- package Tirex::Status; our $SHMKEY = 0x00002468; # use pretty printing of JSON in shared memory # it might make sense to set this to 1 for debugging our $pretty = 1; # permissions for shared memory # you need write permissions even for read-only access! # (probably because of locking) our $mode = 0666; =head1 NAME Tirex::Status - Status of running master daemon in shared memory =head1 SYNOPSIS my $status = Tirex::Status->new(); =head1 DESCRIPTION This package manages the status of the master daemon in shared memory. =head1 METHODS =head2 Tirex::Status->new( master => 1 ); If 'master' is true the shared memory is created (and destroyed afterwards). Only the master server should do this. =cut sub new { my $class = shift; my %args = @_; my $self = bless \%args => $class; # if we are the master, remove pre-existing shared memory segments and # semaphore if ($self->{'master'}) { my $id = shmget($SHMKEY, 0, 0); shmctl($id, IPC::SysV::IPC_RMID, 0) if (defined($id)); $id = semget($SHMKEY, 0, 0); semctl($id, IPC::SysV::IPC_RMID, 0, 0) if (defined($id)); } $self->{'share'} = IPC::ShareLite->new( -key => $SHMKEY, -mode => $mode, -create => $self->{'master'} ? 1 : 0, -destroy => $self->{'master'} ? 1 : 0, -exclusive => $self->{'master'} ? 1 : 0, ) or Carp::croak("cannot connect to shared memory: $!"); return $self; } =head2 $status->destroy() Destroy shared memory segment. =cut sub destroy { my $self = shift; delete $self->{'share'}; } =head2 $status->update(key1 => val1, key2 => val2, ...) Update shared memory with current status. Call with key-value pairs that should be added to status. =cut sub update { my $self = shift; my %status = ( 'pid' => 0 + $$, # force integer for JSON 'updated' => time(), @_ ); $self->write(JSON::to_json(\%status, { pretty => $pretty }) . "\n"); } =head2 $status->write($string) Write a string into shared memory. =cut sub write { my $self = shift; my $str = shift; $self->{'share'}->store($str); } =head2 $status->read() Read a string from shared memory. Returns the string read, or undef if the shared memory was not accessible. =cut sub read { my $self = shift; my $str = eval { $self->{'share'}->fetch(); }; return $str; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/lib/Tirex/Zoomrange.pm000066400000000000000000000036721412262531100170160ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # Tirex/Zoomrange.pm # #----------------------------------------------------------------------------- use strict; use warnings; #----------------------------------------------------------------------------- package Tirex::Zoomrange; =head1 NAME Tirex::Zoomrange - Range of zoom levels =head1 SYNOPSIS my $zr = Tirex::Job->new($name, $min[, $max]) =head1 DESCRIPTION Optionally named zoom level range. =head1 METHODS =head2 Tirex::Zoomrange->new('foo', 4, 5) Create new range. First argument is the name. Second and third argument the min and max zoom levels. If no max level is given, it is the same as min. If the name is undef, it is set to 'zMIN-MAX' or 'zMIN' if MIN==MAX. =cut sub new { my $class = shift; my ($name, $min, $max) = @_; my $self = bless {} => $class; $self->{'min'} = $min; $self->{'max'} = defined($max) ? $max : $min; $self->{'name'} = defined($name) ? $name : ('z' . $self->to_s()); return $self; } =head2 $zr->get_name() Get name. =cut sub get_name { my $self = shift; return $self->{'name'}; } =head2 $zr->get_min() Get minimum zoom level in this range. =cut sub get_min { my $self = shift; return $self->{'min'}; } =head2 $zr->get_max() Get maximum zoom level in this range. =cut sub get_max { my $self = shift; return $self->{'max'}; } =head2 $zr->to_s() Get range as string. Format "MIN-MAX" or "MIN" if MIN==MAX. =cut sub to_s { my $self = shift; return $self->{'min'} == $self->{'max'} ? $self->{'min'} : $self->{'min'} . '-' . $self->{'max'}; } =head2 $zr->get_id() Get id. The id is a simplified version of the name that only contains the characters a-z, 0-9, and _. =cut sub get_id { my $self = shift; (my $id = $self->{'name'}) =~ s/[^a-z0-9]+/_/g; return $id; } 1; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/000077500000000000000000000000001412262531100137745ustar00rootroot00000000000000tirex-0.7.0/munin/tirex-status-active-requests000077500000000000000000000014301412262531100215160ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-status-active-requests # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status::ActiveRequests; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- my @z = ('0-9', 10 .. 17); Tirex::Munin::Status::ActiveRequests->new( z => \@z )->do(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-status-queued-requests000077500000000000000000000014271412262531100215410ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-status-queued-request # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status::QueuedRequests; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- my @z = ('0-9', 10 .. 17); Tirex::Munin::Status::QueuedRequests->new( z => \@z )->do(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-status-queued-requests-age000077500000000000000000000014421412262531100222700ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-status-queued-requests-age # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status::QueuedRequestsAge; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- my @z = ('0-9', 10 .. 17); Tirex::Munin::Status::QueuedRequestsAge->new( z => \@z )->do(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-status-render-time000077500000000000000000000016461412262531100206160ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-status-render-time # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status::RenderTime; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- (my $execname = $0) =~ s{^.*/}{}; my $map = '*'; if ($execname =~ /^tirex-status-render-time-(.*)$/) { $map = $1; } my @z = ('0-9', '10-12', '13-15', '16-17'); Tirex::Munin::Status::RenderTime->new( map => $map, z => \@z )->do(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-status-requests-rendered000077500000000000000000000017171412262531100220430ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-status-requests-rendered # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Status::RequestsRendered; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- (my $execname = $0) =~ s{^.*/}{}; my $map = '*'; if ($execname =~ /^tirex-status-requests-rendered-(.*)$/) { $map = $1; } my @z = ('0-9', '10-12', '13-15', '16-17'); Tirex::Munin::Status::RequestsRendered->new( map => $map, z => \@z, per => 'minute' )->do(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-tiledir-diskusage000077500000000000000000000020151412262531100204620ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-tiledir-diskusage # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Tiledir::Diskusage; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- (my $execname = $0) =~ s{^.*/}{}; my $map = 'default'; if ($execname =~ /^tirex-tiledir-diskusage-(.*)$/) { $map = $1; } my @z = ('0-9', 10 .. 17); my $statsfile = Tirex::Config::get('stats_dir', $Tirex::STATS_DIR) . "/tiles-$map.stats"; Tirex::Munin::Tiledir::Diskusage->new( map => $map, z => \@z )->do( statsfile => $statsfile ); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-tiledir-tileage000077500000000000000000000020051412262531100201140ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-tiledir-tileage # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Tiledir::Tileage; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- (my $execname = $0) =~ s{^.*/}{}; my $map = 'default'; if ($execname =~ /^tirex-tiledir-tileage-(.*)$/) { $map = $1; } my @z = ('0-9', 10 .. 17); my $statsfile = Tirex::Config::get('stats_dir', $Tirex::STATS_DIR) . "/tiles-$map.stats"; Tirex::Munin::Tiledir::Tileage->new( map => $map, z => \@z )->do( statsfile => $statsfile ); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-tiledir-tilecount-absolute000077500000000000000000000020561412262531100223320ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-tiledir-tilecount-absolute # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Tiledir::Tilecount; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- (my $execname = $0) =~ s{^.*/}{}; my $map = 'default'; if ($execname =~ /^tirex-tiledir-tilecount-absolute-(.*)$/) { $map = $1; } my @z = ('0-9', 10 .. 17); my $statsfile = Tirex::Config::get('stats_dir', $Tirex::STATS_DIR) . "/tiles-$map.stats"; Tirex::Munin::Tiledir::Tilecount->new( map => $map, z => \@z, relative => 0 )->do( statsfile => $statsfile ); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/munin/tirex-tiledir-tilecount-relative000077500000000000000000000020561412262531100223270ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # munin/tirex-tiledir-tilecount-relative # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex::Munin::Tiledir::Tilecount; #----------------------------------------------------------------------------- my $config_file = exists($ENV{'TIREX_CONFIGFILE'}) ? $ENV{'TIREX_CONFIGFILE'} : $Tirex::TIREX_CONFIGFILE; Tirex::Config::init($config_file); #----------------------------------------------------------------------------- (my $execname = $0) =~ s{^.*/}{}; my $map = 'default'; if ($execname =~ /^tirex-tiledir-tilecount-relative-(.*)$/) { $map = $1; } my @z = ('0-9', 10 .. 17); my $statsfile = Tirex::Config::get('stats_dir', $Tirex::STATS_DIR) . "/tiles-$map.stats"; Tirex::Munin::Tiledir::Tilecount->new( map => $map, z => \@z, relative => 1 )->do( statsfile => $statsfile ); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/nagios/000077500000000000000000000000001412262531100141265ustar00rootroot00000000000000tirex-0.7.0/nagios/README000066400000000000000000000001511412262531100150030ustar00rootroot00000000000000This directory contains scripts that can be used if you want to monitor a Tirex tile server with Nagios. tirex-0.7.0/nagios/cfg/000077500000000000000000000000001412262531100146655ustar00rootroot00000000000000tirex-0.7.0/nagios/cfg/tirex-health.cfg000066400000000000000000000000731412262531100177440ustar00rootroot00000000000000command[tirex-health]=/usr/lib/nagios/plugins/tirex-health tirex-0.7.0/nagios/cfg/tirex-queue-size.cfg000066400000000000000000000001251412262531100205710ustar00rootroot00000000000000command[tirex-queue-size]=/usr/lib/nagios/plugins/tirex-queue-size -w 10000 -c 50000 tirex-0.7.0/nagios/tirex-health000077500000000000000000000016371412262531100164610ustar00rootroot00000000000000#!/usr/bin/perl # Script that checks whether the Tirex master has recently updated # the status message in shared memory. # # This is *almost* like checking whether the master process is running, # but in case the master process should lock up, this test would catch # that whereas a simple process list test would not. # # This script has no parameters and will either issue an "OK" or a "CRITICAL" # status message. use strict; use JSON; use Tirex::Status; my $status_obj = Tirex::Status->new(); my $status = $status_obj->read(); my $d = JSON::from_json($status); if (!defined($d->{'updated'})) { printf "CRITICAL: Cannot retrieve Tirex status\n"; exit 2; } my $age = time() - $d->{'updated'}; my $q = "| age=${age}s;;30;0;"; if ($age > 30) { printf "CRITICAL: Latest status update is too old (%d seconds ago)$q\n", $age; exit 2; } printf "OK: Latest status update %d seconds ago$q\n", $age; exit 0; tirex-0.7.0/nagios/tirex-queue-size000077500000000000000000000020701412262531100173000ustar00rootroot00000000000000#!/usr/bin/perl # Script that checks the total Tirex queue size. # # Parameters: # -w issue WARNING if queue larger than # -c issue CRITICAL if queue is larger than # # Possible future extensios: # Configure for individual queues, or check age of requests in queue. use strict; use JSON; use Tirex::Status; use Getopt::Std; our $opt_w; our $opt_c; getopt('wc'); if (!defined($opt_w) || $opt_w != (0+$opt_w) || !defined($opt_c) || $opt_c != (0+$opt_c)) { printf STDERR "Usage: tirex-queue-size -w limit -c limit\n"; exit 2; } my $status_obj = Tirex::Status->new(); my $status = $status_obj->read(); my $d = JSON::from_json($status); if (!defined($d->{'updated'})) { printf "CRITICAL: Cannot retrieve Tirex status\n"; exit 2; } my $size = $d->{'queue'}->{'size'}; my $q = "| size=${size};$opt_w;$opt_c;0;"; if ($size > $opt_c) { printf "CRITICAL: Queue size = %d$q\n", $size; exit 2; } elsif ($size > $opt_w) { printf "WARNING: Queue size = %d$q\n", $size; exit 1; } printf "OK: Queue size = %d$q\n", $size; exit 0; tirex-0.7.0/t/000077500000000000000000000000001412262531100131115ustar00rootroot00000000000000tirex-0.7.0/t/bucket.t000066400000000000000000000051221412262531100145530ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/bucket.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Manager::Bucket; #----------------------------------------------------------------------------- eval { Tirex::Manager::Bucket->new( Xname => 'name', minprio => 1, maxproc => 1, maxload => 1); }; if ($@ =~ /need 'name' parameter/) { pass() } else { fail() } eval { Tirex::Manager::Bucket->new( name => 'name', Xminprio => 1, maxproc => 1, maxload => 1); }; if ($@ =~ /need 'minprio' parameter/) { pass() } else { fail() } eval { Tirex::Manager::Bucket->new( name => 'name', minprio => 1, Xmaxproc => 1, maxload => 1); }; if ($@ =~ /need 'maxproc' parameter/) { pass() } else { fail() } eval { Tirex::Manager::Bucket->new( name => 'name', minprio => 1, maxproc => 1, Xmaxload => 1); }; if ($@ =~ /need 'maxload' parameter/) { pass() } else { fail() } #----------------------------------------------------------------------------- my $job = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 1), prio => 2 ); isa_ok($job, 'Tirex::Job', 'job'); my $b_live = Tirex::Manager::Bucket->new( name => 'live', minprio => 1, maxproc => 20, maxload => 50); isa_ok($b_live, 'Tirex::Manager::Bucket', 'bucket live'); my $b_middle = Tirex::Manager::Bucket->new( name => 'middle', minprio => 10, maxproc => 15, maxload => 20); isa_ok($b_middle, 'Tirex::Manager::Bucket', 'bucket middle'); my $b_backg = Tirex::Manager::Bucket->new( name => 'backg', minprio => 50, maxproc => 5, maxload => 10); isa_ok($b_backg, 'Tirex::Manager::Bucket', 'bucket backg'); is($b_live->get_numproc(), 0, 'numproc 0'); is($b_live->get_name, 'live', 'name live'); $b_live->add_job($job); is($b_live->get_numproc(), 1, 'numproc 1'); is($job->get_bucket(), $b_live, 'get_bucket'); $b_live->remove_job($job); is($b_live->get_numproc(), 0, 'numproc 0'); is($job->get_bucket(), undef, 'get_bucket'); #----------------------------------------------------------------------------- ok($b_backg->can_render(4, 0), 'backg can render 4'); is($b_backg->can_render(5, 0), 0, 'backg can not render: procs > maxproc'); is($b_backg->can_render(4, 20), undef, 'backg can not render: load > maxload'); is($b_backg->get_active(), 1, 'active'); $b_backg->set_active(0); is($b_backg->get_active(), 0, 'not active'); is($b_backg->can_render(4, 0), 0, 'can not render because not active'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/config.t000066400000000000000000000031571412262531100145510ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/config.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- Tirex::Config::parse_line('t', '', ''); is_deeply($Tirex::Config::confhash, {}, 'empty 1'); Tirex::Config::parse_line('t', '', ' '); is_deeply($Tirex::Config::confhash, {}, 'empty 2'); Tirex::Config::parse_line('t', '', '# foo'); is_deeply($Tirex::Config::confhash, {}, 'empty 3'); Tirex::Config::parse_line('t', '', 'a=b'); is_deeply($Tirex::Config::confhash, { a => 'b'}, 'a 1'); $Tirex::Config::confhash = {}; Tirex::Config::parse_line('t', '', 'a=b '); is_deeply($Tirex::Config::confhash, { a => 'b'}, 'a 2'); $Tirex::Config::confhash = {}; Tirex::Config::parse_line('t', '', 'a =b'); is_deeply($Tirex::Config::confhash, { a => 'b'}, 'a 3'); $Tirex::Config::confhash = {}; Tirex::Config::parse_line('t', '', 'a = b'); is_deeply($Tirex::Config::confhash, { a => 'b'}, 'a 4'); $Tirex::Config::confhash = {}; Tirex::Config::parse_line('t', '', 'foo a=b '); is_deeply($Tirex::Config::confhash, { foo => [ { a => 'b' } ] }, 'list 1'); $Tirex::Config::confhash = {}; Tirex::Config::parse_line('t', '', 'bucket name=live minprio=1 maxproc=3 maxload=20'); is_deeply($Tirex::Config::confhash, { bucket => [ { name => 'live', minprio => 1, maxproc => 3, maxload => 20 } ] }, 'list 1'); $Tirex::Config::confhash = {}; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/job_basic.t000066400000000000000000000047431412262531100152210ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/job_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- my $mt1 = Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 9); my $j1 = Tirex::Job->new( metatile => $mt1, prio => 1 ); my $j2 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 9), prio => 1 ); my $j3 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 8, y => 1, z => 9), prio => 1 ); isa_ok($j1, 'Tirex::Job', 'class'); is($j1->get_prio(), 1, 'prio'); $j1->set_prio(3); is($j1->get_prio(), 3, 'prio'); is($j1->get_metatile(), $mt1, 'metatile'); isnt($j1, $j2, 'not identical jobs'); ok($j1->same_tile($j2), 'but same tile'); ok(! $j1->same_tile($j3), 'not same tile'); is($j1->hash_key(), 'map=test z=9 x=0 y=0', 'hash key'); isnt($j1->get_id(), $j2->get_id(), 'ids are different 1'); isnt($j2->get_id(), $j3->get_id(), 'ids are different 2'); #----------------------------------------------------------------------------- $j1->add_notify('x'); $j1->add_notify('y'); is_deeply($j1->{'notify'}, [ 'x', 'y' ], 'notify'); #----------------------------------------------------------------------------- my $id1 = $j1->get_id(); my $msg = { 'foo' => 'bar', 'x' => 0, 'y' => 0, 'z' => 9, 'prio' => 3, 'map' => 'test', 'id' => $id1, 'type' => 'metatile_request' }; is_deeply($msg, $j1->to_msg('type' => 'metatile_request', 'foo' => 'bar'), 'message'); is(<to_s('type' => 'metatile_request', 'foo' => 'bar'), 'message'); #----------------------------------------------------------------------------- my $job_not_expired = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 2), prio => 1, expire => time() + 10 ); my $job_is_expired = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 2), prio => 1, expire => time() - 10 ); isa_ok($job_not_expired, 'Tirex::Job', 'create job'); isa_ok($job_is_expired, 'Tirex::Job', 'create job'); ok(!$job_not_expired->expired(), 'not expired'); ok( $job_is_expired->expired(), 'is expired'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/job_merge.t000077500000000000000000000032131412262531100152310ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/job_merge.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- my $j1 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 2, z => 3), prio => 1, expire => 10); my $j2 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 2, z => 3), prio => 10, expire => 20); my $j3 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 2, z => 3), prio => 10); my $j = $j1->merge($j2); is($j->get_map(), 'test', 'map'); is($j->get_x(), 0, 'x'); is($j->get_y(), 0, 'y'); is($j->get_z(), 3, 'z'); is($j->get_prio(), 1, 'prio'); is($j->{'expire'}, 20, 'expire'); is($j->{'request_time'}, $j1->{'request_time'}, 'request_time'); is_deeply($j->{'notify'}, [], 'notify'); isnt($j->get_id(), $j1->get_id(), 'id different 1'); isnt($j->get_id(), $j2->get_id(), 'id different 1'); #----------------------------------------------------------------------------- is($j1->merge($j3)->{'expire'}, undef, 'expire undef'); #----------------------------------------------------------------------------- $j1->add_notify('n1a'); $j1->add_notify('n1b'); $j2->add_notify('n2'); is_deeply($j1->merge($j2)->{'notify'}, ['n1a', 'n1b', 'n2'], 'notify j1/j2'); is_deeply($j1->merge($j3)->{'notify'}, ['n1a', 'n1b'], 'notify j1/j3'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/job_notify.t000077500000000000000000000013111412262531100154370ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/job_notify.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Source::Test; #----------------------------------------------------------------------------- my $j = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 1), prio => 1); $j->add_notify( Tirex::Source::Test->new('x') ); $j->add_notify( Tirex::Source::Test->new('y') ); is_deeply($j->notify(), [ 'x', 'y' ], 'notify'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/map.conf000066400000000000000000000001141412262531100145310ustar00rootroot00000000000000# # test map cfg # name=baz # comment tiledir=/a/b/c minz = 0 maxz = 14 tirex-0.7.0/t/map_basic.t000066400000000000000000000045771412262531100152310ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/map_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Map; #----------------------------------------------------------------------------- my $r = Tirex::Renderer->new( name => 'mapnik', type => 'mapnik', path => '/x', port => 1234, procs => 3 ); eval { Tirex::Map->new( ); }; ($@ =~ qr{missing name} ) ? pass() : fail(); eval { Tirex::Map->new( name => 'foo' ); }; ($@ =~ qr{missing renderer} ) ? pass() : fail(); eval { Tirex::Map->new( name => 'foo', renderer => $r ); }; ($@ =~ qr{missing tiledir} ) ? pass() : fail(); is(Tirex::Map->get('foo'), undef, 'get'); my $m1 = Tirex::Map->new( name => 'foo', renderer => $r, tiledir => '/var/cache/tirex/tiles/foo', minz => 2, maxz => 10 ); isa_ok($m1, 'Tirex::Map', 'class'); is($m1->get_name(), 'foo', 'name'); is($m1->get_tiledir(), '/var/cache/tirex/tiles/foo', 'tiledir'); is($m1->get_minz(), 2, 'minz'); is($m1->get_maxz(), 10, 'maxz'); is($m1->get_renderer(), $r, 'renderer'); is($m1->to_s(), 'Map foo: renderer=mapnik tiledir=/var/cache/tirex/tiles/foo zoom=2-10', 'to_s'); is(Tirex::Map->get('foo'), $m1, 'get'); is(Tirex::Map->get('bar'), undef, 'get'); is_deeply(Tirex::Map->status(), [ { name => 'foo', tiledir => '/var/cache/tirex/tiles/foo', minz => 2, maxz => 10, renderer => $r->get_name(), } ]); eval { Tirex::Map->new( name => 'foo', renderer => $r, tiledir => '/var/cache/tirex/tiles/foo' ); }; ($@ =~ qr{exists}) ? pass() : fail(); my $m2 = Tirex::Map->new( name => 'default_z', renderer => $r, tiledir => '/var/cache/tirex/tiles/foo' ); is($m2->get_minz(), 0, 'minz'); is($m2->get_maxz(), 17, 'maxz'); eval { Tirex::Map->new_from_configfile('does not exist'); }; ($@ =~ qr{Can't open map config file}) ? pass() : fail(); my $m3 = Tirex::Map->new_from_configfile('t/map.conf', $r); is(Tirex::Map->get('baz'), $m3, 'get'); isa_ok($m3, 'Tirex::Map', 'class'); is($m3->get_name(), 'baz', 'name'); is($m3->get_renderer(), $r, 'renderer'); is($m3->get_tiledir(), '/a/b/c', 'tiledir'); is($m3->get_minz(), 0, 'minz'); is($m3->get_maxz(), 14, 'maxz'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/map_metatile.t000066400000000000000000000022461412262531100157430ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/map_metatile.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Map; #----------------------------------------------------------------------------- my $map_foo = Tirex::Map->new( name => 'foo', renderer => 'rand', tiledir => '/x', minz => 2, maxz => 10 ); my $map_bar = Tirex::Map->new( name => 'bar', renderer => 'rand', tiledir => '/x', minz => 0, maxz => 8 ); my $mt1 = Tirex::Metatile->new( map => 'foo', x => 16, y => 17, z => 8 ); my $mt2 = Tirex::Metatile->new( map => 'bar', x => 16, y => 17, z => 12 ); my $mt3 = Tirex::Metatile->new( map => 'baz', x => 16, y => 17, z => 8 ); my $map1 = Tirex::Map->get_map_for_metatile($mt1); is($map1, $map_foo, 'mt1 in foo'); eval { Tirex::Map->get_map_for_metatile($mt2); }; ($@ =~ qr{zoom out of range} ) ? pass() : fail(); eval { Tirex::Map->get_map_for_metatile($mt3); }; ($@ =~ qr{map with name 'baz' not found} ) ? pass() : fail(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/message_basic.t000066400000000000000000000062451412262531100160720ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/message_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- eval{ Tirex::Message->new( foo => 'bar' ); }; if ($@ =~ /need type for new message/) { pass(); } else { fail(); } my $msg = Tirex::Message->new( type => 'test', foo => 1, bar => '' ); isa_ok($msg, 'Tirex::Message', 'create'); is($msg->{'type'}, 'test', 'type'); is($msg->{'foo'}, 1, 'attribute foo'); is($msg->{'bar'}, '', 'attribute bar'); is($msg->to_s(), 'bar= foo=1 type=test', 'to_s'); my $s = $msg->serialize(); is($s, "bar=\nfoo=1\ntype=test\n", 'serialize'); my $smsg = Tirex::Message->new_from_string($s); is_deeply($smsg, $msg, 're-created'); #----------------------------------------------------------------------------- my $crlfmsg = Tirex::Message->new_from_string("type=foo\r\nbar=baz\r\n"); is($crlfmsg->{'type'}, 'foo', 'CRLF parse type'); is($crlfmsg->{'bar'}, 'baz', 'CRLF parse bar'); #----------------------------------------------------------------------------- $msg = Tirex::Message->new( type => 'test', foo => 'x=y', bar => undef ); isa_ok($msg, 'Tirex::Message', 'create'); is($msg->{'type'}, 'test', 'type'); is($msg->{'foo'}, 'x=y', 'attribute foo'); is($msg->{'bar'}, undef, 'attribute bar'); is($msg->serialize(), "foo=x=y\ntype=test\n", 'serialize'); my $pmsg = $msg->new_from_string($msg->serialize()); is($pmsg->{'type'}, 'test', 'pmsg type'); is($pmsg->{'foo'}, 'x=y', 'pmsg attribute foo'); #----------------------------------------------------------------------------- $msg = Tirex::Message->new( type => 'test', foo => 1, bar => '', result => 'ok' ); eval{ $msg->reply('error_foo'); }; if ($@ =~ /can't reply to reply/) { pass(); } else { fail(); } ok($msg->ok(), 'ok'); #----------------------------------------------------------------------------- $msg = Tirex::Message->new( type => 'test', foo => 1, bar => '' ); ok(!$msg->ok(), 'not ok'); my $r1 = $msg->reply('ok'); my $r2 = $msg->reply('error'); my $r3 = $msg->reply('error_foo'); my $r4 = $msg->reply('error_foo', 'broken'); isa_ok($r1, 'Tirex::Message', 'reply r1'); isa_ok($r2, 'Tirex::Message', 'reply r2'); isa_ok($r3, 'Tirex::Message', 'reply r3'); isa_ok($r4, 'Tirex::Message', 'reply r4'); is($r1->{'type'}, 'test', 'r1 type'); is($r1->{'foo'}, '1', 'r1 foo'); is($r1->{'bar'}, '', 'r1 bar'); is($r1->{'result'}, 'ok', 'r1 result'); is($r2->{'result'}, 'error', 'r2 result'); is($r3->{'result'}, 'error_foo', 'r3 result'); is($r3->{'errmsg'}, undef, 'r3 errmsg'); is($r4->{'result'}, 'error_foo', 'r4 result'); is($r4->{'errmsg'}, 'broken', 'r4 errmsg'); #----------------------------------------------------------------------------- $msg = Tirex::Message->new( type => 'foo', map => 'test', x => 1, y => 2, z => 3 ); my $metatile = $msg->to_metatile(); is($metatile->to_s(), 'map=test z=3 x=0 y=0', 'metatile'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/metatile_basic.t000066400000000000000000000070541412262531100162510ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/metatile_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- eval { Tirex::Metatile->new( map => 'test', x => 1, y => 2, z => 1); }; if ($@ =~ /y must be between 0 and/) { pass(); } else { fail(); } eval { Tirex::Metatile->new( map => 'test', x => 1, y => 2, z => 100); }; if ($@ =~ /z must be between 0 and/) { pass(); } else { fail(); } eval { Tirex::Metatile->new( map => 'test', x => -1, y => 1, z => 1); }; if ($@ =~ /x must be between 0 and/) { pass(); } else { fail(); } eval { Tirex::Metatile->new( map => 'test', x => -1, y => 1 ); }; if ($@ =~ /need z for new metatile/) { pass(); } else { fail(); } my $mt1 = Tirex::Metatile->new( map => 'test', x => 1, y => 2, z => 3 ); isa_ok($mt1, 'Tirex::Metatile'); is($mt1->get_x(), 0, 'mt1 x'); is($mt1->get_y(), 0, 'mt1 y'); is($mt1->get_z(), 3, 'mt1 z'); is($mt1->get_map(), 'test', 'mt1 map'); is($mt1->get_filename(), '3/0/0/0/0/0.meta', 'mt1 get_filename'); is($mt1->to_s(), 'map=test z=3 x=0 y=0', 'mt1 to_s'); ok($mt1->equals($mt1), 'mt1 == mt2'); my $mt2 = Tirex::Metatile->new( map => 'test', x => 23423, y => 1234, z => 15 ); isa_ok($mt2, 'Tirex::Metatile'); is($mt2->get_x(), 23416, 'mt2 x'); is($mt2->get_y(), 1232, 'mt2 y'); is($mt2->get_z(), 15, 'mt2 z'); is($mt2->get_map(), 'test', 'mt2 map'); is($mt2->get_filename(), '15/0/80/180/125/128.meta', 'mt2 get_filename'); is($mt2->to_s(), 'map=test z=15 x=23416 y=1232', 'mt2 to_s'); ok(! $mt2->equals($mt1), 'mt1 != mt2'); my $mt2g = Tirex::Metatile->new_from_filename_and_map( $mt2->get_filename(), $mt2->get_map() ); isa_ok($mt2g, 'Tirex::Metatile', 'create mt2g from filename of mt2'); ok($mt2g->equals($mt2), 'mt2g and mt2 are the same'); #----------------------------------------------------------------------------- my $ZOOM = 12; my $LIMIT = 2**$ZOOM; my $m1 = Tirex::Metatile->new( map => 'test', z => $ZOOM, x => 0, y => $LIMIT-1 ); my $m2 = Tirex::Metatile->new_from_filename_and_map( $m1->get_filename(), $m1->get_map() ); ok($m1->equals($m2), 'm1 and m2 are the same'); $m1 = Tirex::Metatile->new( map => 'test', z => $ZOOM, x => $LIMIT-1, y => 0 ); $m2 = Tirex::Metatile->new_from_filename_and_map( $m1->get_filename(), $m1->get_map() ); ok($m1->equals($m2), 'm1 and m2 are the same'); foreach (1..100) { $m1 = Tirex::Metatile->new( map => 'test', z => $ZOOM, x => int(rand($LIMIT)), y => int(rand($LIMIT)) ); $m2 = Tirex::Metatile->new_from_filename_and_map( $m1->get_filename(), $m1->get_map() ); ok($m1->equals($m2), 'm1 and m2 are the same'); } #----------------------------------------------------------------------------- my $mz = Tirex::Metatile->new( map => 'test', z => 18, x => 99640, y => 148248 ); is($mz->get_x(), 99640, 'z18 x'); is($mz->get_y(), 148248, 'z18 y'); $mz = $mz->up(); is($mz->get_x(), 49816, 'z17 x'); is($mz->get_y(), 74120, 'z17 y'); $mz = $mz->up()->up()->up(); is($mz->get_x(), 6224, 'z14 x'); is($mz->get_y(), 9264, 'z14 y'); $mz = $mz->up()->up()->up()->up()->up()->up()->up()->up(); is($mz->get_x(), 24, 'z6 x'); is($mz->get_y(), 32, 'z6 y'); $mz = $mz->up()->up()->up()->up()->up()->up(); is($mz->get_x(), 0, 'z0 x'); is($mz->get_y(), 0, 'z0 y'); is($mz->up(), undef, 'no more up'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/metatile_lonlat.t000066400000000000000000000037151412262531100164610ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/metatile_lonlat.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- my $z = 10; my $limit = 2 ** $z; is(Tirex::Metatile::lon2x(1, $z, -180 ), 0, 'lon2x -180'); is(Tirex::Metatile::lon2x(1, $z, +180 ), $limit-1, 'lon2x +180'); is(Tirex::Metatile::lon2x(1, $z, -0.01 ), $limit/2-1, 'lon2x -0.01'); is(Tirex::Metatile::lon2x(1, $z, +0.01 ), $limit/2, 'lon2x +0.01'); is(Tirex::Metatile::lat2y(1, $z, -90 ), $limit-1, 'lat2y -90'); is(Tirex::Metatile::lat2y(1, $z, +90 ), 0, 'lat2y +90'); is(Tirex::Metatile::lat2y(1, $z, -85.05113), $limit-1, 'lat2y -85.05113'); is(Tirex::Metatile::lat2y(1, $z, +85.05113), 0, 'lat2y +85.05113'); is(Tirex::Metatile::lat2y(1, $z, -0.01 ), $limit/2, 'lat2y -0.01'); is(Tirex::Metatile::lat2y(1, $z, +0.01 ), $limit/2-1, 'lat2y +0.01'); #----------------------------------------------------------------------------- my $m1 = Tirex::Metatile->new_from_lon_lat( map => 'test', z => 17, lon => -1.616, lat => 49.533 ); isa_ok($m1, 'Tirex::Metatile', 'create m1'); is($m1->get_x(), 64944, 'm1 x'); is($m1->get_y(), 44712, 'm1 y'); is($m1->get_z(), 17, 'm1 z'); my $m2 = Tirex::Metatile->new_from_lon_lat( map => 'test', z => 14, lon => -180, lat => 85.05113 ); isa_ok($m2, 'Tirex::Metatile', 'create m2'); is($m2->get_x(), 0, 'm2 x'); is($m2->get_y(), 0, 'm2 y'); is($m2->get_z(), 14, 'm2 z'); my $m3 = Tirex::Metatile->new_from_lon_lat( map => 'test', z => 4, lon => 180, lat => -85.05112 ); isa_ok($m3, 'Tirex::Metatile', 'create m3'); is($m3->get_x(), 8, 'm3 x'); is($m3->get_y(), 8, 'm3 y'); is($m3->get_z(), 4, 'm3 z'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/metatiles_range.t000066400000000000000000000147711412262531100164530ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/metatiles_range.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- eval { Tirex::Metatiles::Range->new( z => 3, zmin => 2 ); }; ($@ =~ qr{you cannot have parameters 'z' and 'zmin'/'zmax'}) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( x => 3, xmin => 2 ); }; ($@ =~ qr{you cannot have parameters 'x' and 'xmin'/'xmax'}) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( y => 3, ymax => 4 ); }; ($@ =~ qr{you cannot have parameters 'y' and 'ymin'/'ymax'}) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( ymax => 4 ); }; ($@ =~ qr{'ymax' but missing 'ymin'} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( lonmin => 4.0 ); }; ($@ =~ qr{'lonmin' but missing 'lonmax'} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( foo => 3 ); }; ($@ =~ qr{unknown parameter: 'foo'} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( xmin => 'x' ); }; ($@ =~ qr{xmin must be zero or positive integer} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( ymin => 'x' ); }; ($@ =~ qr{ymin must be zero or positive integer} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( zmax => -1 ); }; ($@ =~ qr{zmax must be zero or positive integer} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( lonmin => 'x' ); }; ($@ =~ qr{lonmin must be legal degree value} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( latmin => '12.' ); }; ($@ =~ qr{latmin must be legal degree value} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( lonmax => -1000 ); }; ($@ =~ qr{lonmax must be legal longitude value} ) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( latmax => 92 ); }; ($@ =~ qr{latmax must be legal latitude value} ) ? pass() : fail(); #----------------------------------------------------------------------------- my $r = Tirex::Metatiles::Range->new( map => 'test', z => 3, x => 5, y => 8 ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r1'); is_deeply(['test'], $r->{'maps'}, 'r1 map'); is($r->{'zmin'}, 3, 'r1 zmin'); is($r->{'zmax'}, 3, 'r1 zmax'); is($r->{'xmin'}, 0, 'r1 xmin'); is($r->{'xmax'}, 0, 'r1 xmax'); is($r->{'ymin'}, 8, 'r1 ymin'); is($r->{'ymax'}, 8, 'r1 ymax'); is($r->count(), 1, 'r1 count'); is($r->to_s(), 'maps=test z=3 x=0 y=8', 'r1 to_s'); $r = Tirex::Metatiles::Range->new( map => ['t1', 't2'], z => 3, x => '0-6', y => '7-8' ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r2'); is_deeply(['t1', 't2'], $r->{'maps'}, 'r2 map'); is($r->{'zmin'}, 3, 'r2 zmin'); is($r->{'zmax'}, 3, 'r2 zmax'); is($r->{'xmin'}, 0, 'r2 xmin'); is($r->{'xmax'}, 0, 'r2 xmax'); is($r->{'ymin'}, 0, 'r2 ymin'); is($r->{'ymax'}, 8, 'r2 ymax'); is($r->count(), 2*1*2, 'r2 count'); $r = Tirex::Metatiles::Range->new( map => 't', z => 3, xmin => 5, xmax => 7, ymin => 7, ymax => 8 ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r3'); is_deeply(['t'], $r->{'maps'}, 'r3 map'); is($r->{'zmin'}, 3, 'r3 zmin'); is($r->{'zmax'}, 3, 'r3 zmax'); is($r->{'xmin'}, 0, 'r3 xmin'); is($r->{'xmax'}, 0, 'r3 xmax'); is($r->{'ymin'}, 0, 'r3 ymin'); is($r->{'ymax'}, 8, 'r3 ymax'); is($r->count(), 1*1*2, 'r3 count'); $r = Tirex::Metatiles::Range->new( map => 'test', z => 3, lonmin => 8, lonmax => 9, latmin => 48, latmax => 49 ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r4'); is_deeply(['test'], $r->{'maps'}, 'r4 map'); is($r->{'zmin'}, 3, 'r4 zmin'); is($r->{'zmax'}, 3, 'r4 zmax'); is($r->{'lonmin'}, 8, 'r4 lonmin'); is($r->{'lonmax'}, 9, 'r4 lonmax'); is($r->{'latmin'}, 48, 'r4 latmin'); is($r->{'latmax'}, 49, 'r4 latmax'); is($r->{'xmin'}, 0, 'r4 xmin'); is($r->{'xmax'}, 0, 'r4 xmax'); is($r->{'ymin'}, 0, 'r4 ymin'); is($r->{'ymax'}, 0, 'r4 ymax'); is($r->count(), 1, 'r4 count'); $r = Tirex::Metatiles::Range->new( map => 'test', z => 9, lon => '5.1,6.2', lat => '49.7,48.1' ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r5'); is_deeply($r->{'maps'}, ['test'], 'r5 map'); is($r->{'zmin'}, 9, 'r5 zmin'); is($r->{'zmax'}, 9, 'r5 zmax'); is($r->{'lonmin'}, 5.1, 'r5 lonmin'); is($r->{'lonmax'}, 6.2, 'r5 lonmax'); is($r->{'latmin'}, 48.1, 'r5 latmin'); is($r->{'latmax'}, 49.7, 'r5 latmax'); is($r->{'xmin'}, 256, 'r5 xmin'); is($r->{'xmax'}, 264, 'r5 xmax'); is($r->{'ymin'}, 168, 'r5 ymin'); is($r->{'ymax'}, 176, 'r5 ymax'); is($r->count(), 4, 'r5 count'); $r = Tirex::Metatiles::Range->new( map => 'test', z => 9, lon => '4,8', lat => '40,50' ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r6'); eval { Tirex::Metatiles::Range->new( init => 'flub' ); }; ($@ =~ qr{can't parse init string}) ? pass() : fail(); eval { Tirex::Metatiles::Range->new(); }; ($@ =~ qr{missing .* parameter}) ? pass() : fail(); eval { Tirex::Metatiles::Range->new( init => '' ); }; ($@ =~ qr{missing .* parameter}) ? pass() : fail(); $r = Tirex::Metatiles::Range->new( init => 'map=test z=9 lon=4,8 lat=40,50' ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r7'); is_deeply($r->{'maps'}, ['test'], 'r7 map'); is($r->{'zmin'}, 9, 'r7 zmin'); is($r->{'zmax'}, 9, 'r7 zmax'); is($r->{'lonmin'}, 4, 'r7 lonmin'); is($r->{'lonmax'}, 8, 'r7 lonmax'); is($r->{'latmin'}, 40, 'r7 latmin'); is($r->{'latmax'}, 50, 'r7 latmax'); is($r->{'xmin'}, 256, 'r7 xmin'); is($r->{'xmax'}, 264, 'r7 xmax'); is($r->{'ymin'}, 168, 'r7 ymin'); is($r->{'ymax'}, 192, 'r7 ymax'); is($r->count(), 1*2*4, 'r7 count'); $r = Tirex::Metatiles::Range->new( init => 'map=foo,bar z=1-2 lon=4,8 lat=40,50' ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r8'); is_deeply($r->{'maps'}, ['foo', 'bar'], 'r8 map'); is($r->to_s(), 'maps=foo,bar z=1,2 lon=4,8 lat=40,50', 'r8 to_s'); $r = Tirex::Metatiles::Range->new( map => 'foo', z => 10, bbox => '8, 48, 9, 49' ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r9'); is_deeply($r->{'maps'}, ['foo'], 'r9 map'); is($r->to_s(), 'maps=foo z=10 lon=8,9 lat=48,49', 'r9 to_s'); $r = Tirex::Metatiles::Range->new( init => 'map=foo z=10 bbox=8,48,9,49' ); isa_ok($r, 'Tirex::Metatiles::Range', 'create r10'); is_deeply($r->{'maps'}, ['foo'], 'r10 map'); is($r->to_s(), 'maps=foo z=10 lon=8,9 lat=48,49', 'r10 to_s'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/prioqueue_basic.t000066400000000000000000000100701412262531100164530ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/prioqueue_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::PrioQueue; #----------------------------------------------------------------------------- is(Tirex::PrioQueue->new(), undef, 'failed, no prio'); is(Tirex::PrioQueue->new(prio => 'foo'), undef, 'failed, wrong prio'); my $pq = Tirex::PrioQueue->new(prio => 1); isa_ok($pq, 'Tirex::PrioQueue', 'class'); is($pq->size(), 0, 'prio queue size 0'); ok($pq->empty(), 'prio queue empty'); is_deeply($pq->status(), { prio => 1, size => 0, maxsize => 0}, 'status empty'); is($pq->add('fail'), undef, 'can only add jobs'); my $j1 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 4), prio => 1 ); my $j2 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 2, z => 4), prio => 1 ); my $j3 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 3, z => 4), prio => 2 ); is($pq->add($j1), $j1, 'add 1'); is($pq->size(), 1, 'prio queue size 1'); ok(!$pq->empty(), 'prio queue not empty'); is($pq->add($j2), $j2, 'add 2'); is($pq->size(), 2, 'prio queue size 2'); ok(!$pq->empty(), 'prio queue not empty'); is($pq->add($j3), undef, 'wrong prio'); is($pq->size(), 2, 'prio queue size 2'); ok(!$pq->empty(), 'prio queue not empty'); is($pq->{'maxsize'}, 2, 'prio queue maxsize 2'); is($pq->remove($j1), $j1, 'remove 1'); is($pq->size(), 1, 'prio queue size 1'); ok(!$pq->empty(), 'prio queue not empty'); is($pq->{'maxsize'}, 2, 'prio queue maxsize 2'); $pq->reset_maxsize(); is($pq->{'maxsize'}, 1, 'prio queue maxsize 1'); is($pq->remove($j2), $j2, 'remove 2'); is($pq->size(), 0, 'prio queue size 0'); ok($pq->empty(), 'prio queue empty'); is($pq->remove($j2), undef, 'remove failed'); #----------------------------------------------------------------------------- $pq = Tirex::PrioQueue->new(prio => 1); $pq->add($j1); $pq->add($j2); is($pq->reset(), $pq, 'reset'); is($pq->size(), 0, 'prio queue size 0'); ok($pq->empty(), 'prio queue empty'); #----------------------------------------------------------------------------- $pq = Tirex::PrioQueue->new(prio => 1); $pq->add($j1); $pq->add($j2); $pq->remove($j2); is($pq->size(), 1, 'prio queue size 1'); ok(!$pq->empty(), 'prio queue not empty'); $pq->add($j2); is($pq->size(), 2, 'prio queue size 2'); ok(!$pq->empty(), 'prio queue not empty'); is($pq->peek(), $j1, 'peek'); is($pq->size(), 2, 'prio queue size 2'); is($pq->next(), $j1, 'next'); is($pq->size(), 1, 'prio queue size 1'); #----------------------------------------------------------------------------- $pq = Tirex::PrioQueue->new(prio => 1); my @jobs; foreach my $n (0..9) { push(@jobs, $pq->add( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => $n, z => 10), prio => 1 ) )); } is($pq->size(), 10, 'prio queue size 10'); is($pq->remove( $jobs[1] ), $jobs[1], 'remove 1'); is($pq->remove( $jobs[2] ), $jobs[2], 'remove 2'); is($pq->next(), $jobs[0], 'next'); is($pq->next(), $jobs[3], 'next'); is($pq->peek(), $jobs[4], 'next'); is($pq->size(), 6, 'prio queue size 6'); #----------------------------------------------------------------------------- $pq = Tirex::PrioQueue->new(prio => 1); is($pq->age_first(), undef, 'nothing in queue means no age'); is($pq->age_last(), undef, 'nothing in queue means no age'); my $job1 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 1), prio => 1 ); $pq->add($job1); like($pq->age_first(), qr{^[01]$}, 'just added'); like($pq->age_last(), qr{^[01]$}, 'just added'); my $job2 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 2, z => 5), prio => 1 ); $pq->add($job2); $pq->remove($job2); like($pq->age_first(), qr{^[01]$}, 'one in queue with age 0 or 1'); like($pq->age_last(), qr{^[01]$}, 'one in queue with age 0 or 1'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/queue_add.t000077500000000000000000000033471412262531100152440ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/queue_add.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Queue; #----------------------------------------------------------------------------- my $q = Tirex::Queue->new(); my @jobs = ( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 9), prio => 10 ), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 9, y => 1, z => 9), prio => 7 ), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 20, y => 1, z => 9), prio => 6 ), ); isa_ok($jobs[0], 'Tirex::Job', 'job in array'); $q->add($jobs[0], $jobs[1]); is($q->size(), 2, 'added two jobs'); $q->reset(); @jobs = ( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 9), prio => 10 ), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 9, y => 1, z => 9), prio => 7 ), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 20, y => 1, z => 9), prio => 6 ), ); $q->add(@jobs); is($q->size(), 3, 'added three jobs'); $q->reset(); @jobs = ( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 9), prio => 10 ), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 9, y => 1, z => 9), prio => 7 ), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 20, y => 1, z => 9), prio => 6 ), ); $q->add(\@jobs); is($q->size(), 3, 'added three jobs'); $q->reset(); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/queue_basic.t000077500000000000000000000035071412262531100155730ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/queue_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Queue; #----------------------------------------------------------------------------- my $q = Tirex::Queue->new(); isa_ok($q, 'Tirex::Queue', 'type of queue is Tirex::Queue'); #----------------------------------------------------------------------------- ok($q->empty(), 'empty queue'); $q->add( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 2), prio => 1 ) ); ok(! $q->empty(), 'non-empty queue'); is($q->size(), 1, 'one job in queue'); my $j = $q->peek(); is($q->size(), 1, 'one job in queue'); isa_ok($j, 'Tirex::Job', 'first element in queue'); is($j->{'prio'}, 1, 'prio still the same'); $j = $q->next(); is($q->size(), 0, 'no job in queue'); isa_ok($j, 'Tirex::Job', 'got job from queue'); $q->add( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 2), prio => 1 ) ); is($q->size(), 1, 'one job in queue'); isa_ok($q->in_queue( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 2), prio => 1 ) ), 'Tirex::Job', 'in queue'); $q->reset(); is($q->in_queue( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 2), prio => 1 ) ), undef, 'not in queue'); ok($q->empty(), 'queue empty after reset'); is($q->next(), undef, 'next on empty queue returns undef'); is($q->peek(), undef, 'peek on empty queue returns undef'); eval { $q->add(1); }; if ($@ =~ /^Can only add objects of type Tirex::Job to queue!/) { pass(); } else { fail(); } #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/queue_many_jobs.t000066400000000000000000000030761412262531100164710ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/queue_many_jobs.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Queue; #----------------------------------------------------------------------------- my $q = Tirex::Queue->new(); isa_ok($q, 'Tirex::Queue', 'class'); sub create_job { my ($max_z, $max_prio) = @_; my $z = int(rand($max_z+1)); my $limit = 2 ** $z; return Tirex::Job->new( metatile => Tirex::Metatile->new( map => 'test', x => int(rand($limit)), y => int(rand($limit)), z => $z ), prio => int(rand($max_prio))+1 ); } my @jobs; my $count = 0; foreach my $i (1..50) { my $job = create_job(10, 10); isa_ok($job, 'Tirex::Job', 'prime'); $count++ unless ($q->in_queue($job)); $q->add($job); is($q->size(), $count, 'size'); } foreach my $i (0 .. 500) { if (rand() < 0.6) { my $job = create_job(10, 10, 10, 10); isa_ok($job, 'Tirex::Job', 'prime'); $count++ unless ($q->in_queue($job)); $q->add($job); is($q->size(), $count, 'size'); } else { $count--; isa_ok($q->next(), 'Tirex::Job', 'removed'); is($q->size(), $count, 'size when removing'); } } while ($count > 0) { $count--; isa_ok($q->next(), 'Tirex::Job', 'removed'); is($q->size(), $count, 'size when removing'); } ok($q->empty, 'empty'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/queue_multiple_tiles.t000066400000000000000000000031361412262531100175400ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/queue_multiple_tiles.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Queue; #----------------------------------------------------------------------------- my $q = Tirex::Queue->new(); my $mt1 = Tirex::Metatile->new(map => 'test', x => 0, y => 1, z => 5); my $mt2 = Tirex::Metatile->new(map => 'test', x => 8, y => 1, z => 5); my $mt3 = Tirex::Metatile->new(map => 'test', x => 16, y => 1, z => 5); my @jobs = ( Tirex::Job->new(metatile => $mt1, prio => 1), # prio 1, x 1 Tirex::Job->new(metatile => $mt2, prio => 2), Tirex::Job->new(metatile => $mt3, prio => 3), Tirex::Job->new(metatile => $mt2, prio => 1), # prio 1, x 2 Tirex::Job->new(metatile => $mt1, prio => 2), Tirex::Job->new(metatile => $mt2, prio => 3), Tirex::Job->new(metatile => $mt1, prio => 1), Tirex::Job->new(metatile => $mt1, prio => 2), Tirex::Job->new(metatile => $mt3, prio => 2), # prio 2, x 3 Tirex::Job->new(metatile => $mt2, prio => 2), ); $q->add(@jobs); is($q->size(), 3, 'all jobs in queue'); my $j = $q->next(); ok($j->same_tile($jobs[0]), 'jobs from queue 1'); $j = $q->next(); is($j->get_x(), 8, 'x from queue 2'); is($j->get_prio(), 1, 'prio from queue 2'); $j = $q->next(); is($j->get_x(), 16, 'x from queue 3'); is($j->get_prio(), 2, 'prio from queue 3'); ok($q->empty(), 'queue empty again'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/queue_same_tile_with_different_prios.t000077500000000000000000000033711412262531100227500ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/queue_same_tile_with_different_prios.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Queue; #----------------------------------------------------------------------------- my $q = Tirex::Queue->new(); foreach my $prio (5, 3, 10, 8, 1, 9) { $q->add( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 1), prio => $prio ) ); } is($q->size(), 1, 'only one job'); my $j = $q->next(); is($j->get_prio(), 1, 'prio 1'); is($j->get_map(), 'test', 'map test'); $q->reset(); #----------------------------------------------------------------------------- my @jobs = ( Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 7, y => 1, z => 9), prio => 8), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 8, y => 1, z => 9), prio => 5), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 31, y => 1, z => 9), prio => 1), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 32, y => 1, z => 9), prio => 9), Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 99, y => 1, z => 9), prio => 7), ); $q->add(@jobs); is($q->size(), scalar(@jobs), 'all jobs in queue'); is($q->next(), $jobs[2], 'jobs from queue 1'); is($q->next(), $jobs[1], 'jobs from queue 2'); is($q->next(), $jobs[4], 'jobs from queue 3'); is($q->next(), $jobs[0], 'jobs from queue 4'); is($q->next(), $jobs[3], 'jobs from queue 5'); ok($q->empty(), 'queue empty again'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/renderer.conf000066400000000000000000000002741412262531100155710ustar00rootroot00000000000000# # test renderer cfg # name=mapnik2 # comment path=/usr/libexec/tirex-backend-mapnik port=1234 procs=3 fontdir=/usr/lib/mapnik/fonts fontdir_recurse=0 plugindir=/usr/lib/mapnik/input tirex-0.7.0/t/renderer_basic.t000066400000000000000000000075071412262531100162560ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/renderer_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Renderer; #----------------------------------------------------------------------------- eval { Tirex::Renderer->new( ); }; ($@ =~ qr{missing name} ) ? pass() : fail(); eval { Tirex::Renderer->new( name => 'mapnik1', ); }; ($@ =~ qr{missing path} ) ? pass() : fail(); eval { Tirex::Renderer->new( name => 'mapnik1', path => '/usr/libexec/tirex-backend-mapnik' ); }; ($@ =~ qr{missing port} ) ? pass() : fail(); eval { Tirex::Renderer->new( name => 'mapnik1', path => '/usr/libexec/tirex-backend-mapnik', port => 1234 ); }; ($@ =~ qr{missing procs}) ? pass() : fail(); is(Tirex::Renderer->get('mapnik1'), undef, 'get'); my $r1 = Tirex::Renderer->new( name => 'mapnik1', path => '/usr/libexec/tirex-backend-mapnik', port => 1234, procs => 3, fontdir => '/usr/lib/mapnik/fonts', fontdir_recurse => 0, plugindir => '/usr/lib/mapnik/input' ); isa_ok($r1, 'Tirex::Renderer', 'class'); is($r1->get_name(), 'mapnik1', 'name'); is($r1->get_path(), '/usr/libexec/tirex-backend-mapnik', 'path'); is($r1->get_port(), 1234, 'port'); is($r1->get_procs(), 3, 'procs'); is($r1->is_enabled(), 1, 'is_enabled'); my @e = Tirex::Renderer->enabled(); is(scalar(@e), 1, 'enabled'); is($e[0], $r1, 'enabled'); $r1->disable(); is($r1->is_enabled(), 0, 'not is_enabled'); is(scalar(Tirex::Renderer->enabled()), 0, 'enabled'); $r1->enable(); is($r1->is_enabled(), 1, 'is_enabled'); @e = Tirex::Renderer->enabled(); is(scalar(@e), 1, 'enabled'); is($e[0], $r1, 'enabled'); is_deeply([$r1->get_maps()], [], 'maps'); is_deeply($r1->get_config(), { fontdir => '/usr/lib/mapnik/fonts', fontdir_recurse => 0, plugindir => '/usr/lib/mapnik/input' }, 'config'); is($r1->to_s(), 'Renderer mapnik1: port=1234 procs=3 path=/usr/libexec/tirex-backend-mapnik syslog_facility=daemon debug=0 fontdir=/usr/lib/mapnik/fonts fontdir_recurse=0 plugindir=/usr/lib/mapnik/input', 'to_s'); is(Tirex::Renderer->get('mapnik1'), $r1, 'get'); is(Tirex::Renderer->get('foo'), undef, 'get'); is_deeply(Tirex::Renderer->status(), [ { name => 'mapnik1', path => '/usr/libexec/tirex-backend-mapnik', port => 1234, procs => 3, syslog_facility => 'daemon', debug => 0, fontdir => '/usr/lib/mapnik/fonts', fontdir_recurse => 0, plugindir => '/usr/lib/mapnik/input', maps => [], }, ], 'status'); eval { Tirex::Renderer->new( name => 'mapnik1', path => '/usr/libexec/tirex-backend-mapnik', port => 1234, procs => 3, fontdir => '/usr/lib/mapnik/fonts', fontdir_recurse => 0, plugindir => '/usr/lib/mapnik/input' ); }; ($@ =~ qr{exists}) ? pass() : fail(); eval { Tirex::Renderer->new_from_configfile('does not exist'); }; ($@ =~ qr{Can't open renderer config file}) ? pass() : fail(); my $r3 = Tirex::Renderer->new_from_configfile('t/renderer.conf'); is(Tirex::Renderer->get('mapnik2'), $r3, 'get'); isa_ok($r3, 'Tirex::Renderer', 'class'); is($r3->get_name(), 'mapnik2', 'name'); is($r3->get_path(), '/usr/libexec/tirex-backend-mapnik', 'path'); is($r3->get_port(), 1234, 'port'); is($r3->get_procs(), 3, 'procs'); is($r3->num_workers(), 0, 'num workers 0'); $r3->add_worker(123); is($r3->num_workers(), 1, 'num workers 1'); $r3->add_worker(345); is($r3->num_workers(), 2, 'num workers 2'); $r3->remove_worker(123); is($r3->num_workers(), 1, 'num workers 1'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/rendering_basic.t000066400000000000000000000103651412262531100164210ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/rendering_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use JSON; use lib 'lib'; use Tirex; use Tirex::Queue; use Tirex::Manager; #----------------------------------------------------------------------------- my $queue = Tirex::Queue->new(); isa_ok($queue, 'Tirex::Queue', 'queue'); my $rm = Tirex::Manager->new( queue => $queue ); isa_ok($rm, 'Tirex::Manager', 'rendering manager'); my $bucket1 = $rm->add_bucket( name => 'live', minprio => 1, maxproc => 3, maxload => 30 ); my $bucket3 = $rm->add_bucket( name => 'backg', minprio => 20, maxproc => 1, maxload => 10 ); my $bucket2 = $rm->add_bucket( name => 'middle', minprio => 10, maxproc => 2, maxload => 20 ); isa_ok($bucket1, 'Tirex::Manager::Bucket', 'class'); isa_ok($bucket2, 'Tirex::Manager::Bucket', 'class') ; isa_ok($bucket3, 'Tirex::Manager::Bucket', 'class'); is($bucket1->get_maxprio(), 9, 'maxprio 1'); is($bucket2->get_maxprio(), 19, 'maxprio 2'); is($bucket3->get_maxprio(), undef, 'maxprio 3'); #----------------------------------------------------------------------------- ok( $bucket1->for_prio( 1), 'bucket 1 for prio 1'); ok( $bucket1->for_prio( 9), 'bucket 1 for prio 9'); ok(!$bucket1->for_prio(10), 'bucket 1 for prio 10'); ok(!$bucket1->for_prio(19), 'bucket 1 for prio 19'); ok(!$bucket1->for_prio(20), 'bucket 1 for prio 20'); ok(!$bucket1->for_prio(99), 'bucket 1 for prio 99'); ok(!$bucket2->for_prio( 1), 'bucket 2 for prio 1'); ok(!$bucket2->for_prio( 9), 'bucket 2 for prio 9'); ok( $bucket2->for_prio(10), 'bucket 2 for prio 10'); ok( $bucket2->for_prio(19), 'bucket 2 for prio 19'); ok(!$bucket2->for_prio(20), 'bucket 2 for prio 20'); ok(!$bucket2->for_prio(99), 'bucket 2 for prio 99'); ok(!$bucket3->for_prio( 1), 'bucket 3 for prio 1'); ok(!$bucket3->for_prio( 9), 'bucket 3 for prio 9'); ok(!$bucket3->for_prio(10), 'bucket 3 for prio 10'); ok(!$bucket3->for_prio(19), 'bucket 3 for prio 19'); ok( $bucket3->for_prio(20), 'bucket 3 for prio 20'); ok( $bucket3->for_prio(99), 'bucket 3 for prio 99'); #----------------------------------------------------------------------------- ok( $bucket1->can_render(0, 0), 'bucket 1 can 0, 0'); ok( $bucket2->can_render(0, 0), 'bucket 2 can 0, 0'); ok( $bucket3->can_render(0, 0), 'bucket 3 can 0, 0'); ok( $bucket1->can_render(1, 0), 'bucket 1 can 1, 0'); ok( $bucket2->can_render(1, 0), 'bucket 2 can 1, 0'); ok(!$bucket3->can_render(1, 0), 'bucket 3 can 1, 0'); ok( $bucket1->can_render(2, 0), 'bucket 1 can 2, 0'); ok(!$bucket2->can_render(2, 0), 'bucket 2 can 2, 0'); ok(!$bucket3->can_render(2, 0), 'bucket 3 can 2, 0'); ok(!$bucket1->can_render(3, 0), 'bucket 1 can 3, 0'); ok(!$bucket2->can_render(3, 0), 'bucket 2 can 3, 0'); ok(!$bucket3->can_render(3, 0), 'bucket 3 can 3, 0'); ok( $bucket1->can_render(0, 10), 'bucket 1 can 0, 10'); ok( $bucket2->can_render(0, 10), 'bucket 2 can 0, 10'); ok(!$bucket3->can_render(0, 10), 'bucket 3 can 0, 10'); ok(!$bucket1->can_render(0, 30), 'bucket 1 can 0, 30'); ok(!$bucket2->can_render(0, 30), 'bucket 2 can 0, 30'); ok(!$bucket3->can_render(0, 30), 'bucket 3 can 0, 30'); #----------------------------------------------------------------------------- my $expected_status = { buckets => [ { name => 'live', minprio => 1, maxprio => 9, maxproc => 3, maxload => 30, numproc => 0, active => 1, can_render => JSON::true }, { name => 'middle', minprio => 10, maxprio => 19, maxproc => 2, maxload => 20, numproc => 0, active => 1, can_render => JSON::true }, { name => 'backg', minprio => 20, maxprio => 0, maxproc => 1, maxload => 10, numproc => 0, active => 1, can_render => JSON::true }, ], num_rendering => 0, rendering => [], stats => { count_requested => 0, count_expired => 0, count_timeouted => 0, count_error => 0, count_rendered => {}, sum_render_time => {}, }, }; my $is_status = $rm->status(); delete($is_status->{'load'}); # remove load because we don't know what it is is_deeply($is_status, $expected_status, 'status'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/rendering_jobs.t000066400000000000000000000050401412262531100162670ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/rendering_jobs.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Manager::RenderingJobs; #----------------------------------------------------------------------------- eval { Tirex::Manager::RenderingJobs->new(); }; ($@ =~ /missing or illegal timeout/) ? pass() : fail(); eval { Tirex::Manager::RenderingJobs->new( timeout => 'foo' ); }; ($@ =~ /missing or illegal timeout/) ? pass() : fail(); my $rj = Tirex::Manager::RenderingJobs->new( timeout => 1 ); isa_ok($rj, 'Tirex::Manager::RenderingJobs', 'create'); my $job1 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 1, y => 1, z => 1), prio => 1 ); isa_ok($job1, 'Tirex::Job', 'create job 1'); my $job2 = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'test', x => 2, y => 2, z => 2), prio => 2 ); isa_ok($job2, 'Tirex::Job', 'create job 2'); is($rj->count(), 0, 'count 0'); is($rj->add($job1), $job1, 'add 1'); is($rj->count(), 1, 'count 1'); is($rj->add($job2), $job2, 'add 1'); is($rj->count(), 2, 'count 2'); is($rj->find_by_id($job1->get_id()), $job1, 'find_by_id'); is($rj->find_by_id('foo'), undef, 'find_by_id failed'); is($rj->find_by_metatile($job2->hash_key()), $job2, 'find_by_metatile'); is($rj->find_by_metatile('foo'), undef, 'find_by_metatile failed'); is($rj->remove($job1), $job1, 'remove 1'); is($rj->count(), 1, 'count 1'); is($rj->find_by_id($job1->get_id()), undef, 'find_by_id not any more'); is($rj->find_by_metatile($job1->hash_key()), undef, 'find_by_metatile not any more'); #----------------------------------------------------------------------------- my $status = [{ map => 'test', x => 0, y => 0, z => 2, prio => 2, }]; my $rjstatus = $rj->status(); delete $rjstatus->[0]->{'age'}; # remove age because it is unpredictable is_deeply($rjstatus, $status, 'status'); #----------------------------------------------------------------------------- { # we redefine the syslog function to an empty function temporarily, so that the # check_timeout() method doesn't write to syslog no warnings 'redefine'; local *main::syslog = sub { }; is($rj->check_timeout(), 0, 'check_timeout'); is($rj->count(), 1, 'count 1'); sleep(2); is($rj->check_timeout(), 1, 'check_timeout'); is($rj->count(), 0, 'count 0'); } #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/rendering_test.t000066400000000000000000000036771412262531100163270ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/rendering_test.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; use Tirex::Queue; use Tirex::Manager::Test; #----------------------------------------------------------------------------- my $queue = Tirex::Queue->new(); isa_ok($queue, 'Tirex::Queue', 'queue'); # we use the Tirex::Manager::Test class instead of Tirex::Manager because we can simulate different loads on the machine and fake sending and receiving of messages my $rm = Tirex::Manager::Test->new( queue => $queue ); isa_ok($rm, 'Tirex::Manager', 'rendering manager'); my $bucket1 = $rm->add_bucket( name => 'live', minprio => 1, maxproc => 3, maxload => 30 ); my $bucket3 = $rm->add_bucket( name => 'backg', minprio => 20, maxproc => 1, maxload => 10 ); my $bucket2 = $rm->add_bucket( name => 'middle', minprio => 10, maxproc => 2, maxload => 20 ); #----------------------------------------------------------------------------- my $job = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'm', x => 1, y => 1, z => 1), prio => 1 ); is($rm->run(), undef, 'nothing to run'); $queue->add($job); is($rm->run(), 1, 'ok to run'); is($rm->run(), undef, 'nothing to run'); is($rm->get_load(), 0, 'get load'); $rm->set_load(5.5); is($rm->get_load(), 5.5, 'set load'); #----------------------------------------------------------------------------- $job = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'm', x => 1, y => 1, z => 1), prio => 1, expire => time() + 10 ); $queue->add($job); is($rm->run(), 1, 'job not expired'); $job = Tirex::Job->new( metatile => Tirex::Metatile->new(map => 'm', x => 1, y => 1, z => 1), prio => 1, expire => time() - 10 ); $queue->add($job); is($rm->run(), 2, 'job expired'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/tirex_basic.t000066400000000000000000000016731412262531100156010ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/tirex_basic.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- # test message handling #----------------------------------------------------------------------------- my $msg = { foo => 'bar', 1 => 2, blob => 'blib' }; my $str = Tirex::create_msg($msg); is($str, "1=2\nblob=blib\nfoo=bar\n", 'create_msg'); is_deeply(Tirex::parse_msg($str), $msg , 'parse msg'); my $print = Tirex::print_msg($msg); is($print, " 1=2\n blob=blib\n foo=bar\n", 'print_msg'); my $crlf = Tirex::parse_msg("a=b\r\nc=d\ne=f\r\n"); is_deeply($crlf, { a => 'b', c => 'd', e => 'f' }, 'parse msg with crlf'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/t/zoomrange.t000066400000000000000000000022051412262531100152760ustar00rootroot00000000000000#----------------------------------------------------------------------------- # # t/zoomrange.t # #----------------------------------------------------------------------------- use strict; use warnings; use Test::More qw( no_plan ); use lib 'lib'; use Tirex; #----------------------------------------------------------------------------- my $zr = Tirex::Zoomrange->new('z4-7', 4, 7); isa_ok($zr, 'Tirex::Zoomrange', 'create'); is($zr->get_min(), 4, 'min'); is($zr->get_max(), 7, 'max'); is($zr->to_s(), '4-7', 'range'); is($zr->get_name(), 'z4-7', 'name'); is($zr->get_id(), 'z4_7', 'id'); $zr = Tirex::Zoomrange->new('foo', 8, 8); is($zr->get_min(), 8, 'min'); is($zr->get_max(), 8, 'max'); is($zr->to_s(), '8', 'range'); is($zr->get_name(), 'foo', 'name'); is($zr->get_id(), 'foo', 'id'); $zr = Tirex::Zoomrange->new(undef, 6); is($zr->get_min(), 6, 'min'); is($zr->get_max(), 6, 'max'); is($zr->get_name(), 'z6', 'name'); $zr = Tirex::Zoomrange->new(undef, 6, 8); is($zr->get_min(), 6, 'min'); is($zr->get_max(), 8, 'max'); is($zr->get_name(), 'z6-8', 'name'); #-- THE END ------------------------------------------------------------------ tirex-0.7.0/test/000077500000000000000000000000001412262531100136255ustar00rootroot00000000000000tirex-0.7.0/test/backend-test.sh000077500000000000000000000006671412262531100165410ustar00rootroot00000000000000#!/bin/sh # # backend-test.sh # # You can use this script to test a backend # without the tirex-backend-manager. # export TIREX_BACKEND_NAME="test" export TIREX_BACKEND_PORT=9330 export TIREX_BACKEND_SYSLOG_FACILITY="local0" export TIREX_BACKEND_MAP_CONFIGS="etc/renderer/test/checkerboard.conf.dist" export TIREX_BACKEND_DEBUG=1 export TIREX_BACKEND_PIPE_FILENO=1 export TIREX_BACKEND_ALIVE_TIMEOUT=10 exec perl -Ilib backends/test tirex-0.7.0/test/queue_speed_test.pl000077500000000000000000000044361412262531100175370ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # queue_speed_test.pl # #----------------------------------------------------------------------------- # # Tests queue filling and emptying speed # #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use lib 'lib'; use Tirex; use Tirex::Queue; use Time::HiRes; #----------------------------------------------------------------------------- # changes these variables as needed my $NUMJOBS = 100000; my $MAXZOOM = 14; my $MAXPRIO = 3; #----------------------------------------------------------------------------- my $q = Tirex::Queue->new(); my $t0 = [ Time::HiRes::gettimeofday() ]; print "filling queue...\n"; foreach my $n (0 .. $NUMJOBS-1) { my $z = int(rand($MAXZOOM)); my $limit = 2 ** $z; my $mt = Tirex::Metatile->new( map => 'test', x => int(rand($limit)), y => int(rand($limit)), z => $z ); $q->add( Tirex::Job->new( metatile => $mt, prio => int(rand($MAXPRIO))+1) ); } print "queue filled with ", $q->size(), " jobs of ", $NUMJOBS , " added in ", Time::HiRes::tv_interval($t0), " seconds\n"; $t0 = [ Time::HiRes::gettimeofday() ]; until ($q->empty()) { my $job = $q->next(); } print "queue empty again after ", Time::HiRes::tv_interval($t0), " seconds\n"; #-- THE END ------------------------------------------------------------------ tirex-0.7.0/test/randomtest.pl000077500000000000000000000061521412262531100163510ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # randomtest.pl # #----------------------------------------------------------------------------- # # Send rendering requests for random metatiles and random priorities to # master in random intervals. # #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Socket; use IO::Socket; use Time::HiRes; use Tirex; use Tirex::Status; #----------------------------------------------------------------------------- my $MAX_QUEUE_SIZE = 100; my $SLEEP_TIME = 60; my $MAX_PRIO = 20; my $MIN_ZOOM = 8; my $MAX_ZOOM = 18; my $RANDOM_SLEEP = 100_000; # germany on z17 my $minx = 34067*2; my $maxx = 35096*2; my $miny = 21236*2; my $maxy = 22739*2; my $status = eval { Tirex::Status->new(); }; die("Cannot connect to shared memory. Is the tirex-master running?\n") if ($@); my $socket = IO::Socket::INET->new(LocalAddr => 'localhost', Proto => 'udp') or die("Cannot open UDP socket: $!\n"); $socket->connect( Socket::pack_sockaddr_in($Tirex::MASTER_UDP_PORT, Socket::inet_aton('localhost')) ); my $count = 0; while (1) { my $x = int($minx + rand($maxx-$minx)); my $y = int($miny + rand($maxy-$miny)); my $z = int(rand($MAX_ZOOM - $MIN_ZOOM + 1)) + $MIN_ZOOM; my $zz=$z; while ($zz++ < 17) { $x >>= 2; $y >>= 2; } $x = int($x/8)*8; $y = int($y/8)*8; my $prio = 1 + int(rand($MAX_PRIO)); my $msg = Tirex::Message->new( type => 'metatile_enqueue_request', prio => $prio, map => 'test', z => $z, x => $x, y => $y ); print "sending msg ", $msg->to_s(), "\n"; $msg->send($socket); Time::HiRes::usleep( rand($RANDOM_SLEEP) ); if ($count++ % 10 == 0) { my $s = $status->read(); die("Cannot read status\n") unless (defined $s); my $queue_size = JSON::from_json($s)->{'queue'}->{'size'}; print "queue_size=$queue_size\n"; if ($queue_size >= $MAX_QUEUE_SIZE) { print "queue size > $MAX_QUEUE_SIZE. sleeping for $SLEEP_TIME seconds.\n"; sleep($SLEEP_TIME); } } } #-- THE END ------------------------------------------------------------------ tirex-0.7.0/test/test_metatiles_range.pl000077500000000000000000000040211412262531100203640ustar00rootroot00000000000000#!/usr/bin/perl #----------------------------------------------------------------------------- # # Tirex Tile Rendering System # # test_metatiles_range.pl # #----------------------------------------------------------------------------- # # Test program for metatile range. # # Usage: # test_metatiles_range.pl map=foo z=3-7 lon=-5,8 lat=60,70 # #----------------------------------------------------------------------------- # # Copyright (C) 2010 Frederik Ramm and # Jochen Topf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; If not, see . # #----------------------------------------------------------------------------- use strict; use warnings; use Tirex; my $init = join(' ', @ARGV); #----------------------------------------------------------------------------- my $range = Tirex::Metatiles::Range->new( init => $init ); print " Map Zoom X Y\n"; my $lastmap = ''; my $lastz = 0; while (my $metatile = $range->next()) { if ($lastmap ne $metatile->get_map() || $lastz != $metatile->get_z()) { print " ---------------------------------------------\n"; } printf(" %-20s %2d %10d %10d\n", $metatile->get_map(), $metatile->get_z(), $metatile->get_x(), $metatile->get_y()); $lastmap = $metatile->get_map(); $lastz = $metatile->get_z(); } #-- THE END ------------------------------------------------------------------ tirex-0.7.0/tileserver/000077500000000000000000000000001412262531100150325ustar00rootroot00000000000000tirex-0.7.0/tileserver/README000066400000000000000000000014451412262531100157160ustar00rootroot00000000000000 This is a tileserver written for Node.JS that works with the rest of the Tirex system. This is not recommended for production system. It just has the basics for a tile server and does seem to work, but lacks things such as proper logging. And its pretty new and basically untested. See http://www.nodejs.org/ for downloading and installing NodeJS. You need a reasonably new version that supports UDP. The version in Ubuntu 10.10 doesn't work. Start it with node tileserver.js It will read your Tirex configuration in /etc/tirex and look for maps there. It will then open port 9320 and listen for map requests. URLs have the form http://HOST:9320/tiles/MAP/Z/X/Y.png You can ask http://HOST:9320/maps for a list of maps it knows about and http://HOST:9320/stats for statistics. tirex-0.7.0/tileserver/tileserver.js000066400000000000000000000231101412262531100175510ustar00rootroot00000000000000/* ===================================================================== Tirex Tileserver Tileserver for the Tirex system using node.JS (www.nodejs.org). http://wiki.openstreetmap.org/wiki/Tirex ======================================================================== Copyright (C) 2011 Jochen Topf This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; If not, see . ===================================================================== */ var util = require('util'); var net = require('net'); var http = require('http'); var url = require('url'); var path = require('path'); var fs = require('fs'); var dgram = require('dgram'); // size in bytes of metatile header var metatile_header_size = 20 + 8 * 64; // open http connections waiting for answer var open_connections = {}; // number of open http connections waiting for answer var num_open_connections = 0; var stats = { tiles_requested: 0, tiles_from_cache: 0, tiles_rendered: 0, http_requests: 0 }; var config = { configdir: '/etc/tirex', master_udp_port: 9322, tileserver_http_port: 9320 }; // ===================================================================== // get long value at offset from buffer Buffer.prototype.getLong = function(offset) { return ((this[offset+3] * 256 + this[offset+2]) * 256 + this[offset+1]) * 256 + this[offset]; }; // ===================================================================== function send_image(x, y, z, fd, response) { var buffer = new Buffer(metatile_header_size); fs.read(fd, buffer, 0, metatile_header_size, 0, function(err, bytesRead) { if (err || bytesRead !== metatile_header_size) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end('Metatile error\n'); fs.close(fd); return; } var pib = 20 + ((y%8) * 8) + ((x%8) * 64); // offset into lookup table in header var offset = buffer.getLong(pib); var size = buffer.getLong(pib+4); var png = new Buffer(size); fs.read(fd, png, 0, size, offset, function(err, bytesRead) { if (err || bytesRead !== size) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end('Metatile error\n'); fs.close(fd); return; } response.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': size }); response.end(png); fs.close(fd); }); }); } function serialize_tirex_msg(msg) { var string = '', k; for (k in msg) { string += k + '=' + msg[k] + '\n'; } return string; } function deserialize_tirex_msg(string) { var lines = string.split('\n'); var msg = {}, i; for (i=0; i < lines.length; i++) { var line = lines[i].split('='); if (line[0] !== '') { msg[line[0]] = line[1]; } } return msg; } function xyz_to_filename(x, y, z) { var path_components = [], i, v; // make sure we have metatile coordinates x -= x % 8; y -= y % 8; for (i=0; i <= 4; i++) { v = x & 0x0f; v <<= 4; v |= (y & 0x0f); x >>= 4; y >>= 4; path_components.unshift(v); } path_components.unshift(z); return path_components.join('/') + '.meta'; } function fingerprint(map, z, x, y) { return [map, z, x, y].join('/'); } function store_connection(req, res, map, z, x, y) { var l = fingerprint(map, z, x - x%8, y - y%8); if (! open_connections[l]) { open_connections[l] = []; } open_connections[l].push({ res: res, map: map, x: x, y: y, z: z }); } function get_connections(map, z, x, y) { var l = fingerprint(map, z, x, y); var connections = open_connections[l]; delete open_connections[l]; return connections; } // for debugging only function dump_open_connections() { var oclist = '', oc; for (oc in open_connections) { oclist += oc + '/' + open_connections[oc].length + ' '; } console.log("oc:", oclist); } function not_found(response, text) { response.writeHead(404, {'Content-Type': 'text/plain'}); response.end((text || 'Not found') + '\n'); } /* ===================================================================== Read config ===================================================================== */ var maps = {}; var renderers = fs.readdirSync(config.configdir + '/renderer'); var i, j; for (i=0; i < renderers.length; i++) { var rdir = config.configdir + '/renderer/' + renderers[i]; if (fs.statSync(rdir).isDirectory()) { var files = fs.readdirSync(rdir); for (j=0; j < files.length; j++) { var mapfile = rdir + '/' + files[j]; var cfg = fs.readFileSync(mapfile, 'utf-8'); var lines = cfg.split('\n'); var map = { minz: 0, maxz: 0, stats: { tiles_requested: 0, tiles_from_cache: 0, tiles_rendered: 0 }}; lines.forEach(function(line) { if (!line.match('^#') && !line.match('^$')) { var kv = line.split('='); map[kv[0]] = kv[1]; } }); maps[map.name] = map; } } } console.log('Maps:'); var name; for (name in maps) { console.log(' ' + name + ' [' + maps[name].minz + '-' + maps[name].maxz + '] tiledir=' + maps[name].tiledir); } // ===================================================================== var sock = dgram.createSocket("udp4", function(buf, rinfo) { var msg = deserialize_tirex_msg(buf.toString('ascii', 0, rinfo.size)); if (msg.id[0] !== 'n') { return; } // console.log("got msg:", msg); var conns = get_connections(msg.map, msg.z, msg.x, msg.y); if (conns !== undefined) { var imgfile = path.join(maps[msg.map].tiledir, xyz_to_filename(conns[0].x, conns[0].y, msg.z)); fs.open(imgfile, 'r', null, function(err, fd) { while (conns.length > 0) { var conn = conns.shift(); num_open_connections--; // console.log("connection=", msg.map, msg.z, conn.x, conn.y, imgfile); if (err) { not_found(conn.res); } else { stats.tiles_rendered++; maps[msg.map].stats.tiles_rendered++; send_image(conn.x, conn.y, msg.z, fd, conn.res); } } }); } }); //sock.bind(9090, '127.0.0.1'); var server = http.createServer(function(req, res) { console.log("req:", req.url); stats.http_requests++; var p = url.parse(req.url).pathname.split('/'); if (p[0] !== '') { return not_found(res); } if (p[1] === 'maps') { res.writeHead(200, {'Content-Type': 'application/json;charset=utf-8'}); var mapsout = {}, m; for (m in maps) { mapsout[m] = { minz: maps[m].minz, maxz: maps[m].maxz }; } res.end(JSON.stringify(mapsout) + '\n'); return; } if (p[1] === 'stats') { res.writeHead(200, {'Content-Type': 'application/json;charset=utf-8'}); var m, out = { stats: stats, maps: {} }; for (m in maps) { out.maps[m] = maps[m].stats; } res.write(JSON.stringify(out) + '\n'); res.end(); return; } if (p[1] !== 'tiles') { // must always start with "/tiles/" return not_found(res); } var map = p[2]; if (maps[map] === undefined) { return not_found(res, 'Unknown map'); } var z = parseInt(p[3]); if (z < maps[map].minz || z > maps[map].maxz) { return not_found(res, 'z out of range'); } var x = parseInt(p[4]); var y = parseInt(path.basename(p[5], '.png')); var mx = x - x%8; var my = y - y%8; var limit = 1 << z; if (x < 0 || x >= limit || y < 0 || y >= limit) { return not_found(res); } stats.tiles_requested++; maps[map].stats.tiles_requested++; var imgfile = path.join(maps[map].tiledir, xyz_to_filename(x, y, z)); // console.log(map, z, x, y, imgfile); fs.open(imgfile, 'r', null, function(err, fd) { if (err) { var s = serialize_tirex_msg({ id: 'nodets-' + stats.tiles_requested, type: 'metatile_enqueue_request', prio: 8, map: map, x: mx, y: my, z: z }); // console.log("send to tirex", s); var buf = new Buffer(s); sock.send(buf, 0, buf.length, config.master_udp_port, '127.0.0.1'); store_connection(req, res, map, z, x, y); num_open_connections++; } else { stats.tiles_from_cache++; maps[map].stats.tiles_from_cache++; send_image(x, y, z, fd, res); } }); }); server.on('clientError', function(exception) { console.log("exception:", exception); }); server.listen(config.tileserver_http_port); tirex-0.7.0/utils/000077500000000000000000000000001412262531100140065ustar00rootroot00000000000000tirex-0.7.0/utils/analyze_postgis_log.pl000077500000000000000000000162041412262531100204250ustar00rootroot00000000000000#!/usr/bin/perl # # analyze_postgis_log.pl [MAPFILE] : SELECT AsBinary("way",'NDR') AS geom,"disused","name","waterway" from (select way,waterway,disused,name,case when tunnel in ('yes','true','1') then 'yes'::text else tunnel end as tunnel from planet_osm_line where waterway IS NOT NULL order by z_order) as water_lines WHERE "way" && SetSRID('BOX3D(-1.428222656250023 53.31774904749086,-1.032714843749977 53.55336278552809)'::box3d,4326) # # You will have to enable duration logging in Postgres, and you may have to change # the regular expressions in this script if your log looks different. # # This script uses the bounding box from the PostGIS query to find out which # zoom level was requested. You may have to change the "guessZoomLevel" function # further down depending on the projection your database is in. # Written by Frederik Ramm , PD. use strict; #use warnings; use Math::Trig; my $input; my $statement; my $duration; my $timings = {}; my $mapfile = shift || 'osm.xml'; while (1) { $input = <>; chomp $input; if (!defined($input) || $input =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d (.*)/) { my $rest = $1; if ($rest =~ /^.*LOG:\s+duration: (\d+)(\.\d+)? ms\s*$/) { # duration given on extra line $duration = $1; } process_statement($statement, $duration) if (defined $statement); last unless defined($input); undef $statement; if ($rest =~ /^.*LOG:\s+(duration: (\d+)(\.\d+)? ms\s+)?execute <[^>]*>: (SELECT .*)/) { if (defined($1)) { $duration = $2; } else { undef $duration; } $statement = $4; } } else { $statement .= $input if (defined $statement); } } my $potential_originating_layers = findStatements(); foreach my $ds(sort { $timings->{$b}->{-1} <=> $timings->{$a}->{-1} } keys %$timings) { print "$ds\n"; printf "* probable originating layers: "; my $comma = ""; foreach my $pol(@{$potential_originating_layers->{$ds}}) { print $comma.$pol->{layer}; $comma = ", "; } print "none found" if ($comma eq ""); print "\n"; print " zoom count min time avg time max time\n"; print " -----------------------------------------\n"; foreach my $z (sort { $a <=> $b } keys %{$timings->{$ds}}) { next unless $z >= 0; my $t = $timings->{$ds}->{$z}; my $count = $t->{count}; my $min = ($t->{min}//0) / 1000; my $avg = $t->{sum} / $t->{count} / 1000; my $max = $t->{max} / 1000; my $warn = ''; $warn = '***' if ($count > 10 && ($min > 10 || $avg > 20 || $max > 30)); $warn = '*****' if ( ($min > 60 || $avg > 80 || $max > 100)); printf " %2d %6d %8.1fs %8.1fs %8.1fs $warn\n", $z, $count, $min, $avg, $max; } printf "\n"; } sub process_statement { my ($stmt, $dur) = @_; $stmt =~ s/\s+/ /g; if ($stmt =~ /SELECT.*from (\(.*\) as \S+) WHERE \S+ && (ST_)?SetSRID\('BOX3D\((\S+) (\S+),(\S+) (\S+)\)'::box3d,\s*(4326|900913)\)/i) { my ($datasource, $left, $bottom, $right, $top, $proj) = ($1, $3, $4, $5, $6, $7); my $zoom = guessZoomLevel($left, $bottom, $right, $top, $proj); $timings->{$datasource}->{$zoom}->{count} ++; $timings->{$datasource}->{$zoom}->{sum} += $dur; $timings->{$datasource}->{$zoom}->{min} = $dur if ($dur < ($timings->{$datasource}->{$zoom}->{min} // 999_999_999_999_999)); $timings->{$datasource}->{$zoom}->{max} = $dur if ($dur > ($timings->{$datasource}->{$zoom}->{max} // 0)); # the following line determines the sorting of the final results. # currently the queries that use the longest total time are at the # top but you could also do a "max" calculation here or simply count # the invocations. $timings->{$datasource}->{-1} += $dur; } else { print STDERR "cannot understand $stmt\n"; } } # Guesses the zoom level that was rendered by renderd, based on the bounding box # in the PostGIS query. Basically we're looking for a zoom level in which the # requested bbox would be just slightly wider than 8 tiles (=1 meta tile). # This code is for PostGIS databases in lat/lon (osm2pgsql -l) and will have to # be changed to work with spherical mercator coordinates. sub guessZoomLevel { my ($left, $bottom, $right, $top, $proj) = @_; if ($proj == 4326) { my ($left_tile, $bottom_tile) = getTileNumber($bottom, $left, 18); my ($right_tile, $top_tile) = getTileNumber($top, $right, 18); my $width = $right_tile - $left_tile; my $zoom = 18; while ($width >= 16) { $width /= 2; $zoom--; } return $zoom; } elsif ($proj == 900913) { return 99; } } sub getTileNumber { my ($lat,$lon,$zoom) = @_; my $xtile = int(($lon+180)/360 *2**$zoom); my $ytile = int((1 - log(tan($lat*pi/180) + sec($lat*pi/180))/pi)/2 *2**$zoom); return(($xtile, $ytile)); } # parses the style file to find out which layers use which statements sub findStatements { my $result; # Using xmlstarlet resolves all entities for us... open(STYLE, "xmlstarlet c14n $mapfile|") or die; my $styles = {}; my $currentlayername; my $where; while(